mock-service-cli 4.10.0 → 4.11.1
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/README.md +48 -33
- package/dist/api-client/client.css +1016 -0
- package/dist/api-client/client.js +2101 -0
- package/dist/api-client/editor.js +15 -0
- package/dist/api-client/index.html +136 -0
- package/dist/api-client/login.html +20 -0
- package/dist/api-client/login.js +19 -0
- package/dist/api-client/worker.js +84 -0
- package/dist/apiClientServer.js +1 -0
- package/dist/assertionWorker.js +254 -0
- package/dist/meta.json +7912 -5288
- package/dist/runtime.js +201 -43
- package/docs/api-client.md +130 -0
- package/package.json +4 -2
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>API Client · Mock Service</title>
|
|
7
|
+
<link rel="stylesheet" href="/client.css" />
|
|
8
|
+
<script src="/client.js" defer></script>
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<header class="topbar">
|
|
12
|
+
<a class="brand" href="/" aria-label="API Client 首页"
|
|
13
|
+
><span class="logo">m<span>↗</span></span
|
|
14
|
+
><span>API Client <small>MOCK SERVICE</small></span></a
|
|
15
|
+
>
|
|
16
|
+
<div class="workspace-badge" id="workspace">本地工作区</div>
|
|
17
|
+
<div class="spacer"></div>
|
|
18
|
+
<button id="import">↓ 导入</button><button id="cookies">Cookies</button
|
|
19
|
+
><select id="environment" aria-label="当前环境">
|
|
20
|
+
<option value="">无环境</option></select
|
|
21
|
+
><button id="environments">环境管理</button
|
|
22
|
+
><select id="theme" aria-label="主题">
|
|
23
|
+
<option value="system">跟随系统</option>
|
|
24
|
+
<option value="light">浅色</option>
|
|
25
|
+
<option value="dark">深色</option></select
|
|
26
|
+
><button id="shortcuts">快捷键</button><button id="logout" title="清除当前会话与凭据">退出</button>
|
|
27
|
+
</header>
|
|
28
|
+
<main class="layout" id="layout">
|
|
29
|
+
<aside class="sidebar">
|
|
30
|
+
<div class="sidebar-heading">
|
|
31
|
+
<strong>工作区</strong><button id="new-collection" title="新建集合">+ 集合</button>
|
|
32
|
+
</div>
|
|
33
|
+
<div class="search-wrap">
|
|
34
|
+
<input id="search" type="search" placeholder="搜索请求…" aria-label="搜索请求" />
|
|
35
|
+
</div>
|
|
36
|
+
<div class="segment">
|
|
37
|
+
<button id="show-collections" class="active">集合</button><button id="show-history">历史</button>
|
|
38
|
+
</div>
|
|
39
|
+
<div id="tree" class="tree" role="tree" aria-label="请求列表"></div>
|
|
40
|
+
<div class="sidebar-footer"><span class="status-dot"></span> 本地优先 · 配置保存到项目</div>
|
|
41
|
+
</aside>
|
|
42
|
+
<div
|
|
43
|
+
class="splitter"
|
|
44
|
+
id="left-split"
|
|
45
|
+
role="separator"
|
|
46
|
+
aria-label="调整侧栏宽度"
|
|
47
|
+
aria-orientation="vertical"
|
|
48
|
+
tabindex="0"
|
|
49
|
+
></div>
|
|
50
|
+
<section class="request-pane">
|
|
51
|
+
<div class="tabs" id="tabs" role="tablist" aria-label="已打开请求"></div>
|
|
52
|
+
<div class="request-heading">
|
|
53
|
+
<div>
|
|
54
|
+
<span class="eyebrow">REQUEST</span><input id="request-name" aria-label="请求名称" value="新请求" />
|
|
55
|
+
</div>
|
|
56
|
+
<button id="save">保存 <kbd>⌘ S</kbd></button><button id="export">导出</button
|
|
57
|
+
><button id="new-request" title="新建请求">+</button>
|
|
58
|
+
</div>
|
|
59
|
+
<div class="url-bar">
|
|
60
|
+
<select id="method" aria-label="HTTP 方法">
|
|
61
|
+
<option>GET</option>
|
|
62
|
+
<option>POST</option>
|
|
63
|
+
<option>PUT</option>
|
|
64
|
+
<option>PATCH</option>
|
|
65
|
+
<option>DELETE</option>
|
|
66
|
+
<option>HEAD</option>
|
|
67
|
+
<option>OPTIONS</option></select
|
|
68
|
+
><input id="url" aria-label="请求 URL" placeholder="https://api.example.com/users" /><button
|
|
69
|
+
id="send"
|
|
70
|
+
class="primary"
|
|
71
|
+
>
|
|
72
|
+
发送 ↗</button
|
|
73
|
+
><button id="cancel" hidden>取消</button>
|
|
74
|
+
</div>
|
|
75
|
+
<div id="url-preview" class="url-preview"></div>
|
|
76
|
+
<nav class="panel-tabs" id="request-panels" aria-label="请求配置">
|
|
77
|
+
<button data-panel="query" class="active">Params</button><button data-panel="headers">Headers</button
|
|
78
|
+
><button data-panel="body">Body</button><button data-panel="auth">Auth</button
|
|
79
|
+
><button data-panel="tests">Tests</button><button data-panel="settings">设置</button>
|
|
80
|
+
</nav>
|
|
81
|
+
<div id="request-content" class="request-content"></div>
|
|
82
|
+
<div class="request-footer"><span id="save-state">未保存的请求</span><span>⌘ / Ctrl + Enter 发送</span></div>
|
|
83
|
+
</section>
|
|
84
|
+
<div
|
|
85
|
+
class="splitter"
|
|
86
|
+
id="right-split"
|
|
87
|
+
role="separator"
|
|
88
|
+
aria-label="调整响应宽度"
|
|
89
|
+
aria-orientation="vertical"
|
|
90
|
+
tabindex="0"
|
|
91
|
+
></div>
|
|
92
|
+
<section class="response-pane">
|
|
93
|
+
<div class="response-heading">
|
|
94
|
+
<div>
|
|
95
|
+
<span class="eyebrow">RESPONSE</span>
|
|
96
|
+
<h2>响应结果</h2>
|
|
97
|
+
</div>
|
|
98
|
+
<div class="spacer"></div>
|
|
99
|
+
<button id="run">▷ 运行集合</button>
|
|
100
|
+
</div>
|
|
101
|
+
<div class="response-metrics" id="metrics"><span class="muted">准备就绪</span></div>
|
|
102
|
+
<nav class="panel-tabs" id="response-panels" aria-label="响应内容">
|
|
103
|
+
<button data-view="body" class="active">Body</button><button data-view="headers">Headers</button
|
|
104
|
+
><button data-view="request">请求详情</button><button data-view="tests">Tests</button>
|
|
105
|
+
</nav>
|
|
106
|
+
<div class="response-tools">
|
|
107
|
+
<select id="response-format" aria-label="响应格式">
|
|
108
|
+
<option value="pretty">格式化</option>
|
|
109
|
+
<option value="tree">JSON 树</option>
|
|
110
|
+
<option value="raw">原文</option></select
|
|
111
|
+
><input id="response-search" placeholder="搜索响应…" aria-label="搜索响应" /><button id="response-copy">
|
|
112
|
+
复制</button
|
|
113
|
+
><button id="response-download">下载</button>
|
|
114
|
+
</div>
|
|
115
|
+
<div id="response-content" class="response-content">
|
|
116
|
+
<div class="empty-state">
|
|
117
|
+
<div class="empty-symbol">↗</div>
|
|
118
|
+
<h3>从一个请求开始</h3>
|
|
119
|
+
<p>输入接口地址,或导入已有集合。<br />响应、请求详情与断言结果将在这里呈现。</p>
|
|
120
|
+
<span class="shortcut">Ctrl / ⌘ + Enter</span>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
<div class="response-footer" id="response-footer">HTTP 工作台 · 数据保存在本地</div>
|
|
124
|
+
</section>
|
|
125
|
+
</main>
|
|
126
|
+
<div id="toast" role="status" aria-live="polite" hidden></div>
|
|
127
|
+
<dialog id="modal">
|
|
128
|
+
<div class="modal-head">
|
|
129
|
+
<h2 id="modal-title"></h2>
|
|
130
|
+
<button id="modal-close" aria-label="关闭对话框">×</button>
|
|
131
|
+
</div>
|
|
132
|
+
<div id="modal-body"></div>
|
|
133
|
+
<div id="modal-actions"></div>
|
|
134
|
+
</dialog>
|
|
135
|
+
</body>
|
|
136
|
+
</html>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>登录 · API Client</title>
|
|
7
|
+
<link rel="stylesheet" href="/client.css" />
|
|
8
|
+
<script src="/login.js" defer></script>
|
|
9
|
+
</head>
|
|
10
|
+
<body class="login-page">
|
|
11
|
+
<form id="login-form" class="login-card">
|
|
12
|
+
<span class="logo">m↗</span>
|
|
13
|
+
<h1>API Client</h1>
|
|
14
|
+
<p>输入启动服务时设置的密码,进入接口工作台。</p>
|
|
15
|
+
<label>访问密码<input id="password" type="password" autocomplete="current-password" required autofocus /></label
|
|
16
|
+
><button class="primary" type="submit">进入工作台 →</button>
|
|
17
|
+
<p id="error" role="alert"></p>
|
|
18
|
+
</form>
|
|
19
|
+
</body>
|
|
20
|
+
</html>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
document.getElementById('login-form').addEventListener('submit', async event => {
|
|
2
|
+
event.preventDefault();
|
|
3
|
+
const button = event.currentTarget.querySelector('button');
|
|
4
|
+
button.disabled = true;
|
|
5
|
+
try {
|
|
6
|
+
const response = await fetch('/__api-client/api/session', {
|
|
7
|
+
method: 'POST',
|
|
8
|
+
headers: { 'Content-Type': 'application/json' },
|
|
9
|
+
body: JSON.stringify({ password: document.getElementById('password').value })
|
|
10
|
+
});
|
|
11
|
+
const data = await response.json();
|
|
12
|
+
if (!response.ok) throw new Error(data.error);
|
|
13
|
+
location.assign('/');
|
|
14
|
+
} catch (error) {
|
|
15
|
+
document.getElementById('error').textContent = error.message;
|
|
16
|
+
} finally {
|
|
17
|
+
button.disabled = false;
|
|
18
|
+
}
|
|
19
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
let raw = '';
|
|
2
|
+
let parsed;
|
|
3
|
+
let text = '';
|
|
4
|
+
let lines = [];
|
|
5
|
+
function at(path) {
|
|
6
|
+
let value = parsed;
|
|
7
|
+
for (const key of path) value = value[key];
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
self.onmessage = event => {
|
|
11
|
+
const { id, action, value, path = [], query = '', start = 0, count = 200 } = event.data;
|
|
12
|
+
try {
|
|
13
|
+
let result;
|
|
14
|
+
if (action === 'format') {
|
|
15
|
+
result = JSON.stringify(JSON.parse(value), null, 2);
|
|
16
|
+
} else if (action === 'load') {
|
|
17
|
+
raw = value;
|
|
18
|
+
parsed = undefined;
|
|
19
|
+
try {
|
|
20
|
+
parsed = JSON.parse(raw);
|
|
21
|
+
} catch {
|
|
22
|
+
/* Non-JSON responses remain plain text. */
|
|
23
|
+
}
|
|
24
|
+
text = parsed === undefined ? raw : JSON.stringify(parsed, null, 2);
|
|
25
|
+
lines = text.split('\n');
|
|
26
|
+
result = { json: parsed !== undefined, lines: lines.length, length: text.length };
|
|
27
|
+
} else if (action === 'lines') {
|
|
28
|
+
const all = value === 'raw' ? raw.split('\n') : lines;
|
|
29
|
+
const rows = [];
|
|
30
|
+
if (query) {
|
|
31
|
+
for (let i = 0; i < all.length; i++)
|
|
32
|
+
if (all[i].toLowerCase().includes(query.toLowerCase()))
|
|
33
|
+
rows.push({ number: i + 1, text: all[i].slice(0, 20000) });
|
|
34
|
+
}
|
|
35
|
+
result = {
|
|
36
|
+
total: query ? rows.length : all.length,
|
|
37
|
+
rows: query
|
|
38
|
+
? rows.slice(start, start + count)
|
|
39
|
+
: all.slice(start, start + count).map((line, i) => ({ number: start + i + 1, text: line.slice(0, 20000) }))
|
|
40
|
+
};
|
|
41
|
+
if (parsed !== undefined) for (const row of result.rows) row.parts = jsonParts(row.text);
|
|
42
|
+
} else if (action === 'tree') {
|
|
43
|
+
const node = at(path);
|
|
44
|
+
if (node === null || typeof node !== 'object')
|
|
45
|
+
result = { leaf: node === undefined ? '此响应不是 JSON' : JSON.stringify(node) };
|
|
46
|
+
else {
|
|
47
|
+
const keys = Object.keys(node);
|
|
48
|
+
result = {
|
|
49
|
+
total: keys.length,
|
|
50
|
+
array: Array.isArray(node),
|
|
51
|
+
entries: keys.slice(start, start + count).map(key => ({
|
|
52
|
+
key,
|
|
53
|
+
branch: node[key] !== null && typeof node[key] === 'object',
|
|
54
|
+
summary:
|
|
55
|
+
node[key] !== null && typeof node[key] === 'object'
|
|
56
|
+
? Array.isArray(node[key])
|
|
57
|
+
? `[${node[key].length}]`
|
|
58
|
+
: `{${Object.keys(node[key]).length}}`
|
|
59
|
+
: JSON.stringify(node[key])?.slice(0, 2000)
|
|
60
|
+
}))
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
} else if (action === 'copy') result = value === 'raw' ? raw : text;
|
|
64
|
+
self.postMessage({ id, result });
|
|
65
|
+
} catch (error) {
|
|
66
|
+
self.postMessage({ id, error: error.message });
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
function jsonParts(line) {
|
|
71
|
+
const parts = [];
|
|
72
|
+
const tokens = /("(?:\\.|[^"\\])*")(\s*:)?|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?|\b(?:true|false|null)\b/g;
|
|
73
|
+
let start = 0;
|
|
74
|
+
for (const match of line.matchAll(tokens)) {
|
|
75
|
+
if (match.index > start) parts.push({ kind: 'punctuation', text: line.slice(start, match.index) });
|
|
76
|
+
parts.push({
|
|
77
|
+
kind: match[1] ? (match[2] ? 'key' : 'string') : /^[\d-]/.test(match[0]) ? 'number' : 'keyword',
|
|
78
|
+
text: match[0]
|
|
79
|
+
});
|
|
80
|
+
start = match.index + match[0].length;
|
|
81
|
+
}
|
|
82
|
+
if (start < line.length) parts.push({ kind: 'punctuation', text: line.slice(start) });
|
|
83
|
+
return parts;
|
|
84
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require('./runtime').load('apiClientServer');
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
2
|
+
var __commonJS = (cb, mod) => function __require() {
|
|
3
|
+
try {
|
|
4
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
5
|
+
} catch (e) {
|
|
6
|
+
throw mod = 0, e;
|
|
7
|
+
}
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
// src/lib/apiClient/model.js
|
|
11
|
+
var require_model = __commonJS({
|
|
12
|
+
"src/lib/apiClient/model.js"(exports2, module2) {
|
|
13
|
+
var crypto = require("crypto");
|
|
14
|
+
var METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
|
|
15
|
+
var LIMITS = { request: 20 * 1024 * 1024, response: 50 * 1024 * 1024, preview: 1024 * 1024, disk: 256 * 1024 * 1024 };
|
|
16
|
+
var sensitive = /authorization|cookie|password|token|secret|api[-_]?key/i;
|
|
17
|
+
function fail(message, status = 400) {
|
|
18
|
+
const error = new Error(message);
|
|
19
|
+
error.status = status;
|
|
20
|
+
return error;
|
|
21
|
+
}
|
|
22
|
+
function check(value, message, status = 400) {
|
|
23
|
+
if (!value) throw fail(message, status);
|
|
24
|
+
}
|
|
25
|
+
function safeObject(value, depth = 0) {
|
|
26
|
+
check(depth < 60, "\u6570\u636E\u5D4C\u5957\u8FC7\u6DF1");
|
|
27
|
+
if (!value || typeof value !== "object") return;
|
|
28
|
+
for (const key of Object.keys(value)) {
|
|
29
|
+
check(!["__proto__", "prototype", "constructor"].includes(key), "\u4E0D\u5141\u8BB8\u7684\u5BF9\u8C61\u5B57\u6BB5");
|
|
30
|
+
safeObject(value[key], depth + 1);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function entries(value = []) {
|
|
34
|
+
check(Array.isArray(value) && value.length <= 2e3, "\u53C2\u6570\u5FC5\u987B\u662F\u5217\u8868\uFF08\u6700\u591A 2000 \u9879\uFF09");
|
|
35
|
+
return value.map((row) => {
|
|
36
|
+
check(row && typeof row.key === "string" && typeof row.value === "string", "\u53C2\u6570\u9700\u8981\u5B57\u7B26\u4E32 key/value");
|
|
37
|
+
return { ...row, enabled: row.enabled !== false };
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
function request(value) {
|
|
41
|
+
check(value && typeof value === "object", "\u8BF7\u6C42\u4E0D\u80FD\u4E3A\u7A7A");
|
|
42
|
+
safeObject(value);
|
|
43
|
+
check(METHODS.includes(value.method), "\u4E0D\u652F\u6301\u7684 HTTP \u65B9\u6CD5");
|
|
44
|
+
check(typeof value.url === "string" && value.url.length <= 32768, "URL \u65E0\u6548\u6216\u8FC7\u957F");
|
|
45
|
+
const body = value.body || { type: "none" };
|
|
46
|
+
check(["none", "json", "text", "xml", "form", "multipart", "binary"].includes(body.type), "\u4E0D\u652F\u6301\u7684\u6B63\u6587\u7C7B\u578B");
|
|
47
|
+
const settings = { timeout: 3e4, redirects: true, verifyTls: true, cookies: true, ...value.settings };
|
|
48
|
+
check(
|
|
49
|
+
Number.isInteger(settings.timeout) && settings.timeout >= 1e3 && settings.timeout <= 3e5,
|
|
50
|
+
"\u8D85\u65F6\u5FC5\u987B\u4E3A 1000\u2013300000 \u6BEB\u79D2"
|
|
51
|
+
);
|
|
52
|
+
for (const key of ["redirects", "verifyTls", "cookies"])
|
|
53
|
+
check(typeof settings[key] === "boolean", `\u8BBE\u7F6E ${key} \u5FC5\u987B\u662F\u5E03\u5C14\u503C`);
|
|
54
|
+
const auth = value.auth || { type: "inherit" };
|
|
55
|
+
check(["inherit", "none", "basic", "bearer", "apiKey"].includes(auth.type), "\u4E0D\u652F\u6301\u7684\u8BA4\u8BC1\u65B9\u5F0F");
|
|
56
|
+
if (auth.type === "apiKey") check(auth.in === void 0 || ["header", "query"].includes(auth.in), "API Key \u4F4D\u7F6E\u65E0\u6548");
|
|
57
|
+
for (const key of ["username", "password", "token", "key", "value"])
|
|
58
|
+
check(auth[key] === void 0 || typeof auth[key] === "string", `\u8BA4\u8BC1 ${key} \u5FC5\u987B\u662F\u5B57\u7B26\u4E32`);
|
|
59
|
+
if (body.type === "form" || body.type === "multipart") entries(body.fields || []);
|
|
60
|
+
if (body.text !== void 0) check(typeof body.text === "string", "\u6B63\u6587\u5FC5\u987B\u662F\u5B57\u7B26\u4E32");
|
|
61
|
+
for (const key of ["assertions", "extractors"])
|
|
62
|
+
check(value[key] === void 0 || Array.isArray(value[key]) && value[key].length <= 100, `${key} \u6700\u591A 100 \u9879`);
|
|
63
|
+
for (const rule of value.assertions || []) {
|
|
64
|
+
check(
|
|
65
|
+
rule && typeof rule === "object" && ["status", "header", "json", "time"].includes(rule.source),
|
|
66
|
+
"\u65AD\u8A00\u6765\u6E90\u65E0\u6548"
|
|
67
|
+
);
|
|
68
|
+
check(["exists", "equals", "contains", "type", "lessThan"].includes(rule.op), "\u65AD\u8A00\u64CD\u4F5C\u65E0\u6548");
|
|
69
|
+
}
|
|
70
|
+
for (const rule of value.extractors || []) {
|
|
71
|
+
check(
|
|
72
|
+
rule && typeof rule.name === "string" && rule.name && ["json", "header"].includes(rule.source),
|
|
73
|
+
"\u53D8\u91CF\u63D0\u53D6\u89C4\u5219\u65E0\u6548"
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
...value,
|
|
78
|
+
id: value.id || crypto.randomUUID(),
|
|
79
|
+
name: String(value.name || "\u65B0\u8BF7\u6C42").slice(0, 200),
|
|
80
|
+
headers: entries(value.headers),
|
|
81
|
+
query: entries(value.query),
|
|
82
|
+
body,
|
|
83
|
+
auth,
|
|
84
|
+
settings,
|
|
85
|
+
assertions: value.assertions || [],
|
|
86
|
+
extractors: value.extractors || []
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function resource(kind, value) {
|
|
90
|
+
safeObject(value);
|
|
91
|
+
check(value && typeof value.name === "string" && value.name.trim(), "\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A");
|
|
92
|
+
check(value.schemaVersion === void 0 || value.schemaVersion === 1, "\u4E0D\u652F\u6301\u7684 schemaVersion");
|
|
93
|
+
const result = { ...value, schemaVersion: 1, variables: entries(value.variables) };
|
|
94
|
+
if (kind === "collections") {
|
|
95
|
+
if (value.auth) result.auth = request({ method: "GET", url: "", auth: value.auth }).auth;
|
|
96
|
+
let count = 0;
|
|
97
|
+
const ids = /* @__PURE__ */ new Set();
|
|
98
|
+
const visit = (items, depth = 0) => {
|
|
99
|
+
check(Array.isArray(items) && depth <= 20, "\u96C6\u5408\u76EE\u5F55\u65E0\u6548\u6216\u8FC7\u6DF1");
|
|
100
|
+
return items.map((item) => {
|
|
101
|
+
check(++count <= 1e4, "\u96C6\u5408\u6700\u591A 10000 \u4E2A\u6761\u76EE");
|
|
102
|
+
const id = item.id || crypto.randomUUID();
|
|
103
|
+
check(typeof id === "string" && !ids.has(id), "\u6761\u76EE ID \u5FC5\u987B\u552F\u4E00");
|
|
104
|
+
ids.add(id);
|
|
105
|
+
return item.type === "folder" ? { id, type: "folder", name: String(item.name || "\u76EE\u5F55"), items: visit(item.items || [], depth + 1) } : request({ ...item, id });
|
|
106
|
+
});
|
|
107
|
+
};
|
|
108
|
+
result.items = visit(value.items || []);
|
|
109
|
+
}
|
|
110
|
+
return result;
|
|
111
|
+
}
|
|
112
|
+
function variables(...groups) {
|
|
113
|
+
const result = /* @__PURE__ */ Object.create(null);
|
|
114
|
+
for (const group of groups) {
|
|
115
|
+
if (Array.isArray(group))
|
|
116
|
+
for (const row of group) {
|
|
117
|
+
if (row.enabled !== false) result[row.key] = row.value;
|
|
118
|
+
}
|
|
119
|
+
else if (group && typeof group === "object") Object.assign(result, group);
|
|
120
|
+
}
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
function interpolate(value, vars, seen = []) {
|
|
124
|
+
if (typeof value === "string")
|
|
125
|
+
return value.replace(/\{\{\s*([^{}]+?)\s*\}\}/g, (_, key) => {
|
|
126
|
+
check(Object.hasOwn(vars, key) && vars[key] !== void 0, `\u672A\u5B9A\u4E49\u53D8\u91CF: ${key}`);
|
|
127
|
+
check(!seen.includes(key) && seen.length < 20, `\u53D8\u91CF\u5FAA\u73AF\u5F15\u7528: ${key}`);
|
|
128
|
+
return interpolate(String(vars[key]), vars, [...seen, key]);
|
|
129
|
+
});
|
|
130
|
+
if (Array.isArray(value)) return value.map((row) => interpolate(row, vars, seen));
|
|
131
|
+
if (value && typeof value === "object") {
|
|
132
|
+
if (value.enabled === false) return value;
|
|
133
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, interpolate(item, vars, seen)]));
|
|
134
|
+
}
|
|
135
|
+
return value;
|
|
136
|
+
}
|
|
137
|
+
function pointer(value, path) {
|
|
138
|
+
if (path === "") return value;
|
|
139
|
+
check(typeof path === "string" && path.startsWith("/"), "JSON Pointer \u5FC5\u987B\u4EE5 / \u5F00\u59CB");
|
|
140
|
+
for (const key of path.slice(1).split("/").map((part) => part.replace(/~1/g, "/").replace(/~0/g, "~"))) {
|
|
141
|
+
if (value === null || typeof value !== "object" || !Object.hasOwn(value, key)) return void 0;
|
|
142
|
+
value = value[key];
|
|
143
|
+
}
|
|
144
|
+
return value;
|
|
145
|
+
}
|
|
146
|
+
function evaluate2(rules, result, json2) {
|
|
147
|
+
const select = (rule) => {
|
|
148
|
+
if (rule.source === "status") return result.status;
|
|
149
|
+
if (rule.source === "time") return result.duration;
|
|
150
|
+
if (rule.source === "header")
|
|
151
|
+
return result.headers.find((row) => row.key.toLowerCase() === String(rule.path).toLowerCase())?.value;
|
|
152
|
+
if (rule.source === "json") return pointer(json2, rule.path);
|
|
153
|
+
throw fail("\u4E0D\u652F\u6301\u7684\u65AD\u8A00\u6765\u6E90");
|
|
154
|
+
};
|
|
155
|
+
const assertions = (rules.assertions || []).map((rule) => {
|
|
156
|
+
try {
|
|
157
|
+
const actual = select(rule);
|
|
158
|
+
let passed;
|
|
159
|
+
switch (rule.op) {
|
|
160
|
+
case "exists":
|
|
161
|
+
passed = actual !== void 0;
|
|
162
|
+
break;
|
|
163
|
+
case "equals":
|
|
164
|
+
passed = JSON.stringify(actual) === JSON.stringify(rule.value);
|
|
165
|
+
break;
|
|
166
|
+
case "contains":
|
|
167
|
+
passed = (typeof actual === "string" || Array.isArray(actual)) && actual.includes(rule.value);
|
|
168
|
+
break;
|
|
169
|
+
case "type":
|
|
170
|
+
passed = (Array.isArray(actual) ? "array" : actual === null ? "null" : typeof actual) === rule.value;
|
|
171
|
+
break;
|
|
172
|
+
case "lessThan":
|
|
173
|
+
passed = typeof actual === "number" && actual < Number(rule.value);
|
|
174
|
+
break;
|
|
175
|
+
default:
|
|
176
|
+
throw fail("\u4E0D\u652F\u6301\u7684\u65AD\u8A00\u64CD\u4F5C");
|
|
177
|
+
}
|
|
178
|
+
return { name: rule.name || `${rule.source} ${rule.path || ""} ${rule.op}`, passed, actual };
|
|
179
|
+
} catch (error) {
|
|
180
|
+
return { name: rule.name || "\u65AD\u8A00", passed: false, error: error.message };
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
const extracted = /* @__PURE__ */ Object.create(null);
|
|
184
|
+
const errors = [];
|
|
185
|
+
for (const rule of rules.extractors || []) {
|
|
186
|
+
try {
|
|
187
|
+
check(
|
|
188
|
+
typeof rule.name === "string" && rule.name && !["__proto__", "constructor", "prototype"].includes(rule.name),
|
|
189
|
+
"\u63D0\u53D6\u53D8\u91CF\u540D\u65E0\u6548"
|
|
190
|
+
);
|
|
191
|
+
check(["header", "json"].includes(rule.source), "\u63D0\u53D6\u4EC5\u652F\u6301 header/json");
|
|
192
|
+
const value = select(rule);
|
|
193
|
+
check(value !== void 0, `\u63D0\u53D6\u5931\u8D25: ${rule.name}`);
|
|
194
|
+
extracted[rule.name] = typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
195
|
+
} catch (error) {
|
|
196
|
+
errors.push(error.message);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return { assertions, extracted, extractionErrors: errors };
|
|
200
|
+
}
|
|
201
|
+
function redact(value) {
|
|
202
|
+
if (Array.isArray(value)) return value.map(redact);
|
|
203
|
+
if (!value || typeof value !== "object") return value;
|
|
204
|
+
const output = {};
|
|
205
|
+
for (const [key, item] of Object.entries(value)) {
|
|
206
|
+
if (["uploadId", "revision"].includes(key)) continue;
|
|
207
|
+
if (key === "url" && typeof item === "string") {
|
|
208
|
+
output[key] = item.replace(/([?&])([^=&]+)=([^&#]*)/g, (whole, separator, name, val) => {
|
|
209
|
+
try {
|
|
210
|
+
return sensitive.test(decodeURIComponent(name)) && !/\{\{[^{}]+\}\}/.test(decodeURIComponent(val)) ? `${separator}${name}=` : whole;
|
|
211
|
+
} catch {
|
|
212
|
+
return whole;
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
const isSecret = key === "value" && (value.secret || sensitive.test(value.key || "")) || ["password", "token", "username"].includes(key) || key === "value" && value.type === "apiKey";
|
|
218
|
+
output[key] = isSecret && typeof item === "string" && !/\{\{[^{}]+\}\}/.test(item) ? "" : redact(item);
|
|
219
|
+
}
|
|
220
|
+
return output;
|
|
221
|
+
}
|
|
222
|
+
function flatten(items) {
|
|
223
|
+
return items.flatMap((item) => item.type === "folder" ? flatten(item.items) : [item]);
|
|
224
|
+
}
|
|
225
|
+
module2.exports = {
|
|
226
|
+
METHODS,
|
|
227
|
+
LIMITS,
|
|
228
|
+
sensitive,
|
|
229
|
+
fail,
|
|
230
|
+
check,
|
|
231
|
+
safeObject,
|
|
232
|
+
entries,
|
|
233
|
+
request,
|
|
234
|
+
resource,
|
|
235
|
+
variables,
|
|
236
|
+
interpolate,
|
|
237
|
+
pointer,
|
|
238
|
+
evaluate: evaluate2,
|
|
239
|
+
redact,
|
|
240
|
+
flatten
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// src/lib/apiClient/assertionWorker.js
|
|
246
|
+
var { parentPort, workerData } = require("worker_threads");
|
|
247
|
+
var { readFileSync } = require("fs");
|
|
248
|
+
var { evaluate } = require_model();
|
|
249
|
+
var json;
|
|
250
|
+
try {
|
|
251
|
+
json = JSON.parse(readFileSync(workerData.file, "utf8"));
|
|
252
|
+
} catch {
|
|
253
|
+
}
|
|
254
|
+
parentPort.postMessage(evaluate(workerData.rules, workerData.result, json));
|