Skip to content

Commit 39bc58b

Browse files
committed
feat: add new api endpoint POST /api/testers
1 parent 721317c commit 39bc58b

File tree

4 files changed

+248
-7
lines changed

4 files changed

+248
-7
lines changed

client/get-openapi.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,21 @@ import fs from "fs";
22

33
import swaggerJsdoc from "swagger-jsdoc";
44

5+
export const API_VERSION = "1.6.0";
6+
57
const options = {
68
encoding: "utf8",
79
failOnErrors: false, // Whether or not to throw when parsing errors. Defaults to false.
810
format: "json",
911
info: {
1012
title: "Feedback Flow API",
11-
version: "1.5.0",
13+
version: API_VERSION,
1214
},
1315
definition: {
1416
openapi: "3.0.0",
1517
info: {
1618
title: "Feedback Flow API",
17-
version: "1.5.0",
19+
version: API_VERSION,
1820
},
1921
}, // You can move properties from definition here if needed
2022
apis: [

client/public/openapi.json

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"openapi": "3.0.0",
33
"info": {
44
"title": "Feedback Flow API",
5-
"version": "1.5.0"
5+
"version": "1.6.0"
66
},
77
"paths": {
88
"/api/testers": {
@@ -89,6 +89,85 @@
8989
"description": "Unauthorized request"
9090
}
9191
}
92+
},
93+
"post": {
94+
"summary": "Get testers filtered by OAuth IDs (supports pagination)",
95+
"description": "Returns a list of testers filtered by OAuth IDs provided in the request body. Requires admin permission.",
96+
"tags": [
97+
"Testers"
98+
],
99+
"requestBody": {
100+
"required": true,
101+
"content": {
102+
"application/json": {
103+
"schema": {
104+
"type": "object",
105+
"properties": {
106+
"page": {
107+
"type": "integer"
108+
},
109+
"limit": {
110+
"type": "integer"
111+
},
112+
"ids": {
113+
"oneOf": [
114+
{
115+
"type": "string"
116+
},
117+
{
118+
"type": "array",
119+
"items": {
120+
"type": "string"
121+
}
122+
}
123+
]
124+
}
125+
},
126+
"required": [
127+
"ids"
128+
]
129+
}
130+
}
131+
}
132+
},
133+
"responses": {
134+
"200": {
135+
"description": "Successfully retrieved filtered testers",
136+
"content": {
137+
"application/json": {
138+
"schema": {
139+
"type": "object",
140+
"properties": {
141+
"success": {
142+
"type": "boolean"
143+
},
144+
"data": {
145+
"type": "array",
146+
"items": {
147+
"$ref": "#/components/schemas/Tester"
148+
}
149+
},
150+
"total": {
151+
"type": "integer"
152+
},
153+
"page": {
154+
"type": "integer"
155+
},
156+
"limit": {
157+
"type": "integer"
158+
}
159+
}
160+
}
161+
}
162+
}
163+
},
164+
"400": {
165+
"description": "Invalid request or missing required fields"
166+
},
167+
"403": {
168+
"description": "Unauthorized request"
169+
}
170+
}
92171
}
93172
},
94173
"/api/tester": {

cloudflare-worker/src/routes/testers/index.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,101 @@ export const setupTesterRoutes = (router: Router, env: Env) => {
431431
env.ADMIN_PERMISSION,
432432
);
433433

434+
/**
435+
* @openapi
436+
* /api/testers:
437+
* post:
438+
* summary: Get testers filtered by OAuth IDs (supports pagination)
439+
* description: Returns a list of testers filtered by OAuth IDs provided in the request body. Requires admin permission. If limit is not provided, all matching testers are returned.
440+
* tags:
441+
* - Testers
442+
* requestBody:
443+
* required: true
444+
* content:
445+
* application/json:
446+
* schema:
447+
* type: object
448+
* properties:
449+
* page:
450+
* type: integer
451+
* limit:
452+
* type: integer
453+
* ids:
454+
* oneOf:
455+
* - type: string
456+
* - type: array
457+
* items:
458+
* type: string
459+
* required: [ids]
460+
* responses:
461+
* 200:
462+
* description: Successfully retrieved filtered testers
463+
* content:
464+
* application/json:
465+
* schema:
466+
* type: object
467+
* properties:
468+
* success:
469+
* type: boolean
470+
* data:
471+
* type: array
472+
* items:
473+
* $ref: '#/components/schemas/Tester'
474+
* total:
475+
* type: integer
476+
* page:
477+
* type: integer
478+
* limit:
479+
* type: integer
480+
* 400:
481+
* description: Invalid request or missing required fields
482+
* 403:
483+
* description: Unauthorized request
484+
*/
485+
router.post(
486+
"/api/testers",
487+
async (request) => {
488+
const db = getDatabase(env);
489+
try {
490+
const body = (await request.json()) as { page?: number; limit?: number; ids: string | string[] };
491+
if (!body || !body.ids) {
492+
return new Response(JSON.stringify({ success: false, error: 'ids is required' }), { status: 400, headers: { ...router.corsHeaders, 'Content-Type': 'application/json' } });
493+
}
494+
495+
const idArray: string[] = typeof body.ids === 'string' ? [body.ids] : body.ids;
496+
497+
// Find all testers which own at least one of the provided ids
498+
const testers = await db.testers.filter((t) => t.ids.some((id) => idArray.includes(id)));
499+
500+
// Default sort by name asc
501+
const sortedTesters = [...testers].sort((a, b) => (a.name > b.name ? 1 : -1));
502+
503+
const total = sortedTesters.length;
504+
let page = body.page || 1;
505+
let limit = body.limit || total; // if limit not provided, return all
506+
507+
// When limit is provided, apply pagination
508+
let paginatedTesters: typeof sortedTesters = sortedTesters;
509+
if (body.limit) {
510+
const start = (page - 1) * limit;
511+
const end = start + limit;
512+
paginatedTesters = sortedTesters.slice(start, end);
513+
} else {
514+
page = 1;
515+
limit = total;
516+
}
517+
518+
return new Response(
519+
JSON.stringify({ success: true, data: paginatedTesters, total, page, limit }),
520+
{ status: 200, headers: { ...router.corsHeaders, 'Content-Type': 'application/json' } },
521+
);
522+
} catch (error) {
523+
return new Response(JSON.stringify({ success: false, error: `Invalid request: ${(error as Error).message}` }), { status: 400, headers: { ...router.corsHeaders, 'Content-Type': 'application/json' } });
524+
}
525+
},
526+
env.ADMIN_PERMISSION,
527+
);
528+
434529
/**
435530
* @openapi
436531
* /api/tester:

cloudflare-worker/test/basic.api.test.ts

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -219,20 +219,85 @@ describe('Feedback Flow API', () => {
219219
expect(response.data.error).toBe('ID already exists in the database');
220220
});
221221

222+
test('70. Should filter testers by a single id using POST /testers', async () => {
223+
// Use the authenticated user id added to TESTER in test 40
224+
const response = await api.post('/testers', { ids: testerId });
225+
expect(response.status).toBe(200);
226+
expect(response.data.success).toBe(true);
227+
expect(response.data.data).toBeDefined();
228+
// At least one tester should match the provided id
229+
expect(response.data.total).toBeGreaterThanOrEqual(1);
230+
// Ensure that one of the returned testers owns the id provided
231+
const matching = response.data.data.find((t: any) => (t.ids || []).includes(testerId));
232+
expect(matching).toBeDefined();
233+
});
234+
235+
test('71. Should filter testers by multiple ids using POST /testers', async () => {
236+
// Compose multiple ids: the authenticated user and a known other id
237+
const otherId = 'auth0|0987654321';
238+
const response = await api.post('/testers', { ids: [testerId, otherId] });
239+
expect(response.status).toBe(200);
240+
expect(response.data.success).toBe(true);
241+
expect(response.data.data).toBeDefined();
242+
// The total should be greater than or equal to 2 (the two ids match distinct testers)
243+
expect(response.data.total).toBeGreaterThanOrEqual(2);
244+
// Each returned tester should have at least one of the provided ids
245+
for (const t of response.data.data) {
246+
const ids = t.ids || [];
247+
expect(ids.some((id: string) => id === testerId || id === otherId)).toBeTruthy();
248+
}
249+
});
250+
251+
test('72. Should paginate results when limit provided in POST /testers', async () => {
252+
// Retrieve all testers to gather test ids
253+
const all = await api.get('/testers');
254+
expect(all.data.data).toBeDefined();
255+
const allTesters = all.data.data;
256+
const itemIds: string[] = [];
257+
allTesters.forEach((t: any) => {
258+
(t.ids || []).forEach((i: string) => itemIds.push(i));
259+
});
260+
// Request with pagination limit=1 and page=1
261+
const response = await api.post('/testers', { ids: itemIds, limit: 1, page: 1 });
262+
expect(response.status).toBe(200);
263+
expect(response.data.success).toBe(true);
264+
expect(response.data.data.length).toBe(1);
265+
// total should match the number of unique testers matched by ids
266+
expect(response.data.total).toBeGreaterThanOrEqual(1);
267+
});
268+
269+
test('73. Should return all matches when limit not provided in POST /testers', async () => {
270+
// Retrieve all testers to gather test ids
271+
const all = await api.get('/testers');
272+
expect(all.data.data).toBeDefined();
273+
const allTesters = all.data.data;
274+
const itemIds: string[] = [];
275+
allTesters.forEach((t: any) => {
276+
(t.ids || []).forEach((i: string) => itemIds.push(i));
277+
});
278+
// Request without pagination
279+
const response = await api.post('/testers', { ids: itemIds });
280+
expect(response.status).toBe(200);
281+
expect(response.data.success).toBe(true);
282+
// Without limit, the entire result should be returned
283+
expect(response.data.data.length).toBe(response.data.total);
284+
expect(response.data.page).toBe(1);
285+
});
286+
222287
test('900. Should return an Auth0 management token from the system endpoint (if configured)', async () => {
223288
// Only run if Auth0 client credentials are configured in the environment
224-
const AUTH0_CLIENT_ID = process.env.AUTH0_CLIENT_ID || '';
225-
const AUTH0_CLIENT_SECRET = process.env.AUTH0_CLIENT_SECRET || '';
289+
const AUTH0_MANAGEMENT_API_CLIENT_ID = process.env.AUTH0_MANAGEMENT_API_CLIENT_ID || '';
290+
const AUTH0_MANAGEMENT_API_CLIENT_SECRET = process.env.AUTH0_MANAGEMENT_API_CLIENT_SECRET || '';
226291
const AUTH0_DOMAIN = process.env.AUTH0_DOMAIN || '';
227292

228-
if (!AUTH0_CLIENT_ID || !AUTH0_CLIENT_SECRET || !AUTH0_DOMAIN) {
293+
if (!AUTH0_MANAGEMENT_API_CLIENT_ID || !AUTH0_MANAGEMENT_API_CLIENT_SECRET || !AUTH0_DOMAIN) {
229294
// Skip this test if credentials are not present (local environment)
230295
console.warn('Skipping Auth0 management token test because AUTH0_CLIENT_* or AUTH0_DOMAIN is not set');
231296
return;
232297
}
233298

234299
// Attempt to call the system endpoint to get a management token
235-
const response = await api.post('/api/__auth0/token', {});
300+
const response = await api.post('/__auth0/token', {});
236301

237302
// If we are not permitted to call the endpoint, we can get a 403
238303
if (response.status === 403) {

0 commit comments

Comments
 (0)