Skip to content

Commit

Permalink
chore: license v3 invalidation (#30585)
Browse files Browse the repository at this point in the history
  • Loading branch information
ggazzo authored Oct 6, 2023
1 parent b21e9d3 commit 9a1a786
Show file tree
Hide file tree
Showing 12 changed files with 308 additions and 81 deletions.
2 changes: 1 addition & 1 deletion apps/meteor/ee/server/startup/maxRoomsPerGuest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ callbacks.add(
'beforeAddedToRoom',
async ({ user }) => {
if (user.roles?.includes('guest')) {
if (await License.shouldPreventAction('roomsPerGuest', { userId: user._id })) {
if (await License.shouldPreventAction('roomsPerGuest', 0, { userId: user._id })) {
throw new Meteor.Error('error-max-rooms-per-guest-reached', i18n.t('error-max-rooms-per-guest-reached'));
}
}
Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/ee/server/startup/seatsCap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ callbacks.add(
callbacks.add(
'beforeUserImport',
async ({ userCount }) => {
if (await License.shouldPreventAction('activeUsers', {}, userCount)) {
if (await License.shouldPreventAction('activeUsers', userCount)) {
throw new Meteor.Error('error-license-user-limit-reached', i18n.t('error-license-user-limit-reached'));
}
},
Expand Down
53 changes: 51 additions & 2 deletions ee/packages/license/__tests__/setLicense.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const VALID_LICENSE =

describe('License set license procedures', () => {
describe('Invalid formats', () => {
it('by default it should have no license', async () => {
it('should have no license by default', async () => {
const license = new LicenseImp();

expect(license.hasValidLicense()).toBe(false);
Expand All @@ -39,7 +39,7 @@ describe('License set license procedures', () => {
await expect(license.setLicense(VALID_LICENSE)).rejects.toThrow(DuplicatedLicenseError);
});

it('should keep a valid license if a new invalid license is applied', async () => {
it('should keep a valid license if a new invalid formatted license is applied', async () => {
const license = await getReadyLicenseManager();

await expect(license.setLicense(VALID_LICENSE)).resolves.toBe(true);
Expand Down Expand Up @@ -99,5 +99,54 @@ describe('License set license procedures', () => {
await expect(license.hasValidLicense()).toBe(true);
await expect(license.hasModule('livechat-enterprise')).toBe(true);
});

it('should call a validated event after set a valid license', async () => {
const license = await getReadyLicenseManager();
const validateCallback = jest.fn();
license.onValidateLicense(validateCallback);
await expect(license.setLicense(VALID_LICENSE)).resolves.toBe(true);
await expect(license.hasValidLicense()).toBe(true);
expect(validateCallback).toBeCalledTimes(1);
});

describe('License limits', () => {
describe('invalidate license', () => {
it('should trigger an invalidation event when a license with invalid limits is set after a valid one', async () => {
const invalidationCallback = jest.fn();

const licenseManager = await getReadyLicenseManager();
const mocked = await new MockedLicenseBuilder();
const oldToken = await mocked
.withLimits('activeUsers', [
{
max: 10,
behavior: 'invalidate_license',
},
])
.sign();

const newToken = await mocked
.withLimits('activeUsers', [
{
max: 1,
behavior: 'invalidate_license',
},
])
.sign();

licenseManager.onInvalidateLicense(invalidationCallback);

licenseManager.setLicenseLimitCounter('activeUsers', () => 5);

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

await expect(licenseManager.setLicense(newToken)).resolves.toBe(true);
await expect(licenseManager.hasValidLicense()).toBe(false);

await expect(invalidationCallback).toBeCalledTimes(1);
});
});
});
});
});
1 change: 1 addition & 0 deletions ee/packages/license/src/events/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export function moduleRemoved(this: LicenseManager, module: LicenseModule) {

export function behaviorTriggered(this: LicenseManager, options: BehaviorWithContext) {
const { behavior, reason, modules: _, ...rest } = options;

try {
this.emit(`behavior:${behavior}`, {
reason,
Expand Down
3 changes: 1 addition & 2 deletions ee/packages/license/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ interface License {
license: ILicenseV3 | undefined;
activeModules: LicenseModule[];
limits: Record<LicenseLimitKind, { value?: number; max: number }>;
inFairPolicy: boolean;
}>;

// Deprecated:
Expand Down Expand Up @@ -82,7 +81,7 @@ export class LicenseImp extends LicenseManager implements License {
getCurrentValueForLicenseLimit = getCurrentValueForLicenseLimit;

public async isLimitReached<T extends LicenseLimitKind>(action: T, context?: Partial<LimitContext<T>>): Promise<boolean> {
return this.shouldPreventAction(action, context, 0);
return this.shouldPreventAction(action, 0, context);
}

onValidFeature = onValidFeature;
Expand Down
153 changes: 153 additions & 0 deletions ee/packages/license/src/license.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,156 @@ it('should prevent if the counter is equal or over the limit', async () => {
licenseManager.setLicenseLimitCounter('activeUsers', () => 11);
await expect(licenseManager.shouldPreventAction('activeUsers')).resolves.toBe(true);
});

describe('Validate License Limits', () => {
describe('prevent_action behavior', () => {
describe('during the licensing apply', () => {
it('should not trigger the event even if the counter is over the limit', async () => {
const licenseManager = await getReadyLicenseManager();

const preventActionCallback = jest.fn();

const license = await new MockedLicenseBuilder().withLimits('activeUsers', [
{
max: 10,
behavior: 'prevent_action',
},
]);

licenseManager.onBehaviorTriggered('prevent_action', preventActionCallback);
licenseManager.setLicenseLimitCounter('activeUsers', () => 10);

await expect(licenseManager.setLicense(await license.sign())).resolves.toBe(true);

expect(preventActionCallback).toHaveBeenCalledTimes(0);
});
});
});
describe('fair usage behavior', () => {
it('should change the flag to true if the counter is equal or over the limit', async () => {
const licenseManager = await getReadyLicenseManager();

const fairUsageCallback = jest.fn();
const preventActionCallback = jest.fn();

licenseManager.onBehaviorTriggered('start_fair_policy', fairUsageCallback);
licenseManager.onBehaviorTriggered('prevent_action', preventActionCallback);

const license = await new MockedLicenseBuilder().withLimits('activeUsers', [
{
max: 10,
behavior: 'prevent_action',
},
{
max: 10,
behavior: 'start_fair_policy',
},
]);

await expect(licenseManager.setLicense(await license.sign())).resolves.toBe(true);

licenseManager.setLicenseLimitCounter('activeUsers', () => 5);
await expect(licenseManager.shouldPreventAction('activeUsers')).resolves.toBe(false);
await expect(licenseManager.shouldPreventAction('activeUsers')).resolves.toBe(false);
expect(fairUsageCallback).toHaveBeenCalledTimes(0);
expect(preventActionCallback).toHaveBeenCalledTimes(0);

preventActionCallback.mockClear();
fairUsageCallback.mockClear();
licenseManager.setLicenseLimitCounter('activeUsers', () => 10);
await expect(licenseManager.shouldPreventAction('activeUsers')).resolves.toBe(true);
expect(fairUsageCallback).toHaveBeenCalledTimes(0);
expect(preventActionCallback).toHaveBeenCalledTimes(1);

licenseManager.setLicenseLimitCounter('activeUsers', () => 11);
preventActionCallback.mockClear();
fairUsageCallback.mockClear();
await expect(licenseManager.shouldPreventAction('activeUsers')).resolves.toBe(true);
await expect(licenseManager.shouldPreventAction('activeUsers')).resolves.toBe(true);
await expect(licenseManager.shouldPreventAction('activeUsers')).resolves.toBe(true);
await expect(licenseManager.shouldPreventAction('activeUsers')).resolves.toBe(true);
expect(preventActionCallback).toHaveBeenCalledTimes(4);
expect(fairUsageCallback).toHaveBeenCalledTimes(4);
});
});

describe('invalidate_license behavior', () => {
it('should invalidate the license if the counter is over the limit', async () => {
const licenseManager = await getReadyLicenseManager();

const invalidateCallback = jest.fn();

const license = await new MockedLicenseBuilder().withLimits('activeUsers', [
{
max: 10,
behavior: 'prevent_action',
},
{
max: 10,
behavior: 'invalidate_license',
},
]);

licenseManager.on('invalidate', invalidateCallback);

await expect(licenseManager.setLicense(await license.sign())).resolves.toBe(true);

await expect(licenseManager.shouldPreventAction('activeUsers')).resolves.toBe(false);
await expect(licenseManager.hasValidLicense()).toBe(true);

licenseManager.setLicenseLimitCounter('activeUsers', () => 5);
await expect(licenseManager.shouldPreventAction('activeUsers')).resolves.toBe(false);
await expect(licenseManager.hasValidLicense()).toBe(true);

await licenseManager.setLicenseLimitCounter('activeUsers', () => 10);
await expect(licenseManager.shouldPreventAction('activeUsers')).resolves.toBe(true);
await expect(licenseManager.hasValidLicense()).toBe(true);
expect(invalidateCallback).toHaveBeenCalledTimes(0);

await licenseManager.setLicenseLimitCounter('activeUsers', () => 11);
await expect(licenseManager.shouldPreventAction('activeUsers')).resolves.toBe(true);
await expect(licenseManager.hasValidLicense()).toBe(false);
expect(invalidateCallback).toHaveBeenCalledTimes(1);
});
});

describe('prevent action for future limits', () => {
it('should prevent if the counter plus the extra value is equal or over the limit', async () => {
const licenseManager = await getReadyLicenseManager();

const license = await new MockedLicenseBuilder().withLimits('activeUsers', [
{
max: 10,
behavior: 'prevent_action',
},
]);

const fairUsageCallback = jest.fn();
const preventActionCallback = jest.fn();

licenseManager.onBehaviorTriggered('start_fair_policy', fairUsageCallback);
licenseManager.onBehaviorTriggered('prevent_action', preventActionCallback);

await expect(licenseManager.setLicense(await license.sign())).resolves.toBe(true);

licenseManager.setLicenseLimitCounter('activeUsers', () => 5);
await expect(licenseManager.shouldPreventAction('activeUsers')).resolves.toBe(false);
expect(fairUsageCallback).toHaveBeenCalledTimes(0);
expect(preventActionCallback).toHaveBeenCalledTimes(0);

for await (const extraCount of [1, 2, 3, 4, 5]) {
await expect(licenseManager.shouldPreventAction('activeUsers', extraCount)).resolves.toBe(false);
expect(fairUsageCallback).toHaveBeenCalledTimes(0);
expect(preventActionCallback).toHaveBeenCalledTimes(0);
}

/**
* if we are testing the current count 10 should prevent the action, if we are testing the future count 10 should not prevent the action but 11
*/

await expect(licenseManager.shouldPreventAction('activeUsers', 6)).resolves.toBe(true);
expect(fairUsageCallback).toHaveBeenCalledTimes(0);
expect(preventActionCallback).toHaveBeenCalledTimes(0);
});
});
});
Loading

0 comments on commit 9a1a786

Please sign in to comment.