252 lines
9.8 KiB
TypeScript
252 lines
9.8 KiB
TypeScript
import type { TestingModule } from '@nestjs/testing';
|
|
import { Test } from '@nestjs/testing';
|
|
import type { INestApplication } from '@nestjs/common';
|
|
import supertest from 'supertest';
|
|
import {
|
|
OpenFeatureController,
|
|
OpenFeatureContextScopedController,
|
|
OpenFeatureRequireFlagsEnabledController,
|
|
OpenFeatureTestService,
|
|
} from './test-app';
|
|
import { exampleContextFactory, getOpenFeatureDefaultTestModule } from './fixtures';
|
|
import { OpenFeatureModule } from '../src';
|
|
import { defaultProvider, providers } from './fixtures';
|
|
|
|
describe('OpenFeature SDK', () => {
|
|
describe('With global context interceptor', () => {
|
|
let moduleRef: TestingModule;
|
|
let app: INestApplication;
|
|
|
|
beforeAll(async () => {
|
|
moduleRef = await Test.createTestingModule({
|
|
imports: [getOpenFeatureDefaultTestModule()],
|
|
providers: [OpenFeatureTestService],
|
|
controllers: [OpenFeatureController, OpenFeatureRequireFlagsEnabledController],
|
|
}).compile();
|
|
app = moduleRef.createNestApplication();
|
|
app = await app.init();
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await moduleRef.close();
|
|
});
|
|
|
|
describe('openfeature client decorator', () => {
|
|
it('should inject the correct open feature clients', async () => {
|
|
const testService = moduleRef.get(OpenFeatureTestService);
|
|
expect(testService.defaultClient).toBeDefined();
|
|
expect(await testService.defaultClient.getStringValue('testStringFlag', 'wrong-value')).toEqual(
|
|
'expected-string-value-default',
|
|
);
|
|
|
|
expect(testService.domainScopedClient).toBeDefined();
|
|
expect(await testService.domainScopedClient.getStringValue('testStringFlag', 'wrong-value')).toEqual(
|
|
'expected-string-value-scoped',
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('feature flag decorators', () => {
|
|
it('should inject the correct boolean feature flag evaluation details', async () => {
|
|
const testService = app.get(OpenFeatureTestService);
|
|
const testServiceSpy = jest.spyOn(testService, 'serviceMethod');
|
|
|
|
await supertest(app.getHttpServer()).get('/boolean').expect(200).expect('true');
|
|
|
|
expect(testServiceSpy).toHaveBeenCalledWith({
|
|
flagKey: 'testBooleanFlag',
|
|
flagMetadata: {},
|
|
reason: 'STATIC',
|
|
value: true,
|
|
variant: 'default',
|
|
});
|
|
});
|
|
|
|
it('should inject the correct string feature flag evaluation details', async () => {
|
|
const testService = app.get(OpenFeatureTestService);
|
|
const testServiceSpy = jest.spyOn(testService, 'serviceMethod');
|
|
|
|
await supertest(app.getHttpServer()).get('/string').expect(200).expect('expected-string-value-default');
|
|
|
|
expect(testServiceSpy).toHaveBeenCalledWith({
|
|
flagKey: 'testStringFlag',
|
|
flagMetadata: {},
|
|
reason: 'STATIC',
|
|
value: 'expected-string-value-default',
|
|
variant: 'default',
|
|
});
|
|
});
|
|
|
|
it('should inject the correct number feature flag evaluation details', async () => {
|
|
const testService = app.get(OpenFeatureTestService);
|
|
const testServiceSpy = jest.spyOn(testService, 'serviceMethod');
|
|
|
|
await supertest(app.getHttpServer()).get('/number').expect(200).expect('10');
|
|
|
|
expect(testServiceSpy).toHaveBeenCalledWith({
|
|
flagKey: 'testNumberFlag',
|
|
flagMetadata: {},
|
|
reason: 'STATIC',
|
|
value: 10,
|
|
variant: 'default',
|
|
});
|
|
});
|
|
|
|
it('should inject the correct object feature flag evaluation details', async () => {
|
|
const testService = app.get(OpenFeatureTestService);
|
|
const testServiceSpy = jest.spyOn(testService, 'serviceMethod');
|
|
|
|
await supertest(app.getHttpServer()).get('/object').expect(200).expect({ client: 'default' });
|
|
|
|
expect(testServiceSpy).toHaveBeenCalledWith({
|
|
flagKey: 'testObjectFlag',
|
|
flagMetadata: {},
|
|
reason: 'STATIC',
|
|
value: { client: 'default' },
|
|
variant: 'default',
|
|
});
|
|
});
|
|
|
|
it('should use the execution context from contextFactory', async () => {
|
|
const evaluationSpy = jest.spyOn(defaultProvider, 'resolveBooleanEvaluation');
|
|
await supertest(app.getHttpServer()).get('/dynamic-context').set('x-user-id', '123').expect(200).expect('true');
|
|
expect(evaluationSpy).toHaveBeenCalledWith('testBooleanFlag', false, { targetingKey: '123' }, {});
|
|
});
|
|
});
|
|
|
|
describe('evaluation context service should', () => {
|
|
it('inject the evaluation context from contex factory', async function () {
|
|
const evaluationSpy = jest.spyOn(defaultProvider, 'resolveBooleanEvaluation');
|
|
await supertest(app.getHttpServer())
|
|
.get('/dynamic-context-in-service')
|
|
.set('x-user-id', 'dynamic-user')
|
|
.expect(200)
|
|
.expect('true');
|
|
expect(evaluationSpy).toHaveBeenCalledWith('testBooleanFlag', false, { targetingKey: 'dynamic-user' }, {});
|
|
});
|
|
});
|
|
|
|
describe('require flags enabled decorator', () => {
|
|
describe('OpenFeatureController', () => {
|
|
it('should sucessfully return the response if the flag is enabled', async () => {
|
|
await supertest(app.getHttpServer()).get('/flags-enabled').expect(200).expect('Get Boolean Flag Success!');
|
|
});
|
|
|
|
it('should throw an exception if the flag is disabled', async () => {
|
|
jest.spyOn(defaultProvider, 'resolveBooleanEvaluation').mockResolvedValueOnce({
|
|
value: false,
|
|
reason: 'DISABLED',
|
|
});
|
|
await supertest(app.getHttpServer()).get('/flags-enabled').expect(404);
|
|
});
|
|
|
|
it('should throw a custom exception if the flag is disabled', async () => {
|
|
jest.spyOn(defaultProvider, 'resolveBooleanEvaluation').mockResolvedValueOnce({
|
|
value: false,
|
|
reason: 'DISABLED',
|
|
});
|
|
await supertest(app.getHttpServer()).get('/flags-enabled-custom-exception').expect(403);
|
|
});
|
|
|
|
it('should throw a custom exception if the flag is disabled with context', async () => {
|
|
await supertest(app.getHttpServer())
|
|
.get('/flags-enabled-custom-exception-with-context')
|
|
.set('x-user-id', '123')
|
|
.expect(403);
|
|
});
|
|
});
|
|
|
|
describe('OpenFeatureControllerRequireFlagsEnabled', () => {
|
|
it('should allow access to the RequireFlagsEnabled controller with global context interceptor', async () => {
|
|
await supertest(app.getHttpServer())
|
|
.get('/require-flags-enabled')
|
|
.set('x-user-id', '123')
|
|
.expect(200)
|
|
.expect('Hello, world!');
|
|
});
|
|
|
|
it('should throw a 403 - Forbidden exception if user does not match targeting requirements', async () => {
|
|
await supertest(app.getHttpServer()).get('/require-flags-enabled').set('x-user-id', 'not-123').expect(403);
|
|
});
|
|
|
|
it('should throw a 403 - Forbidden exception if one of the flags is disabled', async () => {
|
|
jest.spyOn(defaultProvider, 'resolveBooleanEvaluation').mockResolvedValueOnce({
|
|
value: false,
|
|
reason: 'DISABLED',
|
|
});
|
|
await supertest(app.getHttpServer()).get('/require-flags-enabled').set('x-user-id', '123').expect(403);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Without global context interceptor', () => {
|
|
let moduleRef: TestingModule;
|
|
let app: INestApplication;
|
|
|
|
beforeAll(async () => {
|
|
moduleRef = await Test.createTestingModule({
|
|
imports: [
|
|
OpenFeatureModule.forRoot({
|
|
contextFactory: exampleContextFactory,
|
|
defaultProvider,
|
|
providers,
|
|
useGlobalInterceptor: false,
|
|
}),
|
|
],
|
|
providers: [OpenFeatureTestService],
|
|
controllers: [OpenFeatureController, OpenFeatureContextScopedController],
|
|
}).compile();
|
|
app = moduleRef.createNestApplication();
|
|
app = await app.init();
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await moduleRef.close();
|
|
});
|
|
|
|
it('should not use context if global context interceptor is not configured', async () => {
|
|
const evaluationSpy = jest.spyOn(defaultProvider, 'resolveBooleanEvaluation');
|
|
await supertest(app.getHttpServer()).get('/dynamic-context').set('x-user-id', '123').expect(200).expect('true');
|
|
expect(evaluationSpy).toHaveBeenCalledWith('testBooleanFlag', false, {}, {});
|
|
});
|
|
|
|
describe('evaluation context service should', () => {
|
|
it('inject empty context if no context interceptor is configured', async function () {
|
|
const evaluationSpy = jest.spyOn(defaultProvider, 'resolveBooleanEvaluation');
|
|
await supertest(app.getHttpServer())
|
|
.get('/dynamic-context-in-service')
|
|
.set('x-user-id', 'dynamic-user')
|
|
.expect(200)
|
|
.expect('true');
|
|
expect(evaluationSpy).toHaveBeenCalledWith('testBooleanFlag', false, {}, {});
|
|
});
|
|
});
|
|
|
|
describe('With Controller bound Context interceptor', () => {
|
|
it('should not use context if global context interceptor is not configured', async () => {
|
|
const evaluationSpy = jest.spyOn(defaultProvider, 'resolveBooleanEvaluation');
|
|
await supertest(app.getHttpServer())
|
|
.get('/controller-context')
|
|
.set('x-user-id', '123')
|
|
.expect(200)
|
|
.expect('true');
|
|
expect(evaluationSpy).toHaveBeenCalledWith('testBooleanFlag', false, { targetingKey: '123' }, {});
|
|
});
|
|
});
|
|
|
|
describe('require flags enabled decorator', () => {
|
|
it('should return a 404 - Not Found exception if the flag is disabled', async () => {
|
|
jest.spyOn(providers.domainScopedClient, 'resolveBooleanEvaluation').mockResolvedValueOnce({
|
|
value: false,
|
|
reason: 'DISABLED',
|
|
});
|
|
await supertest(app.getHttpServer())
|
|
.get('/controller-context/flags-enabled')
|
|
.set('x-user-id', '123')
|
|
.expect(404);
|
|
});
|
|
});
|
|
});
|
|
});
|