Skip to content

Commit

Permalink
Fix
Browse files Browse the repository at this point in the history
  • Loading branch information
sjaanus committed Apr 23, 2024
1 parent 5081228 commit 5980b00
Show file tree
Hide file tree
Showing 4 changed files with 28 additions and 149 deletions.
3 changes: 0 additions & 3 deletions src/lib/features/frontend-api/frontend-api-repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ export class FrontendApiRepository
}

getToggle(name: string): FeatureInterface {
//@ts-ignore (we must update the node SDK to allow undefined)
return this.globalFrontendApiCache.getToggle(name, this.token);
}

Expand All @@ -60,8 +59,6 @@ export class FrontendApiRepository
async start(): Promise<void> {
this.running = true;

await this.globalFrontendApiCache.refreshData();

this.emit(UnleashEvents.Ready);
this.emit(UnleashEvents.Changed);
}
Expand Down
4 changes: 4 additions & 0 deletions src/lib/features/frontend-api/frontend-api-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,10 @@ export class FrontendApiService {
this.clients.forEach((promise) => promise.then((c) => c.destroy()));
}

refreshData(): Promise<void> {
return this.globalFrontendApiCache.refreshData();
}

private static assertExpectedTokenType({ type }: IApiUser) {
if (!(type === ApiTokenType.FRONTEND || type === ApiTokenType.ADMIN)) {
throw new InvalidTokenError();
Expand Down
168 changes: 22 additions & 146 deletions src/lib/features/frontend-api/frontend-api.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,16 @@ import {
} from '../../types/models/api-token';
import { startOfHour } from 'date-fns';
import {
FEATURE_UPDATED,
type IConstraint,
type IStrategyConfig,
SYSTEM_USER_AUDIT,
TEST_AUDIT_USER,
} from '../../types';
import { ProxyRepository } from './index';
import type { Logger } from '../../logger';
import type { FrontendApiService } from './frontend-api-service';

let app: IUnleashTest;
let db: ITestDb;
const TEST_USER_ID = -9999;
let frontendApiService: FrontendApiService;
beforeAll(async () => {
db = await dbInit('frontend_api', getLogger);
app = await setupAppWithAuth(
Expand All @@ -33,6 +31,7 @@ beforeAll(async () => {
},
db.rawDatabase,
);
frontendApiService = app.services.frontendApiService;
});

afterEach(() => {
Expand Down Expand Up @@ -161,6 +160,7 @@ test('should allow requests with a token secret alias', async () => {
alias: randomId(),
environment: envB,
});
await frontendApiService.refreshData();
await app.request
.get('/api/frontend')
.expect('Content-Type', /json/)
Expand Down Expand Up @@ -226,6 +226,7 @@ test('should allow requests with an admin token', async () => {
projects: ['*'],
environment: '*',
});
await frontendApiService.refreshData();
await app.request
.get('/api/frontend')
.set('Authorization', adminToken.secret)
Expand Down Expand Up @@ -264,6 +265,7 @@ test('should not allow requests with an invalid frontend token', async () => {

test('should allow requests with a frontend token', async () => {
const frontendToken = await createApiToken(ApiTokenType.FRONTEND);
await frontendApiService.refreshData();
await app.request
.get('/api/frontend')
.set('Authorization', frontendToken.secret)
Expand Down Expand Up @@ -441,6 +443,7 @@ test('should filter features by enabled/disabled', async () => {
enabled: false,
strategies: [{ name: 'default', constraints: [], parameters: {} }],
});
await frontendApiService.refreshData();
await app.request
.get('/api/frontend')
.set('Authorization', frontendToken.secret)
Expand Down Expand Up @@ -488,6 +491,7 @@ test('should filter features by strategies', async () => {
enabled: true,
strategies: [{ name: 'default', constraints: [], parameters: {} }],
});
await frontendApiService.refreshData();
await app.request
.get('/api/frontend')
.set('Authorization', frontendToken.secret)
Expand Down Expand Up @@ -544,6 +548,7 @@ test('should filter features by constraints', async () => {
},
],
});
await frontendApiService.refreshData();
await app.request
.get('/api/frontend?appName=a')
.set('Authorization', frontendToken.secret)
Expand Down Expand Up @@ -585,6 +590,8 @@ test('should be able to set environment as a context variable', async () => {
],
});

await frontendApiService.refreshData();
await ms(100);
await app.request
.get('/api/frontend?environment=staging')
.set('Authorization', frontendToken.secret)
Expand All @@ -605,6 +612,10 @@ test('should be able to set environment as a context variable', async () => {
});
});

function ms(timeMs: number) {
return new Promise((resolve) => setTimeout(resolve, timeMs));
}

test('should filter features by project', async () => {
const projectA = 'projectA';
const projectB = 'projectB';
Expand Down Expand Up @@ -636,6 +647,7 @@ test('should filter features by project', async () => {
enabled: true,
strategies: [{ name: 'default', parameters: {} }],
});
await frontendApiService.refreshData();
await app.request
.get('/api/frontend')
.set('Authorization', frontendTokenDefault.secret)
Expand Down Expand Up @@ -768,6 +780,7 @@ test('should filter features by environment', async () => {
enabled: true,
strategies: [{ name: 'default', parameters: {} }],
});
await frontendApiService.refreshData();
await app.request
.get('/api/frontend')
.set('Authorization', frontendTokenEnvironmentDefault.secret)
Expand Down Expand Up @@ -868,6 +881,7 @@ test('should filter features by segment', async () => {
await app.services.segmentService.addToStrategy(segmentA.id, strategyA.id);
await app.services.segmentService.addToStrategy(segmentB.id, strategyB.id);
const frontendToken = await createApiToken(ApiTokenType.FRONTEND);
await frontendApiService.refreshData();
await app.request
.get('/api/frontend')
.set('Authorization', frontendToken.secret)
Expand Down Expand Up @@ -900,102 +914,6 @@ test('should filter features by segment', async () => {
.expect((res) => expect(res.body).toEqual({ toggles: [] }));
});

test('Should sync proxy for keys on an interval', async () => {
jest.useFakeTimers();

const frontendToken = await createApiToken(ApiTokenType.FRONTEND);
const user = await app.services.apiTokenService.getUserForToken(
frontendToken.secret,
);

const spy = jest.spyOn(
ProxyRepository.prototype as any,
'featuresForToken',
);
expect(user).not.toBeNull();
const proxyRepository = new ProxyRepository(
{
getLogger,
frontendApi: { refreshIntervalInMs: 5000 },
eventBus: <any>{ emit: jest.fn() },
},
db.stores,
app.services,
user!,
);

await proxyRepository.start();

jest.advanceTimersByTime(60000);

proxyRepository.stop();
expect(spy.mock.calls.length > 6).toBe(true);
jest.useRealTimers();
});

test('Should change fetch interval', async () => {
jest.useFakeTimers();

const frontendToken = await createApiToken(ApiTokenType.FRONTEND);
const user = await app.services.apiTokenService.getUserForToken(
frontendToken.secret,
);

const spy = jest.spyOn(
ProxyRepository.prototype as any,
'featuresForToken',
);
const proxyRepository = new ProxyRepository(
{
getLogger,
frontendApi: { refreshIntervalInMs: 1000 },
eventBus: <any>{ emit: jest.fn() },
},
db.stores,
app.services,
user!,
);

await proxyRepository.start();

jest.advanceTimersByTime(60000);

proxyRepository.stop();
expect(spy.mock.calls.length > 30).toBe(true);
jest.useRealTimers();
});

test('Should not recursively set off timers on events', async () => {
jest.useFakeTimers();

const frontendToken = await createApiToken(ApiTokenType.FRONTEND);
const user = await app.services.apiTokenService.getUserForToken(
frontendToken.secret,
);

const spy = jest.spyOn(ProxyRepository.prototype as any, 'dataPolling');
const proxyRepository = new ProxyRepository(
{
getLogger,
frontendApi: { refreshIntervalInMs: 5000 },
eventBus: <any>{ emit: jest.fn() },
},
db.stores,
app.services,
user!,
);

await proxyRepository.start();

db.stores.eventStore.emit(FEATURE_UPDATED);

jest.advanceTimersByTime(10000);

proxyRepository.stop();
expect(spy.mock.calls.length < 3).toBe(true);
jest.useRealTimers();
});

test('should return maxAge header on options call', async () => {
await app.request
.options('/api/frontend')
Expand All @@ -1006,51 +924,6 @@ test('should return maxAge header on options call', async () => {
});
});

test('should terminate data polling when stop is called', async () => {
const frontendToken = await createApiToken(ApiTokenType.FRONTEND);
const user = await app.services.apiTokenService.getUserForToken(
frontendToken.secret,
);

const logTrap: any[] = [];
const getDebugLogger = (): Logger => {
return {
/* eslint-disable-next-line */
debug: (message: any, ...args: any[]) => {
logTrap.push(message);
},
/* eslint-disable-next-line */
info: (...args: any[]) => {},
/* eslint-disable-next-line */
warn: (...args: any[]) => {},
/* eslint-disable-next-line */
error: (...args: any[]) => {},
/* eslint-disable-next-line */
fatal: (...args: any[]) => {},
};
};

/* eslint-disable-next-line */
const proxyRepository = new ProxyRepository(
{
getLogger: getDebugLogger,
frontendApi: { refreshIntervalInMs: 1 },
eventBus: <any>{ emit: jest.fn() },
},
db.stores,
app.services,
user!,
);

await proxyRepository.start();
proxyRepository.stop();
// Polling here is an async recursive call, so we gotta give it a bit of time
await new Promise((r) => setTimeout(r, 10));
expect(logTrap).toContain(
'Shutting down data polling for proxy repository',
);
});

test('should evaluate strategies when returning toggles', async () => {
const frontendToken = await createApiToken(ApiTokenType.FRONTEND);
await createFeatureToggle({
Expand Down Expand Up @@ -1084,6 +957,7 @@ test('should evaluate strategies when returning toggles', async () => {
],
});

await frontendApiService.refreshData();
await app.request
.get('/api/frontend')
.set('Authorization', frontendToken.secret)
Expand Down Expand Up @@ -1144,6 +1018,7 @@ test('should not return all features', async () => {
},
],
});
await frontendApiService.refreshData();
await app.request
.get('/api/frontend')
.set('Authorization', frontendToken.secret)
Expand Down Expand Up @@ -1256,6 +1131,7 @@ test('should NOT evaluate disabled strategies when returning toggles', async ()
],
});

await frontendApiService.refreshData();
await app.request
.get('/api/frontend')
.set('Authorization', frontendToken.secret)
Expand Down Expand Up @@ -1344,7 +1220,7 @@ test('should resolve variable rollout percentage consistently', async () => {
},
],
});

await frontendApiService.refreshData();
for (let i = 0; i < 10; ++i) {
const { body } = await app.request
.get('/api/frontend')
Expand Down
2 changes: 2 additions & 0 deletions src/lib/features/frontend-api/global-frontend-api-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export class GlobalFrontendApiCache extends EventEmitter {
const features = this.getTogglesByEnvironment(
this.environmentNameForToken(token),
);
console.log('get toggle');
return features[name];
}

Expand Down Expand Up @@ -114,6 +115,7 @@ export class GlobalFrontendApiCache extends EventEmitter {
try {
this.featuresByEnvironment = await this.getAllFeatures();
this.segments = await this.getAllSegments();
console.log('features updated');
if (this.status === 'starting') {
this.status = 'ready';
this.emit('ready');
Expand Down

0 comments on commit 5980b00

Please sign in to comment.