Skip to content

Commit

Permalink
feat: License v3 store prevent action results and just fire it if cha…
Browse files Browse the repository at this point in the history
…nges (#30692)
  • Loading branch information
ggazzo authored Oct 20, 2023
1 parent febc716 commit 832df7f
Show file tree
Hide file tree
Showing 8 changed files with 249 additions and 6 deletions.
176 changes: 176 additions & 0 deletions ee/packages/license/__tests__/emitter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,4 +116,180 @@ describe('Event License behaviors', () => {
await expect(fn).toBeCalledWith(undefined);
});
});

/**
* this is only called when the prevent_action behavior is triggered for the first time
* it will not be called again until the behavior is toggled
*/
describe('Toggled behaviors', () => {
it('should emit `behaviorToggled:prevent_action` event when the limit is reached once but `behavior:prevent_action` twice', async () => {
const licenseManager = await getReadyLicenseManager();
const fn = jest.fn();
const toggleFn = jest.fn();

licenseManager.onBehaviorTriggered('prevent_action', fn);

licenseManager.onBehaviorToggled('prevent_action', toggleFn);

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

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

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

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

await expect(fn).toBeCalledTimes(2);
await expect(toggleFn).toBeCalledTimes(1);

await expect(fn).toBeCalledWith({
reason: 'limit',
limit: 'activeUsers',
});
});

it('should emit `behaviorToggled:allow_action` event when the limit is not reached once but `behavior:allow_action` twice', async () => {
const licenseManager = await getReadyLicenseManager();
const fn = jest.fn();
const toggleFn = jest.fn();

licenseManager.onBehaviorTriggered('allow_action', fn);

licenseManager.onBehaviorToggled('allow_action', toggleFn);

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

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

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

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

await expect(fn).toBeCalledTimes(2);
await expect(toggleFn).toBeCalledTimes(1);

await expect(fn).toBeCalledWith({
reason: 'limit',
limit: 'activeUsers',
});
});

it('should emit `behaviorToggled:prevent_action` and `behaviorToggled:allow_action` events when the shouldPreventAction function changes the result', async () => {
const licenseManager = await getReadyLicenseManager();
const preventFn = jest.fn();
const preventToggleFn = jest.fn();
const allowFn = jest.fn();
const allowToggleFn = jest.fn();

licenseManager.onBehaviorTriggered('prevent_action', preventFn);
licenseManager.onBehaviorToggled('prevent_action', preventToggleFn);
licenseManager.onBehaviorTriggered('allow_action', allowFn);
licenseManager.onBehaviorToggled('allow_action', allowToggleFn);

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

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

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

await expect(licenseManager.shouldPreventAction('activeUsers')).resolves.toBe(false);
expect(preventFn).toBeCalledTimes(0);
expect(preventToggleFn).toBeCalledTimes(0);
expect(allowFn).toBeCalledTimes(1);
expect(allowToggleFn).toBeCalledTimes(1);

preventFn.mockClear();
preventToggleFn.mockClear();
allowFn.mockClear();
allowToggleFn.mockClear();
await expect(licenseManager.shouldPreventAction('activeUsers')).resolves.toBe(false);
expect(preventFn).toBeCalledTimes(0);
expect(preventToggleFn).toBeCalledTimes(0);
expect(allowFn).toBeCalledTimes(1);
expect(allowToggleFn).toBeCalledTimes(0);

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

preventFn.mockClear();
preventToggleFn.mockClear();
allowFn.mockClear();
allowToggleFn.mockClear();
await expect(licenseManager.shouldPreventAction('activeUsers')).resolves.toBe(true);
expect(preventFn).toBeCalledTimes(1);
expect(preventToggleFn).toBeCalledTimes(1);
expect(allowFn).toBeCalledTimes(0);
expect(allowToggleFn).toBeCalledTimes(0);

preventFn.mockClear();
preventToggleFn.mockClear();
allowFn.mockClear();
allowToggleFn.mockClear();
await expect(licenseManager.shouldPreventAction('activeUsers')).resolves.toBe(true);
expect(preventFn).toBeCalledTimes(1);
expect(preventToggleFn).toBeCalledTimes(0);
expect(allowFn).toBeCalledTimes(0);
expect(allowToggleFn).toBeCalledTimes(0);

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

preventFn.mockClear();
preventToggleFn.mockClear();
allowFn.mockClear();
allowToggleFn.mockClear();
await expect(licenseManager.shouldPreventAction('activeUsers')).resolves.toBe(false);
expect(preventFn).toBeCalledTimes(0);
expect(preventToggleFn).toBeCalledTimes(0);
expect(allowFn).toBeCalledTimes(1);
expect(allowToggleFn).toBeCalledTimes(1);
});
});

