@glubean/sdk 0.1.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/configure.d.ts +141 -0
- package/dist/configure.d.ts.map +1 -0
- package/dist/configure.js +535 -0
- package/dist/configure.js.map +1 -0
- package/dist/data.d.ts +232 -0
- package/dist/data.d.ts.map +1 -0
- package/dist/data.js +543 -0
- package/dist/data.js.map +1 -0
- package/dist/expect.d.ts +511 -0
- package/dist/expect.d.ts.map +1 -0
- package/dist/expect.js +763 -0
- package/dist/expect.js.map +1 -0
- package/dist/index.d.ts +718 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1015 -0
- package/dist/index.js.map +1 -0
- package/dist/internal.d.ts +39 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +52 -0
- package/dist/internal.js.map +1 -0
- package/dist/plugin.d.ts +56 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +57 -0
- package/dist/plugin.js.map +1 -0
- package/dist/types.d.ts +1971 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +54 -0
- package/dist/types.js.map +1 -0
- package/package.json +40 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File-level configuration for Glubean tests.
|
|
3
|
+
*
|
|
4
|
+
* `configure()` lets you declare shared dependencies (vars, secrets, HTTP config)
|
|
5
|
+
* once at the top of a test file (or in a shared `configure.ts`), eliminating
|
|
6
|
+
* repetitive `ctx.vars.require()` / `ctx.secrets.require()` calls in every test.
|
|
7
|
+
*
|
|
8
|
+
* All returned values are **lazy** — they are not resolved until a test function
|
|
9
|
+
* actually accesses them at runtime. This means:
|
|
10
|
+
* - Safe to call at module top-level (scanner won't trigger resolution)
|
|
11
|
+
* - Safe to share across files via re-exports
|
|
12
|
+
* - Each test execution gets the correct runtime values
|
|
13
|
+
*
|
|
14
|
+
* @example Single file usage
|
|
15
|
+
* ```ts
|
|
16
|
+
* import { test, configure } from "@glubean/sdk";
|
|
17
|
+
*
|
|
18
|
+
* const { vars, secrets, http } = configure({
|
|
19
|
+
* vars: { baseUrl: "BASE_URL" },
|
|
20
|
+
* secrets: { apiKey: "API_KEY" },
|
|
21
|
+
* http: {
|
|
22
|
+
* prefixUrl: "BASE_URL",
|
|
23
|
+
* headers: { Authorization: "Bearer {{API_KEY}}" },
|
|
24
|
+
* },
|
|
25
|
+
* });
|
|
26
|
+
*
|
|
27
|
+
* export const listUsers = test("list-users", async (ctx) => {
|
|
28
|
+
* const res = await http.get("users").json();
|
|
29
|
+
* ctx.assert(res.length > 0, "has users");
|
|
30
|
+
* });
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* @example Shared across files (tests/configure.ts)
|
|
34
|
+
* ```ts
|
|
35
|
+
* // tests/configure.ts
|
|
36
|
+
* import { configure } from "@glubean/sdk";
|
|
37
|
+
* export const { vars, secrets, http } = configure({
|
|
38
|
+
* vars: { baseUrl: "BASE_URL" },
|
|
39
|
+
* http: { prefixUrl: "BASE_URL" },
|
|
40
|
+
* });
|
|
41
|
+
*
|
|
42
|
+
* // tests/users.test.ts
|
|
43
|
+
* import { test } from "@glubean/sdk";
|
|
44
|
+
* import { http } from "./configure.js";
|
|
45
|
+
*
|
|
46
|
+
* export const listUsers = test("list-users", async (ctx) => {
|
|
47
|
+
* const res = await http.get("users").json();
|
|
48
|
+
* });
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* @module configure
|
|
52
|
+
*/
|
|
53
|
+
import type { ConfigureOptions, ConfigureResult, GlubeanAction, GlubeanEvent, GlubeanRuntime, HttpClient, PluginEntry, PluginFactory, ReservedConfigureKeys, ResolvePlugins } from "./types.js";
|
|
54
|
+
/**
|
|
55
|
+
* Shape of the runtime context injected by the harness before test execution.
|
|
56
|
+
* This is the internal shape — the public `GlubeanRuntime` in types.ts adds
|
|
57
|
+
* helper methods (requireVar, requireSecret, resolveTemplate) for plugins.
|
|
58
|
+
*
|
|
59
|
+
* @internal
|
|
60
|
+
*/
|
|
61
|
+
export interface InternalRuntime {
|
|
62
|
+
vars: Record<string, string>;
|
|
63
|
+
secrets: Record<string, string>;
|
|
64
|
+
http: HttpClient;
|
|
65
|
+
test?: GlubeanRuntime["test"];
|
|
66
|
+
action?(a: GlubeanAction): void;
|
|
67
|
+
event?(ev: GlubeanEvent): void;
|
|
68
|
+
log?(message: string, data?: unknown): void;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Resolve `{{key}}` template placeholders in a string using runtime vars and secrets.
|
|
72
|
+
* Secrets take precedence over vars if both have the same key.
|
|
73
|
+
*
|
|
74
|
+
* This is used internally by `buildLazyHttp()` and exposed to plugin authors
|
|
75
|
+
* via `GlubeanRuntime.resolveTemplate()`.
|
|
76
|
+
*/
|
|
77
|
+
export declare function resolveTemplate(template: string, vars: Record<string, string>, secrets: Record<string, string>): string;
|
|
78
|
+
/**
|
|
79
|
+
* Declare file-level dependencies on vars, secrets, and HTTP configuration.
|
|
80
|
+
*
|
|
81
|
+
* Returns lazy accessors that resolve at test runtime, not at import time.
|
|
82
|
+
* All declared vars and secrets are **required** — missing values cause the test
|
|
83
|
+
* to fail immediately with a clear error message.
|
|
84
|
+
*
|
|
85
|
+
* The returned objects can be shared across files via re-exports.
|
|
86
|
+
*
|
|
87
|
+
* @param options Configuration declaring vars, secrets, and HTTP defaults
|
|
88
|
+
* @returns Lazy accessors for vars, secrets, and a pre-configured HTTP client
|
|
89
|
+
*
|
|
90
|
+
* @example Basic usage
|
|
91
|
+
* ```ts
|
|
92
|
+
* import { test, configure } from "@glubean/sdk";
|
|
93
|
+
*
|
|
94
|
+
* const { vars, http } = configure({
|
|
95
|
+
* vars: { baseUrl: "base_url" },
|
|
96
|
+
* http: { prefixUrl: "base_url" },
|
|
97
|
+
* });
|
|
98
|
+
*
|
|
99
|
+
* export const listUsers = test("list-users", async (ctx) => {
|
|
100
|
+
* const res = await http.get("users").json();
|
|
101
|
+
* ctx.log(`Base URL: ${vars.baseUrl}`);
|
|
102
|
+
* });
|
|
103
|
+
* ```
|
|
104
|
+
*
|
|
105
|
+
* @example Full configuration with secrets
|
|
106
|
+
* ```ts
|
|
107
|
+
* const { vars, secrets, http } = configure({
|
|
108
|
+
* vars: { baseUrl: "base_url", orgId: "org_id" },
|
|
109
|
+
* secrets: { apiKey: "api_key" },
|
|
110
|
+
* http: {
|
|
111
|
+
* prefixUrl: "base_url",
|
|
112
|
+
* headers: { Authorization: "Bearer {{api_key}}" },
|
|
113
|
+
* },
|
|
114
|
+
* });
|
|
115
|
+
* ```
|
|
116
|
+
*
|
|
117
|
+
* @example Shared across test files
|
|
118
|
+
* ```ts
|
|
119
|
+
* // tests/configure.ts
|
|
120
|
+
* export const { vars, secrets, http } = configure({ ... });
|
|
121
|
+
*
|
|
122
|
+
* // tests/users.test.ts
|
|
123
|
+
* import { http, vars } from "./configure.js";
|
|
124
|
+
* ```
|
|
125
|
+
*/
|
|
126
|
+
export declare function configure<V extends Record<string, string> = Record<string, string>, S extends Record<string, string> = Record<string, string>, P extends Record<string, PluginFactory<any> | PluginEntry<any>> = Record<string, never>>(options: ConfigureOptions & {
|
|
127
|
+
vars?: {
|
|
128
|
+
[K in keyof V]: string;
|
|
129
|
+
};
|
|
130
|
+
secrets?: {
|
|
131
|
+
[K in keyof S]: string;
|
|
132
|
+
};
|
|
133
|
+
plugins?: P & {
|
|
134
|
+
[K in ReservedConfigureKeys]?: never;
|
|
135
|
+
};
|
|
136
|
+
}): ConfigureResult<{
|
|
137
|
+
[K in keyof V]: string;
|
|
138
|
+
}, {
|
|
139
|
+
[K in keyof S]: string;
|
|
140
|
+
}> & ResolvePlugins<P>;
|
|
141
|
+
//# sourceMappingURL=configure.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"configure.d.ts","sourceRoot":"","sources":["../src/configure.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AAEH,OAAO,KAAK,EAEV,gBAAgB,EAChB,eAAe,EACf,aAAa,EACb,YAAY,EACZ,cAAc,EACd,UAAU,EAGV,WAAW,EACX,aAAa,EAEb,qBAAqB,EACrB,cAAc,EACf,MAAM,YAAY,CAAC;AAMpB;;;;;;GAMG;AACH,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,IAAI,EAAE,UAAU,CAAC;IACjB,IAAI,CAAC,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IAC9B,MAAM,CAAC,CAAC,CAAC,EAAE,aAAa,GAAG,IAAI,CAAC;IAChC,KAAK,CAAC,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CAAC;IAC/B,GAAG,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;CAC7C;AA0GD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC9B,MAAM,CAYR;AA6YD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AACH,wBAAgB,SAAS,CACvB,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EACzD,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAEzD,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,GAAG,CAAC,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC,GAAG,MAAM,CACtE,MAAM,EACN,KAAK,CACN,EAED,OAAO,EAAE,gBAAgB,GAAG;IAC1B,IAAI,CAAC,EAAE;SAAG,CAAC,IAAI,MAAM,CAAC,GAAG,MAAM;KAAE,CAAC;IAClC,OAAO,CAAC,EAAE;SAAG,CAAC,IAAI,MAAM,CAAC,GAAG,MAAM;KAAE,CAAC;IACrC,OAAO,CAAC,EAAE,CAAC,GAAG;SAAG,CAAC,IAAI,qBAAqB,CAAC,CAAC,EAAE,KAAK;KAAE,CAAC;CACxD,GAEC,eAAe,CACf;KAAG,CAAC,IAAI,MAAM,CAAC,GAAG,MAAM;CAAE,EAC1B;KAAG,CAAC,IAAI,MAAM,CAAC,GAAG,MAAM;CAAE,CAC3B,GACC,cAAc,CAAC,CAAC,CAAC,CAuBpB"}
|
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File-level configuration for Glubean tests.
|
|
3
|
+
*
|
|
4
|
+
* `configure()` lets you declare shared dependencies (vars, secrets, HTTP config)
|
|
5
|
+
* once at the top of a test file (or in a shared `configure.ts`), eliminating
|
|
6
|
+
* repetitive `ctx.vars.require()` / `ctx.secrets.require()` calls in every test.
|
|
7
|
+
*
|
|
8
|
+
* All returned values are **lazy** — they are not resolved until a test function
|
|
9
|
+
* actually accesses them at runtime. This means:
|
|
10
|
+
* - Safe to call at module top-level (scanner won't trigger resolution)
|
|
11
|
+
* - Safe to share across files via re-exports
|
|
12
|
+
* - Each test execution gets the correct runtime values
|
|
13
|
+
*
|
|
14
|
+
* @example Single file usage
|
|
15
|
+
* ```ts
|
|
16
|
+
* import { test, configure } from "@glubean/sdk";
|
|
17
|
+
*
|
|
18
|
+
* const { vars, secrets, http } = configure({
|
|
19
|
+
* vars: { baseUrl: "BASE_URL" },
|
|
20
|
+
* secrets: { apiKey: "API_KEY" },
|
|
21
|
+
* http: {
|
|
22
|
+
* prefixUrl: "BASE_URL",
|
|
23
|
+
* headers: { Authorization: "Bearer {{API_KEY}}" },
|
|
24
|
+
* },
|
|
25
|
+
* });
|
|
26
|
+
*
|
|
27
|
+
* export const listUsers = test("list-users", async (ctx) => {
|
|
28
|
+
* const res = await http.get("users").json();
|
|
29
|
+
* ctx.assert(res.length > 0, "has users");
|
|
30
|
+
* });
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* @example Shared across files (tests/configure.ts)
|
|
34
|
+
* ```ts
|
|
35
|
+
* // tests/configure.ts
|
|
36
|
+
* import { configure } from "@glubean/sdk";
|
|
37
|
+
* export const { vars, secrets, http } = configure({
|
|
38
|
+
* vars: { baseUrl: "BASE_URL" },
|
|
39
|
+
* http: { prefixUrl: "BASE_URL" },
|
|
40
|
+
* });
|
|
41
|
+
*
|
|
42
|
+
* // tests/users.test.ts
|
|
43
|
+
* import { test } from "@glubean/sdk";
|
|
44
|
+
* import { http } from "./configure.js";
|
|
45
|
+
*
|
|
46
|
+
* export const listUsers = test("list-users", async (ctx) => {
|
|
47
|
+
* const res = await http.get("users").json();
|
|
48
|
+
* });
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* @module configure
|
|
52
|
+
*/
|
|
53
|
+
/**
|
|
54
|
+
* Get the current runtime context from the global slot.
|
|
55
|
+
* Throws a clear error if accessed outside of test execution (e.g., at scan time).
|
|
56
|
+
*
|
|
57
|
+
* @internal
|
|
58
|
+
*/
|
|
59
|
+
function getRuntime() {
|
|
60
|
+
const runtime = globalThis.__glubeanRuntime;
|
|
61
|
+
if (!runtime) {
|
|
62
|
+
throw new Error("configure() values can only be accessed during test execution. " +
|
|
63
|
+
"Did you try to read a var or secret at module load time? " +
|
|
64
|
+
"Move the access inside a test function.");
|
|
65
|
+
}
|
|
66
|
+
return runtime;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Require a var from the runtime context.
|
|
70
|
+
* Throws if the var is missing or empty.
|
|
71
|
+
*
|
|
72
|
+
* @internal
|
|
73
|
+
*/
|
|
74
|
+
function requireVar(key) {
|
|
75
|
+
const runtime = getRuntime();
|
|
76
|
+
const value = runtime.vars[key];
|
|
77
|
+
if (value === undefined || value === null || value === "") {
|
|
78
|
+
throw new Error(`Missing required var: ${key}`);
|
|
79
|
+
}
|
|
80
|
+
return value;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Require a secret from the runtime context.
|
|
84
|
+
* Throws if the secret is missing or empty.
|
|
85
|
+
*
|
|
86
|
+
* @internal
|
|
87
|
+
*/
|
|
88
|
+
function requireSecret(key) {
|
|
89
|
+
const runtime = getRuntime();
|
|
90
|
+
const value = runtime.secrets[key];
|
|
91
|
+
if (value === undefined || value === null || value === "") {
|
|
92
|
+
throw new Error(`Missing required secret: ${key}`);
|
|
93
|
+
}
|
|
94
|
+
return value;
|
|
95
|
+
}
|
|
96
|
+
// =============================================================================
|
|
97
|
+
// Lazy proxy builders
|
|
98
|
+
// =============================================================================
|
|
99
|
+
/**
|
|
100
|
+
* Regex for `{{key}}` template placeholders in header values.
|
|
101
|
+
*/
|
|
102
|
+
const TEMPLATE_RE = /\{\{([\w-]+)\}\}/g;
|
|
103
|
+
/**
|
|
104
|
+
* Build a lazy vars accessor object.
|
|
105
|
+
* Each property is a getter that calls `requireVar()` on access.
|
|
106
|
+
*
|
|
107
|
+
* @internal
|
|
108
|
+
*/
|
|
109
|
+
function buildLazyVars(mapping) {
|
|
110
|
+
const obj = {};
|
|
111
|
+
for (const [prop, varKey] of Object.entries(mapping)) {
|
|
112
|
+
Object.defineProperty(obj, prop, {
|
|
113
|
+
get() {
|
|
114
|
+
return requireVar(varKey);
|
|
115
|
+
},
|
|
116
|
+
enumerable: true,
|
|
117
|
+
configurable: false,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
return obj;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Build a lazy secrets accessor object.
|
|
124
|
+
* Each property is a getter that calls `requireSecret()` on access.
|
|
125
|
+
*
|
|
126
|
+
* @internal
|
|
127
|
+
*/
|
|
128
|
+
function buildLazySecrets(mapping) {
|
|
129
|
+
const obj = {};
|
|
130
|
+
for (const [prop, secretKey] of Object.entries(mapping)) {
|
|
131
|
+
Object.defineProperty(obj, prop, {
|
|
132
|
+
get() {
|
|
133
|
+
return requireSecret(secretKey);
|
|
134
|
+
},
|
|
135
|
+
enumerable: true,
|
|
136
|
+
configurable: false,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return obj;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Resolve `{{key}}` template placeholders in a string using runtime vars and secrets.
|
|
143
|
+
* Secrets take precedence over vars if both have the same key.
|
|
144
|
+
*
|
|
145
|
+
* This is used internally by `buildLazyHttp()` and exposed to plugin authors
|
|
146
|
+
* via `GlubeanRuntime.resolveTemplate()`.
|
|
147
|
+
*/
|
|
148
|
+
export function resolveTemplate(template, vars, secrets) {
|
|
149
|
+
return template.replace(TEMPLATE_RE, (_match, key) => {
|
|
150
|
+
// Try secrets first (more likely for auth headers), then vars
|
|
151
|
+
const value = secrets[key] ?? vars[key];
|
|
152
|
+
if (value === undefined || value === null || value === "") {
|
|
153
|
+
throw new Error(`Missing value for template placeholder "{{${key}}}" in configure() http headers. ` +
|
|
154
|
+
`Ensure "${key}" is available as a var or secret.`);
|
|
155
|
+
}
|
|
156
|
+
return value;
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Build a lazy HTTP client proxy.
|
|
161
|
+
* On first method call, resolves the config and creates an extended client.
|
|
162
|
+
*
|
|
163
|
+
* @internal
|
|
164
|
+
*/
|
|
165
|
+
function buildLazyHttp(httpOptions) {
|
|
166
|
+
// Cache the resolved client per runtime identity to avoid re-extending on every call.
|
|
167
|
+
// Since each test runs in its own subprocess, a WeakMap keyed on runtime object
|
|
168
|
+
// ensures we get one extended client per test execution.
|
|
169
|
+
const cache = new WeakMap();
|
|
170
|
+
function getClient() {
|
|
171
|
+
const runtime = getRuntime();
|
|
172
|
+
let client = cache.get(runtime);
|
|
173
|
+
if (client)
|
|
174
|
+
return client;
|
|
175
|
+
// Build ky-compatible options from the configure http config
|
|
176
|
+
const extendOptions = {};
|
|
177
|
+
if (httpOptions.prefixUrl) {
|
|
178
|
+
extendOptions.prefixUrl = requireVar(httpOptions.prefixUrl);
|
|
179
|
+
}
|
|
180
|
+
if (httpOptions.headers) {
|
|
181
|
+
const resolvedHeaders = {};
|
|
182
|
+
for (const [name, template] of Object.entries(httpOptions.headers)) {
|
|
183
|
+
resolvedHeaders[name] = resolveTemplate(template, runtime.vars, runtime.secrets);
|
|
184
|
+
}
|
|
185
|
+
extendOptions.headers = resolvedHeaders;
|
|
186
|
+
}
|
|
187
|
+
if (httpOptions.timeout !== undefined) {
|
|
188
|
+
extendOptions.timeout = httpOptions.timeout;
|
|
189
|
+
}
|
|
190
|
+
if (httpOptions.retry !== undefined) {
|
|
191
|
+
extendOptions.retry = httpOptions.retry;
|
|
192
|
+
}
|
|
193
|
+
if (httpOptions.throwHttpErrors !== undefined) {
|
|
194
|
+
extendOptions.throwHttpErrors = httpOptions.throwHttpErrors;
|
|
195
|
+
}
|
|
196
|
+
if (httpOptions.hooks) {
|
|
197
|
+
extendOptions.hooks = httpOptions.hooks;
|
|
198
|
+
}
|
|
199
|
+
client = runtime.http.extend(extendOptions);
|
|
200
|
+
cache.set(runtime, client);
|
|
201
|
+
return client;
|
|
202
|
+
}
|
|
203
|
+
// Create a callable proxy that delegates all method calls to the lazily-resolved client.
|
|
204
|
+
const HTTP_METHODS = [
|
|
205
|
+
"get",
|
|
206
|
+
"post",
|
|
207
|
+
"put",
|
|
208
|
+
"patch",
|
|
209
|
+
"delete",
|
|
210
|
+
"head",
|
|
211
|
+
];
|
|
212
|
+
// The callable function (for `http(url, options)` shorthand)
|
|
213
|
+
const proxy = function (url, options) {
|
|
214
|
+
return getClient()(url, options);
|
|
215
|
+
};
|
|
216
|
+
// Method shortcuts
|
|
217
|
+
for (const method of HTTP_METHODS) {
|
|
218
|
+
proxy[method] = (url, options) => getClient()[method](url, options);
|
|
219
|
+
}
|
|
220
|
+
// extend() — returns a new HttpClient that merges options with the resolved base
|
|
221
|
+
proxy.extend = (options) => getClient().extend(options);
|
|
222
|
+
return proxy;
|
|
223
|
+
}
|
|
224
|
+
// =============================================================================
|
|
225
|
+
// Lazy plugin builder
|
|
226
|
+
// =============================================================================
|
|
227
|
+
/**
|
|
228
|
+
* Build lazy property descriptors for plugin factories.
|
|
229
|
+
* Each plugin is instantiated on first property access with a WeakMap cache
|
|
230
|
+
* keyed by the internal runtime identity.
|
|
231
|
+
*
|
|
232
|
+
* Returns PropertyDescriptorMap (not a ready object) so the caller can use
|
|
233
|
+
* Object.defineProperties() without triggering getters via spread.
|
|
234
|
+
*
|
|
235
|
+
* @internal
|
|
236
|
+
*/
|
|
237
|
+
/** Reserved keys that plugins cannot shadow. */
|
|
238
|
+
const RESERVED_KEYS = new Set(["vars", "secrets", "http"]);
|
|
239
|
+
function normalizePluginEntry(entry) {
|
|
240
|
+
if ("factory" in entry)
|
|
241
|
+
return entry;
|
|
242
|
+
return { factory: entry };
|
|
243
|
+
}
|
|
244
|
+
function toMethodList(value) {
|
|
245
|
+
if (!value)
|
|
246
|
+
return [];
|
|
247
|
+
const list = Array.isArray(value) ? value : [value];
|
|
248
|
+
return list.map((method) => method.toUpperCase());
|
|
249
|
+
}
|
|
250
|
+
function toUrlString(input) {
|
|
251
|
+
if (input instanceof Request)
|
|
252
|
+
return input.url;
|
|
253
|
+
if (input instanceof URL)
|
|
254
|
+
return input.toString();
|
|
255
|
+
return input;
|
|
256
|
+
}
|
|
257
|
+
function toPathname(input) {
|
|
258
|
+
try {
|
|
259
|
+
if (input instanceof Request)
|
|
260
|
+
return new URL(input.url).pathname;
|
|
261
|
+
if (input instanceof URL)
|
|
262
|
+
return input.pathname;
|
|
263
|
+
const isAbsolute = /^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(input);
|
|
264
|
+
const parsed = isAbsolute ? new URL(input) : new URL(input, "http://glubean.local");
|
|
265
|
+
return parsed.pathname;
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
return "";
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
function matchesPattern(value, pattern) {
|
|
272
|
+
if (pattern instanceof RegExp)
|
|
273
|
+
return pattern.test(value);
|
|
274
|
+
return value.startsWith(pattern);
|
|
275
|
+
}
|
|
276
|
+
function matchesRequestMatcher(matcher, method, url) {
|
|
277
|
+
const methods = toMethodList(matcher.method);
|
|
278
|
+
if (methods.length > 0 && !methods.includes(method)) {
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
if (matcher.url) {
|
|
282
|
+
const rawUrl = toUrlString(url);
|
|
283
|
+
if (!matchesPattern(rawUrl, matcher.url)) {
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
if (matcher.path) {
|
|
288
|
+
const pathname = toPathname(url);
|
|
289
|
+
if (!matchesPattern(pathname, matcher.path)) {
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
function evaluateRequestActivation(activation, method, url) {
|
|
296
|
+
const rules = activation?.requests;
|
|
297
|
+
if (!rules)
|
|
298
|
+
return { active: true };
|
|
299
|
+
const exclude = rules.exclude ?? [];
|
|
300
|
+
if (exclude.some((matcher) => matchesRequestMatcher(matcher, method, url))) {
|
|
301
|
+
return {
|
|
302
|
+
active: false,
|
|
303
|
+
reason: "request matches activation.requests.exclude",
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
const include = rules.include ?? [];
|
|
307
|
+
if (include.length > 0 && !include.some((matcher) => matchesRequestMatcher(matcher, method, url))) {
|
|
308
|
+
return {
|
|
309
|
+
active: false,
|
|
310
|
+
reason: "request does not match activation.requests.include",
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
return { active: true };
|
|
314
|
+
}
|
|
315
|
+
function evaluateTagActivation(activation, runtime) {
|
|
316
|
+
const rules = activation?.tags;
|
|
317
|
+
if (!rules)
|
|
318
|
+
return { active: true };
|
|
319
|
+
const runtimeTags = new Set(runtime.test?.tags ?? []);
|
|
320
|
+
const disable = rules.disable ?? [];
|
|
321
|
+
for (const tag of disable) {
|
|
322
|
+
if (runtimeTags.has(tag)) {
|
|
323
|
+
return {
|
|
324
|
+
active: false,
|
|
325
|
+
reason: `test tag "${tag}" matches activation.tags.disable`,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
const enable = rules.enable ?? [];
|
|
330
|
+
if (enable.length > 0) {
|
|
331
|
+
const matched = enable.some((tag) => runtimeTags.has(tag));
|
|
332
|
+
if (!matched) {
|
|
333
|
+
return {
|
|
334
|
+
active: false,
|
|
335
|
+
reason: `current test tags do not match activation.tags.enable (${enable.join(", ")})`,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return { active: true };
|
|
340
|
+
}
|
|
341
|
+
function buildActivationAwareHttpClient(pluginName, activation, http) {
|
|
342
|
+
if (!activation?.requests)
|
|
343
|
+
return http;
|
|
344
|
+
function assertRequestActive(method, url) {
|
|
345
|
+
const decision = evaluateRequestActivation(activation, method, url);
|
|
346
|
+
if (decision.active)
|
|
347
|
+
return;
|
|
348
|
+
throw new Error(`Plugin "${pluginName}" is inactive for request ${method} ${toUrlString(url)}: ${decision.reason}.`);
|
|
349
|
+
}
|
|
350
|
+
const METHODS = ["get", "post", "put", "patch", "delete", "head"];
|
|
351
|
+
const wrapped = function (url, options) {
|
|
352
|
+
const method = (options?.method ?? (url instanceof Request ? url.method : "GET")).toUpperCase();
|
|
353
|
+
assertRequestActive(method, url);
|
|
354
|
+
return http(url, options);
|
|
355
|
+
};
|
|
356
|
+
for (const methodName of METHODS) {
|
|
357
|
+
wrapped[methodName] = (url, options) => {
|
|
358
|
+
assertRequestActive(methodName.toUpperCase(), url);
|
|
359
|
+
return http[methodName](url, options);
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
wrapped.extend = (options) => buildActivationAwareHttpClient(pluginName, activation, http.extend(options));
|
|
363
|
+
return wrapped;
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Resolve (or retrieve cached) the real plugin instance for the current runtime.
|
|
367
|
+
*
|
|
368
|
+
* @internal
|
|
369
|
+
*/
|
|
370
|
+
function resolvePlugin(name, entry, cache) {
|
|
371
|
+
const runtime = getRuntime();
|
|
372
|
+
const tagDecision = evaluateTagActivation(entry.activation, runtime);
|
|
373
|
+
if (!tagDecision.active) {
|
|
374
|
+
const testId = runtime.test?.id;
|
|
375
|
+
throw new Error(`Plugin "${name}" is inactive${testId ? ` for test "${testId}"` : ""}: ${tagDecision.reason}.`);
|
|
376
|
+
}
|
|
377
|
+
if (cache.has(runtime))
|
|
378
|
+
return cache.get(runtime);
|
|
379
|
+
// Build the augmented runtime that plugins see
|
|
380
|
+
const noop = () => { };
|
|
381
|
+
const augmented = {
|
|
382
|
+
vars: runtime.vars,
|
|
383
|
+
secrets: runtime.secrets,
|
|
384
|
+
http: buildActivationAwareHttpClient(name, entry.activation, runtime.http),
|
|
385
|
+
test: runtime.test,
|
|
386
|
+
requireVar,
|
|
387
|
+
requireSecret,
|
|
388
|
+
resolveTemplate: (template) => resolveTemplate(template, runtime.vars, runtime.secrets),
|
|
389
|
+
action: runtime.action?.bind(runtime) ?? noop,
|
|
390
|
+
event: runtime.event?.bind(runtime) ?? noop,
|
|
391
|
+
log: runtime.log?.bind(runtime) ?? noop,
|
|
392
|
+
};
|
|
393
|
+
const instance = entry.factory.create(augmented);
|
|
394
|
+
cache.set(runtime, instance);
|
|
395
|
+
return instance;
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Build a Proxy that defers plugin creation until the plugin is actually used.
|
|
399
|
+
*
|
|
400
|
+
* This allows `const { chrome } = configure(...)` to work at module top-level —
|
|
401
|
+
* the destructured value is a transparent Proxy, not the real plugin instance.
|
|
402
|
+
* The real instance is created lazily on first property access / method call
|
|
403
|
+
* during test execution.
|
|
404
|
+
*
|
|
405
|
+
* @internal
|
|
406
|
+
*/
|
|
407
|
+
function buildLazyPlugin(name, entry) {
|
|
408
|
+
const cache = new WeakMap();
|
|
409
|
+
return new Proxy(Object.create(null), {
|
|
410
|
+
get(_target, prop, receiver) {
|
|
411
|
+
const instance = resolvePlugin(name, entry, cache);
|
|
412
|
+
const value = Reflect.get(instance, prop, receiver);
|
|
413
|
+
return typeof value === "function"
|
|
414
|
+
? value.bind(instance)
|
|
415
|
+
: value;
|
|
416
|
+
},
|
|
417
|
+
set(_target, prop, value) {
|
|
418
|
+
const instance = resolvePlugin(name, entry, cache);
|
|
419
|
+
return Reflect.set(instance, prop, value);
|
|
420
|
+
},
|
|
421
|
+
has(_target, prop) {
|
|
422
|
+
const instance = resolvePlugin(name, entry, cache);
|
|
423
|
+
return Reflect.has(instance, prop);
|
|
424
|
+
},
|
|
425
|
+
ownKeys() {
|
|
426
|
+
const instance = resolvePlugin(name, entry, cache);
|
|
427
|
+
return Reflect.ownKeys(instance);
|
|
428
|
+
},
|
|
429
|
+
getOwnPropertyDescriptor(_target, prop) {
|
|
430
|
+
const instance = resolvePlugin(name, entry, cache);
|
|
431
|
+
return Object.getOwnPropertyDescriptor(instance, prop);
|
|
432
|
+
},
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
function buildLazyPlugins(plugins) {
|
|
436
|
+
const result = {};
|
|
437
|
+
for (const [name, rawEntry] of Object.entries(plugins)) {
|
|
438
|
+
if (RESERVED_KEYS.has(name)) {
|
|
439
|
+
throw new Error(`Plugin name "${name}" conflicts with a reserved configure() field. ` +
|
|
440
|
+
`Choose a different key (reserved: ${[...RESERVED_KEYS].join(", ")}).`);
|
|
441
|
+
}
|
|
442
|
+
result[name] = buildLazyPlugin(name, normalizePluginEntry(rawEntry));
|
|
443
|
+
}
|
|
444
|
+
return result;
|
|
445
|
+
}
|
|
446
|
+
// =============================================================================
|
|
447
|
+
// Public API
|
|
448
|
+
// =============================================================================
|
|
449
|
+
/**
|
|
450
|
+
* Declare file-level dependencies on vars, secrets, and HTTP configuration.
|
|
451
|
+
*
|
|
452
|
+
* Returns lazy accessors that resolve at test runtime, not at import time.
|
|
453
|
+
* All declared vars and secrets are **required** — missing values cause the test
|
|
454
|
+
* to fail immediately with a clear error message.
|
|
455
|
+
*
|
|
456
|
+
* The returned objects can be shared across files via re-exports.
|
|
457
|
+
*
|
|
458
|
+
* @param options Configuration declaring vars, secrets, and HTTP defaults
|
|
459
|
+
* @returns Lazy accessors for vars, secrets, and a pre-configured HTTP client
|
|
460
|
+
*
|
|
461
|
+
* @example Basic usage
|
|
462
|
+
* ```ts
|
|
463
|
+
* import { test, configure } from "@glubean/sdk";
|
|
464
|
+
*
|
|
465
|
+
* const { vars, http } = configure({
|
|
466
|
+
* vars: { baseUrl: "base_url" },
|
|
467
|
+
* http: { prefixUrl: "base_url" },
|
|
468
|
+
* });
|
|
469
|
+
*
|
|
470
|
+
* export const listUsers = test("list-users", async (ctx) => {
|
|
471
|
+
* const res = await http.get("users").json();
|
|
472
|
+
* ctx.log(`Base URL: ${vars.baseUrl}`);
|
|
473
|
+
* });
|
|
474
|
+
* ```
|
|
475
|
+
*
|
|
476
|
+
* @example Full configuration with secrets
|
|
477
|
+
* ```ts
|
|
478
|
+
* const { vars, secrets, http } = configure({
|
|
479
|
+
* vars: { baseUrl: "base_url", orgId: "org_id" },
|
|
480
|
+
* secrets: { apiKey: "api_key" },
|
|
481
|
+
* http: {
|
|
482
|
+
* prefixUrl: "base_url",
|
|
483
|
+
* headers: { Authorization: "Bearer {{api_key}}" },
|
|
484
|
+
* },
|
|
485
|
+
* });
|
|
486
|
+
* ```
|
|
487
|
+
*
|
|
488
|
+
* @example Shared across test files
|
|
489
|
+
* ```ts
|
|
490
|
+
* // tests/configure.ts
|
|
491
|
+
* export const { vars, secrets, http } = configure({ ... });
|
|
492
|
+
*
|
|
493
|
+
* // tests/users.test.ts
|
|
494
|
+
* import { http, vars } from "./configure.js";
|
|
495
|
+
* ```
|
|
496
|
+
*/
|
|
497
|
+
export function configure(options) {
|
|
498
|
+
const vars = options.vars
|
|
499
|
+
? buildLazyVars(options.vars)
|
|
500
|
+
: {};
|
|
501
|
+
const secrets = options.secrets
|
|
502
|
+
? buildLazySecrets(options.secrets)
|
|
503
|
+
: {};
|
|
504
|
+
const http = options.http ? buildLazyHttp(options.http) : buildPassthroughHttp();
|
|
505
|
+
const base = { vars, secrets, http };
|
|
506
|
+
if (options.plugins) {
|
|
507
|
+
Object.assign(base, buildLazyPlugins(options.plugins));
|
|
508
|
+
}
|
|
509
|
+
return base;
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Build a passthrough HTTP client that simply delegates to ctx.http.
|
|
513
|
+
* Used when `configure()` is called without `http` options.
|
|
514
|
+
*
|
|
515
|
+
* @internal
|
|
516
|
+
*/
|
|
517
|
+
function buildPassthroughHttp() {
|
|
518
|
+
const HTTP_METHODS = [
|
|
519
|
+
"get",
|
|
520
|
+
"post",
|
|
521
|
+
"put",
|
|
522
|
+
"patch",
|
|
523
|
+
"delete",
|
|
524
|
+
"head",
|
|
525
|
+
];
|
|
526
|
+
const proxy = function (url, options) {
|
|
527
|
+
return getRuntime().http(url, options);
|
|
528
|
+
};
|
|
529
|
+
for (const method of HTTP_METHODS) {
|
|
530
|
+
proxy[method] = (url, options) => getRuntime().http[method](url, options);
|
|
531
|
+
}
|
|
532
|
+
proxy.extend = (options) => getRuntime().http.extend(options);
|
|
533
|
+
return proxy;
|
|
534
|
+
}
|
|
535
|
+
//# sourceMappingURL=configure.js.map
|