mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 08:48:11 +00:00
fixes
This commit is contained in:
@@ -114,7 +114,7 @@ import { InterchangeDocumentsModule } from "./modules/interchange-documents/inte
|
||||
import { ImportOperationsModule } from "./modules/import-operations/import-operations.module";
|
||||
import { AiModule } from "./modules/ai/ai.module";
|
||||
import { AuditModule } from "./modules/audit/audit.module";
|
||||
import { LoggerMiddleware } from "./logger.middleware";
|
||||
import { RequestLogMiddleware } from "@edr/api-common";
|
||||
import { LoginAudienceMiddleware } from "./modules/auth/login-audience.middleware";
|
||||
import { PositionTypePermissionsCache } from "./common/position-type-permissions.cache";
|
||||
|
||||
@@ -392,7 +392,9 @@ export class AppModule implements OnApplicationBootstrap {
|
||||
}
|
||||
|
||||
configure(consumer: MiddlewareConsumer) {
|
||||
consumer.apply(LoggerMiddleware).forRoutes("*");
|
||||
// FIRST: opens the request log context every later middleware/guard/service
|
||||
// writes into via logCtx(). Anything applied above it logs into the void.
|
||||
consumer.apply(RequestLogMiddleware).forRoutes("*");
|
||||
consumer
|
||||
.apply(LoginAudienceMiddleware)
|
||||
.forRoutes(
|
||||
|
||||
102
apps/edr-freight-api/src/common/request-log-context.spec.ts
Normal file
102
apps/edr-freight-api/src/common/request-log-context.spec.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { Logger } from "@nestjs/common";
|
||||
import {
|
||||
RequestLogMiddleware,
|
||||
getLogContext,
|
||||
logCtx,
|
||||
runWithLogContext,
|
||||
} from "@edr/api-common";
|
||||
|
||||
describe("logCtx", () => {
|
||||
it("is a no-op outside a request", () => {
|
||||
expect(() => logCtx({ bookingId: "b1" })).not.toThrow();
|
||||
expect(getLogContext()).toBeUndefined();
|
||||
});
|
||||
|
||||
it("collects data points across the request and isolates concurrent ones", async () => {
|
||||
const collect = async (id: string) =>
|
||||
runWithLogContext({ requestId: id }, async () => {
|
||||
logCtx({ bookingId: id });
|
||||
await Promise.resolve();
|
||||
logCtx({ from: "DRAFT", to: "SUBMITTED" }, { path: "booking.status" });
|
||||
logCtx({ wagonId: "w1" }, { path: "wagons", mode: "push" });
|
||||
logCtx({ wagonId: "w2" }, { path: "wagons", mode: "push" });
|
||||
logCtx(1, { path: "smsSent", mode: "count" });
|
||||
logCtx(1, { path: "smsSent", mode: "count" });
|
||||
logCtx("PAID", { path: "payment.state", mode: "set" });
|
||||
logCtx({ ignored: true }, (ctx) => {
|
||||
ctx.custom = "yes";
|
||||
});
|
||||
return getLogContext();
|
||||
});
|
||||
|
||||
const [a, b] = await Promise.all([collect("r1"), collect("r2")]);
|
||||
|
||||
expect(a).toEqual({
|
||||
requestId: "r1",
|
||||
bookingId: "r1",
|
||||
booking: { status: { from: "DRAFT", to: "SUBMITTED" } },
|
||||
wagons: [{ wagonId: "w1" }, { wagonId: "w2" }],
|
||||
smsSent: 2,
|
||||
payment: { state: "PAID" },
|
||||
custom: "yes",
|
||||
});
|
||||
expect(b?.requestId).toBe("r2");
|
||||
expect(b?.bookingId).toBe("r2");
|
||||
});
|
||||
});
|
||||
|
||||
describe("RequestLogMiddleware", () => {
|
||||
it("emits one canonical JSON line carrying the collected context", () => {
|
||||
const lines: string[] = [];
|
||||
jest
|
||||
.spyOn(Logger.prototype, "warn")
|
||||
.mockImplementation((m) => lines.push(String(m)));
|
||||
jest.spyOn(Logger.prototype, "log").mockImplementation(() => undefined);
|
||||
|
||||
const listeners: Record<string, () => void> = {};
|
||||
const req = {
|
||||
method: "POST",
|
||||
url: "/api/bookings/1/submit",
|
||||
originalUrl: "/api/bookings/1/submit?dry=1",
|
||||
baseUrl: "/api/bookings",
|
||||
route: { path: "/:id/submit" },
|
||||
headers: { "user-agent": "jest", "x-request-id": "req-42" },
|
||||
ip: "10.0.0.1",
|
||||
query: { dry: "1" },
|
||||
user: { id: "u-7" },
|
||||
};
|
||||
const res = {
|
||||
statusCode: 409,
|
||||
writableEnded: true,
|
||||
setHeader: jest.fn(),
|
||||
on: (event: string, fn: () => void) => {
|
||||
listeners[event] = fn;
|
||||
},
|
||||
};
|
||||
|
||||
new RequestLogMiddleware().use(req, res, () => {
|
||||
logCtx({ bookingId: "b-1" });
|
||||
logCtx("REJECTED", { path: "booking.outcome", mode: "set" });
|
||||
});
|
||||
listeners.finish();
|
||||
listeners.close(); // aborts/close after finish must not double-log
|
||||
|
||||
expect(lines).toHaveLength(1);
|
||||
expect(JSON.parse(lines[0])).toMatchObject({
|
||||
type: "http_request",
|
||||
requestId: "req-42",
|
||||
method: "POST",
|
||||
route: "/api/bookings/:id/submit",
|
||||
url: "/api/bookings/1/submit?dry=1",
|
||||
status: 409,
|
||||
userId: "u-7",
|
||||
ip: "10.0.0.1",
|
||||
userAgent: "jest",
|
||||
query: { dry: "1" },
|
||||
bookingId: "b-1",
|
||||
booking: { outcome: "REJECTED" },
|
||||
});
|
||||
expect(res.setHeader).toHaveBeenCalledWith("x-request-id", "req-42");
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
});
|
||||
@@ -1,21 +0,0 @@
|
||||
import { Injectable, NestMiddleware, Logger } from "@nestjs/common";
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
|
||||
@Injectable()
|
||||
export class LoggerMiddleware implements NestMiddleware {
|
||||
private readonly logger = new Logger("HTTP");
|
||||
|
||||
use(req: Request, res: Response, next: NextFunction) {
|
||||
const start = Date.now();
|
||||
|
||||
res.on("finish", () => {
|
||||
const duration = Date.now() - start;
|
||||
|
||||
this.logger.log(
|
||||
`${req.method} ${req.originalUrl} ${res.statusCode} ${duration}ms`,
|
||||
);
|
||||
});
|
||||
|
||||
next();
|
||||
}
|
||||
}
|
||||
@@ -63,6 +63,21 @@ describe('ETradeService business selection', () => {
|
||||
expect(fetched).toEqual(['MT/AA/14/670/11551235/2017']);
|
||||
});
|
||||
|
||||
it('survives the null entries eTrade puts in SubGroups', () => {
|
||||
const { service } = build();
|
||||
const info = companyInfo();
|
||||
(info.Businesses[0] as any).SubGroups = [
|
||||
null,
|
||||
{ Code: 66331, Description: 'Export trade in minerals' },
|
||||
{ Code: 1, Description: null },
|
||||
];
|
||||
const data = service.extractRegistrationData(
|
||||
{ LicenceNumber: 'x' } as ETradeBusinessInfo,
|
||||
info,
|
||||
);
|
||||
expect(data.businesses?.[0].activity).toBe('Export trade in minerals');
|
||||
});
|
||||
|
||||
it('lists every licence for the picker, code prefixes stripped', () => {
|
||||
const { service } = build();
|
||||
const data = service.extractRegistrationData(
|
||||
|
||||
@@ -142,7 +142,8 @@ export class ETradeService {
|
||||
tradeName: b.TradesName?.trim() || "",
|
||||
activity: (b.SubGroups ?? [])
|
||||
// Some descriptions repeat the code inline ("(65611)Import trade …").
|
||||
.map((g) => g.Description?.replace(/^\(\d+\)\s*/, "").trim())
|
||||
// eTrade also puts null entries in this array, so every hop is optional.
|
||||
.map((g) => g?.Description?.replace(/^\(\d+\)\s*/, "").trim())
|
||||
.filter(Boolean)
|
||||
.join(", "),
|
||||
renewedTo: b.RenewedTo || "",
|
||||
|
||||
Reference in New Issue
Block a user