Files
yara724-api/src/fanavaran/fanavaran-auth.service.spec.ts

193 lines
5.7 KiB
TypeScript

import { FanavaranAuthService } from "./fanavaran-auth.service";
describe("FanavaranAuthService", () => {
const createAuthTokenModel = () => {
const store = new Map<
string,
{
clientKey: string;
authenticationToken: string;
expiresAt: Date;
authFingerprint?: string;
}
>();
return {
findOne: jest.fn((query: { clientKey: string }) => ({
lean: () => ({
exec: async () => store.get(query.clientKey) ?? null,
}),
})),
findOneAndUpdate: jest.fn(
(
query: { clientKey: string },
update: {
$set: {
authenticationToken: string;
expiresAt: Date;
authFingerprint?: string;
};
},
) => ({
exec: async () => {
const next = {
clientKey: query.clientKey,
authenticationToken: update.$set.authenticationToken,
expiresAt: update.$set.expiresAt,
authFingerprint: update.$set.authFingerprint,
};
store.set(query.clientKey, next);
return next;
},
}),
),
deleteOne: jest.fn((query: { clientKey: string }) => ({
exec: async () => {
store.delete(query.clientKey);
return { deletedCount: 1 };
},
})),
_store: store,
};
};
it("detects Fanavaran transient try-later messages", () => {
expect(
FanavaranAuthService.isTransientTryLaterError(
"کد پیگیری خطا: 10573755\r\n1405/05/1016:56:16:276\r\n.لطفا پس از چند لحظه مجدد تلاش فرمایید.",
),
).toBe(true);
expect(
FanavaranAuthService.isTransientTryLaterError(
"فیلد شماره بيمه نامه ضروری میباشد",
),
).toBe(false);
});
it("caches token and single-flights concurrent logins", async () => {
const http = {
post: jest.fn(),
};
const audit = {
recordStep: jest.fn().mockResolvedValue(undefined),
extractErrorMessage: (e: unknown) =>
e instanceof Error ? e.message : String(e),
sanitizeErrorDetails: () => ({}),
formatErrorWithTrackingCode: (m: string) => m,
captureAxiosExchange: () => ({}),
};
const authTokenModel = createAuthTokenModel();
let loginCalls = 0;
const { of, delay } = await import("rxjs");
http.post.mockImplementation((url: string) => {
if (url.includes("GetAppToken")) {
return of({
status: 200,
headers: { apptoken: "app-1" },
data: {},
});
}
loginCalls += 1;
return of({
status: 200,
headers: { authenticationtoken: "auth-1" },
data: {},
}).pipe(delay(20));
});
const service = new FanavaranAuthService(
http as any,
audit as any,
authTokenModel as any,
);
const [a, b, c] = await Promise.all([
service.getAuthenticationToken("parsian"),
service.getAuthenticationToken("parsian"),
service.getAuthenticationToken("parsian"),
]);
expect(a).toBe("auth-1");
expect(b).toBe("auth-1");
expect(c).toBe("auth-1");
expect(loginCalls).toBe(1);
// Cache hit — no extra login
await service.getAuthenticationToken("parsian");
expect(loginCalls).toBe(1);
});
it("reuses persisted token across service instances", async () => {
const http = { post: jest.fn() };
const audit = {
recordStep: jest.fn().mockResolvedValue(undefined),
extractErrorMessage: (e: unknown) =>
e instanceof Error ? e.message : String(e),
sanitizeErrorDetails: () => ({}),
formatErrorWithTrackingCode: (m: string) => m,
captureAxiosExchange: () => ({}),
};
const authTokenModel = createAuthTokenModel();
const { of } = await import("rxjs");
let loginCalls = 0;
http.post.mockImplementation((url: string) => {
if (url.includes("GetAppToken")) {
return of({
status: 200,
headers: { apptoken: "app-1" },
data: {},
});
}
loginCalls += 1;
return of({
status: 200,
headers: { authenticationtoken: "auth-persisted" },
data: {},
});
});
const first = new FanavaranAuthService(
http as any,
audit as any,
authTokenModel as any,
);
await first.getAuthenticationToken("tejaratno");
expect(loginCalls).toBe(1);
const second = new FanavaranAuthService(
http as any,
audit as any,
authTokenModel as any,
);
const token = await second.getAuthenticationToken("tejaratno");
expect(token).toBe("auth-persisted");
expect(loginCalls).toBe(1);
});
it("enters tenant backoff on try-later errors", () => {
const service = new FanavaranAuthService(
{} as any,
{} as any,
createAuthTokenModel() as any,
);
service.registerFailure(
"parsian",
"کد پیگیری خطا: 1\r\n.لطفا پس از چند لحظه مجدد تلاش فرمایید.",
);
expect(service.isInBackoff("parsian")).toBe(true);
expect(() => service.assertNotInBackoff("parsian")).toThrow(/backoff/i);
});
it("computes next Asia/Tehran midnight expiry after now", () => {
const now = Date.parse("2026-08-02T10:00:00.000Z");
const expiry = FanavaranAuthService.getNextMidnightExpiryMs(now);
expect(expiry).toBeGreaterThan(now);
expect(expiry - now).toBeLessThanOrEqual(24 * 60 * 60 * 1000);
expect(expiry - now).toBeGreaterThan(0);
const justAfterMidnight = Date.parse("2026-08-01T20:30:01.000Z");
const next = FanavaranAuthService.getNextMidnightExpiryMs(justAfterMidnight);
expect(next - justAfterMidnight).toBeGreaterThan(23 * 60 * 60 * 1000);
});
});