fanavaran duplication request problems fixed.

This commit is contained in:
2026-08-02 17:00:19 +03:30
parent c2f5c576fa
commit b345818d43
9 changed files with 884 additions and 123 deletions

View File

@@ -1,6 +1,43 @@
import { FanavaranAuthService } from "./fanavaran-auth.service";
describe("FanavaranAuthService", () => {
const createAuthTokenModel = () => {
const store = new Map<
string,
{ clientKey: string; authenticationToken: string; expiresAt: Date }
>();
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 } },
) => ({
exec: async () => {
const next = {
clientKey: query.clientKey,
authenticationToken: update.$set.authenticationToken,
expiresAt: update.$set.expiresAt,
};
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(
@@ -24,22 +61,11 @@ describe("FanavaranAuthService", () => {
e instanceof Error ? e.message : String(e),
sanitizeErrorDetails: () => ({}),
formatErrorWithTrackingCode: (m: string) => m,
captureAxiosExchange: () => ({}),
};
const authTokenModel = createAuthTokenModel();
let loginCalls = 0;
http.post.mockImplementation((url: string) => {
if (url.includes("GetAppToken")) {
return {
toPromise: undefined,
pipe: undefined,
subscribe: undefined,
// firstValueFrom uses Observable — mock as Observable-like via rxjs
};
}
return {};
});
// Use real firstValueFrom path by mocking httpService.post to return an Observable
const { of, delay } = await import("rxjs");
http.post.mockImplementation((url: string) => {
if (url.includes("GetAppToken")) {
@@ -57,7 +83,11 @@ describe("FanavaranAuthService", () => {
}).pipe(delay(20));
});
const service = new FanavaranAuthService(http as any, audit as any);
const service = new FanavaranAuthService(
http as any,
audit as any,
authTokenModel as any,
);
const [a, b, c] = await Promise.all([
service.getAuthenticationToken("parsian"),
@@ -75,28 +105,74 @@ describe("FanavaranAuthService", () => {
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);
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,
);
expect(() => service.assertNotInBackoff("parsian")).toThrow(/backoff/i);
});
it("computes next Asia/Tehran midnight expiry after now", () => {
// 2026-08-02 10:00:00 UTC ≈ 13:30 Tehran (UTC+3:30) → same calendar day midnight
const now = Date.parse("2026-08-02T10:00:00.000Z");
const expiry = FanavaranAuthService.getNextMidnightExpiryMs(now);
expect(expiry).toBeGreaterThan(now);
// Must land within ~14h (before next Tehran midnight)
expect(expiry - now).toBeLessThanOrEqual(24 * 60 * 60 * 1000);
expect(expiry - now).toBeGreaterThan(0);
// Just after Tehran midnight: 2026-08-01 20:30:01 UTC = 2026-08-02 00:00:01 Tehran
const justAfterMidnight = Date.parse("2026-08-01T20:30:01.000Z");
const next = FanavaranAuthService.getNextMidnightExpiryMs(justAfterMidnight);
expect(next - justAfterMidnight).toBeGreaterThan(23 * 60 * 60 * 1000);