Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ jobs:
- run: npm ci
- run: npm run build
- run: npm test
- run: npm run test:worker
- run: npm run test:smoke:worker

lint-format:
Expand Down
3 changes: 3 additions & 0 deletions examples/js-worker/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,16 @@
"deploy": "wrangler deploy",
"presmoke": "npm run build --workspace=@openfeature/flagd-ofrep-cf-worker",
"smoke": "wrangler deploy --dry-run",
Comment thread
jonathannorris marked this conversation as resolved.
"pretest": "npm run build --workspace=@openfeature/flagd-ofrep-cf-worker",
"test": "vitest run",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@openfeature/flagd-ofrep-cf-worker": "*",
"hono": "^4.12.5"
},
"devDependencies": {
"@cloudflare/vitest-pool-workers": "^0.14.0",
"@cloudflare/workers-types": "^4.20260127.0",
"typescript": "^5.7.0",
"wrangler": "^4.61.0"
Expand Down
6 changes: 6 additions & 0 deletions examples/js-worker/test/env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
declare module 'cloudflare:workers' {
interface ProvidedEnv {
FLAGS_R2_BUCKET?: R2Bucket;
FLAG_SOURCE?: string;
}
}
8 changes: 8 additions & 0 deletions examples/js-worker/test/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"moduleResolution": "bundler",
"types": ["@cloudflare/vitest-pool-workers", "@cloudflare/workers-types"]
},
"include": ["./**/*.ts"]
}
135 changes: 135 additions & 0 deletions examples/js-worker/test/worker.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { exports } from 'cloudflare:workers';

function postJson(path: string, body: unknown): Request {
return new Request(`http://localhost${path}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
}

describe('static flag worker', () => {
describe('service endpoints', () => {
it('returns service info at root', async () => {
const response = await exports.default.fetch('http://localhost/');
expect(response.status).toBe(200);

const body = await response.json();
expect(body.status).toBe('ok');
expect(body.service).toBe('flagd-ofrep-js-worker');
expect(body.flagSource).toBe('static');
});

it('returns health check', async () => {
const response = await exports.default.fetch('http://localhost/health');
expect(response.status).toBe(200);

const body = await response.json();
expect(body.status).toBe('ok');
});

it('returns 404 for unknown paths', async () => {
const response = await exports.default.fetch('http://localhost/unknown');
expect(response.status).toBe(404);
});
});

describe('single flag evaluation', () => {
it('evaluates a boolean flag', async () => {
const response = await exports.default.fetch(postJson('/ofrep/v1/evaluate/flags/simple-boolean', {}));
expect(response.status).toBe(200);

const body = await response.json();
expect(body.key).toBe('simple-boolean');
expect(body.value).toBe(false);
expect(body.variant).toBe('off');
expect(body.reason).toBeDefined();
});

it('evaluates a string flag', async () => {
const response = await exports.default.fetch(postJson('/ofrep/v1/evaluate/flags/simple-string', {}));
expect(response.status).toBe(200);

const body = await response.json();
expect(body.key).toBe('simple-string');
expect(body.value).toBe('default-value');
});

it('evaluates with targeting context', async () => {
const response = await exports.default.fetch(
postJson('/ofrep/v1/evaluate/flags/targeted-boolean', {
context: { email: 'user@openfeature.dev' },
}),
);
expect(response.status).toBe(200);

const body = await response.json();
expect(body.value).toBe(true);
expect(body.reason).toBe('TARGETING_MATCH');
});

it('returns 404 for non-existent flag', async () => {
const response = await exports.default.fetch(postJson('/ofrep/v1/evaluate/flags/does-not-exist', {}));
expect(response.status).toBe(404);

const body = await response.json();
expect(body.errorCode).toBe('FLAG_NOT_FOUND');
});

it('defers disabled flags to code defaults', async () => {
const response = await exports.default.fetch(postJson('/ofrep/v1/evaluate/flags/disabled-flag', {}));
expect(response.status).toBe(200);

const body = await response.json();
expect(body.key).toBe('disabled-flag');
expect(body.reason).toBe('DISABLED');
});
});

describe('bulk evaluation', () => {
it('evaluates all flags', async () => {
const response = await exports.default.fetch(postJson('/ofrep/v1/evaluate/flags', {}));
expect(response.status).toBe(200);

const body = await response.json();
expect(body.flags).toBeDefined();
expect(Array.isArray(body.flags)).toBe(true);
expect(body.flags.length).toBeGreaterThan(0);
});

it('includes metadata in bulk response', async () => {
const response = await exports.default.fetch(postJson('/ofrep/v1/evaluate/flags', {}));
const body = await response.json();
expect(body.metadata).toBeDefined();
expect(body.metadata.flagSetId).toBe('js-worker-example');
});

it('passes context to bulk evaluation', async () => {
const response = await exports.default.fetch(
postJson('/ofrep/v1/evaluate/flags', {
context: { email: 'user@openfeature.dev' },
}),
);
const body = await response.json();

const targeted = body.flags.find((f: { key: string }) => f.key === 'targeted-boolean');
expect(targeted).toBeDefined();
expect(targeted.value).toBe(true);
});
});

describe('CORS', () => {
it('handles OPTIONS preflight on OFREP paths', async () => {
const response = await exports.default.fetch(
new Request('http://localhost/ofrep/v1/evaluate/flags/simple-boolean', { method: 'OPTIONS' }),
);
expect(response.status).toBe(204);
expect(response.headers.get('Access-Control-Allow-Origin')).toBe('*');
});

it('includes CORS headers on evaluation responses', async () => {
const response = await exports.default.fetch(postJson('/ofrep/v1/evaluate/flags/simple-boolean', {}));
expect(response.headers.get('Access-Control-Allow-Origin')).toBe('*');
});
});
});
14 changes: 14 additions & 0 deletions examples/js-worker/vitest.config.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { cloudflareTest } from '@cloudflare/vitest-pool-workers';
import { defineConfig } from 'vitest/config';

export default defineConfig({
plugins: [
cloudflareTest({
wrangler: { configPath: './wrangler.toml' },
}),
],
test: {
globals: true,
include: ['test/**/*.spec.ts'],
},
});
29 changes: 0 additions & 29 deletions jest.config.ts

This file was deleted.

Loading
Loading