@elench/testkit 0.1.77 → 0.1.78

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -365,8 +365,31 @@ Named HTTP profiles live in `testkit.config.ts` and can be referenced by name:
365
365
 
366
366
  ```ts
367
367
  import { defineHttpSuite } from "@elench/testkit";
368
+ import { defineConfig, profiles } from "@elench/testkit/config";
368
369
 
369
- const suite = defineHttpSuite({ profile: "default-auth" }, ({ req, setupData }) => {
370
+ export default defineConfig({
371
+ profiles: {
372
+ http: {
373
+ defaultAuth: profiles.localJson({
374
+ password: "password",
375
+ identities: {
376
+ primary: {
377
+ email: "test@example.com",
378
+ },
379
+ },
380
+ session: {
381
+ authCookie: "session",
382
+ },
383
+ headers: {
384
+ contentTypeJson: true,
385
+ forwardedFor: "deterministic",
386
+ },
387
+ }).session(),
388
+ },
389
+ },
390
+ });
391
+
392
+ const suite = defineHttpSuite({ profile: "defaultAuth" }, ({ req, setupData }) => {
370
393
  req("GET", "/api/auth/session", setupData);
371
394
  });
372
395
  ```
@@ -4,6 +4,7 @@ import ts from "typescript";
4
4
  import { discoverTests } from "../discovery/index.mjs";
5
5
  import { loadConfigContext } from "../config/index.mjs";
6
6
  import { runTestkitTypecheck } from "./typecheck.mjs";
7
+ import { findConfigFile } from "../config/config-loader.mjs";
7
8
 
8
9
  export async function runDoctor(options = {}) {
9
10
  const checks = [];
@@ -39,6 +40,17 @@ export async function runDoctor(options = {}) {
39
40
  details: playwrightViolations,
40
41
  });
41
42
 
43
+ const configImportViolations = findConfigImportViolations(productDir);
44
+ checks.push({
45
+ code: "config-import-hygiene",
46
+ level: configImportViolations.length === 0 ? "pass" : "fail",
47
+ message:
48
+ configImportViolations.length === 0
49
+ ? "Repo config does not import __testkit__ helper modules"
50
+ : `Found ${configImportViolations.length} repo config import violation(s)`,
51
+ details: configImportViolations,
52
+ });
53
+
42
54
  const hasBrowserOrNextWork = discovery.files.some((entry) => entry.selectionType === "pw");
