Skip to content

Commit

Permalink
Reduce license plugin api (elastic#53489)
Browse files Browse the repository at this point in the history
* inOneOf --> hasAtLeast. to follow to licensing hierarchical model

* adopt licensing tests

* add license mock and use it in the tests

* adopt security plugin to hasAtLeast and licensing mocks

* adopt uptime to hasAtLeast

* update readme

* add test for unknown license

* fix import in js test

* fix security plugin merge conflict

* Update x-pack/plugins/security/common/licensing/license_service.ts

Co-Authored-By: Larry Gregory <lgregorydev@gmail.com>

* Update x-pack/plugins/licensing/common/types.ts

Co-Authored-By: Josh Dover <me@joshdover.com>

* simplify tests

* remove unused import
  • Loading branch information
mshustov committed Dec 19, 2019
1 parent 3b0e778 commit 5aeec83
Show file tree
Hide file tree
Showing 18 changed files with 155 additions and 151 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,31 +8,31 @@ import { ILicense } from '../../../../../../../plugins/licensing/server';
import { licenseCheck } from '../license';

describe('license check', () => {
let mockLicense: Pick<ILicense, 'isActive' | 'isOneOf'>;
let mockLicense: Pick<ILicense, 'isActive' | 'hasAtLeast'>;

it('throws for null license', () => {
expect(licenseCheck(null)).toMatchSnapshot();
});

it('throws for unsupported license type', () => {
mockLicense = {
isOneOf: jest.fn().mockReturnValue(false),
hasAtLeast: jest.fn().mockReturnValue(false),
isActive: false,
};
expect(licenseCheck(mockLicense)).toMatchSnapshot();
});

it('throws for inactive license', () => {
mockLicense = {
isOneOf: jest.fn().mockReturnValue(true),
hasAtLeast: jest.fn().mockReturnValue(true),
isActive: false,
};
expect(licenseCheck(mockLicense)).toMatchSnapshot();
});

it('returns result for a valid license', () => {
mockLicense = {
isOneOf: jest.fn().mockReturnValue(true),
hasAtLeast: jest.fn().mockReturnValue(true),
isActive: true,
};
expect(licenseCheck(mockLicense)).toMatchSnapshot();
Expand Down
4 changes: 2 additions & 2 deletions x-pack/legacy/plugins/uptime/server/lib/domains/license.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export interface UMLicenseStatusResponse {
message?: string;
}
export type UMLicenseCheck = (
license: Pick<ILicense, 'isActive' | 'isOneOf'> | null
license: Pick<ILicense, 'isActive' | 'hasAtLeast'> | null
) => UMLicenseStatusResponse;

export const licenseCheck: UMLicenseCheck = license => {
Expand All @@ -21,7 +21,7 @@ export const licenseCheck: UMLicenseCheck = license => {
statusCode: 400,
};
}
if (!license.isOneOf(['basic', 'standard', 'gold', 'platinum', 'enterprise', 'trial'])) {
if (!license.hasAtLeast('basic')) {
return {
message: 'License not supported',
statusCode: 401,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { licensingMock } from '../../../../../plugins/licensing/server/licensing.mocks';
import { licensingMock } from '../../../../../plugins/licensing/server/mocks';
import { XPackInfoLicense } from './xpack_info_license';

function getXPackInfoLicense(getRawLicense) {
Expand Down
5 changes: 3 additions & 2 deletions x-pack/plugins/licensing/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ chrome.navLinks.update('myPlugin', {
"requiredPlugins": ["licensing"],

// my_plugin/server/plugin.ts
import { LicensingPluginSetup, LICENSE_CHECK_STATE } from '../licensing'
import { LicensingPluginSetup, LICENSE_CHECK_STATE } from '../licensing/server'

interface SetupDeps {
licensing: LicensingPluginSetup;
Expand All @@ -77,7 +77,8 @@ class MyPlugin {
}
}

// my_plugin/client/plugin.ts
// my_plugin/public/plugin.ts
import { LicensingPluginSetup, LICENSE_CHECK_STATE } from '../licensing/public'
class MyPlugin {
setup(core: CoreSetup, deps: SetupDeps) {
deps.licensing.license$.subscribe(license => {
Expand Down
57 changes: 25 additions & 32 deletions x-pack/plugins/licensing/common/license.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@

import { License } from './license';
import { LICENSE_CHECK_STATE } from './types';
import { licenseMock } from './licensing.mocks';
import { licenseMock } from './licensing.mock';

describe('License', () => {
const basicLicense = licenseMock.create();
const basicExpiredLicense = licenseMock.create({ license: { status: 'expired' } });
const goldLicense = licenseMock.create({ license: { type: 'gold' } });
const enterpriseLicense = licenseMock.create({ license: { type: 'enterprise' } });
const basicLicense = licenseMock.createLicense();
const basicExpiredLicense = licenseMock.createLicense({ license: { status: 'expired' } });
const goldLicense = licenseMock.createLicense({ license: { type: 'gold' } });
const enterpriseLicense = licenseMock.createLicense({ license: { type: 'enterprise' } });

const errorMessage = 'unavailable';
const errorLicense = new License({ error: errorMessage, signature: '' });
Expand Down Expand Up @@ -50,34 +50,23 @@ describe('License', () => {
expect(unavailableLicense.isActive).toBe(false);
});

it('isBasic', () => {
expect(basicLicense.isBasic).toBe(true);
expect(goldLicense.isBasic).toBe(false);
expect(enterpriseLicense.isBasic).toBe(false);
expect(errorLicense.isBasic).toBe(false);
expect(unavailableLicense.isBasic).toBe(false);
});
it('hasAtLeast', () => {
expect(basicLicense.hasAtLeast('platinum')).toBe(false);
expect(basicLicense.hasAtLeast('gold')).toBe(false);
expect(basicLicense.hasAtLeast('basic')).toBe(true);

it('isNotBasic', () => {
expect(basicLicense.isNotBasic).toBe(false);
expect(goldLicense.isNotBasic).toBe(true);
expect(enterpriseLicense.isNotBasic).toBe(true);
expect(errorLicense.isNotBasic).toBe(false);
expect(unavailableLicense.isNotBasic).toBe(false);
});
expect(errorLicense.hasAtLeast('basic')).toBe(false);

it('isOneOf', () => {
expect(basicLicense.isOneOf('platinum')).toBe(false);
expect(basicLicense.isOneOf(['platinum'])).toBe(false);
expect(basicLicense.isOneOf(['gold', 'platinum'])).toBe(false);
expect(basicLicense.isOneOf(['platinum', 'gold'])).toBe(false);
expect(basicLicense.isOneOf(['basic', 'gold'])).toBe(true);
expect(basicLicense.isOneOf(['basic'])).toBe(true);
expect(basicLicense.isOneOf('basic')).toBe(true);
expect(unavailableLicense.hasAtLeast('basic')).toBe(false);

expect(errorLicense.isOneOf(['basic', 'gold', 'platinum'])).toBe(false);
expect(goldLicense.hasAtLeast('basic')).toBe(true);
expect(goldLicense.hasAtLeast('gold')).toBe(true);
expect(goldLicense.hasAtLeast('platinum')).toBe(false);

expect(unavailableLicense.isOneOf(['basic', 'gold', 'platinum'])).toBe(false);
expect(enterpriseLicense.hasAtLeast('basic')).toBe(true);
expect(enterpriseLicense.hasAtLeast('platinum')).toBe(true);
expect(enterpriseLicense.hasAtLeast('enterprise')).toBe(true);
expect(enterpriseLicense.hasAtLeast('trial')).toBe(false);
});

it('getUnavailableReason', () => {
Expand Down Expand Up @@ -115,9 +104,13 @@ describe('License', () => {
});

it('throws in case of unknown license type', () => {
expect(
() => basicLicense.check('ccr', 'any' as any).state
).toThrowErrorMatchingInlineSnapshot(`"\\"any\\" is not a valid license type"`);
expect(() => basicLicense.check('ccr', 'any' as any)).toThrowErrorMatchingInlineSnapshot(
`"\\"any\\" is not a valid license type"`
);

expect(() => basicLicense.hasAtLeast('any' as any)).toThrowErrorMatchingInlineSnapshot(
`"\\"any\\" is not a valid license type"`
);
});
});
});
27 changes: 9 additions & 18 deletions x-pack/plugins/licensing/common/license.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ export class License implements ILicense {
public readonly error?: string;
public readonly isActive: boolean;
public readonly isAvailable: boolean;
public readonly isBasic: boolean;
public readonly isNotBasic: boolean;

public readonly uid?: string;
public readonly status?: LicenseStatus;
Expand Down Expand Up @@ -70,8 +68,6 @@ export class License implements ILicense {
}

this.isActive = this.status === 'active';
this.isBasic = this.isActive && this.type === 'basic';
this.isNotBasic = this.isActive && this.type !== 'basic';
}

toJSON() {
Expand All @@ -89,23 +85,20 @@ export class License implements ILicense {
}
}

isOneOf(candidateLicenses: LicenseType | LicenseType[]) {
if (!this.type) {
hasAtLeast(minimumLicenseRequired: LicenseType) {
const type = this.type;
if (!type) {
return false;
}

if (!Array.isArray(candidateLicenses)) {
candidateLicenses = [candidateLicenses];
if (!(minimumLicenseRequired in LICENSE_TYPE)) {
throw new Error(`"${minimumLicenseRequired}" is not a valid license type`);
}

return candidateLicenses.includes(this.type);
return LICENSE_TYPE[minimumLicenseRequired] <= LICENSE_TYPE[type];
}

check(pluginName: string, minimumLicenseRequired: LicenseType) {
if (!(minimumLicenseRequired in LICENSE_TYPE)) {
throw new Error(`"${minimumLicenseRequired}" is not a valid license type`);
}

if (!this.isAvailable) {
return {
state: LICENSE_CHECK_STATE.Unavailable,
Expand All @@ -117,26 +110,24 @@ export class License implements ILicense {
};
}

const type = this.type!;

if (!this.isActive) {
return {
state: LICENSE_CHECK_STATE.Expired,
message: i18n.translate('xpack.licensing.check.errorExpiredMessage', {
defaultMessage:
'You cannot use {pluginName} because your {licenseType} license has expired.',
values: { licenseType: type, pluginName },
values: { licenseType: this.type!, pluginName },
}),
};
}

if (LICENSE_TYPE[type] < LICENSE_TYPE[minimumLicenseRequired]) {
if (!this.hasAtLeast(minimumLicenseRequired)) {
return {
state: LICENSE_CHECK_STATE.Invalid,
message: i18n.translate('xpack.licensing.check.errorUnsupportedMessage', {
defaultMessage:
'Your {licenseType} license does not support {pluginName}. Please upgrade your license.',
values: { licenseType: type, pluginName },
values: { licenseType: this.type!, pluginName },
}),
};
}
Expand Down
38 changes: 16 additions & 22 deletions x-pack/plugins/licensing/common/license_update.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,19 @@
import { Subject } from 'rxjs';
import { take, toArray } from 'rxjs/operators';

import { ILicense, LicenseType } from './types';
import { ILicense } from './types';
import { createLicenseUpdate } from './license_update';
import { licenseMock } from './licensing.mocks';
import { licenseMock } from './licensing.mock';

const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
const stop$ = new Subject();
describe('licensing update', () => {
it('loads updates when triggered', async () => {
const types: LicenseType[] = ['basic', 'gold'];

const trigger$ = new Subject();
const fetcher = jest
.fn()
.mockImplementation(() =>
Promise.resolve(licenseMock.create({ license: { type: types.shift() } }))
);
.mockResolvedValueOnce(licenseMock.createLicense({ license: { type: 'basic' } }))
.mockResolvedValueOnce(licenseMock.createLicense({ license: { type: 'gold' } }));

const { license$ } = createLicenseUpdate(trigger$, stop$, fetcher);

Expand All @@ -38,8 +35,8 @@ describe('licensing update', () => {
});

it('starts with initial value if presents', async () => {
const initialLicense = licenseMock.create({ license: { type: 'platinum' } });
const fetchedLicense = licenseMock.create({ license: { type: 'gold' } });
const initialLicense = licenseMock.createLicense({ license: { type: 'platinum' } });
const fetchedLicense = licenseMock.createLicense({ license: { type: 'gold' } });
const trigger$ = new Subject();

const fetcher = jest.fn().mockResolvedValue(fetchedLicense);
Expand All @@ -55,14 +52,11 @@ describe('licensing update', () => {
it('does not emit if license has not changed', async () => {
const trigger$ = new Subject();

let i = 0;
const fetcher = jest
.fn()
.mockImplementation(() =>
Promise.resolve(
++i < 3 ? licenseMock.create() : licenseMock.create({ license: { type: 'gold' } })
)
);
.mockResolvedValueOnce(licenseMock.createLicense())
.mockResolvedValueOnce(licenseMock.createLicense())
.mockResolvedValueOnce(licenseMock.createLicense({ license: { type: 'gold' } }));

const { license$ } = createLicenseUpdate(trigger$, stop$, fetcher);
trigger$.next();
Expand All @@ -83,7 +77,7 @@ describe('licensing update', () => {
it('new subscriptions does not force re-fetch', async () => {
const trigger$ = new Subject();

const fetcher = jest.fn().mockResolvedValue(licenseMock.create());
const fetcher = jest.fn().mockResolvedValue(licenseMock.createLicense());

const { license$ } = createLicenseUpdate(trigger$, stop$, fetcher);

Expand All @@ -103,9 +97,9 @@ describe('licensing update', () => {
new Promise(resolve => {
if (firstCall) {
firstCall = false;
setTimeout(() => resolve(licenseMock.create()), delayMs);
setTimeout(() => resolve(licenseMock.createLicense()), delayMs);
} else {
resolve(licenseMock.create({ license: { type: 'gold' } }));
resolve(licenseMock.createLicense({ license: { type: 'gold' } }));
}
})
);
Expand All @@ -126,7 +120,7 @@ describe('licensing update', () => {

it('completes license$ stream when stop$ is triggered', () => {
const trigger$ = new Subject();
const fetcher = jest.fn().mockResolvedValue(licenseMock.create());
const fetcher = jest.fn().mockResolvedValue(licenseMock.createLicense());

const { license$ } = createLicenseUpdate(trigger$, stop$, fetcher);
let completed = false;
Expand All @@ -138,7 +132,7 @@ describe('licensing update', () => {

it('stops fetching when stop$ is triggered', () => {
const trigger$ = new Subject();
const fetcher = jest.fn().mockResolvedValue(licenseMock.create());
const fetcher = jest.fn().mockResolvedValue(licenseMock.createLicense());

const { license$ } = createLicenseUpdate(trigger$, stop$, fetcher);
const values: ILicense[] = [];
Expand All @@ -152,8 +146,8 @@ describe('licensing update', () => {

it('refreshManually guarantees license fetching', async () => {
const trigger$ = new Subject();
const firstLicense = licenseMock.create({ license: { uid: 'first', type: 'basic' } });
const secondLicense = licenseMock.create({ license: { uid: 'second', type: 'gold' } });
const firstLicense = licenseMock.createLicense({ license: { uid: 'first', type: 'basic' } });
const secondLicense = licenseMock.createLicense({ license: { uid: 'second', type: 'gold' } });

const fetcher = jest
.fn()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { PublicLicense, PublicFeatures } from './types';
import { ILicense, PublicLicense, PublicFeatures, LICENSE_CHECK_STATE } from './types';
import { License } from './license';

function createLicense({
Expand Down Expand Up @@ -40,6 +40,22 @@ function createLicense({
});
}

const createLicenseMock = () => {
const mock: jest.Mocked<ILicense> = {
isActive: true,
isAvailable: true,
signature: '',
toJSON: jest.fn(),
getUnavailableReason: jest.fn(),
getFeature: jest.fn(),
check: jest.fn(),
hasAtLeast: jest.fn(),
};
mock.check.mockReturnValue({ state: LICENSE_CHECK_STATE.Valid });
mock.hasAtLeast.mockReturnValue(true);
return mock;
};
export const licenseMock = {
create: createLicense,
createLicense,
createLicenseMock,
};
Loading

0 comments on commit 5aeec83

Please sign in to comment.