Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: Add mocked license #30504

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions apps/meteor/ee/app/license/server/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,24 @@ settings.watch<string>('Enterprise_License', async (license) => {
return;
}

if (!(await License.setLicense(license))) {
await Settings.updateValueById('Enterprise_License_Status', 'Invalid');
return;
try {
if (!(await License.setLicense(license))) {
await Settings.updateValueById('Enterprise_License_Status', 'Invalid');
return;
}
} catch (_error) {
// do nothing
}

await Settings.updateValueById('Enterprise_License_Status', 'Valid');
});

if (process.env.ROCKETCHAT_LICENSE) {
await License.setLicense(process.env.ROCKETCHAT_LICENSE);
try {
await License.setLicense(process.env.ROCKETCHAT_LICENSE);
} catch (_error) {
// do nothing
}

Meteor.startup(async () => {
if (settings.get('Enterprise_License')) {
Expand Down
6 changes: 5 additions & 1 deletion apps/meteor/ee/app/license/server/startup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ settings.watch<string>('Site_Url', (value) => {
});

callbacks.add('workspaceLicenseChanged', async (updatedLicense) => {
await License.setLicense(updatedLicense);
try {
await License.setLicense(updatedLicense);
} catch (_error) {
// Ignore
}
});

License.setLicenseLimitCounter('activeUsers', () => Users.getActiveLocalUserCount());
Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/ee/server/lib/EnterpriseCheck.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export const EnterpriseCheck: ServiceSchema = {
async started(): Promise<void> {
setInterval(async () => {
try {
const hasLicense = await this.broker.call('license.hasLicense', ['scalability']);
const hasLicense = await this.broker.call('license.hasValidLicense', ['scalability']);
if (hasLicense) {
checkFails = 0;
return;
Expand Down
209 changes: 209 additions & 0 deletions ee/packages/license/__tests__/MockedLicenseBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
import { LicenseImp } from '../src';
import type { ILicenseTag } from '../src/definition/ILicenseTag';
import type { ILicenseV3 } from '../src/definition/ILicenseV3';
import type { LicenseLimit } from '../src/definition/LicenseLimit';
import type { LicenseModule } from '../src/definition/LicenseModule';
import type { LicensePeriod, Timestamp } from '../src/definition/LicensePeriod';
import { encrypt } from '../src/token';

export class MockedLicenseBuilder {
information: {
id?: string;
autoRenew: boolean;
visualExpiration: Timestamp;
notifyAdminsAt?: Timestamp;
notifyUsersAt?: Timestamp;
trial: boolean;
offline: boolean;
createdAt: Timestamp;
grantedBy: {
method: 'manual' | 'self-service' | 'sales' | 'support' | 'reseller';
seller?: string;
};
grantedTo?: {
name?: string;
company?: string;
email?: string;
};
legalText?: string;
notes?: string;
tags?: ILicenseTag[];
};

validation: {
serverUrls: {
value: string;
type: 'url' | 'regex' | 'hash';
}[];

serverVersions?: {
value: string;
}[];

serverUniqueId?: string;
cloudWorkspaceId?: string;
validPeriods: LicensePeriod[];
legalTextAgreement?: {
type: 'required' | 'not-required' | 'accepted';
acceptedVia?: 'cloud';
};

statisticsReport: {
required: boolean;
allowedStaleInDays?: number;
};
};

constructor() {
this.information = {
autoRenew: true,
// expires in 1 year
visualExpiration: new Date(new Date().setFullYear(new Date().getFullYear() + 1)).toISOString(),
// 15 days before expiration
notifyAdminsAt: new Date(new Date().setDate(new Date().getDate() + 15)).toISOString(),
// 30 days before expiration
notifyUsersAt: new Date(new Date().setDate(new Date().getDate() + 30)).toISOString(),
trial: false,
offline: false,
createdAt: new Date().toISOString(),
grantedBy: {
method: 'manual',
seller: 'Rocket.Cat',
},
tags: [
{
name: 'Test',
color: 'blue',
},
],
};

this.validation = {
serverUrls: [
{
value: 'localhost:3000',
type: 'url',
},
],
serverVersions: [
{
value: '3.0.0',
},
],

serverUniqueId: '1234567890',
cloudWorkspaceId: '1234567890',

validPeriods: [
{
invalidBehavior: 'disable_modules',
modules: ['livechat-enterprise'],
validFrom: new Date(new Date().setFullYear(new Date().getFullYear() - 1)).toISOString(),
validUntil: new Date(new Date().setFullYear(new Date().getFullYear() + 1)).toISOString(),
},
],

statisticsReport: {
required: true,
allowedStaleInDays: 30,
},
};
}

public resetValidPeriods(): this {
this.validation.validPeriods = [];
return this;
}

public withValidPeriod(period: LicensePeriod): this {
this.validation.validPeriods.push(period);
return this;
}

public withGrantedTo(grantedTo: { name?: string; company?: string; email?: string }): this {
this.information.grantedTo = grantedTo;
return this;
}

grantedModules: {
module: LicenseModule;
}[];

limits: {
activeUsers?: LicenseLimit[];
guestUsers?: LicenseLimit[];
roomsPerGuest?: LicenseLimit<'prevent_action'>[];
privateApps?: LicenseLimit[];
marketplaceApps?: LicenseLimit[];
monthlyActiveContacts?: LicenseLimit[];
};

cloudMeta?: Record<string, any>;

public withServerUrls(urls: { value: string; type: 'url' | 'regex' | 'hash' }): this {
this.validation.serverUrls = this.validation.serverUrls ?? [];
this.validation.serverUrls.push(urls);
return this;
}

public withServerVersions(versions: { value: string }): this {
this.validation.serverVersions = this.validation.serverVersions ?? [];
this.validation.serverVersions.push(versions);
return this;
}

public withGratedModules(modules: LicenseModule[]): this {
this.grantedModules = this.grantedModules ?? [];
this.grantedModules.push(...modules.map((module) => ({ module })));
return this;
}

withNoGratedModules(modules: LicenseModule[]): this {
this.grantedModules = this.grantedModules ?? [];
this.grantedModules = this.grantedModules.filter(({ module }) => !modules.includes(module));
return this;
}

public withLimits<K extends keyof ILicenseV3['limits']>(key: K, value: ILicenseV3['limits'][K]): this {
this.limits = this.limits ?? {};
this.limits[key] = value;
return this;
}

public build(): ILicenseV3 {
return {
version: '3.0',
information: this.information,
validation: this.validation,
grantedModules: [...new Set(this.grantedModules)],
limits: {
activeUsers: [],
guestUsers: [],
roomsPerGuest: [],
privateApps: [],
marketplaceApps: [],
monthlyActiveContacts: [],
...this.limits,
},
cloudMeta: this.cloudMeta,
};
}

public sign(): Promise<string> {
return encrypt(this.build());
}
}

export const getReadyLicenseManager = async () => {
const license = new LicenseImp();
await license.setWorkspaceUrl('http://localhost:3000');
await license.setWorkspaceUrl('http://localhost:3000');

license.setLicenseLimitCounter('activeUsers', () => 0);
license.setLicenseLimitCounter('guestUsers', () => 0);
license.setLicenseLimitCounter('roomsPerGuest', async () => 0);
license.setLicenseLimitCounter('privateApps', () => 0);
license.setLicenseLimitCounter('marketplaceApps', () => 0);
license.setLicenseLimitCounter('monthlyActiveContacts', async () => 0);
return license;
};
66 changes: 66 additions & 0 deletions ee/packages/license/__tests__/emitter.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/**
* @jest-environment node
*/

import { MockedLicenseBuilder, getReadyLicenseManager } from './MockedLicenseBuilder';

describe('Event License behaviors', () => {
it('should call the module as they are enabled/disabled', async () => {
const license = await getReadyLicenseManager();
const validFn = jest.fn();
const invalidFn = jest.fn();

license.onValidFeature('livechat-enterprise', validFn);
license.onInvalidFeature('livechat-enterprise', invalidFn);

const mocked = await new MockedLicenseBuilder();
const oldToken = await mocked.sign();

const newToken = await mocked.withGratedModules(['livechat-enterprise']).sign();

// apply license
await expect(license.setLicense(oldToken)).resolves.toBe(true);
await expect(license.hasValidLicense()).toBe(true);

await expect(license.hasModule('livechat-enterprise')).toBe(false);

await expect(validFn).not.toBeCalled();
await expect(invalidFn).toBeCalledTimes(1);

// apply license containing livechat-enterprise module

validFn.mockClear();
invalidFn.mockClear();

await expect(license.setLicense(newToken)).resolves.toBe(true);
await expect(license.hasValidLicense()).toBe(true);
await expect(license.hasModule('livechat-enterprise')).toBe(true);

await expect(validFn).toBeCalledTimes(1);
await expect(invalidFn).toBeCalledTimes(0);

// apply the old license again

validFn.mockClear();
invalidFn.mockClear();
await expect(license.setLicense(oldToken)).resolves.toBe(true);
await expect(license.hasValidLicense()).toBe(true);
await expect(license.hasModule('livechat-enterprise')).toBe(false);
await expect(validFn).toBeCalledTimes(0);
await expect(invalidFn).toBeCalledTimes(1);
});

it('should call `onValidateLicense` when a valid license is applied', async () => {
const license = await getReadyLicenseManager();
const fn = jest.fn();

license.onValidateLicense(fn);

const mocked = await new MockedLicenseBuilder();
const token = await mocked.sign();

await expect(license.setLicense(token)).resolves.toBe(true);
await expect(license.hasValidLicense()).toBe(true);
await expect(fn).toBeCalledTimes(1);
});
});
43 changes: 29 additions & 14 deletions ee/packages/license/__tests__/setLicense.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,12 @@ import { LicenseImp } from '../src';
import { DuplicatedLicenseError } from '../src/errors/DuplicatedLicenseError';
import { InvalidLicenseError } from '../src/errors/InvalidLicenseError';
import { NotReadyForValidation } from '../src/errors/NotReadyForValidation';
import { MockedLicenseBuilder, getReadyLicenseManager } from './MockedLicenseBuilder';

// Same license used on ci tasks so no I didnt leak it
const VALID_LICENSE =
'WMa5i+/t/LZbYOj8u3XUkivRhWBtWO6ycUjaZoVAw2DxMfdyBIAa2gMMI4x7Z2BrTZIZhFEImfOxcXcgD0QbXHGBJaMI+eYG+eofnVWi2VA7RWbpvWTULgPFgyJ4UEFeCOzVjcBLTQbmMSam3u0RlekWJkfAO0KnmLtsaEYNNA2rz1U+CLI/CdNGfdqrBu5PZZbGkH0KEzyIZMaykOjzvX+C6vd7fRxh23HecwhkBbqE8eQsCBt2ad0qC4MoVXsDaSOmSzGW+aXjuXt/9zjvrLlsmWQTSlkrEHdNkdywm0UkGxqz3+CP99n0WggUBioUiChjMuNMoceWvDvmxYP9Ml2NpYU7SnfhjmMFyXOah8ofzv8w509Y7XODvQBz+iB4Co9YnF3vT96HDDQyAV5t4jATE+0t37EAXmwjTi3qqyP7DLGK/revl+mlcwJ5kS4zZBsm1E4519FkXQOZSyWRnPdjqvh4mCLqoispZ49wKvklDvjPxCSP9us6cVXLDg7NTJr/4pfxLPOkvv7qCgugDvlDx17bXpQFPSDxmpw66FLzvb5Id0dkWjOzrRYSXb0bFWoUQjtHFzmcpFkyVhOKrQ9zA9+Zm7vXmU9Y2l2dK79EloOuHMSYAqsPEag8GMW6vI/cT4iIjHGGDePKnD0HblvTEKzql11cfT/abf2IiaY=';

const getReadyLicenseManager = async () => {
const license = new LicenseImp();
await license.setWorkspaceUrl('http://localhost:3000');
await license.setWorkspaceUrl('http://localhost:3000');

license.setLicenseLimitCounter('activeUsers', () => 0);
license.setLicenseLimitCounter('guestUsers', () => 0);
license.setLicenseLimitCounter('roomsPerGuest', async () => 0);
license.setLicenseLimitCounter('privateApps', () => 0);
license.setLicenseLimitCounter('marketplaceApps', () => 0);
license.setLicenseLimitCounter('monthlyActiveContacts', async () => 0);
return license;
};

describe('License set license procedures', () => {
describe('Invalid formats', () => {
it('by default it should have no license', async () => {
Expand Down Expand Up @@ -85,4 +73,31 @@ describe('License set license procedures', () => {
await expect(license.hasValidLicense()).toBe(true);
});
});

describe('License V3', () => {
it('should return a valid license if the license is ready for validation', async () => {
const license = await getReadyLicenseManager();
const token = await new MockedLicenseBuilder().sign();

await expect(license.setLicense(token)).resolves.toBe(true);
await expect(license.hasValidLicense()).toBe(true);
});

it('should accept new licenses', async () => {
const license = await getReadyLicenseManager();
const mocked = await new MockedLicenseBuilder();
const oldToken = await mocked.sign();

const newToken = await mocked.withGratedModules(['livechat-enterprise']).sign();

await expect(license.setLicense(oldToken)).resolves.toBe(true);
await expect(license.hasValidLicense()).toBe(true);

await expect(license.hasModule('livechat-enterprise')).toBe(false);

await expect(license.setLicense(newToken)).resolves.toBe(true);
await expect(license.hasValidLicense()).toBe(true);
await expect(license.hasModule('livechat-enterprise')).toBe(true);
});
});
});
11 changes: 11 additions & 0 deletions ee/packages/license/babel.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"presets": ["@babel/preset-typescript"],
"plugins": [
[
"transform-inline-environment-variables",
{
"include": ["LICENSE_PUBLIC_KEY_V3"]
}
]
]
}
Loading