Skip to content

Commit

Permalink
Merge pull request #5 from rorteg/feat/decorator-guard
Browse files Browse the repository at this point in the history
feat: added feature toggle guard
  • Loading branch information
rorteg committed Sep 8, 2021
2 parents 7914fba + 3900de4 commit 7febd36
Show file tree
Hide file tree
Showing 5 changed files with 175 additions and 0 deletions.
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,49 @@ httpRequestContext: {

---

## Decorator

Is possible to use a decorator instead of import feature toggle service.

```typescript
// app.module.ts

@Module({
imports: [
FeatureToggleModule.register({
dataSource: DataSourceEnum.MODULE_CONFIG,
httpRequestContext: {
enabled: true,
},
featureSettings: [
{
name: 'FEATURE_TEST',
value: false,
acceptHttpRequestContext: true,
}
]
})
],
providers: [
{
provide: APP_GUARD,
useClass: FeatureToggleGuard,
},
],
})
```

```typescript
// cat.controller.ts

@Post()
@FeatureEnabled('FEATURE_TEST')
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
```
---

## License

[MIT licensed](LICENSE).
5 changes: 5 additions & 0 deletions src/decorators/feature-toggle.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { SetMetadata } from '@nestjs/common';

export const FEATURE_TOGGLE_DECORATOR_KEY = 'FEATURE_TOGGLE';
export const FeatureEnabled = (permissions: string) =>
SetMetadata(FEATURE_TOGGLE_DECORATOR_KEY, permissions);
25 changes: 25 additions & 0 deletions src/guards/feature-toggle.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { FEATURE_TOGGLE_DECORATOR_KEY } from '../decorators/feature-toggle.decorator';
import { FeatureToggleService } from '../feature-toggle.service';

@Injectable()
export class FeatureToggleGuard implements CanActivate {
constructor(
private reflector: Reflector,
private featureToggleService: FeatureToggleService
) {}

async canActivate(
context: ExecutionContext
): Promise<boolean> {
const featureKey = this.reflector.get<string>(
FEATURE_TOGGLE_DECORATOR_KEY,
context.getHandler()
);
if (!featureKey) {
return true;
}
return await this.featureToggleService.isFeatureEnabled(featureKey, context);
}
}
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ export * from './feature-toggle.constants';
export * from './feature-toggle.module';
export * from './feature-toggle.providers';
export * from './feature-toggle.service';
export * from './decorators/feature-toggle.decorator'
export * from './guards/feature-toggle.guard'
100 changes: 100 additions & 0 deletions src/tests/feature-toggle.guard.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Test, TestingModule } from '@nestjs/testing';
import { FeatureToggleGuard, FeatureToggleService } from '..';

const createContextMock = (requestData = {}) => {
const context: ExecutionContext = {
switchToHttp: () => context,
getRequest: () => {
return requestData;
},
getResponse: jest.fn(),
getHandler: jest.fn(),
getClass: jest.fn()
} as unknown as ExecutionContext;
return context;
};

describe('FeatureToggleGuard', () => {
let guard: FeatureToggleGuard;
let reflector: Reflector;
let featureToggleService: FeatureToggleService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
FeatureToggleGuard,
{
provide: Reflector,
useValue: {
constructor: jest.fn(),
get: jest.fn()
}
},
{
provide: FeatureToggleService,
useValue: {
constructor: jest.fn(),
isFeatureEnabled: jest.fn()
}
}
]
}).compile();

guard = module.get<FeatureToggleGuard>(FeatureToggleGuard);
featureToggleService =
module.get<FeatureToggleService>(FeatureToggleService);
reflector = module.get<Reflector>(Reflector);
});

afterEach(async () => {
jest.clearAllMocks();
});

it('should be defined', () => {
expect(guard).toBeDefined();
});

it('should return true if the `Feature` decorator is not set', async () => {
jest.spyOn(reflector, 'get').mockImplementation((a: any, b: any) => []);
jest
.spyOn(featureToggleService, 'isFeatureEnabled')
.mockReturnValue(Promise.resolve(true));

const context = createContextMock({ headers: { TEST_FEATURE: '1' } });
const result = await guard.canActivate(context);

expect(result).toBeTruthy();
expect(reflector.get).toBeCalledTimes(1);
});

it('should return false if the `Feature` decorator is set but the feature is disable', async () => {
jest.spyOn(reflector, 'get').mockReturnValue('TEST_FEATURE');
jest
.spyOn(featureToggleService, 'isFeatureEnabled')
.mockReturnValue(Promise.resolve(false));

const context = createContextMock({
headers: { TEST_FEATURE: '0' }
});

const result = await guard.canActivate(context);
expect(result).toBeFalsy();
expect(reflector.get).toBeCalledTimes(1);
});

it('should return true if the `Feature` decorator is set but the feature is enable', async () => {
jest.spyOn(reflector, 'get').mockReturnValue('TEST_FEATURE');
jest
.spyOn(featureToggleService, 'isFeatureEnabled')
.mockReturnValue(Promise.resolve(true));

const context = createContextMock({
headers: { TEST_FEATURE: '0' }
});

const result = await guard.canActivate(context);
expect(result).toBeTruthy();
expect(reflector.get).toBeCalledTimes(1);
});
});

0 comments on commit 7febd36

Please sign in to comment.