import axios, { AxiosAdapter, AxiosResponse } from "axios"; import { describe, expect, it } from "vitest"; import { SapB1Client } from "../src/client/SapB1Client"; import { SapB1Config } from "../src/config"; const config: SapB1Config = { baseUrl: "https://sap.example.local:50000", companyDB: "SBODEMOUS", username: "manager", password: "secret", timeout: 30_000, rejectUnauthorized: true, retryCount: 1, retryDelayMs: 0 }; function response(data: unknown, requestConfig: Parameters[0], status = 200): AxiosResponse { return { data, status, statusText: "OK", headers: {}, config: requestConfig }; } describe("SapB1Client", () => { it("adds session cookies to resource requests", async () => { const urls: string[] = []; const cookies: unknown[] = []; const http = axios.create({ adapter: async (requestConfig) => { urls.push(String(requestConfig.url)); cookies.push(requestConfig.headers?.Cookie); if (requestConfig.url === "/Login") { return { ...response({ SessionId: "abc123", SessionTimeout: 30 }, requestConfig), headers: { "set-cookie": ["B1SESSION=abc123; Path=/b1s/v1", "ROUTEID=.node1; Path=/b1s/v1"] } }; } return response({ value: [{ CardCode: "C001" }] }, requestConfig); } }); const client = new SapB1Client(config, { http }); const result = await client.get("BusinessPartners", { top: 1 }); expect(result).toEqual({ value: [{ CardCode: "C001" }] }); expect(urls).toEqual(["/Login", "BusinessPartners"]); expect(cookies[1]).toBe("B1SESSION=abc123; ROUTEID=.node1"); }); it("re-logins once after a 401 response", async () => { let resourceCalls = 0; let loginCalls = 0; const http = axios.create({ adapter: async (requestConfig) => { if (requestConfig.url === "/Login") { loginCalls += 1; return { ...response({ SessionId: `session-${loginCalls}`, SessionTimeout: 30 }, requestConfig), headers: { "set-cookie": [`B1SESSION=session-${loginCalls}; Path=/b1s/v1`] } }; } resourceCalls += 1; if (resourceCalls === 1) { return Promise.reject({ isAxiosError: true, message: "Unauthorized", response: { status: 401, data: { error: { message: { value: "Session expired" } } } } }); } return response({ value: [] }, requestConfig); } }); const client = new SapB1Client(config, { http }); await expect(client.get("Items")).resolves.toEqual({ value: [] }); expect(loginCalls).toBe(2); expect(resourceCalls).toBe(2); }); it("re-logins after a 401 even when retries are disabled", async () => { let resourceCalls = 0; let loginCalls = 0; const http = axios.create({ adapter: async (requestConfig) => { if (requestConfig.url === "/Login") { loginCalls += 1; return { ...response({ SessionId: `session-${loginCalls}`, SessionTimeout: 30 }, requestConfig), headers: { "set-cookie": [`B1SESSION=session-${loginCalls}; Path=/b1s/v1`] } }; } resourceCalls += 1; if (resourceCalls === 1) { return Promise.reject({ isAxiosError: true, message: "Unauthorized", response: { status: 401, data: { error: { message: { value: "Session expired" } } } } }); } return response({ value: [] }, requestConfig); } }); const client = new SapB1Client({ ...config, retryCount: 0 }, { http }); await expect(client.get("Items")).resolves.toEqual({ value: [] }); expect(loginCalls).toBe(2); expect(resourceCalls).toBe(2); }); it("follows an absolute nextLink while preserving its query string", async () => { const requestedUrls: string[] = []; const http = axios.create({ adapter: async (requestConfig) => { if (requestConfig.url === "/Login") { return { ...response({ SessionId: "abc123", SessionTimeout: 30 }, requestConfig), headers: { "set-cookie": ["B1SESSION=abc123; Path=/b1s/v1"] } }; } requestedUrls.push(String(requestConfig.url)); if (requestConfig.url === "Items") { return response( { value: [{ ItemCode: "A1" }], "odata.nextLink": "https://sap.example.local:50000/b1s/v1/Items?$skip=20" }, requestConfig ); } return response({ value: [{ ItemCode: "A2" }] }, requestConfig); } }); const client = new SapB1Client(config, { http }); const all = await client.getAll<{ ItemCode: string }>("Items"); expect(all).toEqual([{ ItemCode: "A1" }, { ItemCode: "A2" }]); expect(requestedUrls).toEqual(["Items", "Items?$skip=20"]); }); });