import { Controller, Get } from '@nestjs/common'; import test from 'ava'; import Sinon from 'sinon'; import { AppModule } from '../app.module'; import { CANARY_CLIENT_VERSION_MAX_AGE_DAYS, ConfigFactory, hasNewerVersion, UseNamedGuard, } from '../base'; import { Public } from '../core/auth/guard'; import { VersionService } from '../core/version/service'; import { createTestingApp, TestingApp } from './utils'; @Public() @Controller('/guarded') class GuardedController { @UseNamedGuard('version') @Get('/test') test() { return 'test'; } } let app: TestingApp; let config: ConfigFactory; let version: VersionService; function checkVersion(enabled = true) { config.override({ client: { versionControl: { enabled, requiredVersion: '>=0.25.0', }, }, }); } function makeCanaryDateVersion(date: Date, build = '015') { return `${date.getUTCFullYear()}.${date.getUTCMonth() + 1}.${date.getUTCDate()}-canary.${build}`; } test.before(async () => { app = await createTestingApp({ imports: [AppModule], controllers: [GuardedController], }); version = app.get(VersionService, { strict: false }); config = app.get(ConfigFactory, { strict: false }); }); test.beforeEach(async () => { Sinon.reset(); checkVersion(true); }); test.after.always(async () => { await app.close(); }); test('should passthrough if version check is not enabled', async t => { checkVersion(false); const spy = Sinon.spy(version, 'checkVersion'); let res = await app.GET('/guarded/test'); t.is(res.status, 200); res = await app.GET('/guarded/test').set('x-affine-version', '0.20.0'); t.is(res.status, 200); res = await app.GET('/guarded/test').set('x-affine-version', 'invalid'); t.is(res.status, 200); t.true(spy.notCalled); spy.restore(); }); test('should enforce hard required version when version range is invalid', async t => { config.override({ client: { versionControl: { requiredVersion: 'invalid', }, }, }); let res = await app.GET('/guarded/test').set('x-affine-version', '0.25.0'); t.is(res.status, 200); res = await app.GET('/guarded/test').set('x-affine-version', 'invalid'); t.is(res.status, 403); t.is( res.body.message, 'Unsupported client with version [invalid], required version is [>=0.25.0].' ); }); test('should pass if client version is allowed', async t => { let res = await app.GET('/guarded/test').set('x-affine-version', '0.25.0'); t.is(res.status, 200); res = await app.GET('/guarded/test').set('x-affine-version', '0.26.0'); t.is(res.status, 200); config.override({ client: { versionControl: { requiredVersion: '>=0.25.0', }, }, }); res = await app.GET('/guarded/test').set('x-affine-version', '0.25.0'); t.is(res.status, 200); }); test('should fail if client version is not set or invalid', async t => { let res = await app.GET('/guarded/test'); t.is(res.status, 403); t.is( res.body.message, 'Unsupported client with version [unset_or_invalid], required version is [>=0.25.0].' ); res = await app.GET('/guarded/test').set('x-affine-version', 'invalid'); t.is(res.status, 403); t.is( res.body.message, 'Unsupported client with version [invalid], required version is [>=0.25.0].' ); }); test('should tell upgrade if client version is lower than allowed', async t => { config.override({ client: { versionControl: { requiredVersion: '>=0.26.0 <=0.27.0', }, }, }); let res = await app.GET('/guarded/test').set('x-affine-version', '0.25.0'); t.is(res.status, 403); t.is( res.body.message, 'Unsupported client with version [0.25.0], required version is [>=0.26.0 <=0.27.0].' ); }); test('should tell downgrade if client version is higher than allowed', async t => { config.override({ client: { versionControl: { requiredVersion: '>=0.25.0 <=0.26.0', }, }, }); let res = await app.GET('/guarded/test').set('x-affine-version', '0.27.0'); t.is(res.status, 403); t.is( res.body.message, 'Unsupported client with version [0.27.0], required version is [>=0.25.0 <=0.26.0].' ); }); test('should test prerelease version', async t => { config.override({ client: { versionControl: { requiredVersion: '>=0.25.0', }, }, }); let res = await app .GET('/guarded/test') .set('x-affine-version', '0.25.0-canary.1'); // 0.25.0-canary.1 is lower than 0.25.0 obviously t.is(res.status, 403); res = await app .GET('/guarded/test') .set('x-affine-version', '0.26.0-canary.1'); t.is(res.status, 200); res = await app.GET('/guarded/test').set('x-affine-version', '0.26.0-beta.2'); t.is(res.status, 200); }); test('should allow recent canary date version in canary namespace', async t => { const prevNamespace = env.NAMESPACE; // @ts-expect-error test env.NAMESPACE = 'dev'; try { const res = await app .GET('/guarded/test') .set('x-affine-version', makeCanaryDateVersion(new Date(), '015')); t.is(res.status, 200); } finally { // @ts-expect-error test env.NAMESPACE = prevNamespace; } }); test('should reject old canary date version in canary namespace', async t => { const prevNamespace = env.NAMESPACE; // @ts-expect-error test env.NAMESPACE = 'dev'; try { const old = new Date( Date.now() - (CANARY_CLIENT_VERSION_MAX_AGE_DAYS + 1) * 24 * 60 * 60 * 1000 ); const oldVersion = makeCanaryDateVersion(old, '015'); const res = await app .GET('/guarded/test') .set('x-affine-version', oldVersion); t.is(res.status, 403); t.is( res.body.message, `Unsupported client with version [${oldVersion}], required version is [canary (within 2 months)].` ); } finally { // @ts-expect-error test env.NAMESPACE = prevNamespace; } }); test('should compare release versions for available upgrades', t => { t.false(hasNewerVersion('0.26.5', '0.26.4')); t.false(hasNewerVersion('0.26.5', '0.26.5')); t.true(hasNewerVersion('0.26.5', '0.26.6')); t.true(hasNewerVersion('0.26.5', '0.26.6-beta.1')); t.false(hasNewerVersion('0.26.6-beta.2', '0.26.6-beta.1')); });