@milaboratories/pl-client 3.17.0 → 3.17.2
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/test/test_config.cjs +92 -3
- package/dist/test/test_config.cjs.map +1 -1
- package/dist/test/test_config.d.ts +29 -4
- package/dist/test/test_config.d.ts.map +1 -1
- package/dist/test/test_config.js +88 -4
- package/dist/test/test_config.js.map +1 -1
- package/package.json +5 -5
- package/src/core/client.test.ts +5 -5
- package/src/core/ll_client.test.ts +8 -1
- package/src/core/ll_transaction.test.ts +3 -3
- package/src/core/transaction.test.ts +6 -6
- package/src/test/test_config.test.ts +14 -1
- package/src/test/test_config.ts +102 -4
|
@@ -13,12 +13,16 @@ let node_path = require("node:path");
|
|
|
13
13
|
node_path = require_runtime.__toESM(node_path, 1);
|
|
14
14
|
//#region src/test/test_config.ts
|
|
15
15
|
var test_config_exports = /* @__PURE__ */ require_runtime.__exportAll({
|
|
16
|
-
TEST_REQUEST_TIMEOUT: () =>
|
|
16
|
+
TEST_REQUEST_TIMEOUT: () => TEST_REQUEST_TIMEOUT,
|
|
17
|
+
getTestAdminClient: () => getTestAdminClient,
|
|
18
|
+
getTestAdminClientConf: () => getTestAdminClientConf,
|
|
19
|
+
getTestAdminLLClient: () => getTestAdminLLClient,
|
|
17
20
|
getTestClient: () => getTestClient,
|
|
18
21
|
getTestClientConf: () => getTestClientConf,
|
|
19
22
|
getTestConfig: () => getTestConfig,
|
|
20
23
|
getTestLLClient: () => getTestLLClient,
|
|
21
24
|
plAddressToTestConfig: () => plAddressToTestConfig,
|
|
25
|
+
withAdminTempRoot: () => withAdminTempRoot,
|
|
22
26
|
withTempRoot: () => withTempRoot
|
|
23
27
|
});
|
|
24
28
|
const CONFIG_FILE = "test_config.json";
|
|
@@ -34,13 +38,34 @@ function getTestConfig() {
|
|
|
34
38
|
if (process.env.PL_TEST_USER !== void 0) conf.test_user = process.env.PL_TEST_USER;
|
|
35
39
|
if (process.env.PL_TEST_PASSWORD !== void 0) conf.test_password = process.env.PL_TEST_PASSWORD;
|
|
36
40
|
if (process.env.PL_TEST_PROXY !== void 0) conf.test_proxy = process.env.PL_TEST_PROXY;
|
|
41
|
+
if (process.env.PL_TEST_ADMIN_USER !== void 0) conf.test_admin_user = process.env.PL_TEST_ADMIN_USER;
|
|
42
|
+
if (process.env.PL_TEST_ADMIN_PASSWORD !== void 0) conf.test_admin_password = process.env.PL_TEST_ADMIN_PASSWORD;
|
|
37
43
|
if (conf.address === void 0) throw new Error(`can't resolve platform address (checked ${CONFIG_FILE} file and PL_ADDRESS environment var)`);
|
|
38
44
|
return conf;
|
|
39
45
|
}
|
|
40
|
-
/**
|
|
46
|
+
/**
|
|
47
|
+
* Default request timeout for tests (ms).
|
|
48
|
+
*
|
|
49
|
+
* Kept short so a hung call fails the test instead of the suite's own budget. A deploy the
|
|
50
|
+
* tests reach over cluster DNS needs more than that on first contact, where the deadline
|
|
51
|
+
* covers name resolution too:
|
|
52
|
+
*
|
|
53
|
+
* RpcError: Deadline exceeded after 0.500s,waiting for name resolution
|
|
54
|
+
*
|
|
55
|
+
* That call builds the shared client, so the whole file dies with it and retries only
|
|
56
|
+
* repeat the error against an undefined client. PL_TEST_REQUEST_TIMEOUT raises the budget
|
|
57
|
+
* where resolution is slow, and every other environment keeps the short one.
|
|
58
|
+
*/
|
|
59
|
+
const TEST_REQUEST_TIMEOUT = Number(process.env.PL_TEST_REQUEST_TIMEOUT ?? 500);
|
|
60
|
+
/** True when the address carries an explicit `request-timeout` query parameter. */
|
|
61
|
+
function addressStatesRequestTimeout(address) {
|
|
62
|
+
if (address.indexOf("://") === -1) return false;
|
|
63
|
+
return new URL(address).searchParams.has("request-timeout");
|
|
64
|
+
}
|
|
65
|
+
/** Returns PlClientConfig with reduced timeout for tests, unless the address sets its own */
|
|
41
66
|
function plAddressToTestConfig(address) {
|
|
42
67
|
const plConf = require_config.plAddressToConfig(address);
|
|
43
|
-
plConf.defaultRequestTimeout =
|
|
68
|
+
if (!addressStatesRequestTimeout(address)) plConf.defaultRequestTimeout = TEST_REQUEST_TIMEOUT;
|
|
44
69
|
return plConf;
|
|
45
70
|
}
|
|
46
71
|
function saveAuthInfoCallback(tConf, instanceId) {
|
|
@@ -100,6 +125,65 @@ async function getTestLLClient(confOverrides = {}) {
|
|
|
100
125
|
...confOverrides
|
|
101
126
|
}, { auth });
|
|
102
127
|
}
|
|
128
|
+
/** Returns a config logged in as the admin test user, or the ordinary test config where no
|
|
129
|
+
* admin credentials are set. A backend that needs no auth, and one whose single test user
|
|
130
|
+
* already holds the admin role, both leave PL_TEST_ADMIN_USER and PL_TEST_ADMIN_PASSWORD
|
|
131
|
+
* unset, and a test that asks for admin must still run there. */
|
|
132
|
+
async function getTestAdminClientConf() {
|
|
133
|
+
const tConf = getTestConfig();
|
|
134
|
+
if (tConf.test_admin_user === void 0 || tConf.test_admin_password === void 0) return await getTestClientConf();
|
|
135
|
+
const plConf = plAddressToTestConfig(tConf.address);
|
|
136
|
+
return {
|
|
137
|
+
conf: plConf,
|
|
138
|
+
auth: {
|
|
139
|
+
authInformation: await (await require_unauth_client.UnauthenticatedPlClient.build(plConf)).login(tConf.test_admin_user, tConf.test_admin_password),
|
|
140
|
+
onUpdate: () => {},
|
|
141
|
+
onAuthError: () => {},
|
|
142
|
+
onUpdateError: () => {}
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
async function getTestAdminLLClient(confOverrides = {}) {
|
|
147
|
+
const { conf, auth } = await getTestAdminClientConf();
|
|
148
|
+
return await require_ll_client.LLPlClient.build({
|
|
149
|
+
...conf,
|
|
150
|
+
...confOverrides
|
|
151
|
+
}, { auth });
|
|
152
|
+
}
|
|
153
|
+
async function getTestAdminClient(alternativeRoot, confOverrides = {}) {
|
|
154
|
+
const { conf, auth } = await getTestAdminClientConf();
|
|
155
|
+
if (alternativeRoot !== void 0 && conf.alternativeRoot !== void 0) throw new Error("test pl address configured with alternative root");
|
|
156
|
+
return await require_client.PlClient.init({
|
|
157
|
+
...conf,
|
|
158
|
+
...confOverrides,
|
|
159
|
+
alternativeRoot
|
|
160
|
+
}, auth);
|
|
161
|
+
}
|
|
162
|
+
async function withAdminTempRoot(body) {
|
|
163
|
+
const alternativeRoot = `test_${Date.now()}_${(0, node_crypto.randomUUID)()}`;
|
|
164
|
+
let altRootId = "";
|
|
165
|
+
try {
|
|
166
|
+
const client = await getTestAdminClient(alternativeRoot);
|
|
167
|
+
altRootId = client.clientRoot;
|
|
168
|
+
try {
|
|
169
|
+
const value = await body(client);
|
|
170
|
+
const rawClient = await getTestAdminClient();
|
|
171
|
+
try {
|
|
172
|
+
await rawClient.deleteAlternativeRoot(alternativeRoot);
|
|
173
|
+
} catch (cleanupErr) {
|
|
174
|
+
console.warn(`Failed to clean up alternative root ${alternativeRoot}:`, cleanupErr.message);
|
|
175
|
+
} finally {
|
|
176
|
+
await rawClient.close();
|
|
177
|
+
}
|
|
178
|
+
return value;
|
|
179
|
+
} finally {
|
|
180
|
+
await client.close();
|
|
181
|
+
}
|
|
182
|
+
} catch (err) {
|
|
183
|
+
console.log(`ALTERNATIVE ROOT: ${alternativeRoot} (${require_types.resourceIdToString(altRootId)})`);
|
|
184
|
+
throw err;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
103
187
|
async function getTestClient(alternativeRoot, confOverrides = {}) {
|
|
104
188
|
const { conf, auth } = await getTestClientConf();
|
|
105
189
|
if (alternativeRoot !== void 0 && conf.alternativeRoot !== void 0) throw new Error("test pl address configured with alternative root");
|
|
@@ -162,6 +246,10 @@ async function withTempRoot(body, options = {}) {
|
|
|
162
246
|
}
|
|
163
247
|
}
|
|
164
248
|
//#endregion
|
|
249
|
+
exports.TEST_REQUEST_TIMEOUT = TEST_REQUEST_TIMEOUT;
|
|
250
|
+
exports.getTestAdminClient = getTestAdminClient;
|
|
251
|
+
exports.getTestAdminClientConf = getTestAdminClientConf;
|
|
252
|
+
exports.getTestAdminLLClient = getTestAdminLLClient;
|
|
165
253
|
exports.getTestClient = getTestClient;
|
|
166
254
|
exports.getTestClientConf = getTestClientConf;
|
|
167
255
|
exports.getTestConfig = getTestConfig;
|
|
@@ -173,6 +261,7 @@ Object.defineProperty(exports, "test_config_exports", {
|
|
|
173
261
|
return test_config_exports;
|
|
174
262
|
}
|
|
175
263
|
});
|
|
264
|
+
exports.withAdminTempRoot = withAdminTempRoot;
|
|
176
265
|
exports.withTempRoot = withTempRoot;
|
|
177
266
|
|
|
178
267
|
//# sourceMappingURL=test_config.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"test_config.cjs","names":["path","fs","plAddressToConfig","inferAuthRefreshTime","UnauthenticatedPlClient","LLPlClient","PlClient","startTcpProxy","resourceIdToString"],"sources":["../../src/test/test_config.ts"],"sourcesContent":["import * as fs from \"node:fs\";\nimport { LLPlClient } from \"../core/ll_client\";\nimport type { AuthInformation, AuthOps, PlClientConfig } from \"../core/config\";\nimport { plAddressToConfig } from \"../core/config\";\nimport { UnauthenticatedPlClient } from \"../core/unauth_client\";\nimport { PlClient } from \"../core/client\";\nimport { randomUUID } from \"node:crypto\";\nimport type { OptionalSignedResourceId } from \"../core/types\";\nimport { NullSignedResourceId, resourceIdToString } from \"../core/types\";\nimport { inferAuthRefreshTime } from \"../core/auth\";\nimport * as path from \"node:path\";\nimport type { TestTcpProxy } from \"./tcp-proxy\";\nimport { startTcpProxy } from \"./tcp-proxy\";\n\nexport { TestTcpProxy };\n\nexport interface TestConfig {\n address: string;\n test_proxy?: string;\n test_user?: string;\n test_password?: string;\n}\n\nconst CONFIG_FILE = \"test_config.json\";\n// const AUTH_DATA_FILE = '.test_auth.json';\n\nlet authDataFilePath: string | undefined;\n\nfunction getFullAuthDataFilePath() {\n if (authDataFilePath === undefined) authDataFilePath = path.resolve(\".test_auth.json\");\n return authDataFilePath;\n}\n\nexport function getTestConfig(): TestConfig {\n let conf: Partial<TestConfig> = {};\n if (fs.existsSync(CONFIG_FILE))\n conf = JSON.parse(fs.readFileSync(CONFIG_FILE, { encoding: \"utf-8\" })) as TestConfig;\n\n if (process.env.PL_ADDRESS !== undefined) conf.address = process.env.PL_ADDRESS;\n\n if (process.env.PL_TEST_USER !== undefined) conf.test_user = process.env.PL_TEST_USER;\n\n if (process.env.PL_TEST_PASSWORD !== undefined) conf.test_password = process.env.PL_TEST_PASSWORD;\n\n if (process.env.PL_TEST_PROXY !== undefined) conf.test_proxy = process.env.PL_TEST_PROXY;\n\n if (conf.address === undefined)\n throw new Error(\n `can't resolve platform address (checked ${CONFIG_FILE} file and PL_ADDRESS environment var)`,\n );\n\n return conf as TestConfig;\n}\n\n/** Default request timeout for tests (ms) */\nexport const TEST_REQUEST_TIMEOUT = 500;\n\n/** Returns PlClientConfig with reduced timeout for tests */\nexport function plAddressToTestConfig(address: string): PlClientConfig {\n const plConf = plAddressToConfig(address);\n plConf.defaultRequestTimeout = TEST_REQUEST_TIMEOUT;\n return plConf;\n}\n\ninterface AuthCache {\n /** To check if config changed */\n conf: TestConfig;\n /** Backend's instance ID at the moment the JWT was issued. Restarts that\n * reset state rotate this; the JWT's `iss` claim is bound to it and the\n * backend rejects tokens minted by a different instance. Without this\n * check, a cached JWT from a previous backend run would silently fail\n * the first authenticated call after restart. */\n instanceId: string;\n expiration: number;\n authInformation: AuthInformation;\n}\n\nfunction saveAuthInfoCallback(\n tConf: TestConfig,\n instanceId: string,\n): (authInformation: AuthInformation) => void {\n return (authInformation) => {\n const dst = getFullAuthDataFilePath();\n const tmpDst = getFullAuthDataFilePath() + randomUUID();\n fs.writeFileSync(\n tmpDst,\n Buffer.from(\n JSON.stringify({\n conf: tConf,\n instanceId,\n authInformation,\n expiration: inferAuthRefreshTime(authInformation, 24 * 60 * 60),\n } as AuthCache),\n ),\n \"utf8\",\n );\n fs.renameSync(tmpDst, dst);\n };\n}\n\nconst cleanAuthInfoCallback = () => {\n const p = getFullAuthDataFilePath();\n if (fs.existsSync(p)) {\n console.warn(`Removing: ${p}`);\n fs.rmSync(p);\n }\n};\n\nexport async function getTestClientConf(): Promise<{ conf: PlClientConfig; auth: AuthOps }> {\n const tConf = getTestConfig();\n\n const plConf = plAddressToTestConfig(tConf.address);\n const uClient = await UnauthenticatedPlClient.build(plConf);\n // ll.serverInfo is populated by build()'s ping; instanceId rotates on a\n // backend restart that drops the persisted state.\n const instanceId = uClient.ll.serverInfo.instanceId;\n\n let authInformation: AuthInformation | undefined = undefined;\n\n // Try recover from cache. The cache is keyed by config AND backend\n // instanceId — a backend restart that rotates instanceId invalidates the\n // cached JWT (the token's `iss` claim no longer matches the live\n // instance), so we re-login instead of carrying the dead token through\n // to the first authenticated call.\n if (fs.existsSync(getFullAuthDataFilePath())) {\n try {\n const cache: AuthCache = JSON.parse(\n fs.readFileSync(getFullAuthDataFilePath(), { encoding: \"utf-8\" }),\n ) as AuthCache; // TODO runtime validation\n if (\n cache.conf.address === tConf.address &&\n cache.conf.test_user === tConf.test_user &&\n cache.conf.test_password === tConf.test_password &&\n cache.expiration > Date.now() &&\n cache.instanceId === instanceId\n )\n authInformation = cache.authInformation;\n } catch {\n // removing cache file on any error\n fs.rmSync(getFullAuthDataFilePath());\n }\n }\n\n const requireAuth = await uClient.requireAuth();\n\n if (!requireAuth && (tConf.test_user !== undefined || tConf.test_password !== undefined))\n throw new Error(\n `Server require no auth, but test user name or test password are provided via (${CONFIG_FILE}) or env variables: PL_TEST_USER and PL_TEST_PASSWORD`,\n );\n\n if (requireAuth && (tConf.test_user === undefined || tConf.test_password === undefined))\n throw new Error(\n `No auth information found in config (${CONFIG_FILE}) or env variables: PL_TEST_USER and PL_TEST_PASSWORD`,\n );\n\n if (authInformation === undefined) {\n if (requireAuth) authInformation = await uClient.login(tConf.test_user!, tConf.test_password!);\n // No authorization is required\n else authInformation = {};\n\n // saving cache\n saveAuthInfoCallback(tConf, instanceId)(authInformation);\n }\n\n return {\n conf: plConf,\n auth: {\n authInformation,\n onUpdate: saveAuthInfoCallback(tConf, instanceId),\n onAuthError: cleanAuthInfoCallback,\n onUpdateError: cleanAuthInfoCallback,\n },\n };\n}\n\nexport async function getTestLLClient(confOverrides: Partial<PlClientConfig> = {}) {\n const { conf, auth } = await getTestClientConf();\n return await LLPlClient.build({ ...conf, ...confOverrides }, { auth });\n}\n\nexport async function getTestClient(\n alternativeRoot?: string,\n confOverrides: Partial<PlClientConfig> = {},\n) {\n const { conf, auth } = await getTestClientConf();\n if (alternativeRoot !== undefined && conf.alternativeRoot !== undefined)\n throw new Error(\"test pl address configured with alternative root\");\n return await PlClient.init({ ...conf, ...confOverrides, alternativeRoot }, auth);\n}\n\nexport type WithTempRootOptions =\n | {\n /** If true and PL_ADDRESS is http://localhost or http://127.0.0.1:<port>,\n * a TCP proxy will be started and PL client will connect through it. */\n viaTcpProxy: true;\n /** Artificial latency for proxy (ms). Default 0 */\n proxyLatencyMs?: number;\n }\n | {\n viaTcpProxy?: undefined;\n };\n\nexport async function withTempRoot<T>(body: (pl: PlClient) => Promise<T>): Promise<T | void>;\n\nexport async function withTempRoot<T>(\n body: (pl: PlClient, proxy: Awaited<ReturnType<typeof startTcpProxy>>) => Promise<T>,\n options: {\n viaTcpProxy: true;\n proxyLatencyMs?: number;\n },\n): Promise<T>;\n\nexport async function withTempRoot<T>(\n body: (pl: PlClient, proxy: any) => Promise<T>,\n options: WithTempRootOptions = {},\n): Promise<T | undefined> {\n const alternativeRoot = `test_${Date.now()}_${randomUUID()}`;\n let altRootId: OptionalSignedResourceId = NullSignedResourceId;\n // Proxy management\n let proxy: Awaited<ReturnType<typeof startTcpProxy>> | undefined;\n let confOverrides: Partial<PlClientConfig> = {};\n try {\n // Optionally start TCP proxy and rewrite PL_ADDRESS to point to proxy\n if (options.viaTcpProxy === true && process.env.PL_ADDRESS) {\n try {\n const url = new URL(process.env.PL_ADDRESS);\n const isHttp = url.protocol === \"http:\";\n const isLocal = url.hostname === \"127.0.0.1\" || url.hostname === \"localhost\";\n const port = parseInt(url.port);\n if (isHttp && isLocal && Number.isFinite(port)) {\n proxy = await startTcpProxy({ targetPort: port, latency: options.proxyLatencyMs ?? 0 });\n // Override client connection host:port to proxy\n confOverrides = { hostAndPort: `127.0.0.1:${proxy.port}` } as Partial<PlClientConfig>;\n } else {\n console.warn(\n \"*** skipping proxy-based test, PL_ADDRESS is not localhost\",\n process.env.PL_ADDRESS,\n );\n return;\n }\n } catch {\n // ignore proxy setup errors; tests will run against original address\n }\n }\n\n const client = await getTestClient(alternativeRoot, confOverrides);\n altRootId = client.clientRoot;\n try {\n const value = await body(client, proxy);\n const rawClient = await getTestClient();\n try {\n await rawClient.deleteAlternativeRoot(alternativeRoot);\n } catch (cleanupErr: any) {\n // Cleanup may fail if test intentionally deleted resources\n console.warn(`Failed to clean up alternative root ${alternativeRoot}:`, cleanupErr.message);\n } finally {\n // Close the cleanup client to avoid dangling gRPC channels that can cause\n // segfaults during process exit\n await rawClient.close();\n }\n return value;\n } finally {\n // Close the test client to avoid dangling gRPC channels\n await client.close();\n }\n } catch (err: any) {\n console.log(`ALTERNATIVE ROOT: ${alternativeRoot} (${resourceIdToString(altRootId)})`);\n throw err;\n // throw new Error('withTempRoot error: ' + err.message, { cause: err });\n } finally {\n // Stop proxy if started\n if (proxy) {\n try {\n await proxy.disconnectAll();\n } catch {\n /* ignore */\n }\n try {\n await new Promise<void>((resolve) => proxy!.server.close(() => resolve()));\n } catch {\n /* ignore */\n }\n }\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"test_config.cjs","names":["path","fs","plAddressToConfig","inferAuthRefreshTime","UnauthenticatedPlClient","LLPlClient","PlClient","resourceIdToString","startTcpProxy"],"sources":["../../src/test/test_config.ts"],"sourcesContent":["import * as fs from \"node:fs\";\nimport { LLPlClient } from \"../core/ll_client\";\nimport type { AuthInformation, AuthOps, PlClientConfig } from \"../core/config\";\nimport { plAddressToConfig } from \"../core/config\";\nimport { UnauthenticatedPlClient } from \"../core/unauth_client\";\nimport { PlClient } from \"../core/client\";\nimport { randomUUID } from \"node:crypto\";\nimport type { OptionalSignedResourceId } from \"../core/types\";\nimport { NullSignedResourceId, resourceIdToString } from \"../core/types\";\nimport { inferAuthRefreshTime } from \"../core/auth\";\nimport * as path from \"node:path\";\nimport type { TestTcpProxy } from \"./tcp-proxy\";\nimport { startTcpProxy } from \"./tcp-proxy\";\n\nexport { TestTcpProxy };\n\nexport interface TestConfig {\n address: string;\n test_proxy?: string;\n test_user?: string;\n test_password?: string;\n test_admin_user?: string;\n test_admin_password?: string;\n}\n\nconst CONFIG_FILE = \"test_config.json\";\n// const AUTH_DATA_FILE = '.test_auth.json';\n\nlet authDataFilePath: string | undefined;\n\nfunction getFullAuthDataFilePath() {\n if (authDataFilePath === undefined) authDataFilePath = path.resolve(\".test_auth.json\");\n return authDataFilePath;\n}\n\nexport function getTestConfig(): TestConfig {\n let conf: Partial<TestConfig> = {};\n if (fs.existsSync(CONFIG_FILE))\n conf = JSON.parse(fs.readFileSync(CONFIG_FILE, { encoding: \"utf-8\" })) as TestConfig;\n\n if (process.env.PL_ADDRESS !== undefined) conf.address = process.env.PL_ADDRESS;\n\n if (process.env.PL_TEST_USER !== undefined) conf.test_user = process.env.PL_TEST_USER;\n\n if (process.env.PL_TEST_PASSWORD !== undefined) conf.test_password = process.env.PL_TEST_PASSWORD;\n\n if (process.env.PL_TEST_PROXY !== undefined) conf.test_proxy = process.env.PL_TEST_PROXY;\n\n if (process.env.PL_TEST_ADMIN_USER !== undefined)\n conf.test_admin_user = process.env.PL_TEST_ADMIN_USER;\n\n if (process.env.PL_TEST_ADMIN_PASSWORD !== undefined)\n conf.test_admin_password = process.env.PL_TEST_ADMIN_PASSWORD;\n\n if (conf.address === undefined)\n throw new Error(\n `can't resolve platform address (checked ${CONFIG_FILE} file and PL_ADDRESS environment var)`,\n );\n\n return conf as TestConfig;\n}\n\n/**\n * Default request timeout for tests (ms).\n *\n * Kept short so a hung call fails the test instead of the suite's own budget. A deploy the\n * tests reach over cluster DNS needs more than that on first contact, where the deadline\n * covers name resolution too:\n *\n * RpcError: Deadline exceeded after 0.500s,waiting for name resolution\n *\n * That call builds the shared client, so the whole file dies with it and retries only\n * repeat the error against an undefined client. PL_TEST_REQUEST_TIMEOUT raises the budget\n * where resolution is slow, and every other environment keeps the short one.\n */\nexport const TEST_REQUEST_TIMEOUT = Number(process.env.PL_TEST_REQUEST_TIMEOUT ?? 500);\n\n/** True when the address carries an explicit `request-timeout` query parameter. */\nfunction addressStatesRequestTimeout(address: string): boolean {\n if (address.indexOf(\"://\") === -1) return false;\n return new URL(address).searchParams.has(\"request-timeout\");\n}\n\n/** Returns PlClientConfig with reduced timeout for tests, unless the address sets its own */\nexport function plAddressToTestConfig(address: string): PlClientConfig {\n const plConf = plAddressToConfig(address);\n // An explicit request-timeout in the address wins over TEST_REQUEST_TIMEOUT. In the k8s e2e\n // deploy the client dials a cluster service DNS name, and the first call on a cold channel must\n // finish name resolution and get a load-balancer pick before it is even sent; 500ms does not\n // cover that, so whichever test happened to open the cold channel died with DEADLINE_EXCEEDED\n // (\"Waiting for LB pick\") against a healthy backend. Addresses that carry no request-timeout\n // keep the short default, so local runs still fail fast.\n if (!addressStatesRequestTimeout(address)) plConf.defaultRequestTimeout = TEST_REQUEST_TIMEOUT;\n return plConf;\n}\n\ninterface AuthCache {\n /** To check if config changed */\n conf: TestConfig;\n /** Backend's instance ID at the moment the JWT was issued. Restarts that\n * reset state rotate this; the JWT's `iss` claim is bound to it and the\n * backend rejects tokens minted by a different instance. Without this\n * check, a cached JWT from a previous backend run would silently fail\n * the first authenticated call after restart. */\n instanceId: string;\n expiration: number;\n authInformation: AuthInformation;\n}\n\nfunction saveAuthInfoCallback(\n tConf: TestConfig,\n instanceId: string,\n): (authInformation: AuthInformation) => void {\n return (authInformation) => {\n const dst = getFullAuthDataFilePath();\n const tmpDst = getFullAuthDataFilePath() + randomUUID();\n fs.writeFileSync(\n tmpDst,\n Buffer.from(\n JSON.stringify({\n conf: tConf,\n instanceId,\n authInformation,\n expiration: inferAuthRefreshTime(authInformation, 24 * 60 * 60),\n } as AuthCache),\n ),\n \"utf8\",\n );\n fs.renameSync(tmpDst, dst);\n };\n}\n\nconst cleanAuthInfoCallback = () => {\n const p = getFullAuthDataFilePath();\n if (fs.existsSync(p)) {\n console.warn(`Removing: ${p}`);\n fs.rmSync(p);\n }\n};\n\nexport async function getTestClientConf(): Promise<{ conf: PlClientConfig; auth: AuthOps }> {\n const tConf = getTestConfig();\n\n const plConf = plAddressToTestConfig(tConf.address);\n const uClient = await UnauthenticatedPlClient.build(plConf);\n // ll.serverInfo is populated by build()'s ping; instanceId rotates on a\n // backend restart that drops the persisted state.\n const instanceId = uClient.ll.serverInfo.instanceId;\n\n let authInformation: AuthInformation | undefined = undefined;\n\n // Try recover from cache. The cache is keyed by config AND backend\n // instanceId — a backend restart that rotates instanceId invalidates the\n // cached JWT (the token's `iss` claim no longer matches the live\n // instance), so we re-login instead of carrying the dead token through\n // to the first authenticated call.\n if (fs.existsSync(getFullAuthDataFilePath())) {\n try {\n const cache: AuthCache = JSON.parse(\n fs.readFileSync(getFullAuthDataFilePath(), { encoding: \"utf-8\" }),\n ) as AuthCache; // TODO runtime validation\n if (\n cache.conf.address === tConf.address &&\n cache.conf.test_user === tConf.test_user &&\n cache.conf.test_password === tConf.test_password &&\n cache.expiration > Date.now() &&\n cache.instanceId === instanceId\n )\n authInformation = cache.authInformation;\n } catch {\n // removing cache file on any error\n fs.rmSync(getFullAuthDataFilePath());\n }\n }\n\n const requireAuth = await uClient.requireAuth();\n\n if (!requireAuth && (tConf.test_user !== undefined || tConf.test_password !== undefined))\n throw new Error(\n `Server require no auth, but test user name or test password are provided via (${CONFIG_FILE}) or env variables: PL_TEST_USER and PL_TEST_PASSWORD`,\n );\n\n if (requireAuth && (tConf.test_user === undefined || tConf.test_password === undefined))\n throw new Error(\n `No auth information found in config (${CONFIG_FILE}) or env variables: PL_TEST_USER and PL_TEST_PASSWORD`,\n );\n\n if (authInformation === undefined) {\n if (requireAuth) authInformation = await uClient.login(tConf.test_user!, tConf.test_password!);\n // No authorization is required\n else authInformation = {};\n\n // saving cache\n saveAuthInfoCallback(tConf, instanceId)(authInformation);\n }\n\n return {\n conf: plConf,\n auth: {\n authInformation,\n onUpdate: saveAuthInfoCallback(tConf, instanceId),\n onAuthError: cleanAuthInfoCallback,\n onUpdateError: cleanAuthInfoCallback,\n },\n };\n}\n\nexport async function getTestLLClient(confOverrides: Partial<PlClientConfig> = {}) {\n const { conf, auth } = await getTestClientConf();\n return await LLPlClient.build({ ...conf, ...confOverrides }, { auth });\n}\n\n/** Returns a config logged in as the admin test user, or the ordinary test config where no\n * admin credentials are set. A backend that needs no auth, and one whose single test user\n * already holds the admin role, both leave PL_TEST_ADMIN_USER and PL_TEST_ADMIN_PASSWORD\n * unset, and a test that asks for admin must still run there. */\nexport async function getTestAdminClientConf(): Promise<{ conf: PlClientConfig; auth: AuthOps }> {\n const tConf = getTestConfig();\n\n if (tConf.test_admin_user === undefined || tConf.test_admin_password === undefined)\n return await getTestClientConf();\n\n const plConf = plAddressToTestConfig(tConf.address);\n const uClient = await UnauthenticatedPlClient.build(plConf);\n const authInformation = await uClient.login(tConf.test_admin_user, tConf.test_admin_password);\n\n return {\n conf: plConf,\n auth: {\n authInformation,\n onUpdate: () => {},\n onAuthError: () => {},\n onUpdateError: () => {},\n },\n };\n}\n\nexport async function getTestAdminLLClient(confOverrides: Partial<PlClientConfig> = {}) {\n const { conf, auth } = await getTestAdminClientConf();\n return await LLPlClient.build({ ...conf, ...confOverrides }, { auth });\n}\n\nexport async function getTestAdminClient(\n alternativeRoot?: string,\n confOverrides: Partial<PlClientConfig> = {},\n) {\n const { conf, auth } = await getTestAdminClientConf();\n if (alternativeRoot !== undefined && conf.alternativeRoot !== undefined)\n throw new Error(\"test pl address configured with alternative root\");\n return await PlClient.init({ ...conf, ...confOverrides, alternativeRoot }, auth);\n}\n\nexport async function withAdminTempRoot<T>(body: (pl: PlClient) => Promise<T>): Promise<T | void> {\n const alternativeRoot = `test_${Date.now()}_${randomUUID()}`;\n let altRootId: OptionalSignedResourceId = NullSignedResourceId;\n try {\n const client = await getTestAdminClient(alternativeRoot);\n altRootId = client.clientRoot;\n try {\n const value = await body(client);\n const rawClient = await getTestAdminClient();\n try {\n await rawClient.deleteAlternativeRoot(alternativeRoot);\n } catch (cleanupErr: any) {\n console.warn(`Failed to clean up alternative root ${alternativeRoot}:`, cleanupErr.message);\n } finally {\n await rawClient.close();\n }\n return value;\n } finally {\n await client.close();\n }\n } catch (err: any) {\n console.log(`ALTERNATIVE ROOT: ${alternativeRoot} (${resourceIdToString(altRootId)})`);\n throw err;\n }\n}\n\nexport async function getTestClient(\n alternativeRoot?: string,\n confOverrides: Partial<PlClientConfig> = {},\n) {\n const { conf, auth } = await getTestClientConf();\n if (alternativeRoot !== undefined && conf.alternativeRoot !== undefined)\n throw new Error(\"test pl address configured with alternative root\");\n return await PlClient.init({ ...conf, ...confOverrides, alternativeRoot }, auth);\n}\n\nexport type WithTempRootOptions =\n | {\n /** If true and PL_ADDRESS is http://localhost or http://127.0.0.1:<port>,\n * a TCP proxy will be started and PL client will connect through it. */\n viaTcpProxy: true;\n /** Artificial latency for proxy (ms). Default 0 */\n proxyLatencyMs?: number;\n }\n | {\n viaTcpProxy?: undefined;\n };\n\nexport async function withTempRoot<T>(body: (pl: PlClient) => Promise<T>): Promise<T | void>;\n\nexport async function withTempRoot<T>(\n body: (pl: PlClient, proxy: Awaited<ReturnType<typeof startTcpProxy>>) => Promise<T>,\n options: {\n viaTcpProxy: true;\n proxyLatencyMs?: number;\n },\n): Promise<T>;\n\nexport async function withTempRoot<T>(\n body: (pl: PlClient, proxy: any) => Promise<T>,\n options: WithTempRootOptions = {},\n): Promise<T | undefined> {\n const alternativeRoot = `test_${Date.now()}_${randomUUID()}`;\n let altRootId: OptionalSignedResourceId = NullSignedResourceId;\n // Proxy management\n let proxy: Awaited<ReturnType<typeof startTcpProxy>> | undefined;\n let confOverrides: Partial<PlClientConfig> = {};\n try {\n // Optionally start TCP proxy and rewrite PL_ADDRESS to point to proxy\n if (options.viaTcpProxy === true && process.env.PL_ADDRESS) {\n try {\n const url = new URL(process.env.PL_ADDRESS);\n const isHttp = url.protocol === \"http:\";\n const isLocal = url.hostname === \"127.0.0.1\" || url.hostname === \"localhost\";\n const port = parseInt(url.port);\n if (isHttp && isLocal && Number.isFinite(port)) {\n proxy = await startTcpProxy({ targetPort: port, latency: options.proxyLatencyMs ?? 0 });\n // Override client connection host:port to proxy\n confOverrides = { hostAndPort: `127.0.0.1:${proxy.port}` } as Partial<PlClientConfig>;\n } else {\n console.warn(\n \"*** skipping proxy-based test, PL_ADDRESS is not localhost\",\n process.env.PL_ADDRESS,\n );\n return;\n }\n } catch {\n // ignore proxy setup errors; tests will run against original address\n }\n }\n\n const client = await getTestClient(alternativeRoot, confOverrides);\n altRootId = client.clientRoot;\n try {\n const value = await body(client, proxy);\n const rawClient = await getTestClient();\n try {\n await rawClient.deleteAlternativeRoot(alternativeRoot);\n } catch (cleanupErr: any) {\n // Cleanup may fail if test intentionally deleted resources\n console.warn(`Failed to clean up alternative root ${alternativeRoot}:`, cleanupErr.message);\n } finally {\n // Close the cleanup client to avoid dangling gRPC channels that can cause\n // segfaults during process exit\n await rawClient.close();\n }\n return value;\n } finally {\n // Close the test client to avoid dangling gRPC channels\n await client.close();\n }\n } catch (err: any) {\n console.log(`ALTERNATIVE ROOT: ${alternativeRoot} (${resourceIdToString(altRootId)})`);\n throw err;\n // throw new Error('withTempRoot error: ' + err.message, { cause: err });\n } finally {\n // Stop proxy if started\n if (proxy) {\n try {\n await proxy.disconnectAll();\n } catch {\n /* ignore */\n }\n try {\n await new Promise<void>((resolve) => proxy!.server.close(() => resolve()));\n } catch {\n /* ignore */\n }\n }\n }\n}\n"],"mappings":"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"}
|
|
@@ -3,24 +3,49 @@ import { LLPlClient } from "../core/ll_client.js";
|
|
|
3
3
|
import { PlClient } from "../core/client.js";
|
|
4
4
|
import { TestTcpProxy, startTcpProxy } from "./tcp-proxy.js";
|
|
5
5
|
declare namespace test_config_d_exports {
|
|
6
|
-
export { TEST_REQUEST_TIMEOUT, TestConfig, TestTcpProxy, WithTempRootOptions, getTestClient, getTestClientConf, getTestConfig, getTestLLClient, plAddressToTestConfig, withTempRoot };
|
|
6
|
+
export { TEST_REQUEST_TIMEOUT, TestConfig, TestTcpProxy, WithTempRootOptions, getTestAdminClient, getTestAdminClientConf, getTestAdminLLClient, getTestClient, getTestClientConf, getTestConfig, getTestLLClient, plAddressToTestConfig, withAdminTempRoot, withTempRoot };
|
|
7
7
|
}
|
|
8
8
|
export interface TestConfig {
|
|
9
9
|
address: string;
|
|
10
10
|
test_proxy?: string;
|
|
11
11
|
test_user?: string;
|
|
12
12
|
test_password?: string;
|
|
13
|
+
test_admin_user?: string;
|
|
14
|
+
test_admin_password?: string;
|
|
13
15
|
}
|
|
14
16
|
export declare function getTestConfig(): TestConfig;
|
|
15
|
-
/**
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Default request timeout for tests (ms).
|
|
19
|
+
*
|
|
20
|
+
* Kept short so a hung call fails the test instead of the suite's own budget. A deploy the
|
|
21
|
+
* tests reach over cluster DNS needs more than that on first contact, where the deadline
|
|
22
|
+
* covers name resolution too:
|
|
23
|
+
*
|
|
24
|
+
* RpcError: Deadline exceeded after 0.500s,waiting for name resolution
|
|
25
|
+
*
|
|
26
|
+
* That call builds the shared client, so the whole file dies with it and retries only
|
|
27
|
+
* repeat the error against an undefined client. PL_TEST_REQUEST_TIMEOUT raises the budget
|
|
28
|
+
* where resolution is slow, and every other environment keeps the short one.
|
|
29
|
+
*/
|
|
30
|
+
export declare const TEST_REQUEST_TIMEOUT: number;
|
|
31
|
+
/** Returns PlClientConfig with reduced timeout for tests, unless the address sets its own */
|
|
18
32
|
export declare function plAddressToTestConfig(address: string): PlClientConfig;
|
|
19
33
|
export declare function getTestClientConf(): Promise<{
|
|
20
34
|
conf: PlClientConfig;
|
|
21
35
|
auth: AuthOps;
|
|
22
36
|
}>;
|
|
23
37
|
export declare function getTestLLClient(confOverrides?: Partial<PlClientConfig>): Promise<LLPlClient>;
|
|
38
|
+
/** Returns a config logged in as the admin test user, or the ordinary test config where no
|
|
39
|
+
* admin credentials are set. A backend that needs no auth, and one whose single test user
|
|
40
|
+
* already holds the admin role, both leave PL_TEST_ADMIN_USER and PL_TEST_ADMIN_PASSWORD
|
|
41
|
+
* unset, and a test that asks for admin must still run there. */
|
|
42
|
+
export declare function getTestAdminClientConf(): Promise<{
|
|
43
|
+
conf: PlClientConfig;
|
|
44
|
+
auth: AuthOps;
|
|
45
|
+
}>;
|
|
46
|
+
export declare function getTestAdminLLClient(confOverrides?: Partial<PlClientConfig>): Promise<LLPlClient>;
|
|
47
|
+
export declare function getTestAdminClient(alternativeRoot?: string, confOverrides?: Partial<PlClientConfig>): Promise<PlClient>;
|
|
48
|
+
export declare function withAdminTempRoot<T>(body: (pl: PlClient) => Promise<T>): Promise<T | void>;
|
|
24
49
|
export declare function getTestClient(alternativeRoot?: string, confOverrides?: Partial<PlClientConfig>): Promise<PlClient>;
|
|
25
50
|
export type WithTempRootOptions = {
|
|
26
51
|
/** If true and PL_ADDRESS is http://localhost or http://127.0.0.1:<port>,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"test_config.d.ts","names":[],"sources":["../../src/test/test_config.ts"],"mappings":";;;;;;;iBAgBiB;EACf;EACA;EACA;EACA;;wBAac,iBAAiB;;
|
|
1
|
+
{"version":3,"file":"test_config.d.ts","names":[],"sources":["../../src/test/test_config.ts"],"mappings":";;;;;;;iBAgBiB;EACf;EACA;EACA;EACA;EACA;EACA;;wBAac,iBAAiB;;;;;;;;;;;;;;qBAwCpB;;wBASG,sBAAsB,kBAAkB;wBAwDlC,qBAAqB;EAAU,MAAM;EAAgB,MAAM;;wBAmE3D,gBAAgB,gBAAe,QAAQ,kBAAoB,QAAA;;;;;wBAS3D,0BAA0B;EAAU,MAAM;EAAgB,MAAM;;wBAqBhE,qBAAqB,gBAAe,QAAQ,kBAAoB,QAAA;wBAKhE,mBACpB,0BACA,gBAAe,QAAQ,kBAAoB,QAAA;wBAQvB,kBAAkB,GAAG,OAAO,IAAI,aAAa,QAAQ,KAAK,QAAQ;wBA0BlE,cACpB,0BACA,gBAAe,QAAQ,kBAAoB,QAAA;YAQjC;;;EAIN;;EAEA;;EAGA;;wBAGgB,aAAa,GAAG,OAAO,IAAI,aAAa,QAAQ,KAAK,QAAQ;wBAE7D,aAAa,GACjC,OAAO,IAAI,UAAU,OAAO,QAAQ,kBAAkB,oBAAoB,QAAQ,IAClF;EACE;EACA;IAED,QAAQ"}
|
package/dist/test/test_config.js
CHANGED
|
@@ -11,12 +11,16 @@ import * as fs$1 from "node:fs";
|
|
|
11
11
|
import * as path from "node:path";
|
|
12
12
|
//#region src/test/test_config.ts
|
|
13
13
|
var test_config_exports = /* @__PURE__ */ __exportAll({
|
|
14
|
-
TEST_REQUEST_TIMEOUT: () =>
|
|
14
|
+
TEST_REQUEST_TIMEOUT: () => TEST_REQUEST_TIMEOUT,
|
|
15
|
+
getTestAdminClient: () => getTestAdminClient,
|
|
16
|
+
getTestAdminClientConf: () => getTestAdminClientConf,
|
|
17
|
+
getTestAdminLLClient: () => getTestAdminLLClient,
|
|
15
18
|
getTestClient: () => getTestClient,
|
|
16
19
|
getTestClientConf: () => getTestClientConf,
|
|
17
20
|
getTestConfig: () => getTestConfig,
|
|
18
21
|
getTestLLClient: () => getTestLLClient,
|
|
19
22
|
plAddressToTestConfig: () => plAddressToTestConfig,
|
|
23
|
+
withAdminTempRoot: () => withAdminTempRoot,
|
|
20
24
|
withTempRoot: () => withTempRoot
|
|
21
25
|
});
|
|
22
26
|
const CONFIG_FILE = "test_config.json";
|
|
@@ -32,13 +36,34 @@ function getTestConfig() {
|
|
|
32
36
|
if (process.env.PL_TEST_USER !== void 0) conf.test_user = process.env.PL_TEST_USER;
|
|
33
37
|
if (process.env.PL_TEST_PASSWORD !== void 0) conf.test_password = process.env.PL_TEST_PASSWORD;
|
|
34
38
|
if (process.env.PL_TEST_PROXY !== void 0) conf.test_proxy = process.env.PL_TEST_PROXY;
|
|
39
|
+
if (process.env.PL_TEST_ADMIN_USER !== void 0) conf.test_admin_user = process.env.PL_TEST_ADMIN_USER;
|
|
40
|
+
if (process.env.PL_TEST_ADMIN_PASSWORD !== void 0) conf.test_admin_password = process.env.PL_TEST_ADMIN_PASSWORD;
|
|
35
41
|
if (conf.address === void 0) throw new Error(`can't resolve platform address (checked ${CONFIG_FILE} file and PL_ADDRESS environment var)`);
|
|
36
42
|
return conf;
|
|
37
43
|
}
|
|
38
|
-
/**
|
|
44
|
+
/**
|
|
45
|
+
* Default request timeout for tests (ms).
|
|
46
|
+
*
|
|
47
|
+
* Kept short so a hung call fails the test instead of the suite's own budget. A deploy the
|
|
48
|
+
* tests reach over cluster DNS needs more than that on first contact, where the deadline
|
|
49
|
+
* covers name resolution too:
|
|
50
|
+
*
|
|
51
|
+
* RpcError: Deadline exceeded after 0.500s,waiting for name resolution
|
|
52
|
+
*
|
|
53
|
+
* That call builds the shared client, so the whole file dies with it and retries only
|
|
54
|
+
* repeat the error against an undefined client. PL_TEST_REQUEST_TIMEOUT raises the budget
|
|
55
|
+
* where resolution is slow, and every other environment keeps the short one.
|
|
56
|
+
*/
|
|
57
|
+
const TEST_REQUEST_TIMEOUT = Number(process.env.PL_TEST_REQUEST_TIMEOUT ?? 500);
|
|
58
|
+
/** True when the address carries an explicit `request-timeout` query parameter. */
|
|
59
|
+
function addressStatesRequestTimeout(address) {
|
|
60
|
+
if (address.indexOf("://") === -1) return false;
|
|
61
|
+
return new URL(address).searchParams.has("request-timeout");
|
|
62
|
+
}
|
|
63
|
+
/** Returns PlClientConfig with reduced timeout for tests, unless the address sets its own */
|
|
39
64
|
function plAddressToTestConfig(address) {
|
|
40
65
|
const plConf = plAddressToConfig(address);
|
|
41
|
-
plConf.defaultRequestTimeout =
|
|
66
|
+
if (!addressStatesRequestTimeout(address)) plConf.defaultRequestTimeout = TEST_REQUEST_TIMEOUT;
|
|
42
67
|
return plConf;
|
|
43
68
|
}
|
|
44
69
|
function saveAuthInfoCallback(tConf, instanceId) {
|
|
@@ -98,6 +123,65 @@ async function getTestLLClient(confOverrides = {}) {
|
|
|
98
123
|
...confOverrides
|
|
99
124
|
}, { auth });
|
|
100
125
|
}
|
|
126
|
+
/** Returns a config logged in as the admin test user, or the ordinary test config where no
|
|
127
|
+
* admin credentials are set. A backend that needs no auth, and one whose single test user
|
|
128
|
+
* already holds the admin role, both leave PL_TEST_ADMIN_USER and PL_TEST_ADMIN_PASSWORD
|
|
129
|
+
* unset, and a test that asks for admin must still run there. */
|
|
130
|
+
async function getTestAdminClientConf() {
|
|
131
|
+
const tConf = getTestConfig();
|
|
132
|
+
if (tConf.test_admin_user === void 0 || tConf.test_admin_password === void 0) return await getTestClientConf();
|
|
133
|
+
const plConf = plAddressToTestConfig(tConf.address);
|
|
134
|
+
return {
|
|
135
|
+
conf: plConf,
|
|
136
|
+
auth: {
|
|
137
|
+
authInformation: await (await UnauthenticatedPlClient.build(plConf)).login(tConf.test_admin_user, tConf.test_admin_password),
|
|
138
|
+
onUpdate: () => {},
|
|
139
|
+
onAuthError: () => {},
|
|
140
|
+
onUpdateError: () => {}
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
async function getTestAdminLLClient(confOverrides = {}) {
|
|
145
|
+
const { conf, auth } = await getTestAdminClientConf();
|
|
146
|
+
return await LLPlClient.build({
|
|
147
|
+
...conf,
|
|
148
|
+
...confOverrides
|
|
149
|
+
}, { auth });
|
|
150
|
+
}
|
|
151
|
+
async function getTestAdminClient(alternativeRoot, confOverrides = {}) {
|
|
152
|
+
const { conf, auth } = await getTestAdminClientConf();
|
|
153
|
+
if (alternativeRoot !== void 0 && conf.alternativeRoot !== void 0) throw new Error("test pl address configured with alternative root");
|
|
154
|
+
return await PlClient.init({
|
|
155
|
+
...conf,
|
|
156
|
+
...confOverrides,
|
|
157
|
+
alternativeRoot
|
|
158
|
+
}, auth);
|
|
159
|
+
}
|
|
160
|
+
async function withAdminTempRoot(body) {
|
|
161
|
+
const alternativeRoot = `test_${Date.now()}_${randomUUID()}`;
|
|
162
|
+
let altRootId = "";
|
|
163
|
+
try {
|
|
164
|
+
const client = await getTestAdminClient(alternativeRoot);
|
|
165
|
+
altRootId = client.clientRoot;
|
|
166
|
+
try {
|
|
167
|
+
const value = await body(client);
|
|
168
|
+
const rawClient = await getTestAdminClient();
|
|
169
|
+
try {
|
|
170
|
+
await rawClient.deleteAlternativeRoot(alternativeRoot);
|
|
171
|
+
} catch (cleanupErr) {
|
|
172
|
+
console.warn(`Failed to clean up alternative root ${alternativeRoot}:`, cleanupErr.message);
|
|
173
|
+
} finally {
|
|
174
|
+
await rawClient.close();
|
|
175
|
+
}
|
|
176
|
+
return value;
|
|
177
|
+
} finally {
|
|
178
|
+
await client.close();
|
|
179
|
+
}
|
|
180
|
+
} catch (err) {
|
|
181
|
+
console.log(`ALTERNATIVE ROOT: ${alternativeRoot} (${resourceIdToString(altRootId)})`);
|
|
182
|
+
throw err;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
101
185
|
async function getTestClient(alternativeRoot, confOverrides = {}) {
|
|
102
186
|
const { conf, auth } = await getTestClientConf();
|
|
103
187
|
if (alternativeRoot !== void 0 && conf.alternativeRoot !== void 0) throw new Error("test pl address configured with alternative root");
|
|
@@ -160,6 +244,6 @@ async function withTempRoot(body, options = {}) {
|
|
|
160
244
|
}
|
|
161
245
|
}
|
|
162
246
|
//#endregion
|
|
163
|
-
export { getTestClient, getTestClientConf, getTestConfig, getTestLLClient, plAddressToTestConfig, test_config_exports, withTempRoot };
|
|
247
|
+
export { TEST_REQUEST_TIMEOUT, getTestAdminClient, getTestAdminClientConf, getTestAdminLLClient, getTestClient, getTestClientConf, getTestConfig, getTestLLClient, plAddressToTestConfig, test_config_exports, withAdminTempRoot, withTempRoot };
|
|
164
248
|
|
|
165
249
|
//# sourceMappingURL=test_config.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"test_config.js","names":["fs"],"sources":["../../src/test/test_config.ts"],"sourcesContent":["import * as fs from \"node:fs\";\nimport { LLPlClient } from \"../core/ll_client\";\nimport type { AuthInformation, AuthOps, PlClientConfig } from \"../core/config\";\nimport { plAddressToConfig } from \"../core/config\";\nimport { UnauthenticatedPlClient } from \"../core/unauth_client\";\nimport { PlClient } from \"../core/client\";\nimport { randomUUID } from \"node:crypto\";\nimport type { OptionalSignedResourceId } from \"../core/types\";\nimport { NullSignedResourceId, resourceIdToString } from \"../core/types\";\nimport { inferAuthRefreshTime } from \"../core/auth\";\nimport * as path from \"node:path\";\nimport type { TestTcpProxy } from \"./tcp-proxy\";\nimport { startTcpProxy } from \"./tcp-proxy\";\n\nexport { TestTcpProxy };\n\nexport interface TestConfig {\n address: string;\n test_proxy?: string;\n test_user?: string;\n test_password?: string;\n}\n\nconst CONFIG_FILE = \"test_config.json\";\n// const AUTH_DATA_FILE = '.test_auth.json';\n\nlet authDataFilePath: string | undefined;\n\nfunction getFullAuthDataFilePath() {\n if (authDataFilePath === undefined) authDataFilePath = path.resolve(\".test_auth.json\");\n return authDataFilePath;\n}\n\nexport function getTestConfig(): TestConfig {\n let conf: Partial<TestConfig> = {};\n if (fs.existsSync(CONFIG_FILE))\n conf = JSON.parse(fs.readFileSync(CONFIG_FILE, { encoding: \"utf-8\" })) as TestConfig;\n\n if (process.env.PL_ADDRESS !== undefined) conf.address = process.env.PL_ADDRESS;\n\n if (process.env.PL_TEST_USER !== undefined) conf.test_user = process.env.PL_TEST_USER;\n\n if (process.env.PL_TEST_PASSWORD !== undefined) conf.test_password = process.env.PL_TEST_PASSWORD;\n\n if (process.env.PL_TEST_PROXY !== undefined) conf.test_proxy = process.env.PL_TEST_PROXY;\n\n if (conf.address === undefined)\n throw new Error(\n `can't resolve platform address (checked ${CONFIG_FILE} file and PL_ADDRESS environment var)`,\n );\n\n return conf as TestConfig;\n}\n\n/** Default request timeout for tests (ms) */\nexport const TEST_REQUEST_TIMEOUT = 500;\n\n/** Returns PlClientConfig with reduced timeout for tests */\nexport function plAddressToTestConfig(address: string): PlClientConfig {\n const plConf = plAddressToConfig(address);\n plConf.defaultRequestTimeout = TEST_REQUEST_TIMEOUT;\n return plConf;\n}\n\ninterface AuthCache {\n /** To check if config changed */\n conf: TestConfig;\n /** Backend's instance ID at the moment the JWT was issued. Restarts that\n * reset state rotate this; the JWT's `iss` claim is bound to it and the\n * backend rejects tokens minted by a different instance. Without this\n * check, a cached JWT from a previous backend run would silently fail\n * the first authenticated call after restart. */\n instanceId: string;\n expiration: number;\n authInformation: AuthInformation;\n}\n\nfunction saveAuthInfoCallback(\n tConf: TestConfig,\n instanceId: string,\n): (authInformation: AuthInformation) => void {\n return (authInformation) => {\n const dst = getFullAuthDataFilePath();\n const tmpDst = getFullAuthDataFilePath() + randomUUID();\n fs.writeFileSync(\n tmpDst,\n Buffer.from(\n JSON.stringify({\n conf: tConf,\n instanceId,\n authInformation,\n expiration: inferAuthRefreshTime(authInformation, 24 * 60 * 60),\n } as AuthCache),\n ),\n \"utf8\",\n );\n fs.renameSync(tmpDst, dst);\n };\n}\n\nconst cleanAuthInfoCallback = () => {\n const p = getFullAuthDataFilePath();\n if (fs.existsSync(p)) {\n console.warn(`Removing: ${p}`);\n fs.rmSync(p);\n }\n};\n\nexport async function getTestClientConf(): Promise<{ conf: PlClientConfig; auth: AuthOps }> {\n const tConf = getTestConfig();\n\n const plConf = plAddressToTestConfig(tConf.address);\n const uClient = await UnauthenticatedPlClient.build(plConf);\n // ll.serverInfo is populated by build()'s ping; instanceId rotates on a\n // backend restart that drops the persisted state.\n const instanceId = uClient.ll.serverInfo.instanceId;\n\n let authInformation: AuthInformation | undefined = undefined;\n\n // Try recover from cache. The cache is keyed by config AND backend\n // instanceId — a backend restart that rotates instanceId invalidates the\n // cached JWT (the token's `iss` claim no longer matches the live\n // instance), so we re-login instead of carrying the dead token through\n // to the first authenticated call.\n if (fs.existsSync(getFullAuthDataFilePath())) {\n try {\n const cache: AuthCache = JSON.parse(\n fs.readFileSync(getFullAuthDataFilePath(), { encoding: \"utf-8\" }),\n ) as AuthCache; // TODO runtime validation\n if (\n cache.conf.address === tConf.address &&\n cache.conf.test_user === tConf.test_user &&\n cache.conf.test_password === tConf.test_password &&\n cache.expiration > Date.now() &&\n cache.instanceId === instanceId\n )\n authInformation = cache.authInformation;\n } catch {\n // removing cache file on any error\n fs.rmSync(getFullAuthDataFilePath());\n }\n }\n\n const requireAuth = await uClient.requireAuth();\n\n if (!requireAuth && (tConf.test_user !== undefined || tConf.test_password !== undefined))\n throw new Error(\n `Server require no auth, but test user name or test password are provided via (${CONFIG_FILE}) or env variables: PL_TEST_USER and PL_TEST_PASSWORD`,\n );\n\n if (requireAuth && (tConf.test_user === undefined || tConf.test_password === undefined))\n throw new Error(\n `No auth information found in config (${CONFIG_FILE}) or env variables: PL_TEST_USER and PL_TEST_PASSWORD`,\n );\n\n if (authInformation === undefined) {\n if (requireAuth) authInformation = await uClient.login(tConf.test_user!, tConf.test_password!);\n // No authorization is required\n else authInformation = {};\n\n // saving cache\n saveAuthInfoCallback(tConf, instanceId)(authInformation);\n }\n\n return {\n conf: plConf,\n auth: {\n authInformation,\n onUpdate: saveAuthInfoCallback(tConf, instanceId),\n onAuthError: cleanAuthInfoCallback,\n onUpdateError: cleanAuthInfoCallback,\n },\n };\n}\n\nexport async function getTestLLClient(confOverrides: Partial<PlClientConfig> = {}) {\n const { conf, auth } = await getTestClientConf();\n return await LLPlClient.build({ ...conf, ...confOverrides }, { auth });\n}\n\nexport async function getTestClient(\n alternativeRoot?: string,\n confOverrides: Partial<PlClientConfig> = {},\n) {\n const { conf, auth } = await getTestClientConf();\n if (alternativeRoot !== undefined && conf.alternativeRoot !== undefined)\n throw new Error(\"test pl address configured with alternative root\");\n return await PlClient.init({ ...conf, ...confOverrides, alternativeRoot }, auth);\n}\n\nexport type WithTempRootOptions =\n | {\n /** If true and PL_ADDRESS is http://localhost or http://127.0.0.1:<port>,\n * a TCP proxy will be started and PL client will connect through it. */\n viaTcpProxy: true;\n /** Artificial latency for proxy (ms). Default 0 */\n proxyLatencyMs?: number;\n }\n | {\n viaTcpProxy?: undefined;\n };\n\nexport async function withTempRoot<T>(body: (pl: PlClient) => Promise<T>): Promise<T | void>;\n\nexport async function withTempRoot<T>(\n body: (pl: PlClient, proxy: Awaited<ReturnType<typeof startTcpProxy>>) => Promise<T>,\n options: {\n viaTcpProxy: true;\n proxyLatencyMs?: number;\n },\n): Promise<T>;\n\nexport async function withTempRoot<T>(\n body: (pl: PlClient, proxy: any) => Promise<T>,\n options: WithTempRootOptions = {},\n): Promise<T | undefined> {\n const alternativeRoot = `test_${Date.now()}_${randomUUID()}`;\n let altRootId: OptionalSignedResourceId = NullSignedResourceId;\n // Proxy management\n let proxy: Awaited<ReturnType<typeof startTcpProxy>> | undefined;\n let confOverrides: Partial<PlClientConfig> = {};\n try {\n // Optionally start TCP proxy and rewrite PL_ADDRESS to point to proxy\n if (options.viaTcpProxy === true && process.env.PL_ADDRESS) {\n try {\n const url = new URL(process.env.PL_ADDRESS);\n const isHttp = url.protocol === \"http:\";\n const isLocal = url.hostname === \"127.0.0.1\" || url.hostname === \"localhost\";\n const port = parseInt(url.port);\n if (isHttp && isLocal && Number.isFinite(port)) {\n proxy = await startTcpProxy({ targetPort: port, latency: options.proxyLatencyMs ?? 0 });\n // Override client connection host:port to proxy\n confOverrides = { hostAndPort: `127.0.0.1:${proxy.port}` } as Partial<PlClientConfig>;\n } else {\n console.warn(\n \"*** skipping proxy-based test, PL_ADDRESS is not localhost\",\n process.env.PL_ADDRESS,\n );\n return;\n }\n } catch {\n // ignore proxy setup errors; tests will run against original address\n }\n }\n\n const client = await getTestClient(alternativeRoot, confOverrides);\n altRootId = client.clientRoot;\n try {\n const value = await body(client, proxy);\n const rawClient = await getTestClient();\n try {\n await rawClient.deleteAlternativeRoot(alternativeRoot);\n } catch (cleanupErr: any) {\n // Cleanup may fail if test intentionally deleted resources\n console.warn(`Failed to clean up alternative root ${alternativeRoot}:`, cleanupErr.message);\n } finally {\n // Close the cleanup client to avoid dangling gRPC channels that can cause\n // segfaults during process exit\n await rawClient.close();\n }\n return value;\n } finally {\n // Close the test client to avoid dangling gRPC channels\n await client.close();\n }\n } catch (err: any) {\n console.log(`ALTERNATIVE ROOT: ${alternativeRoot} (${resourceIdToString(altRootId)})`);\n throw err;\n // throw new Error('withTempRoot error: ' + err.message, { cause: err });\n } finally {\n // Stop proxy if started\n if (proxy) {\n try {\n await proxy.disconnectAll();\n } catch {\n /* ignore */\n }\n try {\n await new Promise<void>((resolve) => proxy!.server.close(() => resolve()));\n } catch {\n /* ignore */\n }\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAuBA,MAAM,cAAc;AAGpB,IAAI;AAEJ,SAAS,0BAA0B;CACjC,IAAI,qBAAqB,KAAA,GAAW,mBAAmB,KAAK,QAAQ,iBAAiB;CACrF,OAAO;AACT;AAEA,SAAgB,gBAA4B;CAC1C,IAAI,OAA4B,CAAC;CACjC,IAAIA,KAAG,WAAW,WAAW,GAC3B,OAAO,KAAK,MAAMA,KAAG,aAAa,aAAa,EAAE,UAAU,QAAQ,CAAC,CAAC;CAEvE,IAAI,QAAQ,IAAI,eAAe,KAAA,GAAW,KAAK,UAAU,QAAQ,IAAI;CAErE,IAAI,QAAQ,IAAI,iBAAiB,KAAA,GAAW,KAAK,YAAY,QAAQ,IAAI;CAEzE,IAAI,QAAQ,IAAI,qBAAqB,KAAA,GAAW,KAAK,gBAAgB,QAAQ,IAAI;CAEjF,IAAI,QAAQ,IAAI,kBAAkB,KAAA,GAAW,KAAK,aAAa,QAAQ,IAAI;CAE3E,IAAI,KAAK,YAAY,KAAA,GACnB,MAAM,IAAI,MACR,2CAA2C,YAAY,sCACzD;CAEF,OAAO;AACT;;AAMA,SAAgB,sBAAsB,SAAiC;CACrE,MAAM,SAAS,kBAAkB,OAAO;CACxC,OAAO,wBAAA;CACP,OAAO;AACT;AAeA,SAAS,qBACP,OACA,YAC4C;CAC5C,QAAQ,oBAAoB;EAC1B,MAAM,MAAM,wBAAwB;EACpC,MAAM,SAAS,wBAAwB,IAAI,WAAW;EACtD,KAAG,cACD,QACA,OAAO,KACL,KAAK,UAAU;GACb,MAAM;GACN;GACA;GACA,YAAY,qBAAqB,iBAAiB,OAAU,EAAE;EAChE,CAAc,CAChB,GACA,MACF;EACA,KAAG,WAAW,QAAQ,GAAG;CAC3B;AACF;AAEA,MAAM,8BAA8B;CAClC,MAAM,IAAI,wBAAwB;CAClC,IAAIA,KAAG,WAAW,CAAC,GAAG;EACpB,QAAQ,KAAK,aAAa,GAAG;EAC7B,KAAG,OAAO,CAAC;CACb;AACF;AAEA,eAAsB,oBAAsE;CAC1F,MAAM,QAAQ,cAAc;CAE5B,MAAM,SAAS,sBAAsB,MAAM,OAAO;CAClD,MAAM,UAAU,MAAM,wBAAwB,MAAM,MAAM;CAG1D,MAAM,aAAa,QAAQ,GAAG,WAAW;CAEzC,IAAI,kBAA+C,KAAA;CAOnD,IAAIA,KAAG,WAAW,wBAAwB,CAAC,GACzC,IAAI;EACF,MAAM,QAAmB,KAAK,MAC5BA,KAAG,aAAa,wBAAwB,GAAG,EAAE,UAAU,QAAQ,CAAC,CAClE;EACA,IACE,MAAM,KAAK,YAAY,MAAM,WAC7B,MAAM,KAAK,cAAc,MAAM,aAC/B,MAAM,KAAK,kBAAkB,MAAM,iBACnC,MAAM,aAAa,KAAK,IAAI,KAC5B,MAAM,eAAe,YAErB,kBAAkB,MAAM;CAC5B,QAAQ;EAEN,KAAG,OAAO,wBAAwB,CAAC;CACrC;CAGF,MAAM,cAAc,MAAM,QAAQ,YAAY;CAE9C,IAAI,CAAC,gBAAgB,MAAM,cAAc,KAAA,KAAa,MAAM,kBAAkB,KAAA,IAC5E,MAAM,IAAI,MACR,iFAAiF,YAAY,sDAC/F;CAEF,IAAI,gBAAgB,MAAM,cAAc,KAAA,KAAa,MAAM,kBAAkB,KAAA,IAC3E,MAAM,IAAI,MACR,wCAAwC,YAAY,sDACtD;CAEF,IAAI,oBAAoB,KAAA,GAAW;EACjC,IAAI,aAAa,kBAAkB,MAAM,QAAQ,MAAM,MAAM,WAAY,MAAM,aAAc;OAExF,kBAAkB,CAAC;EAGxB,qBAAqB,OAAO,UAAU,CAAC,CAAC,eAAe;CACzD;CAEA,OAAO;EACL,MAAM;EACN,MAAM;GACJ;GACA,UAAU,qBAAqB,OAAO,UAAU;GAChD,aAAa;GACb,eAAe;EACjB;CACF;AACF;AAEA,eAAsB,gBAAgB,gBAAyC,CAAC,GAAG;CACjF,MAAM,EAAE,MAAM,SAAS,MAAM,kBAAkB;CAC/C,OAAO,MAAM,WAAW,MAAM;EAAE,GAAG;EAAM,GAAG;CAAc,GAAG,EAAE,KAAK,CAAC;AACvE;AAEA,eAAsB,cACpB,iBACA,gBAAyC,CAAC,GAC1C;CACA,MAAM,EAAE,MAAM,SAAS,MAAM,kBAAkB;CAC/C,IAAI,oBAAoB,KAAA,KAAa,KAAK,oBAAoB,KAAA,GAC5D,MAAM,IAAI,MAAM,kDAAkD;CACpE,OAAO,MAAM,SAAS,KAAK;EAAE,GAAG;EAAM,GAAG;EAAe;CAAgB,GAAG,IAAI;AACjF;AAwBA,eAAsB,aACpB,MACA,UAA+B,CAAC,GACR;CACxB,MAAM,kBAAkB,QAAQ,KAAK,IAAI,EAAE,GAAG,WAAW;CACzD,IAAI,YAAA;CAEJ,IAAI;CACJ,IAAI,gBAAyC,CAAC;CAC9C,IAAI;EAEF,IAAI,QAAQ,gBAAgB,QAAQ,QAAQ,IAAI,YAC9C,IAAI;GACF,MAAM,MAAM,IAAI,IAAI,QAAQ,IAAI,UAAU;GAC1C,MAAM,SAAS,IAAI,aAAa;GAChC,MAAM,UAAU,IAAI,aAAa,eAAe,IAAI,aAAa;GACjE,MAAM,OAAO,SAAS,IAAI,IAAI;GAC9B,IAAI,UAAU,WAAW,OAAO,SAAS,IAAI,GAAG;IAC9C,QAAQ,MAAM,cAAc;KAAE,YAAY;KAAM,SAAS,QAAQ,kBAAkB;IAAE,CAAC;IAEtF,gBAAgB,EAAE,aAAa,aAAa,MAAM,OAAO;GAC3D,OAAO;IACL,QAAQ,KACN,8DACA,QAAQ,IAAI,UACd;IACA;GACF;EACF,QAAQ,CAER;EAGF,MAAM,SAAS,MAAM,cAAc,iBAAiB,aAAa;EACjE,YAAY,OAAO;EACnB,IAAI;GACF,MAAM,QAAQ,MAAM,KAAK,QAAQ,KAAK;GACtC,MAAM,YAAY,MAAM,cAAc;GACtC,IAAI;IACF,MAAM,UAAU,sBAAsB,eAAe;GACvD,SAAS,YAAiB;IAExB,QAAQ,KAAK,uCAAuC,gBAAgB,IAAI,WAAW,OAAO;GAC5F,UAAU;IAGR,MAAM,UAAU,MAAM;GACxB;GACA,OAAO;EACT,UAAU;GAER,MAAM,OAAO,MAAM;EACrB;CACF,SAAS,KAAU;EACjB,QAAQ,IAAI,qBAAqB,gBAAgB,IAAI,mBAAmB,SAAS,EAAE,EAAE;EACrF,MAAM;CAER,UAAU;EAER,IAAI,OAAO;GACT,IAAI;IACF,MAAM,MAAM,cAAc;GAC5B,QAAQ,CAER;GACA,IAAI;IACF,MAAM,IAAI,SAAe,YAAY,MAAO,OAAO,YAAY,QAAQ,CAAC,CAAC;GAC3E,QAAQ,CAER;EACF;CACF;AACF"}
|
|
1
|
+
{"version":3,"file":"test_config.js","names":["fs"],"sources":["../../src/test/test_config.ts"],"sourcesContent":["import * as fs from \"node:fs\";\nimport { LLPlClient } from \"../core/ll_client\";\nimport type { AuthInformation, AuthOps, PlClientConfig } from \"../core/config\";\nimport { plAddressToConfig } from \"../core/config\";\nimport { UnauthenticatedPlClient } from \"../core/unauth_client\";\nimport { PlClient } from \"../core/client\";\nimport { randomUUID } from \"node:crypto\";\nimport type { OptionalSignedResourceId } from \"../core/types\";\nimport { NullSignedResourceId, resourceIdToString } from \"../core/types\";\nimport { inferAuthRefreshTime } from \"../core/auth\";\nimport * as path from \"node:path\";\nimport type { TestTcpProxy } from \"./tcp-proxy\";\nimport { startTcpProxy } from \"./tcp-proxy\";\n\nexport { TestTcpProxy };\n\nexport interface TestConfig {\n address: string;\n test_proxy?: string;\n test_user?: string;\n test_password?: string;\n test_admin_user?: string;\n test_admin_password?: string;\n}\n\nconst CONFIG_FILE = \"test_config.json\";\n// const AUTH_DATA_FILE = '.test_auth.json';\n\nlet authDataFilePath: string | undefined;\n\nfunction getFullAuthDataFilePath() {\n if (authDataFilePath === undefined) authDataFilePath = path.resolve(\".test_auth.json\");\n return authDataFilePath;\n}\n\nexport function getTestConfig(): TestConfig {\n let conf: Partial<TestConfig> = {};\n if (fs.existsSync(CONFIG_FILE))\n conf = JSON.parse(fs.readFileSync(CONFIG_FILE, { encoding: \"utf-8\" })) as TestConfig;\n\n if (process.env.PL_ADDRESS !== undefined) conf.address = process.env.PL_ADDRESS;\n\n if (process.env.PL_TEST_USER !== undefined) conf.test_user = process.env.PL_TEST_USER;\n\n if (process.env.PL_TEST_PASSWORD !== undefined) conf.test_password = process.env.PL_TEST_PASSWORD;\n\n if (process.env.PL_TEST_PROXY !== undefined) conf.test_proxy = process.env.PL_TEST_PROXY;\n\n if (process.env.PL_TEST_ADMIN_USER !== undefined)\n conf.test_admin_user = process.env.PL_TEST_ADMIN_USER;\n\n if (process.env.PL_TEST_ADMIN_PASSWORD !== undefined)\n conf.test_admin_password = process.env.PL_TEST_ADMIN_PASSWORD;\n\n if (conf.address === undefined)\n throw new Error(\n `can't resolve platform address (checked ${CONFIG_FILE} file and PL_ADDRESS environment var)`,\n );\n\n return conf as TestConfig;\n}\n\n/**\n * Default request timeout for tests (ms).\n *\n * Kept short so a hung call fails the test instead of the suite's own budget. A deploy the\n * tests reach over cluster DNS needs more than that on first contact, where the deadline\n * covers name resolution too:\n *\n * RpcError: Deadline exceeded after 0.500s,waiting for name resolution\n *\n * That call builds the shared client, so the whole file dies with it and retries only\n * repeat the error against an undefined client. PL_TEST_REQUEST_TIMEOUT raises the budget\n * where resolution is slow, and every other environment keeps the short one.\n */\nexport const TEST_REQUEST_TIMEOUT = Number(process.env.PL_TEST_REQUEST_TIMEOUT ?? 500);\n\n/** True when the address carries an explicit `request-timeout` query parameter. */\nfunction addressStatesRequestTimeout(address: string): boolean {\n if (address.indexOf(\"://\") === -1) return false;\n return new URL(address).searchParams.has(\"request-timeout\");\n}\n\n/** Returns PlClientConfig with reduced timeout for tests, unless the address sets its own */\nexport function plAddressToTestConfig(address: string): PlClientConfig {\n const plConf = plAddressToConfig(address);\n // An explicit request-timeout in the address wins over TEST_REQUEST_TIMEOUT. In the k8s e2e\n // deploy the client dials a cluster service DNS name, and the first call on a cold channel must\n // finish name resolution and get a load-balancer pick before it is even sent; 500ms does not\n // cover that, so whichever test happened to open the cold channel died with DEADLINE_EXCEEDED\n // (\"Waiting for LB pick\") against a healthy backend. Addresses that carry no request-timeout\n // keep the short default, so local runs still fail fast.\n if (!addressStatesRequestTimeout(address)) plConf.defaultRequestTimeout = TEST_REQUEST_TIMEOUT;\n return plConf;\n}\n\ninterface AuthCache {\n /** To check if config changed */\n conf: TestConfig;\n /** Backend's instance ID at the moment the JWT was issued. Restarts that\n * reset state rotate this; the JWT's `iss` claim is bound to it and the\n * backend rejects tokens minted by a different instance. Without this\n * check, a cached JWT from a previous backend run would silently fail\n * the first authenticated call after restart. */\n instanceId: string;\n expiration: number;\n authInformation: AuthInformation;\n}\n\nfunction saveAuthInfoCallback(\n tConf: TestConfig,\n instanceId: string,\n): (authInformation: AuthInformation) => void {\n return (authInformation) => {\n const dst = getFullAuthDataFilePath();\n const tmpDst = getFullAuthDataFilePath() + randomUUID();\n fs.writeFileSync(\n tmpDst,\n Buffer.from(\n JSON.stringify({\n conf: tConf,\n instanceId,\n authInformation,\n expiration: inferAuthRefreshTime(authInformation, 24 * 60 * 60),\n } as AuthCache),\n ),\n \"utf8\",\n );\n fs.renameSync(tmpDst, dst);\n };\n}\n\nconst cleanAuthInfoCallback = () => {\n const p = getFullAuthDataFilePath();\n if (fs.existsSync(p)) {\n console.warn(`Removing: ${p}`);\n fs.rmSync(p);\n }\n};\n\nexport async function getTestClientConf(): Promise<{ conf: PlClientConfig; auth: AuthOps }> {\n const tConf = getTestConfig();\n\n const plConf = plAddressToTestConfig(tConf.address);\n const uClient = await UnauthenticatedPlClient.build(plConf);\n // ll.serverInfo is populated by build()'s ping; instanceId rotates on a\n // backend restart that drops the persisted state.\n const instanceId = uClient.ll.serverInfo.instanceId;\n\n let authInformation: AuthInformation | undefined = undefined;\n\n // Try recover from cache. The cache is keyed by config AND backend\n // instanceId — a backend restart that rotates instanceId invalidates the\n // cached JWT (the token's `iss` claim no longer matches the live\n // instance), so we re-login instead of carrying the dead token through\n // to the first authenticated call.\n if (fs.existsSync(getFullAuthDataFilePath())) {\n try {\n const cache: AuthCache = JSON.parse(\n fs.readFileSync(getFullAuthDataFilePath(), { encoding: \"utf-8\" }),\n ) as AuthCache; // TODO runtime validation\n if (\n cache.conf.address === tConf.address &&\n cache.conf.test_user === tConf.test_user &&\n cache.conf.test_password === tConf.test_password &&\n cache.expiration > Date.now() &&\n cache.instanceId === instanceId\n )\n authInformation = cache.authInformation;\n } catch {\n // removing cache file on any error\n fs.rmSync(getFullAuthDataFilePath());\n }\n }\n\n const requireAuth = await uClient.requireAuth();\n\n if (!requireAuth && (tConf.test_user !== undefined || tConf.test_password !== undefined))\n throw new Error(\n `Server require no auth, but test user name or test password are provided via (${CONFIG_FILE}) or env variables: PL_TEST_USER and PL_TEST_PASSWORD`,\n );\n\n if (requireAuth && (tConf.test_user === undefined || tConf.test_password === undefined))\n throw new Error(\n `No auth information found in config (${CONFIG_FILE}) or env variables: PL_TEST_USER and PL_TEST_PASSWORD`,\n );\n\n if (authInformation === undefined) {\n if (requireAuth) authInformation = await uClient.login(tConf.test_user!, tConf.test_password!);\n // No authorization is required\n else authInformation = {};\n\n // saving cache\n saveAuthInfoCallback(tConf, instanceId)(authInformation);\n }\n\n return {\n conf: plConf,\n auth: {\n authInformation,\n onUpdate: saveAuthInfoCallback(tConf, instanceId),\n onAuthError: cleanAuthInfoCallback,\n onUpdateError: cleanAuthInfoCallback,\n },\n };\n}\n\nexport async function getTestLLClient(confOverrides: Partial<PlClientConfig> = {}) {\n const { conf, auth } = await getTestClientConf();\n return await LLPlClient.build({ ...conf, ...confOverrides }, { auth });\n}\n\n/** Returns a config logged in as the admin test user, or the ordinary test config where no\n * admin credentials are set. A backend that needs no auth, and one whose single test user\n * already holds the admin role, both leave PL_TEST_ADMIN_USER and PL_TEST_ADMIN_PASSWORD\n * unset, and a test that asks for admin must still run there. */\nexport async function getTestAdminClientConf(): Promise<{ conf: PlClientConfig; auth: AuthOps }> {\n const tConf = getTestConfig();\n\n if (tConf.test_admin_user === undefined || tConf.test_admin_password === undefined)\n return await getTestClientConf();\n\n const plConf = plAddressToTestConfig(tConf.address);\n const uClient = await UnauthenticatedPlClient.build(plConf);\n const authInformation = await uClient.login(tConf.test_admin_user, tConf.test_admin_password);\n\n return {\n conf: plConf,\n auth: {\n authInformation,\n onUpdate: () => {},\n onAuthError: () => {},\n onUpdateError: () => {},\n },\n };\n}\n\nexport async function getTestAdminLLClient(confOverrides: Partial<PlClientConfig> = {}) {\n const { conf, auth } = await getTestAdminClientConf();\n return await LLPlClient.build({ ...conf, ...confOverrides }, { auth });\n}\n\nexport async function getTestAdminClient(\n alternativeRoot?: string,\n confOverrides: Partial<PlClientConfig> = {},\n) {\n const { conf, auth } = await getTestAdminClientConf();\n if (alternativeRoot !== undefined && conf.alternativeRoot !== undefined)\n throw new Error(\"test pl address configured with alternative root\");\n return await PlClient.init({ ...conf, ...confOverrides, alternativeRoot }, auth);\n}\n\nexport async function withAdminTempRoot<T>(body: (pl: PlClient) => Promise<T>): Promise<T | void> {\n const alternativeRoot = `test_${Date.now()}_${randomUUID()}`;\n let altRootId: OptionalSignedResourceId = NullSignedResourceId;\n try {\n const client = await getTestAdminClient(alternativeRoot);\n altRootId = client.clientRoot;\n try {\n const value = await body(client);\n const rawClient = await getTestAdminClient();\n try {\n await rawClient.deleteAlternativeRoot(alternativeRoot);\n } catch (cleanupErr: any) {\n console.warn(`Failed to clean up alternative root ${alternativeRoot}:`, cleanupErr.message);\n } finally {\n await rawClient.close();\n }\n return value;\n } finally {\n await client.close();\n }\n } catch (err: any) {\n console.log(`ALTERNATIVE ROOT: ${alternativeRoot} (${resourceIdToString(altRootId)})`);\n throw err;\n }\n}\n\nexport async function getTestClient(\n alternativeRoot?: string,\n confOverrides: Partial<PlClientConfig> = {},\n) {\n const { conf, auth } = await getTestClientConf();\n if (alternativeRoot !== undefined && conf.alternativeRoot !== undefined)\n throw new Error(\"test pl address configured with alternative root\");\n return await PlClient.init({ ...conf, ...confOverrides, alternativeRoot }, auth);\n}\n\nexport type WithTempRootOptions =\n | {\n /** If true and PL_ADDRESS is http://localhost or http://127.0.0.1:<port>,\n * a TCP proxy will be started and PL client will connect through it. */\n viaTcpProxy: true;\n /** Artificial latency for proxy (ms). Default 0 */\n proxyLatencyMs?: number;\n }\n | {\n viaTcpProxy?: undefined;\n };\n\nexport async function withTempRoot<T>(body: (pl: PlClient) => Promise<T>): Promise<T | void>;\n\nexport async function withTempRoot<T>(\n body: (pl: PlClient, proxy: Awaited<ReturnType<typeof startTcpProxy>>) => Promise<T>,\n options: {\n viaTcpProxy: true;\n proxyLatencyMs?: number;\n },\n): Promise<T>;\n\nexport async function withTempRoot<T>(\n body: (pl: PlClient, proxy: any) => Promise<T>,\n options: WithTempRootOptions = {},\n): Promise<T | undefined> {\n const alternativeRoot = `test_${Date.now()}_${randomUUID()}`;\n let altRootId: OptionalSignedResourceId = NullSignedResourceId;\n // Proxy management\n let proxy: Awaited<ReturnType<typeof startTcpProxy>> | undefined;\n let confOverrides: Partial<PlClientConfig> = {};\n try {\n // Optionally start TCP proxy and rewrite PL_ADDRESS to point to proxy\n if (options.viaTcpProxy === true && process.env.PL_ADDRESS) {\n try {\n const url = new URL(process.env.PL_ADDRESS);\n const isHttp = url.protocol === \"http:\";\n const isLocal = url.hostname === \"127.0.0.1\" || url.hostname === \"localhost\";\n const port = parseInt(url.port);\n if (isHttp && isLocal && Number.isFinite(port)) {\n proxy = await startTcpProxy({ targetPort: port, latency: options.proxyLatencyMs ?? 0 });\n // Override client connection host:port to proxy\n confOverrides = { hostAndPort: `127.0.0.1:${proxy.port}` } as Partial<PlClientConfig>;\n } else {\n console.warn(\n \"*** skipping proxy-based test, PL_ADDRESS is not localhost\",\n process.env.PL_ADDRESS,\n );\n return;\n }\n } catch {\n // ignore proxy setup errors; tests will run against original address\n }\n }\n\n const client = await getTestClient(alternativeRoot, confOverrides);\n altRootId = client.clientRoot;\n try {\n const value = await body(client, proxy);\n const rawClient = await getTestClient();\n try {\n await rawClient.deleteAlternativeRoot(alternativeRoot);\n } catch (cleanupErr: any) {\n // Cleanup may fail if test intentionally deleted resources\n console.warn(`Failed to clean up alternative root ${alternativeRoot}:`, cleanupErr.message);\n } finally {\n // Close the cleanup client to avoid dangling gRPC channels that can cause\n // segfaults during process exit\n await rawClient.close();\n }\n return value;\n } finally {\n // Close the test client to avoid dangling gRPC channels\n await client.close();\n }\n } catch (err: any) {\n console.log(`ALTERNATIVE ROOT: ${alternativeRoot} (${resourceIdToString(altRootId)})`);\n throw err;\n // throw new Error('withTempRoot error: ' + err.message, { cause: err });\n } finally {\n // Stop proxy if started\n if (proxy) {\n try {\n await proxy.disconnectAll();\n } catch {\n /* ignore */\n }\n try {\n await new Promise<void>((resolve) => proxy!.server.close(() => resolve()));\n } catch {\n /* ignore */\n }\n }\n }\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@milaboratories/pl-client",
|
|
3
|
-
"version": "3.17.
|
|
3
|
+
"version": "3.17.2",
|
|
4
4
|
"description": "New TS/JS client for Platform API",
|
|
5
5
|
"files": [
|
|
6
6
|
"./dist/**/*",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"utility-types": "^3.11.0",
|
|
32
32
|
"yaml": "^2.8.0",
|
|
33
33
|
"@milaboratories/pl-http": "1.2.4",
|
|
34
|
-
"@milaboratories/pl-model-common": "1.
|
|
34
|
+
"@milaboratories/pl-model-common": "1.50.0",
|
|
35
35
|
"@milaboratories/ts-helpers": "1.8.6"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
@@ -41,9 +41,9 @@
|
|
|
41
41
|
"openapi-typescript": "^7.13.0",
|
|
42
42
|
"typescript": "7.0.2",
|
|
43
43
|
"vitest": "^4.1.3",
|
|
44
|
-
"@milaboratories/build-configs": "2.0.
|
|
45
|
-
"@milaboratories/ts-
|
|
46
|
-
"@milaboratories/ts-
|
|
44
|
+
"@milaboratories/build-configs": "2.0.2",
|
|
45
|
+
"@milaboratories/ts-builder": "1.7.2",
|
|
46
|
+
"@milaboratories/ts-configs": "1.4.0"
|
|
47
47
|
},
|
|
48
48
|
"engines": {
|
|
49
49
|
"node": ">=22.19.0"
|
package/src/core/client.test.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getTestAdminClient, getTestAdminClientConf } from "../test/test_config";
|
|
2
2
|
import { PlClient } from "./client";
|
|
3
3
|
import { PlDriver, PlDriverDefinition } from "./driver";
|
|
4
4
|
import { Dispatcher, request } from "undici";
|
|
@@ -6,12 +6,12 @@ import { GrpcClientProviderFactory } from "./grpc";
|
|
|
6
6
|
import { test, expect } from "vitest";
|
|
7
7
|
|
|
8
8
|
test("test client init", async () => {
|
|
9
|
-
await
|
|
9
|
+
await getTestAdminClient(undefined);
|
|
10
10
|
});
|
|
11
11
|
|
|
12
12
|
test("test client alternative root init", async () => {
|
|
13
13
|
const aRootName = "test_root";
|
|
14
|
-
const { conf, auth } = await
|
|
14
|
+
const { conf, auth } = await getTestAdminClientConf();
|
|
15
15
|
await PlClient.init({ ...conf, alternativeRoot: aRootName }, auth);
|
|
16
16
|
const clientB = await PlClient.init(conf, auth);
|
|
17
17
|
const result = await clientB.deleteAlternativeRoot(aRootName);
|
|
@@ -19,7 +19,7 @@ test("test client alternative root init", async () => {
|
|
|
19
19
|
});
|
|
20
20
|
|
|
21
21
|
test("test client init 2", async () => {
|
|
22
|
-
await
|
|
22
|
+
await getTestAdminClient();
|
|
23
23
|
});
|
|
24
24
|
|
|
25
25
|
interface SimpleDriver extends PlDriver {
|
|
@@ -46,7 +46,7 @@ const SimpleDriverDefinition: PlDriverDefinition<SimpleDriver> = {
|
|
|
46
46
|
};
|
|
47
47
|
|
|
48
48
|
test("test driver", async () => {
|
|
49
|
-
const client = await
|
|
49
|
+
const client = await getTestAdminClient();
|
|
50
50
|
const drv = client.getDriver(SimpleDriverDefinition);
|
|
51
51
|
expect(await drv.ping()).toEqual("pong");
|
|
52
52
|
});
|
|
@@ -3,6 +3,8 @@ import {
|
|
|
3
3
|
getTestConfig,
|
|
4
4
|
plAddressToTestConfig,
|
|
5
5
|
getTestLLClient,
|
|
6
|
+
getTestAdminLLClient,
|
|
7
|
+
getTestAdminClient,
|
|
6
8
|
getTestClientConf,
|
|
7
9
|
} from "../test/test_config";
|
|
8
10
|
import {
|
|
@@ -160,7 +162,12 @@ test("test https call via proxy", async () => {
|
|
|
160
162
|
});
|
|
161
163
|
|
|
162
164
|
test("list user resources returns user root", async () => {
|
|
163
|
-
|
|
165
|
+
// PlClient.init() creates the user root via the legacy path, which requires the admin
|
|
166
|
+
// role. After that, listUserResources returns it.
|
|
167
|
+
const plClient = await getTestAdminClient();
|
|
168
|
+
await plClient.close();
|
|
169
|
+
|
|
170
|
+
const client = await getTestAdminLLClient();
|
|
164
171
|
|
|
165
172
|
let responses: AuthAPI_ListUserResources_Response[] = [];
|
|
166
173
|
try {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getTestClient, getTestLLClient } from "../test/test_config";
|
|
1
|
+
import { getTestClient, getTestLLClient, getTestAdminLLClient } from "../test/test_config";
|
|
2
2
|
import { TxAPI_Open_Request_WritableTx } from "../proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api";
|
|
3
3
|
import { createLocalResourceId, parseSignedResourceId } from "./types";
|
|
4
4
|
import { test, expect } from "vitest";
|
|
@@ -95,7 +95,7 @@ test("check timeout error type (passive)", async () => {
|
|
|
95
95
|
});
|
|
96
96
|
|
|
97
97
|
test("check timeout error type (active)", async () => {
|
|
98
|
-
const client = await
|
|
98
|
+
const client = await getTestAdminLLClient();
|
|
99
99
|
const rootSig = await getRootSignature();
|
|
100
100
|
const tx = client.createTx(true, { timeout: 500 });
|
|
101
101
|
|
|
@@ -172,7 +172,7 @@ test("check timeout error type (active)", async () => {
|
|
|
172
172
|
});
|
|
173
173
|
|
|
174
174
|
test("check is abort error (active)", async () => {
|
|
175
|
-
const client = await
|
|
175
|
+
const client = await getTestAdminLLClient();
|
|
176
176
|
const rootSig = await getRootSignature();
|
|
177
177
|
const tx = client.createTx(true, { abortSignal: AbortSignal.timeout(100) });
|
|
178
178
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { withTempRoot } from "../test/test_config";
|
|
1
|
+
import { withAdminTempRoot, withTempRoot } from "../test/test_config";
|
|
2
2
|
import { StructTestResource, ValueTestResource } from "../helpers/pl";
|
|
3
3
|
import { field, toGlobalFieldId, toGlobalResourceId } from "./transaction";
|
|
4
4
|
import { RecoverablePlError } from "./errors";
|
|
@@ -7,7 +7,7 @@ import { test, expect } from "vitest";
|
|
|
7
7
|
import { StatefulPromise } from "./StatefulPromise";
|
|
8
8
|
|
|
9
9
|
test("get field", async () => {
|
|
10
|
-
await
|
|
10
|
+
await withAdminTempRoot(async (pl) => {
|
|
11
11
|
const [rr0, theField1] = await pl.withWriteTx("resource1", async (tx) => {
|
|
12
12
|
const r0 = tx.createStruct(StructTestResource);
|
|
13
13
|
const r1 = tx.createStruct(StructTestResource);
|
|
@@ -54,7 +54,7 @@ test("get field", async () => {
|
|
|
54
54
|
});
|
|
55
55
|
|
|
56
56
|
test("handle absent resource error", async () => {
|
|
57
|
-
await
|
|
57
|
+
await withAdminTempRoot(async (pl) => {
|
|
58
58
|
const [rr0, ff0] = await pl.withWriteTx("testCreateResource", async (tx) => {
|
|
59
59
|
const r0 = tx.createStruct(StructTestResource);
|
|
60
60
|
const f0 = { resourceId: tx.clientRoot, fieldName: "test0" };
|
|
@@ -114,7 +114,7 @@ test("handle absent resource error", async () => {
|
|
|
114
114
|
});
|
|
115
115
|
|
|
116
116
|
test("handle KV storage", async () => {
|
|
117
|
-
await
|
|
117
|
+
await withAdminTempRoot(async (pl) => {
|
|
118
118
|
await pl.withWriteTx("writeKV", async (tx) => {
|
|
119
119
|
tx.setKValue(tx.clientRoot, "a", "a");
|
|
120
120
|
tx.setKValue(tx.clientRoot, "b", "b");
|
|
@@ -151,7 +151,7 @@ test("handle KV storage", async () => {
|
|
|
151
151
|
});
|
|
152
152
|
|
|
153
153
|
test("handle empty KV storage", async () => {
|
|
154
|
-
await
|
|
154
|
+
await withAdminTempRoot(async (pl) => {
|
|
155
155
|
await pl.withReadTx("testReadIndividualAndList", async (tx) => {
|
|
156
156
|
expect(await tx.listKeyValuesString(tx.clientRoot)).toEqual([]);
|
|
157
157
|
});
|
|
@@ -159,7 +159,7 @@ test("handle empty KV storage", async () => {
|
|
|
159
159
|
});
|
|
160
160
|
|
|
161
161
|
test("handle KV storage 2", async () => {
|
|
162
|
-
await
|
|
162
|
+
await withAdminTempRoot(async (pl) => {
|
|
163
163
|
const r1 = await pl.withWriteTx("writeKV", async (tx) => {
|
|
164
164
|
const rr1 = tx.createEphemeral(StructTestResource);
|
|
165
165
|
tx.createField(field(rr1, "a"), "Dynamic", rr1);
|
|
@@ -1,7 +1,20 @@
|
|
|
1
|
-
import { getTestClientConf } from "./test_config";
|
|
1
|
+
import { getTestClientConf, plAddressToTestConfig, TEST_REQUEST_TIMEOUT } from "./test_config";
|
|
2
2
|
import { test, expect } from "vitest";
|
|
3
3
|
|
|
4
4
|
test("test that test config have no alternative root set", async () => {
|
|
5
5
|
const { conf } = await getTestClientConf();
|
|
6
6
|
expect(conf.alternativeRoot).toBeUndefined();
|
|
7
7
|
});
|
|
8
|
+
|
|
9
|
+
test("address request-timeout survives the test config", () => {
|
|
10
|
+
expect(
|
|
11
|
+
plAddressToTestConfig("http://pl.example:6345?request-timeout=3000").defaultRequestTimeout,
|
|
12
|
+
).toBe(3000);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("address without request-timeout takes the test one", () => {
|
|
16
|
+
expect(plAddressToTestConfig("http://pl.example:6345").defaultRequestTimeout).toBe(
|
|
17
|
+
TEST_REQUEST_TIMEOUT,
|
|
18
|
+
);
|
|
19
|
+
expect(plAddressToTestConfig("pl.example:6345").defaultRequestTimeout).toBe(TEST_REQUEST_TIMEOUT);
|
|
20
|
+
});
|
package/src/test/test_config.ts
CHANGED
|
@@ -19,6 +19,8 @@ export interface TestConfig {
|
|
|
19
19
|
test_proxy?: string;
|
|
20
20
|
test_user?: string;
|
|
21
21
|
test_password?: string;
|
|
22
|
+
test_admin_user?: string;
|
|
23
|
+
test_admin_password?: string;
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
const CONFIG_FILE = "test_config.json";
|
|
@@ -44,6 +46,12 @@ export function getTestConfig(): TestConfig {
|
|
|
44
46
|
|
|
45
47
|
if (process.env.PL_TEST_PROXY !== undefined) conf.test_proxy = process.env.PL_TEST_PROXY;
|
|
46
48
|
|
|
49
|
+
if (process.env.PL_TEST_ADMIN_USER !== undefined)
|
|
50
|
+
conf.test_admin_user = process.env.PL_TEST_ADMIN_USER;
|
|
51
|
+
|
|
52
|
+
if (process.env.PL_TEST_ADMIN_PASSWORD !== undefined)
|
|
53
|
+
conf.test_admin_password = process.env.PL_TEST_ADMIN_PASSWORD;
|
|
54
|
+
|
|
47
55
|
if (conf.address === undefined)
|
|
48
56
|
throw new Error(
|
|
49
57
|
`can't resolve platform address (checked ${CONFIG_FILE} file and PL_ADDRESS environment var)`,
|
|
@@ -52,13 +60,37 @@ export function getTestConfig(): TestConfig {
|
|
|
52
60
|
return conf as TestConfig;
|
|
53
61
|
}
|
|
54
62
|
|
|
55
|
-
/**
|
|
56
|
-
|
|
63
|
+
/**
|
|
64
|
+
* Default request timeout for tests (ms).
|
|
65
|
+
*
|
|
66
|
+
* Kept short so a hung call fails the test instead of the suite's own budget. A deploy the
|
|
67
|
+
* tests reach over cluster DNS needs more than that on first contact, where the deadline
|
|
68
|
+
* covers name resolution too:
|
|
69
|
+
*
|
|
70
|
+
* RpcError: Deadline exceeded after 0.500s,waiting for name resolution
|
|
71
|
+
*
|
|
72
|
+
* That call builds the shared client, so the whole file dies with it and retries only
|
|
73
|
+
* repeat the error against an undefined client. PL_TEST_REQUEST_TIMEOUT raises the budget
|
|
74
|
+
* where resolution is slow, and every other environment keeps the short one.
|
|
75
|
+
*/
|
|
76
|
+
export const TEST_REQUEST_TIMEOUT = Number(process.env.PL_TEST_REQUEST_TIMEOUT ?? 500);
|
|
77
|
+
|
|
78
|
+
/** True when the address carries an explicit `request-timeout` query parameter. */
|
|
79
|
+
function addressStatesRequestTimeout(address: string): boolean {
|
|
80
|
+
if (address.indexOf("://") === -1) return false;
|
|
81
|
+
return new URL(address).searchParams.has("request-timeout");
|
|
82
|
+
}
|
|
57
83
|
|
|
58
|
-
/** Returns PlClientConfig with reduced timeout for tests */
|
|
84
|
+
/** Returns PlClientConfig with reduced timeout for tests, unless the address sets its own */
|
|
59
85
|
export function plAddressToTestConfig(address: string): PlClientConfig {
|
|
60
86
|
const plConf = plAddressToConfig(address);
|
|
61
|
-
|
|
87
|
+
// An explicit request-timeout in the address wins over TEST_REQUEST_TIMEOUT. In the k8s e2e
|
|
88
|
+
// deploy the client dials a cluster service DNS name, and the first call on a cold channel must
|
|
89
|
+
// finish name resolution and get a load-balancer pick before it is even sent; 500ms does not
|
|
90
|
+
// cover that, so whichever test happened to open the cold channel died with DEADLINE_EXCEEDED
|
|
91
|
+
// ("Waiting for LB pick") against a healthy backend. Addresses that carry no request-timeout
|
|
92
|
+
// keep the short default, so local runs still fail fast.
|
|
93
|
+
if (!addressStatesRequestTimeout(address)) plConf.defaultRequestTimeout = TEST_REQUEST_TIMEOUT;
|
|
62
94
|
return plConf;
|
|
63
95
|
}
|
|
64
96
|
|
|
@@ -178,6 +210,72 @@ export async function getTestLLClient(confOverrides: Partial<PlClientConfig> = {
|
|
|
178
210
|
return await LLPlClient.build({ ...conf, ...confOverrides }, { auth });
|
|
179
211
|
}
|
|
180
212
|
|
|
213
|
+
/** Returns a config logged in as the admin test user, or the ordinary test config where no
|
|
214
|
+
* admin credentials are set. A backend that needs no auth, and one whose single test user
|
|
215
|
+
* already holds the admin role, both leave PL_TEST_ADMIN_USER and PL_TEST_ADMIN_PASSWORD
|
|
216
|
+
* unset, and a test that asks for admin must still run there. */
|
|
217
|
+
export async function getTestAdminClientConf(): Promise<{ conf: PlClientConfig; auth: AuthOps }> {
|
|
218
|
+
const tConf = getTestConfig();
|
|
219
|
+
|
|
220
|
+
if (tConf.test_admin_user === undefined || tConf.test_admin_password === undefined)
|
|
221
|
+
return await getTestClientConf();
|
|
222
|
+
|
|
223
|
+
const plConf = plAddressToTestConfig(tConf.address);
|
|
224
|
+
const uClient = await UnauthenticatedPlClient.build(plConf);
|
|
225
|
+
const authInformation = await uClient.login(tConf.test_admin_user, tConf.test_admin_password);
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
conf: plConf,
|
|
229
|
+
auth: {
|
|
230
|
+
authInformation,
|
|
231
|
+
onUpdate: () => {},
|
|
232
|
+
onAuthError: () => {},
|
|
233
|
+
onUpdateError: () => {},
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export async function getTestAdminLLClient(confOverrides: Partial<PlClientConfig> = {}) {
|
|
239
|
+
const { conf, auth } = await getTestAdminClientConf();
|
|
240
|
+
return await LLPlClient.build({ ...conf, ...confOverrides }, { auth });
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export async function getTestAdminClient(
|
|
244
|
+
alternativeRoot?: string,
|
|
245
|
+
confOverrides: Partial<PlClientConfig> = {},
|
|
246
|
+
) {
|
|
247
|
+
const { conf, auth } = await getTestAdminClientConf();
|
|
248
|
+
if (alternativeRoot !== undefined && conf.alternativeRoot !== undefined)
|
|
249
|
+
throw new Error("test pl address configured with alternative root");
|
|
250
|
+
return await PlClient.init({ ...conf, ...confOverrides, alternativeRoot }, auth);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export async function withAdminTempRoot<T>(body: (pl: PlClient) => Promise<T>): Promise<T | void> {
|
|
254
|
+
const alternativeRoot = `test_${Date.now()}_${randomUUID()}`;
|
|
255
|
+
let altRootId: OptionalSignedResourceId = NullSignedResourceId;
|
|
256
|
+
try {
|
|
257
|
+
const client = await getTestAdminClient(alternativeRoot);
|
|
258
|
+
altRootId = client.clientRoot;
|
|
259
|
+
try {
|
|
260
|
+
const value = await body(client);
|
|
261
|
+
const rawClient = await getTestAdminClient();
|
|
262
|
+
try {
|
|
263
|
+
await rawClient.deleteAlternativeRoot(alternativeRoot);
|
|
264
|
+
} catch (cleanupErr: any) {
|
|
265
|
+
console.warn(`Failed to clean up alternative root ${alternativeRoot}:`, cleanupErr.message);
|
|
266
|
+
} finally {
|
|
267
|
+
await rawClient.close();
|
|
268
|
+
}
|
|
269
|
+
return value;
|
|
270
|
+
} finally {
|
|
271
|
+
await client.close();
|
|
272
|
+
}
|
|
273
|
+
} catch (err: any) {
|
|
274
|
+
console.log(`ALTERNATIVE ROOT: ${alternativeRoot} (${resourceIdToString(altRootId)})`);
|
|
275
|
+
throw err;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
181
279
|
export async function getTestClient(
|
|
182
280
|
alternativeRoot?: string,
|
|
183
281
|
confOverrides: Partial<PlClientConfig> = {},
|