describe('Allow actions', () => {
it('should emit `behavior:allow_action` event when the limit is not reached', async () => {
const licenseManager = await getReadyLicenseManager();
const fn = jest.fn();
const preventFn = jest.fn();

licenseManager.onBehaviorTriggered('allow_action', fn);
licenseManager.onBehaviorTriggered('prevent_action', preventFn);

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

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

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

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

await expect(fn).toBeCalledTimes(1);
await expect(preventFn).toBeCalledTimes(0);

await expect(fn).toBeCalledWith({
reason: 'limit',
limit: 'activeUsers',
});
});
});
});
8 changes: 7 additions & 1 deletion ee/packages/license/src/definition/LicenseBehavior.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import type { LicenseLimitKind } from './ILicenseV3';
import type { LicenseModule } from './LicenseModule';

export type LicenseBehavior = 'invalidate_license' | 'start_fair_policy' | 'prevent_action' | 'prevent_installation' | 'disable_modules';
export type LicenseBehavior =
| 'invalidate_license'
| 'start_fair_policy'
| 'prevent_action'
| 'allow_action'
| 'prevent_installation'
| 'disable_modules';

export type BehaviorWithContext =
| {
Expand Down
1 change: 1 addition & 0 deletions ee/packages/license/src/definition/LicenseInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { LicenseModule } from './LicenseModule';
export type LicenseInfo = {
license?: ILicenseV3;
activeModules: LicenseModule[];
preventedActions: Record<LicenseLimitKind, boolean>;
limits: Record<LicenseLimitKind, { value?: number; max: number }>;
tags: ILicenseTag[];
trial: boolean;
Expand Down
6 changes: 6 additions & 0 deletions ee/packages/license/src/definition/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,15 @@ import type { LicenseModule } from './LicenseModule';

type ModuleValidation = Record<`${'invalid' | 'valid'}:${LicenseModule}`, undefined>;
type BehaviorTriggered = Record<`behavior:${LicenseBehavior}`, { reason: BehaviorWithContext['reason']; limit?: LicenseLimitKind }>;
type BehaviorTriggeredToggled = Record<
`behaviorToggled:${LicenseBehavior}`,
{ reason: BehaviorWithContext['reason']; limit?: LicenseLimitKind }
>;

type LimitReached = Record<`limitReached:${LicenseLimitKind}`, undefined>;

export type LicenseEvents = ModuleValidation &
BehaviorTriggeredToggled &
BehaviorTriggered &
LimitReached & {
validate: undefined;
Expand Down
15 changes: 14 additions & 1 deletion ee/packages/license/src/events/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export function behaviorTriggered(this: LicenseManager, options: BehaviorWithCon
logger.error({ msg: 'Error running behavior triggered event', error });
}

if (behavior !== 'prevent_action') {
if (!['prevent_action'].includes(behavior)) {
return;
}

Expand All @@ -48,6 +48,19 @@ export function behaviorTriggered(this: LicenseManager, options: BehaviorWithCon
}
}

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

try {
this.emit(`behaviorToggled:${behavior}`, {
reason,
...rest,
});
} catch (error) {
logger.error({ msg: 'Error running behavior triggered event', error });
}
}

export function licenseValidated(this: LicenseManager) {
try {
this.emit('validate');
Expand Down
8 changes: 8 additions & 0 deletions ee/packages/license/src/events/listeners.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,14 @@ export function onBehaviorTriggered(
this.on(`behavior:${behavior}`, cb);
}

export function onBehaviorToggled(
this: LicenseManager,
behavior: Exclude<LicenseBehavior, 'prevent_installation'>,
cb: (data: { reason: BehaviorWithContext['reason']; limit?: LicenseLimitKind }) => void,
) {
this.on(`behaviorToggled:${behavior}`, cb);
}

export function onLimitReached(this: LicenseManager, limitKind: LicenseLimitKind, cb: () => void) {
this.on(`limitReached:${limitKind}`, cb);
}
3 changes: 3 additions & 0 deletions ee/packages/license/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { LimitContext } from './definition/LimitContext';
import { getAppsConfig, getMaxActiveUsers, getUnmodifiedLicenseAndModules } from './deprecated';
import { onLicense } from './events/deprecated';
import {
onBehaviorToggled,
onBehaviorTriggered,
onInvalidFeature,
onInvalidateLicense,
Expand Down Expand Up @@ -97,6 +98,8 @@ export class LicenseImp extends LicenseManager implements License {

onBehaviorTriggered = onBehaviorTriggered;

onBehaviorToggled = onBehaviorToggled;

// Deprecated:
onLicense = onLicense;

Expand Down
38 changes: 34 additions & 4 deletions ee/packages/license/src/license.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import type { LicenseEvents } from './definition/events';
import { DuplicatedLicenseError } from './errors/DuplicatedLicenseError';
import { InvalidLicenseError } from './errors/InvalidLicenseError';
import { NotReadyForValidation } from './errors/NotReadyForValidation';
import { behaviorTriggered, licenseInvalidated, licenseValidated } from './events/emitter';
import { behaviorTriggered, behaviorTriggeredToggled, licenseInvalidated, licenseValidated } from './events/emitter';
import { logger } from './logger';
import { getModules, invalidateAll, replaceModules } from './modules';
import { applyPendingLicense, clearPendingLicense, hasPendingLicense, isPendingLicense, setPendingLicense } from './pendingLicense';
Expand Down Expand Up @@ -49,6 +49,8 @@ export class LicenseManager extends Emitter<LicenseEvents> {

private _lockedLicense: string | undefined;

public shouldPreventActionResults = new Map<LicenseLimitKind, boolean>();

constructor() {
super();

Expand Down Expand Up @@ -106,6 +108,8 @@ export class LicenseManager extends Emitter<LicenseEvents> {
this._unmodifiedLicense = undefined;
this._valid = false;
this._lockedLicense = undefined;

this.shouldPreventActionResults.clear();
clearPendingLicense.call(this);
}

Expand Down Expand Up @@ -243,6 +247,12 @@ export class LicenseManager extends Emitter<LicenseEvents> {
}
}

private triggerBehaviorEventsToggled(validationResult: BehaviorWithContext[]): void {
for (const { ...options } of validationResult) {
behaviorTriggeredToggled.call(this, { ...options });
}
}

public hasValidLicense(): boolean {
return Boolean(this.getLicense());
}
Expand Down Expand Up @@ -279,18 +289,37 @@ export class LicenseManager extends Emitter<LicenseEvents> {

const validationResult = await runValidation.call(this, license, options);

const shouldPreventAction = isBehaviorsInResult(validationResult, ['prevent_action']);

// extra values should not call events since they are not actually reaching the limit just checking if they would
if (extraCount) {
return isBehaviorsInResult(validationResult, ['prevent_action']);
return shouldPreventAction;
}

if (isBehaviorsInResult(validationResult, ['invalidate_license', 'disable_modules', 'start_fair_policy'])) {
await this.revalidateLicense();
}

this.triggerBehaviorEvents(filterBehaviorsResult(validationResult, ['prevent_action']));
const eventsToEmit = shouldPreventAction
? filterBehaviorsResult(validationResult, ['prevent_action'])
: [
{
behavior: 'allow_action',
modules: [],
reason: 'limit',
limit: action,
} as BehaviorWithContext,
];

if (this.shouldPreventActionResults.get(action) !== shouldPreventAction) {
this.shouldPreventActionResults.set(action, shouldPreventAction);

this.triggerBehaviorEventsToggled(eventsToEmit);
}

this.triggerBehaviorEvents(eventsToEmit);

return isBehaviorsInResult(validationResult, ['prevent_action']);
return shouldPreventAction;
}

public async getInfo({
Expand Down Expand Up @@ -331,6 +360,7 @@ export class LicenseManager extends Emitter<LicenseEvents> {
return {
license: (includeLicense && license) || undefined,
activeModules,
preventedActions: Object.fromEntries(this.shouldPreventActionResults.entries()) as Record<LicenseLimitKind, boolean>,
limits: limits as Record<LicenseLimitKind, { max: number; value: number }>,
tags: license?.information.tags || [],
trial: Boolean(license?.information.trial),
Expand Down

0 comments on commit 832df7f

Please sign in to comment.