forked from Yara724/api
193 lines
5.7 KiB
TypeScript
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);
|
|
});
|
|
});
|