Add GET /api/system/info and return SAP version from login
- capture Version + SessionTimeout from the Service Layer login
response (SessionManager.loginInfo, survives logout) and return
them from POST /api/session/login
- new GET /api/system/info: entity sets (service document),
UserFieldsMD/UserTablesMD/UserObjectsMD customizations and
CompanyService_GetAdminInfo; unreadable sections come back as
{ error } instead of failing the whole request
- OpenAPI: new System tag, examples for both endpoints
- tests for getSystemInfo section isolation and loginInfo retention
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -8,8 +8,9 @@ Součástí repozitáře je i malý AppFactory HTTP wrapper s endpointy:
|
|||||||
- `GET /health`
|
- `GET /health`
|
||||||
- `GET /docs`
|
- `GET /docs`
|
||||||
- `GET /openapi.json`
|
- `GET /openapi.json`
|
||||||
- `POST /api/session/login`
|
- `POST /api/session/login` (vrací i verzi Service Layeru a session timeout)
|
||||||
- `POST /api/session/logout`
|
- `POST /api/session/logout`
|
||||||
|
- `GET /api/system/info` (verze SAP, dostupné entity sety, UDF/UDT/UDO, admin info)
|
||||||
- `GET /api/<resource>`
|
- `GET /api/<resource>`
|
||||||
- `GET /api/<resource>/all`
|
- `GET /api/<resource>/all`
|
||||||
- `GET /api/<resource>/{id}`
|
- `GET /api/<resource>/{id}`
|
||||||
|
|||||||
@@ -43,6 +43,14 @@ HTTP API hlavičky:
|
|||||||
takže funguje i při `SAP_B1_RETRY_COUNT=0`.
|
takže funguje i při `SAP_B1_RETRY_COUNT=0`.
|
||||||
- **Logout** – `POST /b1s/v1/Logout` se posílá **s aktivní session cookie**, jinak by
|
- **Logout** – `POST /b1s/v1/Logout` se posílá **s aktivní session cookie**, jinak by
|
||||||
Service Layer nevěděl, kterou session ukončit, a nechal by ji běžet až do timeoutu.
|
Service Layer nevěděl, kterou session ukončit, a nechal by ji běžet až do timeoutu.
|
||||||
|
- **Verze a system info** – login response obsahuje `Version` (např. `1000230` = SAP B1
|
||||||
|
10.0 PL 23) a `SessionTimeout`; connector si je ukládá (`client.loginInfo`, přežije
|
||||||
|
logout) a HTTP endpoint `POST /api/session/login` je vrací v odpovědi.
|
||||||
|
`GET /api/system/info` navíc vrátí přehled instalace: service document (`GET /b1s/v1/`
|
||||||
|
→ dostupné entity sety), `UserFieldsMD` (UDF), `UserTablesMD` (UDT), `UserObjectsMD`
|
||||||
|
(UDO) a `CompanyService_GetAdminInfo`. Sekce, na kterou SAP uživatel nemá práva, se
|
||||||
|
vrátí jako `{ "error": "…" }`, aby jedno chybějící oprávnění neshodilo celý přehled
|
||||||
|
(chyba je v odpovědi vidět, nejde o tiché selhání).
|
||||||
- **Stránkování** – následuje `odata.nextLink` i `@odata.nextLink`. U absolutních
|
- **Stránkování** – následuje `odata.nextLink` i `@odata.nextLink`. U absolutních
|
||||||
nextLinků se zachová i query string (`$skip` apod.), takže `listAll` nezacyklí.
|
nextLinků se zachová i query string (`$skip` apod.), takže `listAll` nezacyklí.
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,24 @@
|
|||||||
|
import { SapB1LoginInfo } from "./auth/SessionManager";
|
||||||
import { SapB1Client, SapB1ClientOptions } from "./client/SapB1Client";
|
import { SapB1Client, SapB1ClientOptions } from "./client/SapB1Client";
|
||||||
import { SapB1Config } from "./config";
|
import { SapB1Config } from "./config";
|
||||||
import { BusinessPartnersResource } from "./resources/BusinessPartners";
|
import { BusinessPartnersResource } from "./resources/BusinessPartners";
|
||||||
import { DeliveryNotesResource, InvoicesResource, OrdersResource, PurchaseOrdersResource } from "./resources/Documents";
|
import { DeliveryNotesResource, InvoicesResource, OrdersResource, PurchaseOrdersResource } from "./resources/Documents";
|
||||||
import { ItemsResource } from "./resources/Items";
|
import { ItemsResource } from "./resources/Items";
|
||||||
import { StockTransfersResource } from "./resources/StockTransfers";
|
import { StockTransfersResource } from "./resources/StockTransfers";
|
||||||
|
import { ODataResponse } from "./types/odata";
|
||||||
|
|
||||||
|
export interface SapB1SectionError {
|
||||||
|
error: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SapB1SystemInfo {
|
||||||
|
serviceLayer: SapB1LoginInfo;
|
||||||
|
entitySets: string[] | SapB1SectionError;
|
||||||
|
userFields: unknown[] | SapB1SectionError;
|
||||||
|
userTables: unknown[] | SapB1SectionError;
|
||||||
|
userObjects: unknown[] | SapB1SectionError;
|
||||||
|
adminInfo: unknown | SapB1SectionError;
|
||||||
|
}
|
||||||
|
|
||||||
export class SapBusinessOneServiceLayer {
|
export class SapBusinessOneServiceLayer {
|
||||||
public readonly client: SapB1Client;
|
public readonly client: SapB1Client;
|
||||||
@@ -33,4 +48,56 @@ export class SapBusinessOneServiceLayer {
|
|||||||
public logout(): Promise<void> {
|
public logout(): Promise<void> {
|
||||||
return this.client.logout();
|
return this.client.logout();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One-shot overview of the connected SAP Business One installation: Service Layer
|
||||||
|
* version (from the login response), available entity sets (service document) and
|
||||||
|
* the customization metadata (UserFieldsMD/UserTablesMD/UserObjectsMD) plus company
|
||||||
|
* admin info. Sections the SAP user cannot read come back as { error } so one missing
|
||||||
|
* permission does not fail the whole overview.
|
||||||
|
*/
|
||||||
|
public async getSystemInfo(): Promise<SapB1SystemInfo> {
|
||||||
|
await this.client.login();
|
||||||
|
|
||||||
|
const [entitySets, userFields, userTables, userObjects, adminInfo] = await Promise.all([
|
||||||
|
this.loadSection(() =>
|
||||||
|
this.client
|
||||||
|
.get<ODataResponse<{ name: string }>>("/")
|
||||||
|
.then((document) => document.value.map((entitySet) => entitySet.name).sort())
|
||||||
|
),
|
||||||
|
this.loadSection(() =>
|
||||||
|
this.client.getAll<unknown>("UserFieldsMD", {
|
||||||
|
select: ["TableName", "FieldID", "Name", "Type", "SubType", "Description"]
|
||||||
|
})
|
||||||
|
),
|
||||||
|
this.loadSection(() =>
|
||||||
|
this.client.getAll<unknown>("UserTablesMD", {
|
||||||
|
select: ["TableName", "TableDescription", "TableType"]
|
||||||
|
})
|
||||||
|
),
|
||||||
|
this.loadSection(() =>
|
||||||
|
this.client.getAll<unknown>("UserObjectsMD", {
|
||||||
|
select: ["Code", "Name", "TableName", "ObjectType"]
|
||||||
|
})
|
||||||
|
),
|
||||||
|
this.loadSection(() => this.client.post<unknown>("CompanyService_GetAdminInfo"))
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
serviceLayer: this.client.loginInfo ?? {},
|
||||||
|
entitySets,
|
||||||
|
userFields,
|
||||||
|
userTables,
|
||||||
|
userObjects,
|
||||||
|
adminInfo
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loadSection<T>(load: () => Promise<T>): Promise<T | SapB1SectionError> {
|
||||||
|
try {
|
||||||
|
return await load();
|
||||||
|
} catch (error) {
|
||||||
|
return { error: error instanceof Error ? error.message : String(error) };
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,9 +17,15 @@ export interface SapB1Session {
|
|||||||
expiresAt?: number;
|
expiresAt?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SapB1LoginInfo {
|
||||||
|
version?: string;
|
||||||
|
sessionTimeoutMinutes?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export class SessionManager {
|
export class SessionManager {
|
||||||
private session?: SapB1Session;
|
private session?: SapB1Session;
|
||||||
private loginPromise?: Promise<SapB1Session>;
|
private loginPromise?: Promise<SapB1Session>;
|
||||||
|
private lastLoginInfo?: SapB1LoginInfo;
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly http: AxiosInstance,
|
private readonly http: AxiosInstance,
|
||||||
@@ -31,6 +37,12 @@ export class SessionManager {
|
|||||||
return this.session;
|
return this.session;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Survives logout on purpose: the login/logout roundtrip in the HTTP layer still
|
||||||
|
// needs to report which Service Layer version answered.
|
||||||
|
public get loginInfo(): SapB1LoginInfo | undefined {
|
||||||
|
return this.lastLoginInfo;
|
||||||
|
}
|
||||||
|
|
||||||
public async getSession(forceRefresh = false): Promise<SapB1Session> {
|
public async getSession(forceRefresh = false): Promise<SapB1Session> {
|
||||||
if (!forceRefresh && this.session && !this.isExpired(this.session)) {
|
if (!forceRefresh && this.session && !this.isExpired(this.session)) {
|
||||||
return this.session;
|
return this.session;
|
||||||
@@ -61,15 +73,19 @@ export class SessionManager {
|
|||||||
{ skipAuth: true }
|
{ skipAuth: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
LoginResponseSchema.parse(response.data);
|
const loginData = LoginResponseSchema.parse(response.data);
|
||||||
const cookies = this.extractCookies(response.headers["set-cookie"]);
|
const cookies = this.extractCookies(response.headers["set-cookie"]);
|
||||||
const b1session = cookies.B1SESSION || response.data.SessionId;
|
const b1session = cookies.B1SESSION || loginData.SessionId;
|
||||||
|
|
||||||
if (!b1session) {
|
if (!b1session) {
|
||||||
throw new SapB1Error({ message: "SAP B1 login did not return B1SESSION cookie or SessionId" });
|
throw new SapB1Error({ message: "SAP B1 login did not return B1SESSION cookie or SessionId" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const timeoutMinutes = typeof response.data.SessionTimeout === "number" ? response.data.SessionTimeout : 30;
|
const timeoutMinutes = typeof loginData.SessionTimeout === "number" ? loginData.SessionTimeout : 30;
|
||||||
|
this.lastLoginInfo = {
|
||||||
|
version: loginData.Version,
|
||||||
|
sessionTimeoutMinutes: loginData.SessionTimeout
|
||||||
|
};
|
||||||
this.session = {
|
this.session = {
|
||||||
b1session,
|
b1session,
|
||||||
routeId: cookies.ROUTEID,
|
routeId: cookies.ROUTEID,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import "./axiosTypes";
|
|||||||
import https from "node:https";
|
import https from "node:https";
|
||||||
import axios, { AxiosInstance, AxiosRequestConfig, Method } from "axios";
|
import axios, { AxiosInstance, AxiosRequestConfig, Method } from "axios";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { SessionManager } from "../auth/SessionManager";
|
import { SapB1LoginInfo, SessionManager } from "../auth/SessionManager";
|
||||||
import { SapB1Config, SapB1ConfigSchema } from "../config";
|
import { SapB1Config, SapB1ConfigSchema } from "../config";
|
||||||
import { SapB1Error, toSapB1Error } from "../errors/SapB1Error";
|
import { SapB1Error, toSapB1Error } from "../errors/SapB1Error";
|
||||||
import { ODataQuery, ODataResponse } from "../types/odata";
|
import { ODataQuery, ODataResponse } from "../types/odata";
|
||||||
@@ -38,6 +38,10 @@ export class SapB1Client {
|
|||||||
return this.sessionManager.getSession(true);
|
return this.sessionManager.getSession(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get loginInfo(): SapB1LoginInfo | undefined {
|
||||||
|
return this.sessionManager.loginInfo;
|
||||||
|
}
|
||||||
|
|
||||||
public logout(): Promise<void> {
|
public logout(): Promise<void> {
|
||||||
return this.sessionManager.logout();
|
return this.sessionManager.logout();
|
||||||
}
|
}
|
||||||
|
|||||||
+51
-5
@@ -6,12 +6,12 @@ import { loadSapB1ConfigFromEnv, SapB1Config, SapB1ConfigSchema } from "./config
|
|||||||
import { SapB1Error } from "./errors/SapB1Error";
|
import { SapB1Error } from "./errors/SapB1Error";
|
||||||
import { ODataQuery } from "./types/odata";
|
import { ODataQuery } from "./types/odata";
|
||||||
|
|
||||||
export { SessionManager, SapB1Session } from "./auth/SessionManager";
|
export { SessionManager, SapB1Session, SapB1LoginInfo } from "./auth/SessionManager";
|
||||||
export { SapB1Client, SapB1ClientOptions } from "./client/SapB1Client";
|
export { SapB1Client, SapB1ClientOptions } from "./client/SapB1Client";
|
||||||
export { buildODataParams, extractNextLink } from "./client/odata";
|
export { buildODataParams, extractNextLink } from "./client/odata";
|
||||||
export { loadSapB1ConfigFromEnv, SapB1Config, SapB1ConfigSchema } from "./config";
|
export { loadSapB1ConfigFromEnv, SapB1Config, SapB1ConfigSchema } from "./config";
|
||||||
export { SapB1Error } from "./errors/SapB1Error";
|
export { SapB1Error } from "./errors/SapB1Error";
|
||||||
export { SapBusinessOneServiceLayer } from "./SapBusinessOneServiceLayer";
|
export { SapBusinessOneServiceLayer, SapB1SystemInfo, SapB1SectionError } from "./SapBusinessOneServiceLayer";
|
||||||
export * from "./types/entities";
|
export * from "./types/entities";
|
||||||
export * from "./types/odata";
|
export * from "./types/odata";
|
||||||
|
|
||||||
@@ -252,8 +252,11 @@ function addResourceEndpoints(prefix = "") {
|
|||||||
app.post(
|
app.post(
|
||||||
`${prefix}/api/session/login`,
|
`${prefix}/api/session/login`,
|
||||||
asyncHandler(async (req, res) => {
|
asyncHandler(async (req, res) => {
|
||||||
await withSap(req, (sap) => sap.login());
|
const serviceLayer = await withSap(req, async (sap) => {
|
||||||
res.json({ status: "ok" });
|
await sap.login();
|
||||||
|
return sap.client.loginInfo ?? {};
|
||||||
|
});
|
||||||
|
res.json({ status: "ok", ...serviceLayer });
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -265,6 +268,14 @@ function addResourceEndpoints(prefix = "") {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
app.get(
|
||||||
|
`${prefix}/api/system/info`,
|
||||||
|
asyncHandler(async (req, res) => {
|
||||||
|
const result = await withSap(req, (sap) => sap.getSystemInfo());
|
||||||
|
res.json(result);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
for (const route of resourceRoutes) {
|
for (const route of resourceRoutes) {
|
||||||
const base = `${prefix}/api/${route.slug}`;
|
const base = `${prefix}/api/${route.slug}`;
|
||||||
|
|
||||||
@@ -445,7 +456,14 @@ function openApiDocument(basePath = "") {
|
|||||||
"Uses SAP credentials from X-SAP-B1-* request headers. Credentials are never accepted in the request body, logged, or returned.",
|
"Uses SAP credentials from X-SAP-B1-* request headers. Credentials are never accepted in the request body, logged, or returned.",
|
||||||
parameters: sapCredentialParameters(),
|
parameters: sapCredentialParameters(),
|
||||||
responses: {
|
responses: {
|
||||||
"200": { description: "Session is available" },
|
"200": {
|
||||||
|
description: "Session is available; returns SAP Service Layer version and session timeout",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
example: { status: "ok", version: "1000230", sessionTimeoutMinutes: 30 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"502": { description: "SAP login failed" }
|
"502": { description: "SAP login failed" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -459,6 +477,33 @@ function openApiDocument(basePath = "") {
|
|||||||
"204": { description: "Session closed or was not active" }
|
"204": { description: "Session closed or was not active" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"/api/system/info": {
|
||||||
|
get: {
|
||||||
|
tags: ["System"],
|
||||||
|
summary: "SAP Business One system and customization overview",
|
||||||
|
description:
|
||||||
|
"Logs in and returns the Service Layer version (from the login response), the list of available entity sets (service document), user-defined fields (UserFieldsMD), user-defined tables (UserTablesMD), user-defined objects (UserObjectsMD) and company admin info (CompanyService_GetAdminInfo). Sections the SAP user cannot read are returned as { \"error\": \"...\" } instead of failing the whole request.",
|
||||||
|
parameters: sapCredentialParameters(),
|
||||||
|
responses: {
|
||||||
|
"200": {
|
||||||
|
description: "System overview",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
example: {
|
||||||
|
serviceLayer: { version: "1000230", sessionTimeoutMinutes: 30 },
|
||||||
|
entitySets: ["BusinessPartners", "Items", "Orders"],
|
||||||
|
userFields: [{ TableName: "OCRD", FieldID: 0, Name: "MyField", Type: "db_Alpha", Description: "Custom field" }],
|
||||||
|
userTables: [{ TableName: "MY_TABLE", TableDescription: "Custom table", TableType: "bott_NoObject" }],
|
||||||
|
userObjects: [{ Code: "MY_UDO", Name: "My UDO", TableName: "MY_TABLE", ObjectType: "boud_Document" }],
|
||||||
|
adminInfo: { error: "No permission (example of a section the SAP user cannot read)" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"502": { description: "SAP login failed" }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -556,6 +601,7 @@ function openApiDocument(basePath = "") {
|
|||||||
tags: [
|
tags: [
|
||||||
{ name: "Service" },
|
{ name: "Service" },
|
||||||
{ name: "Session" },
|
{ name: "Session" },
|
||||||
|
{ name: "System", description: "SAP version and customization overview" },
|
||||||
...resourceRoutes.map((route) => ({ name: route.tag, description: `SAP Service Layer ${route.sapEntitySet}` }))
|
...resourceRoutes.map((route) => ({ name: route.tag, description: `SAP Service Layer ${route.sapEntitySet}` }))
|
||||||
],
|
],
|
||||||
paths,
|
paths,
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
import axios, { AxiosAdapter, AxiosResponse } from "axios";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { SapBusinessOneServiceLayer } from "../src/SapBusinessOneServiceLayer";
|
||||||
|
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: 0,
|
||||||
|
retryDelayMs: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
function response(data: unknown, requestConfig: Parameters<AxiosAdapter>[0], status = 200): AxiosResponse {
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
status,
|
||||||
|
statusText: "OK",
|
||||||
|
headers: {},
|
||||||
|
config: requestConfig
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("getSystemInfo", () => {
|
||||||
|
it("collects version, entity sets and customization metadata, isolating section errors", async () => {
|
||||||
|
const http = axios.create({
|
||||||
|
adapter: async (requestConfig) => {
|
||||||
|
const url = String(requestConfig.url);
|
||||||
|
|
||||||
|
if (url === "/Login") {
|
||||||
|
return {
|
||||||
|
...response({ SessionId: "abc123", Version: "1000230", SessionTimeout: 30 }, requestConfig),
|
||||||
|
headers: { "set-cookie": ["B1SESSION=abc123; Path=/b1s/v1"] }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url === "/Logout") {
|
||||||
|
return response({}, requestConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url === "/") {
|
||||||
|
return response(
|
||||||
|
{ value: [{ name: "Items", url: "Items" }, { name: "BusinessPartners", url: "BusinessPartners" }] },
|
||||||
|
requestConfig
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url === "UserFieldsMD") {
|
||||||
|
return response({ value: [{ TableName: "OCRD", FieldID: 0, Name: "MyField", Type: "db_Alpha" }] }, requestConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url === "UserTablesMD") {
|
||||||
|
return response({ value: [{ TableName: "MY_TABLE", TableDescription: "Custom table" }] }, requestConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url === "UserObjectsMD") {
|
||||||
|
return response({ value: [] }, requestConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url === "CompanyService_GetAdminInfo") {
|
||||||
|
return Promise.reject({
|
||||||
|
isAxiosError: true,
|
||||||
|
message: "Forbidden",
|
||||||
|
response: { status: 403, data: { error: { message: { value: "No permission" } } } }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Unexpected request URL: ${url}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const sap = new SapBusinessOneServiceLayer(config, { http });
|
||||||
|
const info = await sap.getSystemInfo();
|
||||||
|
|
||||||
|
expect(info.serviceLayer).toEqual({ version: "1000230", sessionTimeoutMinutes: 30 });
|
||||||
|
expect(info.entitySets).toEqual(["BusinessPartners", "Items"]);
|
||||||
|
expect(info.userFields).toEqual([{ TableName: "OCRD", FieldID: 0, Name: "MyField", Type: "db_Alpha" }]);
|
||||||
|
expect(info.userTables).toEqual([{ TableName: "MY_TABLE", TableDescription: "Custom table" }]);
|
||||||
|
expect(info.userObjects).toEqual([]);
|
||||||
|
expect(info.adminInfo).toEqual({ error: "No permission" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps login info available after logout so the login endpoint can report the version", async () => {
|
||||||
|
const http = axios.create({
|
||||||
|
adapter: async (requestConfig) => {
|
||||||
|
if (requestConfig.url === "/Login") {
|
||||||
|
return {
|
||||||
|
...response({ SessionId: "abc123", Version: "1000230", SessionTimeout: 30 }, requestConfig),
|
||||||
|
headers: { "set-cookie": ["B1SESSION=abc123; Path=/b1s/v1"] }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return response({}, requestConfig);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const sap = new SapBusinessOneServiceLayer(config, { http });
|
||||||
|
await sap.login();
|
||||||
|
await sap.logout();
|
||||||
|
|
||||||
|
expect(sap.client.loginInfo).toEqual({ version: "1000230", sessionTimeoutMinutes: 30 });
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user