@linkup-ai/abap-ai 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/abap/security-agent/README.md +30 -0
- package/dist/abap/security-agent/zcl_lkp_ai_guard.clas.abap +282 -0
- package/dist/abap/security-agent/zcl_lkp_ai_icf_handler.clas.abap +275 -0
- package/dist/abap/security-agent/zcx_lkp_ai_denied.clas.abap +50 -0
- package/dist/abap/security-agent/zlkp_ai_audit.tabl.asdic +19 -0
- package/dist/abap/security-agent/zlkp_ai_config.tabl.asdic +21 -0
- package/dist/abap/security-agent/zlkp_ai_setup.prog.abap +359 -0
- package/dist/adt-client.js +63 -74
- package/dist/cli/init.js +40 -6
- package/dist/cli/install-agent.js +290 -0
- package/dist/cli.js +20 -2
- package/dist/index.js +59 -31
- package/dist/license-guard.js +43 -1
- package/dist/sap-policy-loader.js +255 -0
- package/dist/security-policy.js +41 -12
- package/dist/tests/create-payload.test.js +213 -0
- package/dist/tests/security-policy.test.js +167 -0
- package/dist/tool-handler.js +80 -0
- package/dist/tools/activate.js +27 -4
- package/dist/tools/create.js +2 -0
- package/dist/tools/delete.js +12 -8
- package/dist/tools/execute.js +112 -0
- package/dist/tools/transports.js +4 -3
- package/package.json +7 -4
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* SAP Policy Loader — lê a security policy de dentro do SAP (ABAP Security Agent).
|
|
4
|
+
*
|
|
5
|
+
* Tenta ler a tabela ZLKP_AI_CONFIG via SQL Console (ADT datapreview/freestyle).
|
|
6
|
+
* Se a tabela existe, usa como fonte de verdade da policy.
|
|
7
|
+
* Se não existe (agente não instalado), retorna null e o fallback local é usado.
|
|
8
|
+
*
|
|
9
|
+
* Integração por plano:
|
|
10
|
+
* - Pro: lê via SQL Console (Opção A)
|
|
11
|
+
* - Enterprise/Corporate: lê via SQL Console no startup, valida via ICF em runtime (Opção B)
|
|
12
|
+
*/
|
|
13
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
16
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
17
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
18
|
+
}
|
|
19
|
+
Object.defineProperty(o, k2, desc);
|
|
20
|
+
}) : (function(o, m, k, k2) {
|
|
21
|
+
if (k2 === undefined) k2 = k;
|
|
22
|
+
o[k2] = m[k];
|
|
23
|
+
}));
|
|
24
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
25
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
26
|
+
}) : function(o, v) {
|
|
27
|
+
o["default"] = v;
|
|
28
|
+
});
|
|
29
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
30
|
+
var ownKeys = function(o) {
|
|
31
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
32
|
+
var ar = [];
|
|
33
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
34
|
+
return ar;
|
|
35
|
+
};
|
|
36
|
+
return ownKeys(o);
|
|
37
|
+
};
|
|
38
|
+
return function (mod) {
|
|
39
|
+
if (mod && mod.__esModule) return mod;
|
|
40
|
+
var result = {};
|
|
41
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
42
|
+
__setModuleDefault(result, mod);
|
|
43
|
+
return result;
|
|
44
|
+
};
|
|
45
|
+
})();
|
|
46
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
47
|
+
exports.loadPolicyFromSap = loadPolicyFromSap;
|
|
48
|
+
exports.isSecurityAgentInstalled = isSecurityAgentInstalled;
|
|
49
|
+
exports.checkAndAuditViaSap = checkAndAuditViaSap;
|
|
50
|
+
exports.isIcfHandlerAvailable = isIcfHandlerAvailable;
|
|
51
|
+
const adt_client_js_1 = require("./adt-client.js");
|
|
52
|
+
const data_format_js_1 = require("./tools/shared/data-format.js");
|
|
53
|
+
/**
|
|
54
|
+
* Tenta carregar a policy do SAP via SQL Console.
|
|
55
|
+
* Retorna null se a tabela ZLKP_AI_CONFIG não existe (agente não instalado).
|
|
56
|
+
*/
|
|
57
|
+
async function loadPolicyFromSap(userName) {
|
|
58
|
+
try {
|
|
59
|
+
await (0, adt_client_js_1.ensureSession)();
|
|
60
|
+
// Ler toda a config do ambiente para o usuário atual
|
|
61
|
+
const sql = `SELECT env_role, user_group, tool_name, allowed, max_rows `
|
|
62
|
+
+ `FROM zlkp_ai_config `
|
|
63
|
+
+ `ORDER BY env_role, user_group, tool_name`;
|
|
64
|
+
const { data, status } = await (0, adt_client_js_1.adtPostText)("/datapreview/freestyle", sql, { rowNumber: "500" }, "application/vnd.sap.adt.datapreview.table.v1+xml");
|
|
65
|
+
// 404 ou erro = tabela não existe = agente não instalado
|
|
66
|
+
if (status >= 400)
|
|
67
|
+
return null;
|
|
68
|
+
const result = (0, data_format_js_1.parseDataPreview)(data);
|
|
69
|
+
if (result.rows.length === 0)
|
|
70
|
+
return null;
|
|
71
|
+
// Parsear as rows em SapPolicyConfig[]
|
|
72
|
+
const configs = parseConfigRows(result.columns.map((c) => c.name), result.rows);
|
|
73
|
+
// Determinar env_role (pegar o primeiro que aparecer — assumindo sistema single-env)
|
|
74
|
+
const envRoles = [...new Set(configs.map((c) => c.env_role))];
|
|
75
|
+
const envRole = envRoles[0] || "DEVELOPMENT";
|
|
76
|
+
// Filtrar configs para o env_role + resolver prioridade user > grupo > wildcard
|
|
77
|
+
const relevantConfigs = resolveUserConfigs(configs, envRole, userName.toUpperCase());
|
|
78
|
+
// Montar SecurityPolicy — usar always_blocked para tools EXPLICITAMENTE bloqueadas
|
|
79
|
+
// e allowed_levels amplos (o bloqueio individual é feito via always_blocked)
|
|
80
|
+
const blockedTools = [];
|
|
81
|
+
let maxRows = 0;
|
|
82
|
+
let allBlocked = false;
|
|
83
|
+
for (const cfg of relevantConfigs) {
|
|
84
|
+
if (cfg.tool_name === "*") {
|
|
85
|
+
if (!cfg.allowed)
|
|
86
|
+
allBlocked = true;
|
|
87
|
+
if (cfg.max_rows > 0)
|
|
88
|
+
maxRows = cfg.max_rows;
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
if (!cfg.allowed) {
|
|
92
|
+
blockedTools.push(cfg.tool_name);
|
|
93
|
+
}
|
|
94
|
+
if (cfg.max_rows > 0 && (maxRows === 0 || cfg.max_rows < maxRows)) {
|
|
95
|
+
maxRows = cfg.max_rows;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// Allowed levels: amplos para DEV, restrito para QAS/PRD
|
|
100
|
+
// O bloqueio granular é feito via always_blocked (tool por tool)
|
|
101
|
+
const allowedLevels = allBlocked
|
|
102
|
+
? ["READ"]
|
|
103
|
+
: envRole === "DEVELOPMENT"
|
|
104
|
+
? ["READ", "WRITE", "CREATE"]
|
|
105
|
+
: ["READ"];
|
|
106
|
+
const policy = {
|
|
107
|
+
environment_role: envRole,
|
|
108
|
+
allowed_levels: allowedLevels,
|
|
109
|
+
always_blocked: blockedTools,
|
|
110
|
+
require_transport: envRole !== "DEVELOPMENT",
|
|
111
|
+
max_preview_rows: maxRows || 0,
|
|
112
|
+
};
|
|
113
|
+
return policy;
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// Qualquer erro = fallback para policy local (silencioso)
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Verifica se o ABAP Security Agent está instalado (tabela existe).
|
|
122
|
+
*/
|
|
123
|
+
async function isSecurityAgentInstalled() {
|
|
124
|
+
try {
|
|
125
|
+
await (0, adt_client_js_1.ensureSession)();
|
|
126
|
+
const { status } = await (0, adt_client_js_1.adtPostText)("/datapreview/freestyle", "SELECT COUNT(*) AS cnt FROM zlkp_ai_config", { rowNumber: "1" }, "application/vnd.sap.adt.datapreview.table.v1+xml");
|
|
127
|
+
return status >= 200 && status < 300;
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Chama o ICF handler do Security Agent para validar + gravar audit no SAP.
|
|
135
|
+
* Usado por Enterprise/Corporate (Opção B — ICF Handler).
|
|
136
|
+
*
|
|
137
|
+
* O handler /sap/z/lkpai/guard/check faz:
|
|
138
|
+
* 1. AUTHORITY-CHECK no ABAP
|
|
139
|
+
* 2. Grava ZLKP_AI_AUDIT automaticamente
|
|
140
|
+
* 3. Retorna { allowed: true/false, reason?, policy? }
|
|
141
|
+
*
|
|
142
|
+
* Para Pro (Opção A), o audit é gravado localmente (JSONL) — o SAP não tem
|
|
143
|
+
* o ICF handler instalado. O audit SAP-side fica apenas para Enterprise/Corporate.
|
|
144
|
+
*/
|
|
145
|
+
async function checkAndAuditViaSap(toolName, objType, objName) {
|
|
146
|
+
try {
|
|
147
|
+
const csrf = await (0, adt_client_js_1.ensureSession)();
|
|
148
|
+
const sapUrl = process.env.SAP_URL || "";
|
|
149
|
+
const sapClient = process.env.SAP_CLIENT || "100";
|
|
150
|
+
// Chamar ICF handler diretamente (fora do baseURL /sap/bc/adt)
|
|
151
|
+
const { default: axios } = await Promise.resolve().then(() => __importStar(require("axios")));
|
|
152
|
+
const response = await axios.post(`${sapUrl}/sap/z/lkpai/guard/check?sap-client=${sapClient}`, JSON.stringify({
|
|
153
|
+
tool: toolName,
|
|
154
|
+
obj_type: objType || "",
|
|
155
|
+
obj_name: objName || "",
|
|
156
|
+
}), {
|
|
157
|
+
headers: {
|
|
158
|
+
"Content-Type": "application/json",
|
|
159
|
+
"X-CSRF-Token": csrf,
|
|
160
|
+
},
|
|
161
|
+
auth: {
|
|
162
|
+
username: process.env.SAP_USER || "",
|
|
163
|
+
password: process.env.SAP_PASS || "",
|
|
164
|
+
},
|
|
165
|
+
validateStatus: (s) => s < 500,
|
|
166
|
+
timeout: 10000,
|
|
167
|
+
});
|
|
168
|
+
if (response.status === 200) {
|
|
169
|
+
return { allowed: true };
|
|
170
|
+
}
|
|
171
|
+
if (response.status === 403) {
|
|
172
|
+
const data = typeof response.data === "string" ? JSON.parse(response.data) : response.data;
|
|
173
|
+
return { allowed: false, reason: data?.reason || "Bloqueado pela policy SAP" };
|
|
174
|
+
}
|
|
175
|
+
// 404 = ICF handler não instalado → fallback
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
// ICF não disponível → fallback para policy local
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
/** Cache: se o ICF handler está disponível (checado 1x no startup) */
|
|
184
|
+
let icfAvailable = null;
|
|
185
|
+
/**
|
|
186
|
+
* Verifica se o ICF handler /sap/z/lkpai/guard está ativo.
|
|
187
|
+
*/
|
|
188
|
+
async function isIcfHandlerAvailable() {
|
|
189
|
+
if (icfAvailable !== null)
|
|
190
|
+
return icfAvailable;
|
|
191
|
+
try {
|
|
192
|
+
const result = await checkAndAuditViaSap("_probe_");
|
|
193
|
+
// Se retornou algo (mesmo denied), o handler existe
|
|
194
|
+
icfAvailable = result !== null;
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
icfAvailable = false;
|
|
198
|
+
}
|
|
199
|
+
return icfAvailable;
|
|
200
|
+
}
|
|
201
|
+
// ─── Helpers ──────────────────────────────────────────────────────
|
|
202
|
+
function parseConfigRows(columnNames, rows) {
|
|
203
|
+
const idxEnv = columnNames.indexOf("ENV_ROLE");
|
|
204
|
+
const idxGroup = columnNames.indexOf("USER_GROUP");
|
|
205
|
+
const idxTool = columnNames.indexOf("TOOL_NAME");
|
|
206
|
+
const idxAllowed = columnNames.indexOf("ALLOWED");
|
|
207
|
+
const idxMaxRows = columnNames.indexOf("MAX_ROWS");
|
|
208
|
+
return rows.map((row) => ({
|
|
209
|
+
env_role: (row[idxEnv] || "").trim(),
|
|
210
|
+
user_group: (row[idxGroup] || "*").trim(),
|
|
211
|
+
tool_name: (row[idxTool] || "*").trim(),
|
|
212
|
+
allowed: (row[idxAllowed] || "").trim() === "X",
|
|
213
|
+
max_rows: parseInt(row[idxMaxRows] || "0", 10) || 0,
|
|
214
|
+
}));
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Resolve prioridade de configs: usuário individual > grupo SU01 > wildcard '*'.
|
|
218
|
+
*/
|
|
219
|
+
function resolveUserConfigs(configs, envRole, userName) {
|
|
220
|
+
const envConfigs = configs.filter((c) => c.env_role === envRole);
|
|
221
|
+
// Agrupar por tool_name e resolver prioridade
|
|
222
|
+
const toolMap = new Map();
|
|
223
|
+
for (const cfg of envConfigs) {
|
|
224
|
+
const existing = toolMap.get(cfg.tool_name);
|
|
225
|
+
const priority = getUserGroupPriority(cfg.user_group, userName);
|
|
226
|
+
if (priority < 0)
|
|
227
|
+
continue; // Não se aplica a este usuário
|
|
228
|
+
if (!existing || priority > getUserGroupPriority(existing.user_group, userName)) {
|
|
229
|
+
toolMap.set(cfg.tool_name, cfg);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return Array.from(toolMap.values());
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Retorna a prioridade do user_group para o usuário (maior = mais específico).
|
|
236
|
+
* -1 = não se aplica, 0 = wildcard, 2 = usuário individual.
|
|
237
|
+
*
|
|
238
|
+
* Nota: lookup de grupo SU01 (USR02-CLASS) não implementado no TypeScript.
|
|
239
|
+
* Entradas com user_group diferente de '*' e do userName são ignoradas (-1).
|
|
240
|
+
* O lookup de grupo é feito pelo ZCL_LKP_AI_GUARD no ABAP-side (Enterprise/Corporate).
|
|
241
|
+
*/
|
|
242
|
+
function getUserGroupPriority(userGroup, userName) {
|
|
243
|
+
if (userGroup === "*")
|
|
244
|
+
return 0;
|
|
245
|
+
if (userGroup.toUpperCase() === userName.toUpperCase())
|
|
246
|
+
return 2;
|
|
247
|
+
// Entradas para outros usuários/grupos não se aplicam a este user
|
|
248
|
+
return -1;
|
|
249
|
+
}
|
|
250
|
+
function generateSimpleGuid() {
|
|
251
|
+
return "xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
252
|
+
const r = (Math.random() * 16) | 0;
|
|
253
|
+
return (c === "x" ? r : (r & 0x3) | 0x8).toString(16);
|
|
254
|
+
});
|
|
255
|
+
}
|
package/dist/security-policy.js
CHANGED
|
@@ -22,6 +22,30 @@ exports.getDefaultPolicy = getDefaultPolicy;
|
|
|
22
22
|
exports.listEnvironmentRoles = listEnvironmentRoles;
|
|
23
23
|
const fs_1 = require("fs");
|
|
24
24
|
const path_1 = require("path");
|
|
25
|
+
const zod_1 = require("zod");
|
|
26
|
+
// ─── Schema Zod para validação de policy carregada ───────────────
|
|
27
|
+
const SecurityPolicySchema = zod_1.z.object({
|
|
28
|
+
environment_role: zod_1.z.enum(["DEVELOPMENT", "QUALITY", "PRODUCTION"]).optional(),
|
|
29
|
+
allowed_levels: zod_1.z.array(zod_1.z.enum(["READ", "WRITE", "CREATE", "DESTRUCTIVE", "ADMIN"])).optional(),
|
|
30
|
+
always_blocked: zod_1.z.array(zod_1.z.string()).optional(),
|
|
31
|
+
require_transport: zod_1.z.boolean().optional(),
|
|
32
|
+
max_preview_rows: zod_1.z.number().int().min(0).optional(),
|
|
33
|
+
}).strict();
|
|
34
|
+
/**
|
|
35
|
+
* Valida e parseia um objeto parcial de policy.
|
|
36
|
+
* Lança erro descritivo se o JSON não bate com o schema esperado.
|
|
37
|
+
*/
|
|
38
|
+
function validatePolicyInput(input, source) {
|
|
39
|
+
const result = SecurityPolicySchema.safeParse(input);
|
|
40
|
+
if (!result.success) {
|
|
41
|
+
const issues = result.error.issues
|
|
42
|
+
.map((i) => ` - ${i.path.join(".")}: ${i.message}`)
|
|
43
|
+
.join("\n");
|
|
44
|
+
throw new Error(`[SECURITY] Policy inválida (${source}):\n${issues}\n` +
|
|
45
|
+
`Corrija o arquivo/variável ou remova para usar o default (DEVELOPMENT).`);
|
|
46
|
+
}
|
|
47
|
+
return result.data;
|
|
48
|
+
}
|
|
25
49
|
/**
|
|
26
50
|
* Mapa de cada tool para seu nível de risco.
|
|
27
51
|
* TODA tool registrada DEVE estar neste mapa — tools não classificadas causam erro no startup.
|
|
@@ -83,9 +107,10 @@ const TOOL_RISK = {
|
|
|
83
107
|
abap_create_dcl: "CREATE",
|
|
84
108
|
abap_create_amdp: "CREATE",
|
|
85
109
|
abap_scaffold_rap: "CREATE",
|
|
86
|
-
// WRITE —
|
|
87
|
-
|
|
88
|
-
// ADMIN — operações
|
|
110
|
+
// WRITE — execução de programas (requer ICF handler)
|
|
111
|
+
abap_execute: "WRITE",
|
|
112
|
+
// ADMIN — operações de alto impacto (sempre bloqueadas pelo MCP server)
|
|
113
|
+
abap_delete: "ADMIN",
|
|
89
114
|
abap_release_transport: "ADMIN",
|
|
90
115
|
};
|
|
91
116
|
/**
|
|
@@ -126,7 +151,6 @@ const POLICY_QUALITY = {
|
|
|
126
151
|
allowed_levels: ["READ"],
|
|
127
152
|
always_blocked: [
|
|
128
153
|
"abap_release_transport",
|
|
129
|
-
"abap_delete",
|
|
130
154
|
"abap_scaffold_rap",
|
|
131
155
|
"abap_create",
|
|
132
156
|
"abap_create_dcl",
|
|
@@ -148,7 +172,6 @@ const POLICY_PRODUCTION = {
|
|
|
148
172
|
allowed_levels: ["READ"],
|
|
149
173
|
always_blocked: [
|
|
150
174
|
"abap_release_transport",
|
|
151
|
-
"abap_delete",
|
|
152
175
|
"abap_scaffold_rap",
|
|
153
176
|
"abap_create",
|
|
154
177
|
"abap_create_dcl",
|
|
@@ -191,22 +214,28 @@ function loadPolicy() {
|
|
|
191
214
|
const policyJson = process.env.ABAP_AI_SECURITY_POLICY;
|
|
192
215
|
if (policyJson) {
|
|
193
216
|
try {
|
|
194
|
-
const
|
|
217
|
+
const raw = JSON.parse(policyJson);
|
|
218
|
+
const parsed = validatePolicyInput(raw, "env ABAP_AI_SECURITY_POLICY");
|
|
195
219
|
return mergeWithDefault(parsed);
|
|
196
220
|
}
|
|
197
|
-
catch {
|
|
198
|
-
//
|
|
221
|
+
catch (e) {
|
|
222
|
+
// Se é erro de validação Zod, propagar (não silenciar)
|
|
223
|
+
if (e instanceof Error && e.message.includes("[SECURITY]"))
|
|
224
|
+
throw e;
|
|
225
|
+
// JSON parse error — fallthrough
|
|
199
226
|
}
|
|
200
227
|
}
|
|
201
228
|
// 2. Arquivo central (gestores Enterprise/Corporate)
|
|
202
229
|
if ((0, fs_1.existsSync)(CENTRAL_POLICY_PATH)) {
|
|
203
230
|
try {
|
|
204
|
-
const raw = (0, fs_1.readFileSync)(CENTRAL_POLICY_PATH, "utf-8");
|
|
205
|
-
const parsed =
|
|
231
|
+
const raw = JSON.parse((0, fs_1.readFileSync)(CENTRAL_POLICY_PATH, "utf-8"));
|
|
232
|
+
const parsed = validatePolicyInput(raw, CENTRAL_POLICY_PATH);
|
|
206
233
|
return mergeWithDefault(parsed);
|
|
207
234
|
}
|
|
208
|
-
catch {
|
|
209
|
-
|
|
235
|
+
catch (e) {
|
|
236
|
+
if (e instanceof Error && e.message.includes("[SECURITY]"))
|
|
237
|
+
throw e;
|
|
238
|
+
// JSON parse error — fallthrough
|
|
210
239
|
}
|
|
211
240
|
}
|
|
212
241
|
// 3. Env role simples
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Testes unitários para buildPayload() — geração de XML ADT para criação de objetos ABAP.
|
|
4
|
+
*
|
|
5
|
+
* Testa os 15 tipos de objeto suportados por abap_create:
|
|
6
|
+
* PROG/P, CLAS/OC, INTF/OI, FUGR/F, DOMA/D, DTEL/D, TABL/DT, TABL/DS,
|
|
7
|
+
* TTYP/TT, MSAG/N, DDLS/DF, DDLX/MX, SRVD/SRV, BDEF/BDO, SRVB/SVB
|
|
8
|
+
*/
|
|
9
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
12
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
13
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
14
|
+
}
|
|
15
|
+
Object.defineProperty(o, k2, desc);
|
|
16
|
+
}) : (function(o, m, k, k2) {
|
|
17
|
+
if (k2 === undefined) k2 = k;
|
|
18
|
+
o[k2] = m[k];
|
|
19
|
+
}));
|
|
20
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
21
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
22
|
+
}) : function(o, v) {
|
|
23
|
+
o["default"] = v;
|
|
24
|
+
});
|
|
25
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
26
|
+
var ownKeys = function(o) {
|
|
27
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
28
|
+
var ar = [];
|
|
29
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
30
|
+
return ar;
|
|
31
|
+
};
|
|
32
|
+
return ownKeys(o);
|
|
33
|
+
};
|
|
34
|
+
return function (mod) {
|
|
35
|
+
if (mod && mod.__esModule) return mod;
|
|
36
|
+
var result = {};
|
|
37
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
38
|
+
__setModuleDefault(result, mod);
|
|
39
|
+
return result;
|
|
40
|
+
};
|
|
41
|
+
})();
|
|
42
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
|
+
const vitest_1 = require("vitest");
|
|
44
|
+
// Setar env vars antes de importar (adt-client.ts lê no import time)
|
|
45
|
+
(0, vitest_1.beforeAll)(() => {
|
|
46
|
+
process.env.SAP_URL = "https://test.sap.local:8443";
|
|
47
|
+
process.env.SAP_USER = "TESTUSER";
|
|
48
|
+
process.env.SAP_PASS = "testpass";
|
|
49
|
+
process.env.SAP_CLIENT = "100";
|
|
50
|
+
process.env.SAP_LANGUAGE = "EN";
|
|
51
|
+
});
|
|
52
|
+
// Import dinâmico para garantir que env vars estão setadas
|
|
53
|
+
async function getBuildPayload() {
|
|
54
|
+
const mod = await Promise.resolve().then(() => __importStar(require("../tools/create.js")));
|
|
55
|
+
return mod.buildPayload;
|
|
56
|
+
}
|
|
57
|
+
// ─── Helpers ──────────────────────────────────────────────────────
|
|
58
|
+
function assertValidXml(xml) {
|
|
59
|
+
(0, vitest_1.expect)(xml).toMatch(/^<\?xml version="1\.0" encoding="UTF-8"\?>/);
|
|
60
|
+
// Verifica que tem pelo menos uma tag com namespace
|
|
61
|
+
(0, vitest_1.expect)(xml).toMatch(/<[a-zA-Z]+:[a-zA-Z]+/);
|
|
62
|
+
// Verifica que tem tag de fechamento ou é self-closing
|
|
63
|
+
(0, vitest_1.expect)(xml).toMatch(/<\/[a-zA-Z]+:[a-zA-Z]+>|\/>/);
|
|
64
|
+
}
|
|
65
|
+
function assertHasAttr(xml, attr, value) {
|
|
66
|
+
const regex = new RegExp(`${attr}="${value}"`, "i");
|
|
67
|
+
(0, vitest_1.expect)(xml).toMatch(regex);
|
|
68
|
+
}
|
|
69
|
+
function assertHasPackage(xml, pkg) {
|
|
70
|
+
(0, vitest_1.expect)(xml).toContain(`adtcore:name="${pkg}"`);
|
|
71
|
+
}
|
|
72
|
+
// ─── Tests ────────────────────────────────────────────────────────
|
|
73
|
+
(0, vitest_1.describe)("buildPayload", () => {
|
|
74
|
+
const baseInput = {
|
|
75
|
+
object_name: "ztest_object",
|
|
76
|
+
description: "Test object description",
|
|
77
|
+
package: "ZTEST_PKG",
|
|
78
|
+
};
|
|
79
|
+
(0, vitest_1.describe)("campos comuns em todos os tipos", () => {
|
|
80
|
+
vitest_1.it.each([
|
|
81
|
+
"PROG/P", "CLAS/OC", "INTF/OI", "FUGR/F", "DOMA/D", "DTEL/D",
|
|
82
|
+
"TABL/DT", "TABL/DS", "TTYP/TT", "MSAG/N", "DDLS/DF", "DDLX/MX",
|
|
83
|
+
"SRVD/SRV", "BDEF/BDO",
|
|
84
|
+
])("%s — gera XML válido com name, description e package", async (type) => {
|
|
85
|
+
const buildPayload = await getBuildPayload();
|
|
86
|
+
const xml = buildPayload({ ...baseInput, object_type: type });
|
|
87
|
+
assertValidXml(xml);
|
|
88
|
+
assertHasAttr(xml, "adtcore:name", "ZTEST_OBJECT");
|
|
89
|
+
assertHasAttr(xml, "adtcore:description", "Test object description");
|
|
90
|
+
assertHasPackage(xml, "ZTEST_PKG");
|
|
91
|
+
assertHasAttr(xml, "adtcore:language", "EN");
|
|
92
|
+
assertHasAttr(xml, "adtcore:responsible", "TESTUSER");
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
(0, vitest_1.describe)("PROG/P — Report", () => {
|
|
96
|
+
(0, vitest_1.it)("inclui programType executableProgram", async () => {
|
|
97
|
+
const buildPayload = await getBuildPayload();
|
|
98
|
+
const xml = buildPayload({ ...baseInput, object_type: "PROG/P" });
|
|
99
|
+
(0, vitest_1.expect)(xml).toContain("program:programType=\"executableProgram\"");
|
|
100
|
+
(0, vitest_1.expect)(xml).toContain("xmlns:program=");
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
(0, vitest_1.describe)("CLAS/OC — Classe", () => {
|
|
104
|
+
(0, vitest_1.it)("usa namespace class", async () => {
|
|
105
|
+
const buildPayload = await getBuildPayload();
|
|
106
|
+
const xml = buildPayload({ ...baseInput, object_type: "CLAS/OC" });
|
|
107
|
+
(0, vitest_1.expect)(xml).toContain("xmlns:class=");
|
|
108
|
+
(0, vitest_1.expect)(xml).toContain("class:abapClass");
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
(0, vitest_1.describe)("INTF/OI — Interface", () => {
|
|
112
|
+
(0, vitest_1.it)("usa namespace intf", async () => {
|
|
113
|
+
const buildPayload = await getBuildPayload();
|
|
114
|
+
const xml = buildPayload({ ...baseInput, object_type: "INTF/OI" });
|
|
115
|
+
(0, vitest_1.expect)(xml).toContain("xmlns:intf=");
|
|
116
|
+
(0, vitest_1.expect)(xml).toContain("intf:abapInterface");
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
(0, vitest_1.describe)("FUGR/F — Function Group", () => {
|
|
120
|
+
(0, vitest_1.it)("usa namespace group", async () => {
|
|
121
|
+
const buildPayload = await getBuildPayload();
|
|
122
|
+
const xml = buildPayload({ ...baseInput, object_type: "FUGR/F" });
|
|
123
|
+
(0, vitest_1.expect)(xml).toContain("xmlns:group=");
|
|
124
|
+
(0, vitest_1.expect)(xml).toContain("group:abapFunctionGroup");
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
(0, vitest_1.describe)("DDLS/DF — CDS View", () => {
|
|
128
|
+
(0, vitest_1.it)("usa namespace ddlSource", async () => {
|
|
129
|
+
const buildPayload = await getBuildPayload();
|
|
130
|
+
const xml = buildPayload({ ...baseInput, object_type: "DDLS/DF" });
|
|
131
|
+
(0, vitest_1.expect)(xml).toContain("xmlns:ddlSource=");
|
|
132
|
+
(0, vitest_1.expect)(xml).toContain("ddlSource:ddlSource");
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
(0, vitest_1.describe)("SRVD/SRV — Service Definition", () => {
|
|
136
|
+
(0, vitest_1.it)("inclui srvdSourceType S", async () => {
|
|
137
|
+
const buildPayload = await getBuildPayload();
|
|
138
|
+
const xml = buildPayload({ ...baseInput, object_type: "SRVD/SRV" });
|
|
139
|
+
(0, vitest_1.expect)(xml).toContain("srvdSource:srvdSourceType=\"S\"");
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
(0, vitest_1.describe)("SRVB/SVB — Service Binding", () => {
|
|
143
|
+
(0, vitest_1.it)("exige srvd_name", async () => {
|
|
144
|
+
const buildPayload = await getBuildPayload();
|
|
145
|
+
(0, vitest_1.expect)(() => buildPayload({ ...baseInput, object_type: "SRVB/SVB" }))
|
|
146
|
+
.toThrow("srvd_name é obrigatório");
|
|
147
|
+
});
|
|
148
|
+
(0, vitest_1.it)("gera XML com service definition e binding type padrão (ODATA V4 UI)", async () => {
|
|
149
|
+
const buildPayload = await getBuildPayload();
|
|
150
|
+
const xml = buildPayload({
|
|
151
|
+
...baseInput,
|
|
152
|
+
object_type: "SRVB/SVB",
|
|
153
|
+
srvd_name: "ZSD_PEDIDOS",
|
|
154
|
+
});
|
|
155
|
+
assertValidXml(xml);
|
|
156
|
+
(0, vitest_1.expect)(xml).toContain("adtcore:name=\"ZSD_PEDIDOS\"");
|
|
157
|
+
(0, vitest_1.expect)(xml).toContain("adtcore:type=\"SRVD/SRV\"");
|
|
158
|
+
(0, vitest_1.expect)(xml).toContain("srvb:type=\"ODATA\"");
|
|
159
|
+
(0, vitest_1.expect)(xml).toContain("srvb:version=\"V4\"");
|
|
160
|
+
(0, vitest_1.expect)(xml).toContain("srvb:category=\"0\""); // UI = category 0
|
|
161
|
+
});
|
|
162
|
+
(0, vitest_1.it)("suporta binding_type WEB_API", async () => {
|
|
163
|
+
const buildPayload = await getBuildPayload();
|
|
164
|
+
const xml = buildPayload({
|
|
165
|
+
...baseInput,
|
|
166
|
+
object_type: "SRVB/SVB",
|
|
167
|
+
srvd_name: "ZSD_PEDIDOS",
|
|
168
|
+
binding_type: "ODATA;V4;WEB_API",
|
|
169
|
+
});
|
|
170
|
+
(0, vitest_1.expect)(xml).toContain("srvb:category=\"1\""); // WEB_API = category 1
|
|
171
|
+
});
|
|
172
|
+
(0, vitest_1.it)("suporta binding_type V2", async () => {
|
|
173
|
+
const buildPayload = await getBuildPayload();
|
|
174
|
+
const xml = buildPayload({
|
|
175
|
+
...baseInput,
|
|
176
|
+
object_type: "SRVB/SVB",
|
|
177
|
+
srvd_name: "ZSD_PEDIDOS",
|
|
178
|
+
binding_type: "ODATA;V2;UI",
|
|
179
|
+
});
|
|
180
|
+
(0, vitest_1.expect)(xml).toContain("srvb:version=\"V2\"");
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
(0, vitest_1.describe)("nome em uppercase", () => {
|
|
184
|
+
(0, vitest_1.it)("converte object_name para maiúsculas", async () => {
|
|
185
|
+
const buildPayload = await getBuildPayload();
|
|
186
|
+
const xml = buildPayload({
|
|
187
|
+
...baseInput,
|
|
188
|
+
object_type: "PROG/P",
|
|
189
|
+
object_name: "ztest_lowercase",
|
|
190
|
+
});
|
|
191
|
+
assertHasAttr(xml, "adtcore:name", "ZTEST_LOWERCASE");
|
|
192
|
+
(0, vitest_1.expect)(xml).not.toContain("ztest_lowercase");
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
(0, vitest_1.describe)("package padrão $TMP", () => {
|
|
196
|
+
(0, vitest_1.it)("usa $TMP quando package não informado", async () => {
|
|
197
|
+
const buildPayload = await getBuildPayload();
|
|
198
|
+
const xml = buildPayload({
|
|
199
|
+
object_type: "PROG/P",
|
|
200
|
+
object_name: "ZTEST",
|
|
201
|
+
description: "Test",
|
|
202
|
+
});
|
|
203
|
+
assertHasPackage(xml, "$TMP");
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
(0, vitest_1.describe)("tipo não suportado", () => {
|
|
207
|
+
(0, vitest_1.it)("lança erro para tipo desconhecido", async () => {
|
|
208
|
+
const buildPayload = await getBuildPayload();
|
|
209
|
+
(0, vitest_1.expect)(() => buildPayload({ ...baseInput, object_type: "FAKE/XX" }))
|
|
210
|
+
.toThrow("não suportada");
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
});
|