Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion openfeature-provider/js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"build": "tsdown",
"dev": "tsdown --watch",
"test": "vitest",
"proto:gen": "rm -rf src/proto && mkdir -p src/proto && protoc --plugin=node_modules/.bin/protoc-gen-ts_proto --ts_proto_opt useOptionals=messages --ts_proto_opt esModuleInterop=true --ts_proto_out src/proto -Iproto api.proto messages.proto"
"proto:gen": "rm -rf src/proto && mkdir -p src/proto && protoc --plugin=node_modules/.bin/protoc-gen-ts_proto --ts_proto_opt useOptionals=messages --ts_proto_opt esModuleInterop=true --ts_proto_out src/proto -Iproto api.proto messages.proto test-only.proto"
},
"dependencies": {
"@bufbuild/protobuf": "^2.9.0"
Expand Down
10 changes: 10 additions & 0 deletions openfeature-provider/js/proto/test-only.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
syntax = "proto3";

message WriteFlagLogsRequest {
repeated bytes flag_assigned = 1;

bytes telemetry_data = 2;

repeated bytes client_resolve_info = 3;
repeated bytes flag_resolve_info = 4;
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const {

const moduleBytes = readFileSync(__dirname + '/../../../wasm/confidence_resolver.wasm');
const module = new WebAssembly.Module(moduleBytes);
const resolver = await WasmResolver.load(module);
const resolver = new WasmResolver(module);
const confidenceProvider = new ConfidenceServerProviderLocal(resolver, {
flagClientSecret: 'RxDVTrXvc6op1XxiQ4OaR31dKbJ39aYV',
apiClientId: JS_E2E_CONFIDENCE_API_CLIENT_ID,
Expand Down
211 changes: 126 additions & 85 deletions openfeature-provider/js/src/WasmResolver.test.ts
Original file line number Diff line number Diff line change
@@ -1,108 +1,149 @@
import { beforeEach, describe, expect, it, test } from 'vitest';
import { WasmResolver } from './WasmResolver';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { UnsafeWasmResolver, WasmResolver } from './WasmResolver';
import { readFileSync } from 'node:fs';
import { ResolveReason } from './proto/api';
import { spawnSync } from 'node:child_process';
import { error } from 'node:console';
import { stderr } from 'node:process';
import { ResolveWithStickyRequest, ResolveReason } from './proto/api';
import { WriteFlagLogsRequest } from './proto/test-only';

const moduleBytes = readFileSync(__dirname + '/../../../wasm/confidence_resolver.wasm');
const stateBytes = readFileSync(__dirname + '/../../../wasm/resolver_state.pb');

const module = new WebAssembly.Module(moduleBytes);
const CLIENT_SECRET = 'mkjJruAATQWjeY7foFIWfVAcBWnci2YF';

const RESOLVE_REQUEST:ResolveWithStickyRequest = {
resolveRequest: {
flags: ['flags/tutorial-feature'],
clientSecret: CLIENT_SECRET,
apply: true,
evaluationContext: {
targeting_key: 'tutorial_visitor',
visitor_id: 'tutorial_visitor',
},
},
materializationsPerUnit: {},
failFastOnSticky: false
};

const SET_STATE_REQUEST = { state: stateBytes, accountId: 'confidence-test' };


let wasmResolver: WasmResolver;
beforeEach(async () => {
wasmResolver = await WasmResolver.load(new WebAssembly.Module(moduleBytes));
});

it('should fail to resolve without state', () => {
expect(() => {
wasmResolver.resolveWithSticky({
resolveRequest: { flags: [], clientSecret: 'xyz', apply: false },
materializationsPerUnit: {},
failFastOnSticky: false
});
}).toThrowError('Resolver state not set');
});

describe('with state', () => {
describe('basic operation', () => {

beforeEach(() => {
wasmResolver.setResolverState({ state: stateBytes, accountId: 'confidence-test' });
wasmResolver = new WasmResolver(module);
});

it('should fail to resolve without state', () => {
expect(() => {
wasmResolver.resolveWithSticky(RESOLVE_REQUEST);
}).toThrowError('Resolver state not set');
});

describe('with state', () => {
beforeEach(() => {
wasmResolver.setResolverState(SET_STATE_REQUEST);
});

it('should resolve flags', () => {
const resp = wasmResolver.resolveWithSticky(RESOLVE_REQUEST);

it('should resolve flags', () => {
try {
const resp = wasmResolver.resolveWithSticky({
resolveRequest: {
flags: ['flags/tutorial-feature'],
clientSecret: CLIENT_SECRET,
apply: true,
evaluationContext: {
targeting_key: 'tutorial_visitor',
visitor_id: 'tutorial_visitor',
},
},
materializationsPerUnit: {},
failFastOnSticky: false
expect(resp).toMatchObject({
success: {
response: {
resolvedFlags: [
{
reason: ResolveReason.RESOLVE_REASON_MATCH,
},
],
}
}
});
});

describe('flushLogs', () => {

it('should be empty before any resolve', () => {
const logs = wasmResolver.flushLogs();
expect(logs.length).toBe(0);
})

it('should contain logs after a resolve', () => {
wasmResolver.resolveWithSticky(RESOLVE_REQUEST);

const decoded = WriteFlagLogsRequest.decode(wasmResolver.flushLogs());

expect(decoded.flagAssigned.length).toBe(1)
expect(decoded.clientResolveInfo.length).toBe(1);
expect(decoded.flagResolveInfo.length).toBe(1);
})
})
});
})

expect(resp.success).toBeDefined();
expect(resp.success?.response).toMatchObject({
resolvedFlags: [
{
reason: ResolveReason.RESOLVE_REASON_MATCH,
},
],
});
} catch (e) {
console.log('yo', e);
}
describe('panic handling', () => {

const resolveWithStickySpy = vi.spyOn(UnsafeWasmResolver.prototype, 'resolveWithSticky');
const setResolverStateSpy = vi.spyOn(UnsafeWasmResolver.prototype, 'setResolverState');

const throwUnreachable = () => {
throw new WebAssembly.RuntimeError('unreachable');
}

beforeEach(() => {
vi.resetAllMocks();
wasmResolver = new WasmResolver(module);
});

describe('flushLogs', () => {

it('should be empty before any resolve', () => {
const logs = wasmResolver.flushLogs();
expect(logs.length).toBe(0);
})
it('throws and reloads the instance on panic', () => {
wasmResolver.setResolverState(SET_STATE_REQUEST)
resolveWithStickySpy.mockImplementationOnce(throwUnreachable);

it('should contain logs after a resolve', () => {
wasmResolver.resolveWithSticky({
resolveRequest: {
flags: ['flags/tutorial-feature'],
clientSecret: CLIENT_SECRET,
apply: true,
evaluationContext: {
targeting_key: 'tutorial_visitor',
visitor_id: 'tutorial_visitor',
},
},
materializationsPerUnit: {},
failFastOnSticky: false
});

expect(() => {
wasmResolver.resolveWithSticky(RESOLVE_REQUEST)
}).to.throw('unreachable');

const decoded = decodeBuffer(wasmResolver.flushLogs());
// now it should succeed since the instance is reloaded
expect(() => {
wasmResolver.resolveWithSticky(RESOLVE_REQUEST)
}).to.not.throw();

expect(decoded).contains('flag_assigned');
expect(decoded).contains('client_resolve_info');
expect(decoded).contains('flag_resolve_info');
})
})
});

it('can handle panic in setResolverState', () => {
setResolverStateSpy.mockImplementation(throwUnreachable);

expect(() => {
wasmResolver.setResolverState(SET_STATE_REQUEST)
}).to.throw('unreachable');

expect(() => {
wasmResolver.resolveWithSticky(RESOLVE_REQUEST)
}).to.throw('state not set');

})

it('tries to extracts logs from panicked instance', () => {
wasmResolver.setResolverState(SET_STATE_REQUEST)

// create some logs
wasmResolver.resolveWithSticky(RESOLVE_REQUEST);

resolveWithStickySpy.mockImplementationOnce(throwUnreachable);

expect(() => {
wasmResolver.resolveWithSticky(RESOLVE_REQUEST)
}).to.throw('unreachable');

const logs = wasmResolver.flushLogs();

expect(logs.length).toBeGreaterThan(0);

});


})

function decodeBuffer(input:Uint8Array):string {
const res = spawnSync('protoc',[
`-I${__dirname}/../../../confidence-resolver/protos`,
`--decode=confidence.flags.resolver.v1.WriteFlagLogsRequest`,
`confidence/flags/resolver/v1/internal_api.proto`
], { input, encoding: 'utf8' });
if(res.error) {
throw res.error;
}
if(res.status !== 0) {
throw new Error(res.stderr)
}
return res.stdout;
}
Loading