feat(pagination): add generic typed pagination with ordering to all list endpoints
- add PaginationQueryDto<TOrderBy> generic with orderBy and orderDir fields - add SessionOrderBy, EnvironmentOrderBy, ScenarioOrderBy type aliases - update session, environment, scenario services and controllers to use typed pagination - update MCP list_sessions, list_environments, list_scenarios tools to expose orderBy/orderDir - update tests for all list endpoints to cover page, limit, ordering, and invalid param rejection
This commit is contained in:
@@ -64,9 +64,51 @@ describe('EnvironmentController', () => {
|
||||
// ── GET /environments ──────────────────────────────────────────────────────
|
||||
|
||||
describe('GET /environments', () => {
|
||||
it('returns 200 with an array', async () => {
|
||||
it('returns 200 with a paginated result', async () => {
|
||||
const res = await request(app.getHttpServer()).get('/environments').expect(200);
|
||||
expect(Array.isArray(res.body)).toBe(true);
|
||||
expect(Array.isArray(res.body.data)).toBe(true);
|
||||
expect(typeof res.body.total).toBe('number');
|
||||
expect(res.body).toHaveProperty('page');
|
||||
expect(res.body).toHaveProperty('limit');
|
||||
});
|
||||
|
||||
it('respects page and limit params', async () => {
|
||||
// seed two extra environments
|
||||
await request(app.getHttpServer()).post('/environments').send({ name: 'env-page-1', urls: {} }).expect(201);
|
||||
await request(app.getHttpServer()).post('/environments').send({ name: 'env-page-2', urls: {} }).expect(201);
|
||||
|
||||
const res = await request(app.getHttpServer()).get('/environments?page=1&limit=1').expect(200);
|
||||
expect(res.body.data).toHaveLength(1);
|
||||
expect(res.body.page).toBe(1);
|
||||
expect(res.body.limit).toBe(1);
|
||||
});
|
||||
|
||||
it('returns empty data array for out-of-range page', async () => {
|
||||
const res = await request(app.getHttpServer()).get('/environments?page=9999&limit=20').expect(200);
|
||||
expect(res.body.data).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('returns 400 for invalid page param', async () => {
|
||||
await request(app.getHttpServer()).get('/environments?page=0').expect(400);
|
||||
});
|
||||
|
||||
it('orders by name ASC', async () => {
|
||||
await request(app.getHttpServer()).post('/environments').send({ name: 'zzz-env', urls: {} });
|
||||
await request(app.getHttpServer()).post('/environments').send({ name: 'aaa-env', urls: {} });
|
||||
|
||||
const res = await request(app.getHttpServer()).get('/environments?orderBy=name&orderDir=ASC').expect(200);
|
||||
const names: string[] = res.body.data.map((e: any) => e.name);
|
||||
expect(names).toEqual([...names].sort());
|
||||
});
|
||||
|
||||
it('orders by name DESC', async () => {
|
||||
const res = await request(app.getHttpServer()).get('/environments?orderBy=name&orderDir=DESC').expect(200);
|
||||
const names: string[] = res.body.data.map((e: any) => e.name);
|
||||
expect(names).toEqual([...names].sort().reverse());
|
||||
});
|
||||
|
||||
it('returns 400 for invalid orderDir', async () => {
|
||||
await request(app.getHttpServer()).get('/environments?orderDir=SIDEWAYS').expect(400);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -83,22 +83,26 @@ describe('McpController', () => {
|
||||
// ── list_sessions tool ─────────────────────────────────────────────────────
|
||||
|
||||
describe('list_sessions', () => {
|
||||
it('returns a result with text content containing a JSON array', async () => {
|
||||
it('returns a paginated result with a data array', async () => {
|
||||
const { status, rpc } = await mcpCall('list_sessions');
|
||||
expect(status).toBe(200);
|
||||
const result = rpc.result as { content: { text: string }[] };
|
||||
expect(Array.isArray(JSON.parse(result.content[0].text))).toBe(true);
|
||||
const body = JSON.parse(result.content[0].text) as { data: unknown[]; total: number };
|
||||
expect(Array.isArray(body.data)).toBe(true);
|
||||
expect(typeof body.total).toBe('number');
|
||||
});
|
||||
});
|
||||
|
||||
// ── list_environments tool ─────────────────────────────────────────────────
|
||||
|
||||
describe('list_environments', () => {
|
||||
it('returns a result with text content containing a JSON array', async () => {
|
||||
it('returns a paginated result with a data array', async () => {
|
||||
const { status, rpc } = await mcpCall('list_environments');
|
||||
expect(status).toBe(200);
|
||||
const result = rpc.result as { content: { text: string }[] };
|
||||
expect(Array.isArray(JSON.parse(result.content[0].text))).toBe(true);
|
||||
const body = JSON.parse(result.content[0].text) as { data: unknown[]; total: number };
|
||||
expect(Array.isArray(body.data)).toBe(true);
|
||||
expect(typeof body.total).toBe('number');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -68,16 +68,44 @@ describe('ScenarioController', () => {
|
||||
});
|
||||
|
||||
it('respects page and limit params', async () => {
|
||||
await createScenario('paged-sc-a');
|
||||
await createScenario('paged-sc-b');
|
||||
|
||||
const res = await request(app.getHttpServer())
|
||||
.get('/scenarios?page=1&limit=2')
|
||||
.get('/scenarios?page=1&limit=1')
|
||||
.expect(200);
|
||||
expect(res.body.limit).toBe(2);
|
||||
expect(res.body.data).toHaveLength(1);
|
||||
expect(res.body.limit).toBe(1);
|
||||
expect(res.body.page).toBe(1);
|
||||
});
|
||||
|
||||
it('returns empty data for out-of-range page', async () => {
|
||||
const res = await request(app.getHttpServer()).get('/scenarios?page=9999&limit=20').expect(200);
|
||||
expect(res.body.data).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('returns 400 for invalid pagination params', async () => {
|
||||
await request(app.getHttpServer()).get('/scenarios?page=0').expect(400);
|
||||
});
|
||||
|
||||
it('orders by name ASC', async () => {
|
||||
await createScenario('zzz-order-sc');
|
||||
await createScenario('aaa-order-sc');
|
||||
|
||||
const res = await request(app.getHttpServer()).get('/scenarios?orderBy=name&orderDir=ASC').expect(200);
|
||||
const names: string[] = res.body.data.map((s: any) => s.name);
|
||||
expect(names).toEqual([...names].sort());
|
||||
});
|
||||
|
||||
it('orders by name DESC', async () => {
|
||||
const res = await request(app.getHttpServer()).get('/scenarios?orderBy=name&orderDir=DESC').expect(200);
|
||||
const names: string[] = res.body.data.map((s: any) => s.name);
|
||||
expect(names).toEqual([...names].sort().reverse());
|
||||
});
|
||||
|
||||
it('returns 400 for invalid orderDir', async () => {
|
||||
await request(app.getHttpServer()).get('/scenarios?orderDir=SIDEWAYS').expect(400);
|
||||
});
|
||||
});
|
||||
|
||||
// ── GET /scenarios/:id ─────────────────────────────────────────────────────
|
||||
|
||||
@@ -32,27 +32,68 @@ describe('SessionController', () => {
|
||||
// ── GET /sessions ──────────────────────────────────────────────────────────
|
||||
|
||||
describe('GET /sessions', () => {
|
||||
it('returns 200 with an array', async () => {
|
||||
it('returns 200 with a paginated result', async () => {
|
||||
const res = await request(app.getHttpServer()).get('/sessions').expect(200);
|
||||
expect(Array.isArray(res.body)).toBe(true);
|
||||
expect(Array.isArray(res.body.data)).toBe(true);
|
||||
expect(typeof res.body.total).toBe('number');
|
||||
expect(res.body).toHaveProperty('page');
|
||||
expect(res.body).toHaveProperty('limit');
|
||||
});
|
||||
|
||||
it('includes seeded sessions', async () => {
|
||||
await seedSession('visible-session');
|
||||
const res = await request(app.getHttpServer()).get('/sessions').expect(200);
|
||||
const names = res.body.map((s: any) => s.sessionName);
|
||||
const names = res.body.data.map((s: any) => s.sessionName);
|
||||
expect(names).toContain('visible-session');
|
||||
});
|
||||
|
||||
it('does not expose token, cookies or localStorage fields', async () => {
|
||||
await seedSession('private-session');
|
||||
const res = await request(app.getHttpServer()).get('/sessions').expect(200);
|
||||
const item = res.body.find((s: any) => s.sessionName === 'private-session');
|
||||
const item = res.body.data.find((s: any) => s.sessionName === 'private-session');
|
||||
expect(item).toBeDefined();
|
||||
expect(item.token).toBeUndefined();
|
||||
expect(item.cookies).toBeUndefined();
|
||||
expect(item.localStorage).toBeUndefined();
|
||||
});
|
||||
|
||||
it('respects page and limit params', async () => {
|
||||
await seedSession('paged-session-a');
|
||||
await seedSession('paged-session-b');
|
||||
|
||||
const res = await request(app.getHttpServer()).get('/sessions?page=1&limit=1').expect(200);
|
||||
expect(res.body.data).toHaveLength(1);
|
||||
expect(res.body.page).toBe(1);
|
||||
expect(res.body.limit).toBe(1);
|
||||
});
|
||||
|
||||
it('returns empty data array for out-of-range page', async () => {
|
||||
const res = await request(app.getHttpServer()).get('/sessions?page=9999&limit=20').expect(200);
|
||||
expect(res.body.data).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('returns 400 for invalid page param', async () => {
|
||||
await request(app.getHttpServer()).get('/sessions?page=0').expect(400);
|
||||
});
|
||||
|
||||
it('orders by sessionName ASC', async () => {
|
||||
await seedSession('zzz-sort-session');
|
||||
await seedSession('aaa-sort-session');
|
||||
|
||||
const res = await request(app.getHttpServer()).get('/sessions?orderBy=sessionName&orderDir=ASC').expect(200);
|
||||
const names: string[] = res.body.data.map((s: any) => s.sessionName);
|
||||
expect(names).toEqual([...names].sort());
|
||||
});
|
||||
|
||||
it('orders by sessionName DESC', async () => {
|
||||
const res = await request(app.getHttpServer()).get('/sessions?orderBy=sessionName&orderDir=DESC').expect(200);
|
||||
const names: string[] = res.body.data.map((s: any) => s.sessionName);
|
||||
expect(names).toEqual([...names].sort().reverse());
|
||||
});
|
||||
|
||||
it('returns 400 for invalid orderDir', async () => {
|
||||
await request(app.getHttpServer()).get('/sessions?orderDir=SIDEWAYS').expect(400);
|
||||
});
|
||||
});
|
||||
|
||||
// ── DELETE /sessions/:id ───────────────────────────────────────────────────
|
||||
@@ -63,7 +104,7 @@ describe('SessionController', () => {
|
||||
await request(app.getHttpServer()).delete(`/sessions/${s.id}`).expect(200);
|
||||
|
||||
const res = await request(app.getHttpServer()).get('/sessions').expect(200);
|
||||
const names = res.body.map((sess: any) => sess.sessionName);
|
||||
const names = res.body.data.map((sess: any) => sess.sessionName);
|
||||
expect(names).not.toContain('delete-me-session');
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user