@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.
@@ -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: () => 500,
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
- /** Returns PlClientConfig with reduced timeout for tests */
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 = 500;
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
- /** Default request timeout for tests (ms) */
16
- export declare const TEST_REQUEST_TIMEOUT = 500;
17
- /** Returns PlClientConfig with reduced timeout for tests */
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;;qBAsBpB;;wBAGG,sBAAsB,kBAAkB;wBAkDlC,qBAAqB;EAAU,MAAM;EAAgB,MAAM;;wBAmE3D,gBAAgB,gBAAe,QAAQ,kBAAoB,QAAA;wBAK3D,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"}
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"}
@@ -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: () => 500,
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
- /** Returns PlClientConfig with reduced timeout for tests */
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 = 500;
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":"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"}
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.0",
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.49.0",
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.1",
45
- "@milaboratories/ts-configs": "1.4.0",
46
- "@milaboratories/ts-builder": "1.7.2"
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"
@@ -1,4 +1,4 @@
1
- import { getTestClient, getTestClientConf } from "../test/test_config";
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 getTestClient(undefined);
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 getTestClientConf();
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 getTestClient();
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 getTestClient();
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
- const client = await getTestLLClient();
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 getTestLLClient();
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 getTestLLClient();
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 withTempRoot(async (pl) => {
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 withTempRoot(async (pl) => {
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 withTempRoot(async (pl) => {
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 withTempRoot(async (pl) => {
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 withTempRoot(async (pl) => {
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
+ });
@@ -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
- /** Default request timeout for tests (ms) */
56
- export const TEST_REQUEST_TIMEOUT = 500;
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
- plConf.defaultRequestTimeout = TEST_REQUEST_TIMEOUT;
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> = {},