Skip to content

Commit

Permalink
feat: delta api e2e test (#9003)
Browse files Browse the repository at this point in the history
WIP PR for delta-api tests.

---------

Co-authored-by: sjaanus <[email protected]>
  • Loading branch information
FredrikOseberg and sjaanus authored Dec 30, 2024
1 parent 5633529 commit 71eb6b1
Show file tree
Hide file tree
Showing 3 changed files with 189 additions and 65 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,47 @@ export default class ClientFeatureToggleDeltaController extends Controller {
});
}

async getDelta(
req: IAuthRequest,
res: Response<RevisionDeltaEntry>,
): Promise<void> {
if (!this.flagResolver.isEnabled('deltaApi')) {
throw new NotFoundError();
}
const query = await this.resolveQuery(req);
const etag = req.headers['if-none-match'];

const currentSdkRevisionId = etag ? Number.parseInt(etag) : undefined;

const changedFeatures =
await this.clientFeatureToggleService.getClientDelta(
currentSdkRevisionId,
query,
);

if (!changedFeatures) {
res.status(304);
res.getHeaderNames().forEach((header) => res.removeHeader(header));
res.end();
return;
}

if (changedFeatures.revisionId === currentSdkRevisionId) {
res.status(304);
res.getHeaderNames().forEach((header) => res.removeHeader(header));
res.end();
return;
}

res.setHeader('ETag', changedFeatures.revisionId.toString());
this.openApiService.respondWithValidation(
200,
res,
clientFeaturesDeltaSchema.$id,
changedFeatures,
);
}

private async resolveQuery(
req: IAuthRequest,
): Promise<IFeatureToggleQuery> {
Expand Down Expand Up @@ -139,45 +180,4 @@ export default class ClientFeatureToggleDeltaController extends Controller {

return query;
}

async getDelta(
req: IAuthRequest,
res: Response<RevisionDeltaEntry>,
): Promise<void> {
if (!this.flagResolver.isEnabled('deltaApi')) {
throw new NotFoundError();
}
const query = await this.resolveQuery(req);
const etag = req.headers['if-none-match'];

const currentSdkRevisionId = etag ? Number.parseInt(etag) : undefined;

const changedFeatures =
await this.clientFeatureToggleService.getClientDelta(
currentSdkRevisionId,
query,
);

if (!changedFeatures) {
res.status(304);
res.getHeaderNames().forEach((header) => res.removeHeader(header));
res.end();
return;
}

if (changedFeatures.revisionId === currentSdkRevisionId) {
res.status(304);
res.getHeaderNames().forEach((header) => res.removeHeader(header));
res.end();
return;
}

res.setHeader('ETag', changedFeatures.revisionId.toString());
this.openApiService.respondWithValidation(
200,
res,
clientFeaturesDeltaSchema.$id,
changedFeatures,
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -169,28 +169,6 @@ export class ClientFeatureToggleDelta {
await this.updateSegments();
}

// TODO: 19.12 this logic seems to be not logical, when no revisionId is coming, it should not go to db, but take latest from cache

// Should get the latest state if revision does not exist or if sdkRevision is not present
// We should be able to do this without going to the database by merging revisions from the delta with
// the base case
const firstTimeCalling = !sdkRevisionId;
if (
firstTimeCalling ||
(sdkRevisionId &&
sdkRevisionId !== this.currentRevisionId &&
!this.delta[environment].hasRevision(sdkRevisionId))
) {
//TODO: populate delta based on this?
return {
revisionId: this.currentRevisionId,
// @ts-ignore
updated: await this.getClientFeatures({ environment }),
segments: this.segments,
removed: [],
};
}

if (requiredRevisionId >= this.currentRevisionId) {
return undefined;
}
Expand All @@ -211,15 +189,19 @@ export class ClientFeatureToggleDelta {
return Promise.resolve(revisionResponse);
}

private async onUpdateRevisionEvent() {
public async onUpdateRevisionEvent() {
if (this.flagResolver.isEnabled('deltaApi')) {
await this.updateFeaturesDelta();
await this.updateSegments();
this.storeFootprint();
}
}

public async updateFeaturesDelta() {
public resetDelta() {
this.delta = {};
}

private async updateFeaturesDelta() {
const keys = Object.keys(this.delta);

if (keys.length === 0) return;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import dbInit, {
type ITestDb,
} from '../../../../test/e2e/helpers/database-init';
import {
type IUnleashTest,
setupAppWithCustomConfig,
} from '../../../../test/e2e/helpers/test-helper';
import getLogger from '../../../../test/fixtures/no-logger';
import { DEFAULT_ENV } from '../../../util/constants';

let app: IUnleashTest;
let db: ITestDb;

const setupFeatures = async (
db: ITestDb,
app: IUnleashTest,
project = 'default',
) => {
await app.createFeature('test1', project);
await app.createFeature('test2', project);

await app.addStrategyToFeatureEnv(
{
name: 'flexibleRollout',
constraints: [],
parameters: {
rollout: '100',
stickiness: 'default',
groupId: 'test1',
},
},
DEFAULT_ENV,
'test1',
project,
);
await app.addStrategyToFeatureEnv(
{
name: 'default',
constraints: [
{ contextName: 'userId', operator: 'IN', values: ['123'] },
],
parameters: {},
},
DEFAULT_ENV,
'test2',
project,
);
};

beforeAll(async () => {
db = await dbInit('client_feature_toggles_delta', getLogger);
app = await setupAppWithCustomConfig(
db.stores,
{
experimental: {
flags: {
strictSchemaValidation: true,
deltaApi: true,
},
},
},
db.rawDatabase,
);
});

beforeEach(async () => {
await db.stores.eventStore.deleteAll();
await db.stores.featureToggleStore.deleteAll();
// @ts-ignore
app.services.clientFeatureToggleService.clientFeatureToggleDelta.resetDelta();
});

afterAll(async () => {
await app.destroy();
await db.destroy();
});

test('should match with /api/client/delta', async () => {
await setupFeatures(db, app);

const { body } = await app.request
.get('/api/client/features')
.expect('Content-Type', /json/)
.expect(200);

const { body: deltaBody } = await app.request
.get('/api/client/delta')
.expect('Content-Type', /json/)
.expect(200);

expect(body.features).toMatchObject(deltaBody.updated);
});

test('should get 304 if asked for latest revision', async () => {
await setupFeatures(db, app);

const { body } = await app.request.get('/api/client/delta').expect(200);
const currentRevisionId = body.revisionId;

await app.request
.set('If-None-Match', currentRevisionId)
.get('/api/client/delta')
.expect(304);
});

test('should return correct delta after feature created', async () => {
await app.createFeature('base_feature');
await syncRevisions();
const { body } = await app.request.get('/api/client/delta').expect(200);
const currentRevisionId = body.revisionId;

expect(body).toMatchObject({
updated: [
{
name: 'base_feature',
},
],
});

await app.createFeature('new_feature');

await syncRevisions();

const { body: deltaBody } = await app.request
.get('/api/client/delta')
.set('If-None-Match', currentRevisionId)
.expect(200);

expect(deltaBody).toMatchObject({
updated: [
{
name: 'new_feature',
},
],
});
});

const syncRevisions = async () => {
await app.services.configurationRevisionService.updateMaxRevisionId();
// @ts-ignore
await app.services.clientFeatureToggleService.clientFeatureToggleDelta.onUpdateRevisionEvent();
};

0 comments on commit 71eb6b1

Please sign in to comment.