This commit is contained in:
JiriUhlir
2026-06-29 09:47:05 +02:00
parent 60cb139a33
commit 9c08dacc69
29 changed files with 3703 additions and 23 deletions
+63
View File
@@ -0,0 +1,63 @@
import { describe, expect, it, vi } from "vitest";
import { SessionManager } from "../src/auth/SessionManager";
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: 2,
retryDelayMs: 0
};
describe("SessionManager", () => {
it("logs in and stores B1SESSION and ROUTEID cookies", async () => {
const http = {
post: vi.fn().mockResolvedValue({
data: { SessionId: "fallback", SessionTimeout: 30 },
headers: {
"set-cookie": ["B1SESSION=abc123; Path=/b1s/v1", "ROUTEID=.node1; Path=/b1s/v1"]
}
})
};
const sessionManager = new SessionManager(http as never, config);
const session = await sessionManager.getSession();
expect(http.post).toHaveBeenCalledWith(
"/Login",
{
CompanyDB: "SBODEMOUS",
UserName: "manager",
Password: "secret",
Language: undefined
},
{ skipAuth: true }
);
expect(session.b1session).toBe("abc123");
expect(session.routeId).toBe(".node1");
expect(sessionManager.buildCookieHeader(session)).toBe("B1SESSION=abc123; ROUTEID=.node1");
});
it("logs out and clears the current session", async () => {
const http = {
post: vi
.fn()
.mockResolvedValueOnce({
data: { SessionId: "abc123" },
headers: {}
})
.mockResolvedValueOnce({ data: {}, headers: {} })
};
const sessionManager = new SessionManager(http as never, config);
await sessionManager.getSession();
await sessionManager.logout();
expect(http.post).toHaveBeenLastCalledWith("/Logout", undefined, { skipAuth: false });
expect(sessionManager.currentSession).toBeUndefined();
});
});
+87
View File
@@ -0,0 +1,87 @@
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<AxiosAdapter>[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);
});
});
+27
View File
@@ -0,0 +1,27 @@
import { describe, expect, it } from "vitest";
import { buildODataParams, extractNextLink } from "../src/client/odata";
describe("OData helpers", () => {
it("builds SAP Service Layer OData query parameters", () => {
expect(
buildODataParams({
select: ["CardCode", "CardName"],
filter: "CardType eq 'cCustomer'",
top: 25,
skip: 50,
orderby: ["CardName asc", "CardCode desc"]
})
).toEqual({
$select: "CardCode,CardName",
$filter: "CardType eq 'cCustomer'",
$top: 25,
$skip: 50,
$orderby: "CardName asc,CardCode desc"
});
});
it("supports old and new nextLink field names", () => {
expect(extractNextLink({ "@odata.nextLink": "Items?$skip=20" })).toBe("Items?$skip=20");
expect(extractNextLink({ "odata.nextLink": "Items?$skip=40" })).toBe("Items?$skip=40");
});
});
+22
View File
@@ -0,0 +1,22 @@
import { describe, expect, it, vi } from "vitest";
import { BusinessPartnersResource } from "../src/resources/BusinessPartners";
import { OrdersResource } from "../src/resources/Documents";
describe("resources", () => {
it("formats string keys for SAP Service Layer entity URLs", async () => {
const client = {
get: vi.fn().mockResolvedValue({ CardCode: "C'001" })
};
const resource = new BusinessPartnersResource(client as never);
await resource.get("C'001");
expect(client.get).toHaveBeenCalledWith("BusinessPartners('C''001')", undefined, expect.anything());
});
it("blocks delete for marketing documents by default", async () => {
const resource = new OrdersResource({} as never);
await expect(() => resource.delete(1)).toThrow("Orders does not support delete through this connector");
});
});