This commit is contained in:
JiriUhlir
2026-07-15 09:03:28 +02:00
parent 4b98f37407
commit fea25d7429
31 changed files with 4906 additions and 30 deletions
+123
View File
@@ -0,0 +1,123 @@
import { AxiosError } from "axios";
import { describe, expect, it, vi } from "vitest";
import { SessionManager } from "../src/auth/SessionManager";
import { SapB1Config } from "../src/config";
import { SapB1Error } from "../src/errors/SapB1Error";
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("sends the language as an integer code to the Login action", async () => {
const http = {
post: vi.fn().mockResolvedValue({
data: { SessionId: "abc123", SessionTimeout: 30 },
headers: {}
})
};
const sessionManager = new SessionManager(http as never, { ...config, language: "3" });
await sessionManager.getSession();
expect(http.post).toHaveBeenCalledWith(
"/Login",
{
CompanyDB: "SBODEMOUS",
UserName: "manager",
Password: "secret",
Language: 3
},
{ skipAuth: true }
);
});
it("surfaces the SAP status, code and message when SAP rejects the login", async () => {
const rejection = new AxiosError("Request failed with status code 401", "ERR_BAD_REQUEST");
rejection.response = {
status: 401,
statusText: "Unauthorized",
headers: {},
config: {} as never,
data: { error: { code: "301", message: { value: "Invalid company database" } } }
};
const http = { post: vi.fn().mockRejectedValue(rejection) };
const sessionManager = new SessionManager(http as never, config);
const error = await sessionManager.getSession().catch((e) => e);
expect(error).toBeInstanceOf(SapB1Error);
expect(error.status).toBe(401);
expect(error.code).toBe("301");
expect(error.message).toBe("Invalid company database");
});
it("keeps the generic login message for a transport error with no HTTP response", async () => {
const transportError = new AxiosError("connect ECONNREFUSED 10.0.0.1:50000", "ECONNREFUSED");
const http = { post: vi.fn().mockRejectedValue(transportError) };
const sessionManager = new SessionManager(http as never, config);
const error = await sessionManager.getSession().catch((e) => e);
expect(error).toBeInstanceOf(SapB1Error);
expect(error.status).toBeUndefined();
expect(error.message).toBe("connect ECONNREFUSED 10.0.0.1:50000");
});
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: true,
headers: { Cookie: "B1SESSION=abc123" }
});
expect(sessionManager.currentSession).toBeUndefined();
});
});