feat(F-146): completed feature
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import type pg from 'pg';
|
||||
import Fastify, { type FastifyInstance } from 'fastify';
|
||||
import { AppError, errorEnvelope } from '../../../shared/errors.js';
|
||||
import { registerReportingRoutes } from './reporting.routes.js';
|
||||
@@ -30,8 +31,12 @@ async function buildApp(authenticatedUser: unknown) {
|
||||
.code(500)
|
||||
.send(errorEnvelope(500, 'INTERNAL_ERROR', 'Internal Server Error', 'test-request-id'));
|
||||
});
|
||||
const mockPool = {
|
||||
query: vi.fn().mockResolvedValue({ rows: [] }),
|
||||
} as unknown as pg.Pool;
|
||||
const deps = {
|
||||
authenticate: vi.fn().mockResolvedValue(authenticatedUser),
|
||||
pool: mockPool,
|
||||
} as unknown as ReportingRoutesDeps;
|
||||
await registerReportingRoutes(app, deps);
|
||||
await app.ready();
|
||||
@@ -137,3 +142,99 @@ describe('GET /reporting/filters/validate (REPORTING_SALES)', () => {
|
||||
expect(res.statusCode).toBe(403);
|
||||
});
|
||||
});
|
||||
|
||||
// ── F-146: Summary and Sales endpoint tests ──────────────────────────────
|
||||
|
||||
describe('GET /reporting/summary (REPORTING_SALES)', () => {
|
||||
const BASE = '/reporting/summary?from=2026-08-01T00:00:00Z&to=2026-08-31T23:59:59Z';
|
||||
|
||||
it('returns summary DTO with all required fields (AC1)', async () => {
|
||||
const { app } = await buildApp(ADMIN);
|
||||
const res = await app.inject({ method: 'GET', url: BASE });
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
const body = JSON.parse(res.body ?? '') as Record<string, unknown>;
|
||||
expect(body).toHaveProperty('range');
|
||||
expect(body).toHaveProperty('filters');
|
||||
expect(body).toHaveProperty('comparison');
|
||||
expect(body).toHaveProperty('dataAvailability');
|
||||
expect(body).toHaveProperty('totals');
|
||||
expect(body).toHaveProperty('updatedAt');
|
||||
expect(body).toHaveProperty('cache');
|
||||
});
|
||||
|
||||
it('returns correct dataAvailability flags (AC5)', async () => {
|
||||
const { app } = await buildApp(ADMIN);
|
||||
const res = await app.inject({ method: 'GET', url: BASE });
|
||||
|
||||
const body = JSON.parse(res.body ?? '') as {
|
||||
dataAvailability: Record<string, string>;
|
||||
};
|
||||
expect(body.dataAvailability.grossSales).toBe('available');
|
||||
expect(body.dataAvailability.netSales).toBe('unavailable');
|
||||
expect(body.dataAvailability.margin).toBe('unavailable');
|
||||
expect(body.dataAvailability.paymentMethod).toBe('unavailable');
|
||||
expect(body.dataAvailability.shipping).toBe('available');
|
||||
expect(body.dataAvailability.discounts).toBe('available');
|
||||
});
|
||||
|
||||
it('returns 400 on inverted dates (AC7)', async () => {
|
||||
const { app } = await buildApp(ADMIN);
|
||||
const res = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/reporting/summary?from=2026-08-31T00:00:00Z&to=2026-08-01T00:00:00Z',
|
||||
});
|
||||
expect(res.statusCode).toBe(400);
|
||||
});
|
||||
|
||||
it('customer is forbidden (RBAC, AC8)', async () => {
|
||||
const { app } = await buildApp(CUSTOMER);
|
||||
const res = await app.inject({ method: 'GET', url: BASE });
|
||||
expect(res.statusCode).toBe(403);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /reporting/sales (REPORTING_SALES)', () => {
|
||||
const BASE = '/reporting/sales?from=2026-08-01T00:00:00Z&to=2026-08-31T23:59:59Z';
|
||||
|
||||
it('returns sales DTO with items and pagination (AC2)', async () => {
|
||||
const { app } = await buildApp(ADMIN);
|
||||
const res = await app.inject({ method: 'GET', url: `${BASE}&groupBy=day` });
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
const body = JSON.parse(res.body ?? '') as Record<string, unknown>;
|
||||
expect(body).toHaveProperty('range');
|
||||
expect(body).toHaveProperty('filters');
|
||||
expect(body).toHaveProperty('items');
|
||||
expect(body).toHaveProperty('totals');
|
||||
expect(body).toHaveProperty('pagination');
|
||||
expect(body).toHaveProperty('updatedAt');
|
||||
expect(Array.isArray((body as Record<string, unknown>).items)).toBe(true);
|
||||
});
|
||||
|
||||
it('pagination fields present (AC4)', async () => {
|
||||
const { app } = await buildApp(ADMIN);
|
||||
const res = await app.inject({ method: 'GET', url: `${BASE}&groupBy=month&page=2&pageSize=20` });
|
||||
|
||||
const body = JSON.parse(res.body ?? '') as {
|
||||
pagination: { page: number; pageSize: number; totalRows: number };
|
||||
};
|
||||
expect(body.pagination.page).toBe(2);
|
||||
expect(body.pagination.pageSize).toBe(20);
|
||||
});
|
||||
|
||||
it('returns 400 on invalid channel (AC7)', async () => {
|
||||
const { app } = await buildApp(ADMIN);
|
||||
const res = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/reporting/sales?from=2026-08-01T00:00:00Z&to=2026-08-31T23:59:59Z&channel=invalid',
|
||||
});
|
||||
expect(res.statusCode).toBe(400);
|
||||
});
|
||||
|
||||
it('customer is forbidden (RBAC, AC8)', async () => {
|
||||
const { app } = await buildApp(CUSTOMER);
|
||||
const res = await app.inject({ method: 'GET', url: BASE });
|
||||
expect(res.statusCode).toBe(403);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user