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

[7.x] Reduce license plugin api (#53489) #53564

Merged
merged 1 commit into from
Dec 19, 2019
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
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