43
55
  if (hasBrowserOrNextWork) {
44
56
  const nodeCount = discovery.coverageGraph?.nodes?.length || 0;
@@ -113,6 +125,31 @@ function findPlaywrightRuntimeImportViolations(productDir) {
113
125
  return violations;
114
126
  }
115
127
 
128
+ function findConfigImportViolations(productDir) {
129
+ const configFile = findConfigFile(productDir);
130
+ if (!configFile || !fs.existsSync(configFile)) return [];
131
+
132
+ const sourceText = fs.readFileSync(configFile, "utf8");
133
+ const sourceFile = ts.createSourceFile(configFile, sourceText, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
134
+ const violations = [];
135
+
136
+ for (const statement of sourceFile.statements) {
137
+ if (!ts.isImportDeclaration(statement)) continue;
138
+ if (!ts.isStringLiteral(statement.moduleSpecifier)) continue;
139
+ const specifier = statement.moduleSpecifier.text;
140
+ if (!isRepoLocalConfigImportViolation(specifier)) continue;
141
+ const position = sourceFile.getLineAndCharacterOfPosition(statement.getStart(sourceFile));
142
+ violations.push({
143
+ file: path.relative(productDir, configFile).split(path.sep).join("/"),
144
+ line: position.line + 1,
145
+ specifier,
146
+ snippet: statement.getText(sourceFile),
147
+ });
148
+ }
149
+
150
+ return violations;
151
+ }
152
+
116
153
  function collectFiles(rootDir, out = []) {
117
154
  if (!fs.existsSync(rootDir)) return out;
118
155
  for (const entry of fs.readdirSync(rootDir, { withFileTypes: true })) {
@@ -129,6 +166,12 @@ function collectFiles(rootDir, out = []) {
129
166
  return out.sort((left, right) => left.localeCompare(right));
130
167
  }
131
168
 
169
+ function isRepoLocalConfigImportViolation(specifier) {
170
+ if (typeof specifier !== "string") return false;
171
+ if (!specifier.startsWith(".") && !specifier.startsWith("/")) return false;
172
+ return specifier.includes("__testkit__");
173
+ }
174
+
132
175
  function relativeViolation(productDir, absolutePath, sourceFile, statement) {
133
176
  const position = sourceFile.getLineAndCharacterOfPosition(statement.getStart(sourceFile));
134
177
  return {
@@ -1,4 +1,4 @@
1
- import type { AuthAdapter, HeaderBuilder, HttpSuiteConfig } from "../index";
1
+ import type { HttpSuiteConfig, RuntimeEnv, RuntimeOptions } from "../index";
2
2
 
3
3
  export interface DatabaseTemplateConfig {
4
4
  inputs?: string[];
@@ -192,6 +192,187 @@ export interface TestkitFileMetadata {
192
192
  skip?: string | { reason: string };
193
193
  }
194
194
 
195
+ export interface ProfileRequestContext {
196
+ actor: string;
197
+ actorIndex: number;
198
+ env: RuntimeEnv;
199
+ phase: string;
200
+ }
201
+
202
+ export type ProfileValueFactory<TValue> =
203
+ | TValue
204
+ | ((context: ProfileRequestContext) => TValue);
205
+
206
+ export interface ProfileRequestConfig {
207
+ body?: ProfileValueFactory<unknown>;
208
+ contentTypeJson?: boolean;
209
+ expect?: number | number[];
210
+ headers?: ProfileValueFactory<Record<string, string>>;
211
+ method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
212
+ path: string;
213
+ }
214
+
215
+ export interface SessionAuthSourceConfig {
216
+ key: string;
217
+ }
218
+
219
+ export interface SessionAuthHeaderConfig {
220
+ header?: string;
221
+ prefix?: string;
222
+ source: SessionAuthSourceConfig;
223
+ }
224
+
225
+ export interface SessionCaptureConfig {
226
+ auth?: SessionAuthHeaderConfig;
227
+ cookies?: Record<string, string>;
228
+ fields?: Record<string, string>;
229
+ }
230
+
231
+ export interface SessionActorConfig {
232
+ bootstrap?: ProfileRequestConfig | ProfileRequestConfig[];
233
+ login: ProfileRequestConfig;
234
+ session: SessionCaptureConfig;
235
+ }
236
+
237
+ export interface ProfileHeaderContext<TSession = unknown> {
238
+ actor: string | null;
239
+ env: RuntimeEnv;
240
+ session: TSession | null;
241
+ }
242
+
243
+ export interface ProfileSessionHeaderConfig {
244
+ actor?: string;
245
+ field: string;
246
+ header: string;
247
+ }
248
+
249
+ export interface DeterministicForwardedForConfig {
250
+ actor?: string;
251
+ header?: string;
252
+ seed?: string;
253
+ }
254
+
255
+ export interface ProfileHeaderConfig<TSession = unknown> {
256
+ contentTypeJson?: boolean;
257
+ forwardedFor?: "deterministic" | DeterministicForwardedForConfig;
258
+ fromSession?: ProfileSessionHeaderConfig[];
259
+ values?: Record<string, string> | ((context: ProfileHeaderContext<TSession>) => Record<string, string>);
260
+ }
261
+
262
+ export interface RawHttpProfileOptions {
263
+ env?: RuntimeEnv;
264
+ headers?: ProfileHeaderConfig;
265
+ options?: RuntimeOptions;
266
+ }
267
+
268
+ export interface SessionHttpProfileOptions {
269
+ actor: SessionActorConfig;
270
+ env?: RuntimeEnv;
271
+ headers?: ProfileHeaderConfig<Record<string, unknown>>;
272
+ options?: RuntimeOptions;
273
+ }
274
+
275
+ export interface MultiActorHttpProfileOptions {
276
+ actors: Record<string, SessionActorConfig>;
277
+ env?: RuntimeEnv;
278
+ headers?: ProfileHeaderConfig<Record<string, Record<string, unknown>>>;
279
+ options?: RuntimeOptions;
280
+ primaryActor?: string;
281
+ }
282
+
283
+ export interface LocalJsonIdentityContext {
284
+ actor: string;
285
+ }
286
+
287
+ export type LocalJsonIdentityValue = string | ((context: LocalJsonIdentityContext) => string);
288
+
289
+ export interface LocalJsonActorIdentityConfig {
290
+ email?: LocalJsonIdentityValue;
291
+ loginBody?: Record<string, unknown>;
292
+ name?: LocalJsonIdentityValue;
293
+ organizationName?: LocalJsonIdentityValue;
294
+ password?: LocalJsonIdentityValue;
295
+ signupBody?: Record<string, unknown>;
296
+ }
297
+
298
+ export interface LocalJsonSessionAuthOptions {
299
+ header?: string;
300
+ prefix?: string;
301
+ sourceKey?: string;
302
+ }
303
+
304
+ export interface LocalJsonSessionOptions {
305
+ auth?: LocalJsonSessionAuthOptions;
306
+ authCookie?: string;
307
+ cookies?: Record<string, string>;
308
+ fields?: Record<string, string>;
309
+ organizationIdPath?: string;
310
+ refreshCookie?: string;
311
+ }
312
+
313
+ export interface LocalJsonOrganizationHeaderConfig {
314
+ actor?: string;
315
+ field?: string;
316
+ header?: string;
317
+ }
318
+
319
+ export interface LocalJsonHeaderOptions {
320
+ contentTypeJson?: boolean;
321
+ forwardedFor?: "deterministic" | DeterministicForwardedForConfig;
322
+ organization?: false | string | LocalJsonOrganizationHeaderConfig;
323
+ values?: Record<string, string> | ((context: ProfileHeaderContext<unknown>) => Record<string, string>);
324
+ }
325
+
326
+ export interface LocalJsonSignupOptions {
327
+ enabled?: boolean;
328
+ expect?: number | number[];
329
+ path?: string;
330
+ }
331
+
332
+ export interface LocalJsonLoginOptions {
333
+ expect?: number | number[];
334
+ path?: string;
335
+ }
336
+
337
+ export interface LocalJsonProfileOptions {
338
+ env?: RuntimeEnv;
339
+ headers?: LocalJsonHeaderOptions;
340
+ identities?: Record<string, LocalJsonActorIdentityConfig>;
341
+ login?: LocalJsonLoginOptions;
342
+ options?: RuntimeOptions;
343
+ password?: LocalJsonIdentityValue;
344
+ session?: LocalJsonSessionOptions;
345
+ signup?: false | LocalJsonSignupOptions;
346
+ }
347
+
348
+ export interface LocalJsonSessionProfileOptions {
349
+ actor?: string;
350
+ env?: RuntimeEnv;
351
+ headers?: LocalJsonHeaderOptions;
352
+ identity?: LocalJsonActorIdentityConfig;
353
+ options?: RuntimeOptions;
354
+ }
355
+
356
+ export interface LocalJsonMultiActorProfileOptions {
357
+ actors?: string[] | Record<string, LocalJsonActorIdentityConfig>;
358
+ env?: RuntimeEnv;
359
+ headers?: LocalJsonHeaderOptions;
360
+ options?: RuntimeOptions;
361
+ primaryActor?: string;
362
+ }
363
+
364
+ export interface LocalJsonRawProfileOptions {
365
+ env?: RuntimeEnv;
366
+ headers?: LocalJsonHeaderOptions;
367
+ options?: RuntimeOptions;
368
+ }
369
+
370
+ export interface LocalJsonProfileBuilder {
371
+ multiActor(options?: LocalJsonMultiActorProfileOptions): HttpSuiteConfig<Record<string, Record<string, unknown>>>;
372
+ raw(options?: LocalJsonRawProfileOptions): HttpSuiteConfig<any>;
373
+ session(options?: LocalJsonSessionProfileOptions): HttpSuiteConfig<Record<string, unknown>>;
374
+ }
375
+
195
376
  export interface NodeAppOptions extends Omit<ServiceConfig, "local" | "runtime" | "env"> {
196
377
  baseUrl?: string;
197
378
  build?: BuildConfig | null;
@@ -246,7 +427,6 @@ export interface TestkitConfig {
246
427
  }
247
428
 
248
429
  export declare function defineConfig<T extends TestkitConfig>(config: T): T;
249
- export declare function defineHttpProfile<T extends HttpSuiteConfig>(profile: T): T;
250
430
  export declare function defineFile<T extends TestkitFileMetadata>(metadata: T): T;
251
431
  export declare const app: {
252
432
  node(options: NodeAppOptions): ServiceConfig;
@@ -269,20 +449,13 @@ export declare const database: {
269
449
  export declare const toolchain: {
270
450
  node(options?: NodeToolchainConfig): NodeToolchainConfig;
271
451
  };
272
- export declare function clerkSessionProfile(options?: {
273
- apiBase?: string;
274
- needsAuth?: boolean;
275
- secretKeyEnv?: string;
276
- }): HttpSuiteConfig;
277
- export declare function jsonSessionProfile(options?: {
278
- body?: (context: { env: Record<string, string> }) => unknown;
279
- cookieName?: string;
280
- headers?: Record<string, string>;
281
- loginPath?: string;
282
- passwordEnv?: string;
283
- successStatus?: number;
284
- usernameEnv?: string;
285
- }): HttpSuiteConfig;
452
+ export declare const profiles: {
453
+ custom<T extends HttpSuiteConfig>(profile: T): T;
454
+ localJson(options?: LocalJsonProfileOptions): LocalJsonProfileBuilder;
455
+ multiActor(options: MultiActorHttpProfileOptions): HttpSuiteConfig<Record<string, Record<string, unknown>>>;
456
+ raw(options?: RawHttpProfileOptions): HttpSuiteConfig<any>;
457
+ session(options: SessionHttpProfileOptions): HttpSuiteConfig<Record<string, unknown>>;
458
+ };
286
459
 
287
460
  export declare function registerRepoConfig(config: unknown): void;
288
461
  export declare function getRepoConfig(): unknown;
@@ -3,21 +3,23 @@ import {
3
3
  clearRuntimeContext,
4
4
  getRepoConfig,
5
5
  getRuntimeContext,
6
- getRuntimeEnv,
7
6
  registerRepoConfig,
8
7
  registerRuntimeContext,
9
8
  runtimeHttp,
10
9
  runtimeJson,
11
10
  } from "./runtime.mjs";
11
+ import {
12
+ customProfile,
13
+ localJsonProfile,
14
+ multiActorProfile,
15
+ rawProfile,
16
+ sessionProfile,
17
+ } from "./profiles.mjs";
12
18
 
13
19
  export function defineConfig(config) {
14
20
  return config || {};
15
21
  }
16
22
 
17
- export function defineHttpProfile(profile) {
18
- return profile || {};
19
- }
20
-
21
23
  export function defineFile(metadata) {
22
24
  return metadata || {};
23
25
  }
@@ -262,73 +264,13 @@ export const toolchain = {
262
264
  node: nodeToolchain,
263
265
  };
264
266
 
265
- export function clerkSessionProfile(options = {}) {
266
- const apiBase = options.apiBase || "https://api.clerk.com/v1";
267
- const secretKeyEnv = options.secretKeyEnv || "CLERK_SECRET_KEY";
268
- const needsAuth = options.needsAuth !== false;
269
-
270
- return defineHttpProfile({
271
- auth: {
272
- setup() {
273
- return resolveClerkSession({
274
- apiBase,
275
- secretKey: envValue(secretKeyEnv),
276
- needsAuth,
277
- });
278
- },
279
- headers(setupData) {
280
- return getClerkAuthHeaders(setupData, { apiBase });
281
- },
282
- },
283
- });
284
- }
285
-
286
- export function jsonSessionProfile(options = {}) {
287
- const loginPath = options.loginPath || "/api/auth/login";
288
- const cookieName = options.cookieName || "session";
289
- const body =
290
- typeof options.body === "function"
291
- ? options.body
292
- : () => ({
293
- username: envValue(options.usernameEnv || "TESTKIT_USERNAME"),
294
- password: envValue(options.passwordEnv || "TESTKIT_PASSWORD"),
295
- });
296
-
297
- return defineHttpProfile({
298
- auth: {
299
- setup({ env }) {
300
- const requestBody = body({ env });
301
- const res = runtimeHttp.post(`${env.BASE}${loginPath}`, JSON.stringify(requestBody), {
302
- headers: {
303
- "Content-Type": "application/json",
304
- ...(env.routeParams || {}),
305
- ...(options.headers || {}),
306
- },
307
- });
308
-
309
- if (res.status !== (options.successStatus || 200)) {
310
- throw new Error(`Login failed (${res.status}): ${res.body}`);
311
- }
312
-
313
- const cookie = res.cookies?.[cookieName]?.[0]?.value ?? null;
314
- if (!cookie) {
315
- throw new Error(`No ${cookieName} cookie returned from login`);
316
- }
317
-
318
- return {
319
- cookie,
320
- body: safeJson(res),
321
- };
322
- },
323
- headers(session) {
324
- if (!session?.cookie) return {};
325
- return {
326
- Cookie: `${cookieName}=${session.cookie}`,
327
- };
328
- },
329
- },
330
- });
331
- }
267
+ export const profiles = {
268
+ custom: customProfile,
269
+ localJson: localJsonProfile,
270
+ multiActor: multiActorProfile,
271
+ raw: rawProfile,
272
+ session: sessionProfile,
273
+ };
332
274
 
333
275
  export {
334
276
  clearRepoConfig,
@@ -430,101 +372,3 @@ function compiledEntryFromSource(entry, outDir) {
430
372
  const relative = compiled.startsWith("src/") ? compiled.slice(4) : compiled;
431
373
  return `${outDir}/${relative}`.replace(/\/+/g, "/");
432
374
  }
433
-
434
- function envValue(name) {
435
- const env = getRuntimeEnv();
436
- const value = env?.rawEnv?.[name] || env?.[name];
437
- if (!value) {
438
- throw new Error(`Missing required env var "${name}" for testkit config`);
439
- }
440
- return value;
441
- }
442
-
443
- function safeJson(response) {
444
- try {
445
- return runtimeJson(response);
446
- } catch {
447
- return null;
448
- }
449
- }
450
-
451
- function resolveClerkSession({ apiBase, secretKey, needsAuth }) {
452
- if (!needsAuth) {
453
- return { jwt: null };
454
- }
455
-
456
- const headers = { Authorization: `Bearer ${secretKey}` };
457
- const usersRes = runtimeHttp.get(`${apiBase}/users?limit=1&order_by=-created_at`, {
458
- headers,
459
- });
460
- if (usersRes.status !== 200) {
461
- throw new Error(`Clerk list users failed (${usersRes.status}): ${usersRes.body}`);
462
- }
463
-
464
- const users = runtimeJson(usersRes);
465
- if (!users.length) {
466
- throw new Error("No Clerk users found for testkit Clerk auth profile");
467
- }
468
-
469
- const userId = users[0].id;
470
- const sessionsRes = runtimeHttp.get(
471
- `${apiBase}/sessions?user_id=${userId}&status=active&limit=1`,
472
- { headers }
473
- );
474
- if (sessionsRes.status !== 200) {
475
- throw new Error(`Clerk list sessions failed (${sessionsRes.status}): ${sessionsRes.body}`);
476
- }
477
-
478
- const sessions = runtimeJson(sessionsRes);
479
- if (!sessions?.length) {
480
- throw new Error("No active Clerk session found for testkit Clerk auth profile");
481
- }
482
-
483
- const sessionId = sessions[0].id;
484
- const tokenRes = runtimeHttp.post(`${apiBase}/sessions/${sessionId}/tokens`, null, {
485
- headers: {
486
- ...headers,
487
- "Content-Type": "application/json",
488
- },
489
- });
490
- if (tokenRes.status !== 200) {
491
- throw new Error(`Clerk create token failed (${tokenRes.status}): ${tokenRes.body}`);
492
- }
493
-
494
- return {
495
- jwt: runtimeJson(tokenRes).jwt,
496
- sessionId,
497
- secretKey,
498
- };
499
- }
500
-
501
- function getClerkAuthHeaders(setupData, { apiBase }) {
502
- if (!setupData?.jwt) return {};
503
-
504
- if (!setupData.sessionId || !setupData.secretKey) {
505
- return {
506
- Authorization: `Bearer ${setupData.jwt}`,
507
- "Content-Type": "application/json",
508
- };
509
- }
510
-
511
- const tokenRes = runtimeHttp.post(`${apiBase}/sessions/${setupData.sessionId}/tokens`, null, {
512
- headers: {
513
- Authorization: `Bearer ${setupData.secretKey}`,
514
- "Content-Type": "application/json",
515
- },
516
- });
517
- if (tokenRes.status !== 200) {
518
- return {
519
- Authorization: `Bearer ${setupData.jwt}`,
520
- "Content-Type": "application/json",
521
- };
522
- }
523
-
524
- const nextJwt = runtimeJson(tokenRes).jwt;
525
- setupData.jwt = nextJwt;
526
- return {
527
- Authorization: `Bearer ${nextJwt}`,
528
- "Content-Type": "application/json",
529
- };
530
- }