mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-05 18:53:38 +00:00
169 lines
6.4 KiB
TypeScript
169 lines
6.4 KiB
TypeScript
import { HttpService } from "@nestjs/axios";
|
|
import { Logger } from "@nestjs/common";
|
|
import { ConfigService } from "@nestjs/config";
|
|
import { AxiosError, AxiosHeaders } from "axios";
|
|
import { of, throwError } from "rxjs";
|
|
|
|
import { EimsConfig } from "../../config/eims.config";
|
|
import { EimsAuthService } from "./eims-auth.service";
|
|
import { EimsClientService } from "./eims-client.service";
|
|
import { EimsSignerService } from "./eims-signer.service";
|
|
import { eimsConfig } from "./eims-test-fixtures";
|
|
|
|
const API_KEY = "super-secret-apikey";
|
|
const CLIENT_SECRET = "super-secret-value";
|
|
const TOKEN = "access-token-value";
|
|
|
|
/** Stub signer: the real signing path has its own spec and needs no key material here. */
|
|
const signer = {
|
|
signRequest: <T>(request: T) => ({ request, signature: "SIGNATURE", certificate: "CERTIFICATE" }),
|
|
} as unknown as EimsSignerService;
|
|
|
|
const build = (post: jest.Mock, config: EimsConfig = eimsConfig(), token: string = TOKEN) =>
|
|
new EimsClientService(
|
|
{ post } as unknown as HttpService,
|
|
{ get: () => config } as unknown as ConfigService,
|
|
{
|
|
getValidAccessToken: jest.fn().mockResolvedValue(token),
|
|
invalidate: jest.fn(),
|
|
} as unknown as EimsAuthService,
|
|
signer,
|
|
);
|
|
|
|
const ok = (data: unknown = { statusCode: 200, body: { Irn: "irn-echoed" } }) =>
|
|
jest.fn().mockReturnValue(of({ data }));
|
|
|
|
const axiosErr = (status: number, data: unknown) =>
|
|
new AxiosError("Request failed", undefined, undefined, undefined, {
|
|
status,
|
|
statusText: "",
|
|
data,
|
|
headers: new AxiosHeaders(),
|
|
config: { headers: new AxiosHeaders() },
|
|
});
|
|
|
|
/** `post(url, body, config)` — the config argument every assertion below reads. */
|
|
const sentConfig = (post: jest.Mock, call = 0) => post.mock.calls[call][2];
|
|
const sentBody = (post: jest.Mock, call = 0) => post.mock.calls[call][1];
|
|
|
|
describe("EimsClientService transport", () => {
|
|
const protectedHeaders = {
|
|
"Content-Type": "application/json",
|
|
Authorization: `Bearer ${TOKEN}`,
|
|
apikey: API_KEY,
|
|
};
|
|
|
|
it.each([
|
|
["verify", "/v1/verify", { irn: "irn-1" }],
|
|
["sales receipt", "/v1/receipt/sales", { receipt: "sales" }],
|
|
["withholding receipt", "/v1/receipt/withholding", { receipt: "withholding" }],
|
|
["cancel", "/v1/cancel", { Irn: "irn-1" }],
|
|
["bulk cancel", "/v1/bulkCancel", [{ Irn: "irn-1" }]],
|
|
])("authenticates the raw %s endpoint without changing its body", async (_name, path, body) => {
|
|
const post = ok();
|
|
await build(post).postBearer(path, body);
|
|
|
|
expect(sentConfig(post).headers).toEqual(protectedHeaders);
|
|
expect(sentBody(post)).toBe(body);
|
|
});
|
|
|
|
it.each([
|
|
["invoice", { DocumentDetails: { Type: "INV" } }],
|
|
["credit memo", { DocumentDetails: { Type: "CRE" } }],
|
|
["debit memo", { DocumentDetails: { Type: "DEB" } }],
|
|
])("authenticates and signs a %s registration", async (_name, request) => {
|
|
const post = ok({ statusCode: 200, body: { irn: "irn-1" } });
|
|
await build(post).postSigned("/v1/register", request);
|
|
|
|
expect(sentConfig(post).headers).toEqual(protectedHeaders);
|
|
expect(JSON.parse(sentBody(post) as string)).toEqual({
|
|
request,
|
|
signature: "SIGNATURE",
|
|
certificate: "CERTIFICATE",
|
|
});
|
|
});
|
|
|
|
it("authenticates bulk registration through the same signed path", async () => {
|
|
const post = ok({ conversationId: "conversation-1", status: 202 });
|
|
const request = [{ DocumentDetails: { Type: "INV" } }];
|
|
await build(post).postSigned("/v1/bulkRegister", request);
|
|
|
|
expect(sentConfig(post).headers).toEqual(protectedHeaders);
|
|
});
|
|
|
|
it("wraps a signed call in the {request,signature,certificate} envelope", async () => {
|
|
const post = ok({ statusCode: 200, body: { irn: "irn-1" } });
|
|
await build(post).postSigned("/v1/register", { Invoice: 1 });
|
|
|
|
expect(JSON.parse(sentBody(post) as string)).toEqual({
|
|
request: { Invoice: 1 },
|
|
signature: "SIGNATURE",
|
|
certificate: "CERTIFICATE",
|
|
});
|
|
});
|
|
|
|
it("leaves an unsigned body verbatim", async () => {
|
|
const post = ok();
|
|
await build(post).postBearer("/v1/cancel", { Irn: "irn-1" });
|
|
|
|
// Raw object, not the JSON string `toSignedBody` produces.
|
|
expect(sentBody(post)).toEqual({ Irn: "irn-1" });
|
|
});
|
|
|
|
it("re-authenticates a raw verify call through the one 401 retry without changing its body", async () => {
|
|
const post = jest
|
|
.fn()
|
|
.mockReturnValueOnce(throwError(() => axiosErr(401, { message: "expired" })))
|
|
.mockReturnValueOnce(of({ data: { statusCode: 200, body: { Irn: "irn-1" } } }));
|
|
|
|
await build(post).postBearer("/v1/verify", { irn: "irn-1" });
|
|
|
|
expect(post).toHaveBeenCalledTimes(2);
|
|
expect(sentConfig(post, 1).headers.Authorization).toBe(`Bearer ${TOKEN}`);
|
|
expect(sentBody(post, 1)).toEqual({ irn: "irn-1" });
|
|
});
|
|
|
|
it("never leaks the api key, bearer token or client secret into a thrown failure", async () => {
|
|
const logError = jest.spyOn(Logger.prototype, "error").mockImplementation(() => undefined);
|
|
const post = jest.fn().mockReturnValue(
|
|
throwError(() =>
|
|
// A gateway rejection may echo request data; redaction must remove it before logging.
|
|
axiosErr(400, {
|
|
message: "GATEWAY ERROR",
|
|
code: "4001",
|
|
details: [
|
|
{ field: "certificate", errorMessage: "must not be null" },
|
|
{ field: "signature", errorMessage: "must not be null" },
|
|
{ field: "request", errorMessage: "must not be null" },
|
|
],
|
|
// An echoed request is exactly what redaction has to drop.
|
|
request: { apikey: API_KEY, clientSecret: CLIENT_SECRET },
|
|
}),
|
|
),
|
|
);
|
|
|
|
const error: Error = await build(post)
|
|
.postBearer("/v1/verify", { irn: "irn-1" })
|
|
.then(() => {
|
|
throw new Error("expected the call to reject");
|
|
})
|
|
.catch((err: Error) => err);
|
|
|
|
const serialized = JSON.stringify({
|
|
message: error.message,
|
|
response: (error as { getResponse?: () => unknown }).getResponse?.(),
|
|
details: (error as { details?: unknown }).details,
|
|
});
|
|
expect(serialized).not.toContain(API_KEY);
|
|
expect(serialized).not.toContain(CLIENT_SECRET);
|
|
expect(serialized).not.toContain(TOKEN);
|
|
const serializedLogs = JSON.stringify(logError.mock.calls);
|
|
expect(serializedLogs).not.toContain(API_KEY);
|
|
expect(serializedLogs).not.toContain(CLIENT_SECRET);
|
|
expect(serializedLogs).not.toContain(TOKEN);
|
|
// The gateway's own reporting still survives redaction.
|
|
expect(error.message).toContain("4001");
|
|
logError.mockRestore();
|
|
});
|
|
});
|