mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-19 08:46:56 +02:00
fix: read frontend entitlements and trigger quotas from license state
This commit is contained in:
parent
1043504c7a
commit
3fb0ae4e3e
|
|
@ -2,6 +2,7 @@ import type { LicenseState } from '@n8n/backend-common';
|
|||
import { markHttpRequestError } from '@n8n/backend-network';
|
||||
import type { HttpRequestClient, OutboundHttp } from '@n8n/backend-network';
|
||||
import type { EventService } from '@n8n/backend-services';
|
||||
import { UNLIMITED_LICENSE_QUOTA } from '@n8n/constants';
|
||||
import type { WorkflowRepository } from '@n8n/db';
|
||||
import type { TEntitlement } from '@n8n_io/license-sdk';
|
||||
import { AxiosError } from 'axios';
|
||||
|
|
@ -31,7 +32,7 @@ describe('LicenseService', () => {
|
|||
);
|
||||
|
||||
license.getMainPlan.mockReturnValue(entitlement);
|
||||
license.getTriggerLimit.mockReturnValue(400);
|
||||
licenseState.getMaxActiveWorkflows.mockReturnValue(400);
|
||||
license.getPlanName.mockReturnValue('Test Plan');
|
||||
licenseState.getMaxWorkflowsWithEvaluations.mockReturnValue(2);
|
||||
workflowRepository.getActiveTriggerCount.mockResolvedValue(7);
|
||||
|
|
@ -66,6 +67,22 @@ describe('LicenseService', () => {
|
|||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return the unlimited active workflow quota', async () => {
|
||||
licenseState.getMaxActiveWorkflows.mockReturnValueOnce(UNLIMITED_LICENSE_QUOTA);
|
||||
|
||||
const data = await licenseService.getLicenseData();
|
||||
|
||||
expect(data.usage.activeWorkflowTriggers.limit).toBe(UNLIMITED_LICENSE_QUOTA);
|
||||
});
|
||||
|
||||
it('should return a zero active workflow quota', async () => {
|
||||
licenseState.getMaxActiveWorkflows.mockReturnValueOnce(0);
|
||||
|
||||
const data = await licenseService.getLicenseData();
|
||||
|
||||
expect(data.usage.activeWorkflowTriggers.limit).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('activateLicense', () => {
|
||||
|
|
|
|||
|
|
@ -51,8 +51,7 @@ export class LicenseService {
|
|||
usage: {
|
||||
activeWorkflowTriggers: {
|
||||
value: triggerCount,
|
||||
// oxlint-disable-next-line typescript/no-deprecated - Tech debt, tests fail if we move to this.licenseState.getMaxActiveWorkflows
|
||||
limit: this.license.getTriggerLimit(),
|
||||
limit: this.licenseState.getMaxActiveWorkflows(),
|
||||
warningThreshold: 0.8,
|
||||
},
|
||||
workflowsHavingEvaluations: {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import type { Mock } from 'vitest';
|
||||
import type { LicenseState, Logger, ModuleRegistry } from '@n8n/backend-common';
|
||||
import type { GlobalConfig, SecurityConfig } from '@n8n/config';
|
||||
import { UNLIMITED_LICENSE_QUOTA } from '@n8n/constants';
|
||||
import type { WorkflowRepository } from '@n8n/db';
|
||||
import { Container } from '@n8n/di';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
|
|
@ -122,30 +123,9 @@ describe('FrontendService', () => {
|
|||
});
|
||||
|
||||
const license = mock<License>({
|
||||
getUsersLimit: vi.fn().mockReturnValue(100),
|
||||
getPlanName: vi.fn().mockReturnValue('Community'),
|
||||
getConsumerId: vi.fn().mockReturnValue('test-consumer'),
|
||||
isSharingEnabled: vi.fn().mockReturnValue(false),
|
||||
isLogStreamingEnabled: vi.fn().mockReturnValue(false),
|
||||
isLdapEnabled: vi.fn().mockReturnValue(false),
|
||||
isSamlEnabled: vi.fn().mockReturnValue(false),
|
||||
isAdvancedExecutionFiltersEnabled: vi.fn().mockReturnValue(false),
|
||||
isVariablesEnabled: vi.fn().mockReturnValue(false),
|
||||
isSourceControlLicensed: vi.fn().mockReturnValue(false),
|
||||
isExternalSecretsEnabled: vi.fn().mockReturnValue(false),
|
||||
isLicensed: vi.fn().mockReturnValue(false),
|
||||
isDebugInEditorLicensed: vi.fn().mockReturnValue(false),
|
||||
isWorkerViewLicensed: vi.fn().mockReturnValue(false),
|
||||
isAdvancedPermissionsLicensed: vi.fn().mockReturnValue(false),
|
||||
|
||||
getVariablesLimit: vi.fn().mockReturnValue(0),
|
||||
getTeamProjectLimit: vi.fn().mockReturnValue(0),
|
||||
isBinaryDataS3Licensed: vi.fn().mockReturnValue(false),
|
||||
isAiAssistantEnabled: vi.fn().mockReturnValue(false),
|
||||
isAskAiEnabled: vi.fn().mockReturnValue(false),
|
||||
isAiCreditsEnabled: vi.fn().mockReturnValue(false),
|
||||
getAiCredits: vi.fn().mockReturnValue(0),
|
||||
isFoldersEnabled: vi.fn().mockReturnValue(false),
|
||||
});
|
||||
|
||||
const mailer = mock<UserManagementMailer>({
|
||||
|
|
@ -168,6 +148,12 @@ describe('FrontendService', () => {
|
|||
});
|
||||
|
||||
const licenseState = mock<LicenseState>({
|
||||
getMaxUsers: vi.fn().mockReturnValue(100),
|
||||
getMaxVariables: vi.fn().mockReturnValue(0),
|
||||
getMaxTeamProjects: vi.fn().mockReturnValue(0),
|
||||
getMaxAiCredits: vi.fn().mockReturnValue(0),
|
||||
isLdapLicensed: vi.fn().mockReturnValue(false),
|
||||
isSamlLicensed: vi.fn().mockReturnValue(false),
|
||||
isOidcLicensed: vi.fn().mockReturnValue(false),
|
||||
isMFAEnforcementLicensed: vi.fn().mockReturnValue(false),
|
||||
isOtelCustomSpanAttributesLicensed: vi.fn().mockReturnValue(false),
|
||||
|
|
@ -280,6 +266,38 @@ describe('FrontendService', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('should expose licensed features and quotas from license state', async () => {
|
||||
licenseState.isSharingLicensed.mockReturnValueOnce(true);
|
||||
licenseState.isSamlLicensed.mockReturnValueOnce(true).mockReturnValueOnce(true);
|
||||
licenseState.isVariablesLicensed.mockReturnValueOnce(true).mockReturnValueOnce(true);
|
||||
licenseState.isFoldersLicensed.mockReturnValueOnce(true);
|
||||
licenseState.getMaxUsers.mockReturnValueOnce(25).mockReturnValueOnce(25);
|
||||
licenseState.getMaxVariables.mockReturnValueOnce(50);
|
||||
licenseState.getMaxTeamProjects.mockReturnValueOnce(3);
|
||||
const { service } = createMockService();
|
||||
|
||||
const settings = await service.getSettings();
|
||||
|
||||
expect(settings.enterprise).toMatchObject({ sharing: true, saml: true, variables: true });
|
||||
expect(settings.userManagement.quota).toBe(25);
|
||||
expect(settings.variables.limit).toBe(50);
|
||||
expect(settings.enterprise.projects.team.limit).toBe(3);
|
||||
expect(settings.folders.enabled).toBe(true);
|
||||
});
|
||||
|
||||
it('should expose unlicensed features with unlimited user quotas', async () => {
|
||||
licenseState.getMaxUsers
|
||||
.mockReturnValueOnce(UNLIMITED_LICENSE_QUOTA)
|
||||
.mockReturnValueOnce(UNLIMITED_LICENSE_QUOTA);
|
||||
const { service } = createMockService();
|
||||
|
||||
const settings = await service.getSettings();
|
||||
|
||||
expect(settings.userManagement.quota).toBe(UNLIMITED_LICENSE_QUOTA);
|
||||
expect(settings.enterprise.sharing).toBeFalsy();
|
||||
expect(settings.folders.enabled).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should expose excluded node types from NODES_EXCLUDE', async () => {
|
||||
globalConfig.nodes.exclude = ['n8n-nodes-base.executeWorkflow'];
|
||||
const { service } = createMockService();
|
||||
|
|
@ -325,8 +343,8 @@ describe('FrontendService', () => {
|
|||
it('should surface the assistant Cloud UBB entitlement when the AI Assistant is enabled and entitled', async () => {
|
||||
globalConfig.aiAssistant.baseUrl = 'https://ai-assistant.n8n.io';
|
||||
licenseState.isAiAssistantCloudUbbEntitlementLicensed.mockReturnValue(true);
|
||||
const { service, license } = createMockService();
|
||||
license.isAiAssistantEnabled.mockReturnValue(true);
|
||||
licenseState.isAiAssistantLicensed.mockReturnValueOnce(true);
|
||||
const { service } = createMockService();
|
||||
|
||||
const settings = await service.getSettings();
|
||||
|
||||
|
|
@ -336,8 +354,7 @@ describe('FrontendService', () => {
|
|||
it('should keep the assistant Cloud UBB entitlement off when the AI Assistant is disabled', async () => {
|
||||
globalConfig.aiAssistant.baseUrl = '';
|
||||
licenseState.isAiAssistantCloudUbbEntitlementLicensed.mockReturnValue(true);
|
||||
const { service, license } = createMockService();
|
||||
license.isAiAssistantEnabled.mockReturnValue(false);
|
||||
const { service } = createMockService();
|
||||
|
||||
const settings = await service.getSettings();
|
||||
|
||||
|
|
@ -852,9 +869,9 @@ describe('FrontendService', () => {
|
|||
});
|
||||
|
||||
it('should set aiBuilder.enabled to true when license has feat:aiBuilder', async () => {
|
||||
const { service, license } = createMockService();
|
||||
const { service } = createMockService();
|
||||
|
||||
license.isLicensed.mockImplementation((feature) => {
|
||||
licenseState.isLicensed.mockImplementation((feature) => {
|
||||
return feature === 'feat:aiBuilder';
|
||||
});
|
||||
|
||||
|
|
@ -864,9 +881,9 @@ describe('FrontendService', () => {
|
|||
});
|
||||
|
||||
it('should keep aiBuilder.enabled as false when license does not have feat:aiBuilder', async () => {
|
||||
const { service, license } = createMockService();
|
||||
const { service } = createMockService();
|
||||
|
||||
license.isLicensed.mockReturnValue(false);
|
||||
licenseState.isLicensed.mockReturnValue(false);
|
||||
|
||||
const settings = await service.getSettings();
|
||||
|
||||
|
|
|
|||
|
|
@ -262,8 +262,7 @@ export class FrontendService {
|
|||
this.globalConfig.personalization.enabled && this.globalConfig.diagnostics.enabled,
|
||||
defaultLocale: this.globalConfig.defaultLocale,
|
||||
userManagement: {
|
||||
// oxlint-disable-next-line typescript/no-deprecated
|
||||
quota: this.license.getUsersLimit(),
|
||||
quota: this.licenseState.getMaxUsers(),
|
||||
showSetupOnFirstLoad: await this.getShowSetupOnFirstLoad(),
|
||||
smtpSetup: this.mailer.isEmailSetUp,
|
||||
authenticationMethod: getCurrentAuthenticationMethod(),
|
||||
|
|
@ -484,8 +483,7 @@ export class FrontendService {
|
|||
|
||||
// refresh user management status
|
||||
Object.assign(this.settings.userManagement, {
|
||||
// oxlint-disable-next-line typescript/no-deprecated
|
||||
quota: this.license.getUsersLimit(),
|
||||
quota: this.licenseState.getMaxUsers(),
|
||||
authenticationMethod: getCurrentAuthenticationMethod(),
|
||||
showSetupOnFirstLoad: await this.getShowSetupOnFirstLoad(),
|
||||
});
|
||||
|
|
@ -514,15 +512,11 @@ export class FrontendService {
|
|||
|
||||
const isS3Selected = this.binaryDataConfig.mode === 's3';
|
||||
const isS3Available = this.binaryDataConfig.availableModes.includes('s3');
|
||||
// oxlint-disable-next-line typescript/no-deprecated
|
||||
const isS3Licensed = this.license.isBinaryDataS3Licensed();
|
||||
// oxlint-disable-next-line typescript/no-deprecated
|
||||
const isAiAssistantEnabled = this.license.isAiAssistantEnabled();
|
||||
// oxlint-disable-next-line typescript/no-deprecated
|
||||
const isAskAiEnabled = this.license.isAskAiEnabled();
|
||||
// oxlint-disable-next-line typescript/no-deprecated
|
||||
const isAiCreditsEnabled = this.license.isAiCreditsEnabled();
|
||||
const isAiBuilderEnabled = this.license.isLicensed(LICENSE_FEATURES.AI_BUILDER);
|
||||
const isS3Licensed = this.licenseState.isBinaryDataS3Licensed();
|
||||
const isAiAssistantEnabled = this.licenseState.isAiAssistantLicensed();
|
||||
const isAskAiEnabled = this.licenseState.isAskAiLicensed();
|
||||
const isAiCreditsEnabled = this.licenseState.isAiCreditsLicensed();
|
||||
const isAiBuilderEnabled = this.licenseState.isLicensed(LICENSE_FEATURES.AI_BUILDER);
|
||||
|
||||
this.settings.license.planName = this.license.getPlanName();
|
||||
this.settings.license.consumerId = this.license.getConsumerId();
|
||||
|
|
@ -537,36 +531,25 @@ export class FrontendService {
|
|||
|
||||
// refresh enterprise status
|
||||
Object.assign(this.settings.enterprise, {
|
||||
// oxlint-disable-next-line typescript/no-deprecated
|
||||
sharing: this.license.isSharingEnabled(),
|
||||
// oxlint-disable-next-line typescript/no-deprecated
|
||||
logStreaming: this.license.isLogStreamingEnabled(),
|
||||
// oxlint-disable-next-line typescript/no-deprecated
|
||||
ldap: this.license.isLdapEnabled(),
|
||||
// oxlint-disable-next-line typescript/no-deprecated
|
||||
saml: this.license.isSamlEnabled(),
|
||||
sharing: this.licenseState.isSharingLicensed(),
|
||||
logStreaming: this.licenseState.isLogStreamingLicensed(),
|
||||
ldap: this.licenseState.isLdapLicensed(),
|
||||
saml: this.licenseState.isSamlLicensed(),
|
||||
oidc: this.licenseState.isOidcLicensed(),
|
||||
mfaEnforcement: this.licenseState.isMFAEnforcementLicensed(),
|
||||
provisioning: false, // temporarily disabled until this feature is ready for release
|
||||
// oxlint-disable-next-line typescript/no-deprecated
|
||||
advancedExecutionFilters: this.license.isAdvancedExecutionFiltersEnabled(),
|
||||
// oxlint-disable-next-line typescript/no-deprecated
|
||||
variables: this.license.isVariablesEnabled(),
|
||||
// oxlint-disable-next-line typescript/no-deprecated
|
||||
sourceControl: this.license.isSourceControlLicensed(),
|
||||
// oxlint-disable-next-line typescript/no-deprecated
|
||||
externalSecrets: this.license.isExternalSecretsEnabled(),
|
||||
showNonProdBanner: this.license.isLicensed(LICENSE_FEATURES.SHOW_NON_PROD_BANNER),
|
||||
// oxlint-disable-next-line typescript/no-deprecated
|
||||
debugInEditor: this.license.isDebugInEditorLicensed(),
|
||||
advancedExecutionFilters: this.licenseState.isAdvancedExecutionFiltersLicensed(),
|
||||
variables: this.licenseState.isVariablesLicensed(),
|
||||
sourceControl: this.licenseState.isSourceControlLicensed(),
|
||||
externalSecrets: this.licenseState.isExternalSecretsLicensed(),
|
||||
showNonProdBanner: this.licenseState.isLicensed(LICENSE_FEATURES.SHOW_NON_PROD_BANNER),
|
||||
debugInEditor: this.licenseState.isDebugInEditorLicensed(),
|
||||
binaryDataS3: isS3Available && isS3Selected && isS3Licensed,
|
||||
// oxlint-disable-next-line typescript/no-deprecated
|
||||
workerView: this.license.isWorkerViewLicensed(),
|
||||
// oxlint-disable-next-line typescript/no-deprecated
|
||||
advancedPermissions: this.license.isAdvancedPermissionsLicensed(),
|
||||
workerView: this.licenseState.isWorkerViewLicensed(),
|
||||
advancedPermissions: this.licenseState.isAdvancedPermissionsLicensed(),
|
||||
|
||||
workflowDiffs: this.licenseState.isWorkflowDiffsLicensed(),
|
||||
namedVersions: this.license.isLicensed(LICENSE_FEATURES.NAMED_VERSIONS),
|
||||
namedVersions: this.licenseState.isLicensed(LICENSE_FEATURES.NAMED_VERSIONS),
|
||||
customRoles: this.licenseState.isCustomRolesLicensed(),
|
||||
personalSpacePolicy: this.licenseState.isPersonalSpacePolicyLicensed(),
|
||||
dataRedaction: this.licenseState.isDataRedactionLicensed(),
|
||||
|
|
@ -577,16 +560,14 @@ export class FrontendService {
|
|||
this.settings.workerPools.enabled =
|
||||
this.globalConfig.queue.workerPool.enabled && this.licenseState.isWorkerPoolsLicensed();
|
||||
|
||||
// oxlint-disable-next-line typescript/no-deprecated
|
||||
if (this.license.isLdapEnabled()) {
|
||||
if (this.licenseState.isLdapLicensed()) {
|
||||
Object.assign(this.settings.sso.ldap, {
|
||||
loginLabel: this.globalConfig.sso.ldap.loginLabel,
|
||||
loginEnabled: this.globalConfig.sso.ldap.loginEnabled,
|
||||
});
|
||||
}
|
||||
|
||||
// oxlint-disable-next-line typescript/no-deprecated
|
||||
if (this.license.isSamlEnabled()) {
|
||||
if (this.licenseState.isSamlLicensed()) {
|
||||
Object.assign(this.settings.sso.saml, {
|
||||
loginLabel: getSamlLoginLabel(),
|
||||
loginEnabled: this.globalConfig.sso.saml.loginEnabled,
|
||||
|
|
@ -599,10 +580,8 @@ export class FrontendService {
|
|||
});
|
||||
}
|
||||
|
||||
// oxlint-disable-next-line typescript/no-deprecated
|
||||
if (this.license.isVariablesEnabled()) {
|
||||
// oxlint-disable-next-line typescript/no-deprecated
|
||||
this.settings.variables.limit = this.license.getVariablesLimit();
|
||||
if (this.licenseState.isVariablesLicensed()) {
|
||||
this.settings.variables.limit = this.licenseState.getMaxVariables();
|
||||
}
|
||||
|
||||
if (isAiAssistantEnabled) {
|
||||
|
|
@ -620,8 +599,7 @@ export class FrontendService {
|
|||
|
||||
if (isAiCreditsEnabled) {
|
||||
this.settings.aiCredits.enabled = isAiCreditsEnabled;
|
||||
// oxlint-disable-next-line typescript/no-deprecated
|
||||
this.settings.aiCredits.credits = this.license.getAiCredits();
|
||||
this.settings.aiCredits.credits = this.licenseState.getMaxAiCredits();
|
||||
this.settings.aiCredits.setup = !!this.globalConfig.aiAssistant.baseUrl;
|
||||
}
|
||||
|
||||
|
|
@ -632,7 +610,7 @@ export class FrontendService {
|
|||
if (isAiGatewayEnabled) {
|
||||
this.settings.aiGateway = {
|
||||
enabled: true,
|
||||
budget: this.license.getValue(LICENSE_QUOTAS.AI_GATEWAY_BUDGET) ?? 0,
|
||||
budget: this.licenseState.getValue(LICENSE_QUOTAS.AI_GATEWAY_BUDGET) ?? 0,
|
||||
cloudUbbEnabled: this.licenseState.isAiGatewayCloudUbbLicensed(),
|
||||
};
|
||||
}
|
||||
|
|
@ -658,11 +636,9 @@ export class FrontendService {
|
|||
|
||||
this.settings.binaryDataMode = this.binaryDataConfig.mode;
|
||||
|
||||
// oxlint-disable-next-line typescript/no-deprecated
|
||||
this.settings.enterprise.projects.team.limit = this.license.getTeamProjectLimit();
|
||||
this.settings.enterprise.projects.team.limit = this.licenseState.getMaxTeamProjects();
|
||||
|
||||
// oxlint-disable-next-line typescript/no-deprecated
|
||||
this.settings.folders.enabled = this.license.isFoldersEnabled();
|
||||
this.settings.folders.enabled = this.licenseState.isFoldersLicensed();
|
||||
|
||||
// Refresh evaluation settings
|
||||
this.settings.evaluation.quota = this.licenseState.getMaxWorkflowsWithEvaluations();
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user