@ripplo/testing 0.4.7 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -19
- package/dist/actions.d.ts +10 -2
- package/dist/actions.js +24 -2
- package/dist/assert.d.ts +1 -1
- package/dist/{builder-BMjy83Iy.d.ts → builder-DiVz3t1D.d.ts} +10 -3
- package/dist/{chunk-V6LMXKGL.js → chunk-XO36IU66.js} +32 -38
- package/dist/{chunk-DL3HLCD7.js → chunk-YFOTJIVF.js} +1 -1
- package/dist/compiler.d.ts +8 -3
- package/dist/compiler.js +1 -1
- package/dist/elysia.d.ts +3 -3
- package/dist/elysia.js +16 -23
- package/dist/{engine-DMOkJdjd.d.ts → engine-DVbF4E5A.d.ts} +20 -6
- package/dist/express.d.ts +3 -3
- package/dist/express.js +16 -45
- package/dist/fastify.d.ts +3 -3
- package/dist/fastify.js +16 -24
- package/dist/hono.d.ts +3 -3
- package/dist/hono.js +17 -24
- package/dist/index.d.ts +32 -13
- package/dist/index.js +247 -88
- package/dist/koa.d.ts +3 -3
- package/dist/koa.js +17 -20
- package/dist/lockfile.d.ts +20 -5
- package/dist/lockfile.js +89 -3
- package/dist/nestjs.d.ts +3 -3
- package/dist/nestjs.js +15 -20
- package/dist/nextjs.d.ts +3 -3
- package/dist/nextjs.js +16 -23
- package/dist/{types-16SB7zjP.d.ts → types-BzZrl65Z.d.ts} +9 -2
- package/package.json +3 -3
package/dist/lockfile.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import "./chunk-4MGIQFAJ.js";
|
|
2
2
|
|
|
3
3
|
// src/lockfile.ts
|
|
4
|
+
import { createHash } from "crypto";
|
|
4
5
|
import fs from "fs/promises";
|
|
5
6
|
import path from "path";
|
|
6
7
|
|
|
@@ -500,6 +501,7 @@ var observerSchema = z9.object({
|
|
|
500
501
|
// src/lockfile.ts
|
|
501
502
|
import { z as z10 } from "zod";
|
|
502
503
|
var LOCKFILE_RELATIVE_PATH = ".ripplo/ripplo.lock";
|
|
504
|
+
var FIXTURES_RELATIVE_PATH = ".ripplo/fixtures";
|
|
503
505
|
var MAX_TESTS = 5e3;
|
|
504
506
|
var requiresKeysSchema = z10.record(z10.string().max(200), z10.string().max(200));
|
|
505
507
|
var compiledTestSchema = z10.object({
|
|
@@ -512,14 +514,32 @@ var compiledTestSchema = z10.object({
|
|
|
512
514
|
sourcePath: z10.string().max(500).optional(),
|
|
513
515
|
spec: workflowSpecSchema
|
|
514
516
|
});
|
|
515
|
-
var
|
|
517
|
+
var fixtureEntrySchema = z10.object({
|
|
518
|
+
sha256: z10.string().regex(/^[0-9a-f]{64}$/u),
|
|
519
|
+
size: z10.number().int().nonnegative()
|
|
520
|
+
});
|
|
521
|
+
var fixturesMapSchema = z10.record(z10.string().min(1).max(500), fixtureEntrySchema);
|
|
522
|
+
var lockfileBodyV1Schema = z10.object({
|
|
523
|
+
observers: z10.record(z10.string().max(200), observerSchema),
|
|
524
|
+
preconditions: z10.record(z10.string().max(200), preconditionSchema),
|
|
525
|
+
tests: z10.array(compiledTestSchema).max(MAX_TESTS)
|
|
526
|
+
});
|
|
527
|
+
var lockfileBodyV2Schema = z10.object({
|
|
528
|
+
fixtures: fixturesMapSchema,
|
|
516
529
|
observers: z10.record(z10.string().max(200), observerSchema),
|
|
517
530
|
preconditions: z10.record(z10.string().max(200), preconditionSchema),
|
|
518
531
|
tests: z10.array(compiledTestSchema).max(MAX_TESTS)
|
|
519
532
|
});
|
|
520
|
-
var lockfileCodec = defineCodec("ripplo-lockfile").initial(
|
|
533
|
+
var lockfileCodec = defineCodec("ripplo-lockfile").initial(lockfileBodyV1Schema).upgrade(
|
|
534
|
+
lockfileBodyV2Schema,
|
|
535
|
+
(prev) => ({
|
|
536
|
+
...prev,
|
|
537
|
+
fixtures: {}
|
|
538
|
+
})
|
|
539
|
+
).build();
|
|
521
540
|
function compileResultToLockfile(result) {
|
|
522
541
|
return {
|
|
542
|
+
fixtures: { ...result.fixtures },
|
|
523
543
|
observers: result.observers,
|
|
524
544
|
preconditions: result.preconditions,
|
|
525
545
|
tests: result.tests.filter((test) => test.implemented).map((test) => ({
|
|
@@ -547,12 +567,76 @@ async function readLockfile({ cwd }) {
|
|
|
547
567
|
}
|
|
548
568
|
return decodeJson(lockfileCodec, raw);
|
|
549
569
|
}
|
|
570
|
+
var MAX_FIXTURE_BYTES = 10 * 1024 * 1024;
|
|
571
|
+
var MAX_TOTAL_FIXTURE_BYTES = 50 * 1024 * 1024;
|
|
550
572
|
async function writeLockfile({ cwd, result }) {
|
|
551
|
-
const
|
|
573
|
+
const hydrated = await hashFixturesIntoCompileResult({ cwd, result });
|
|
574
|
+
const lockfile = compileResultToLockfile(hydrated);
|
|
552
575
|
const lockfilePath = path.join(cwd, LOCKFILE_RELATIVE_PATH);
|
|
553
576
|
await fs.mkdir(path.dirname(lockfilePath), { recursive: true });
|
|
554
577
|
await fs.writeFile(lockfilePath, serializeLockfile(lockfile), "utf8");
|
|
555
578
|
}
|
|
579
|
+
async function hashFixturesIntoCompileResult({
|
|
580
|
+
cwd,
|
|
581
|
+
result
|
|
582
|
+
}) {
|
|
583
|
+
const referenced = collectFixtureReferences(result);
|
|
584
|
+
if (referenced.size === 0) {
|
|
585
|
+
return { ...result, fixtures: {} };
|
|
586
|
+
}
|
|
587
|
+
const fixturesRoot = path.join(cwd, FIXTURES_RELATIVE_PATH);
|
|
588
|
+
const sortedNames = [...referenced].toSorted((a, b) => a.localeCompare(b));
|
|
589
|
+
const hashed = await Promise.all(
|
|
590
|
+
sortedNames.map(async (name) => {
|
|
591
|
+
const entry = await hashOneFixture({ fixturesRoot, name });
|
|
592
|
+
if (entry.size > MAX_FIXTURE_BYTES) {
|
|
593
|
+
throw new Error(
|
|
594
|
+
`Fixture "${name}" is ${String(entry.size)} bytes; exceeds per-file limit of ${String(MAX_FIXTURE_BYTES)} bytes`
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
return [name, entry];
|
|
598
|
+
})
|
|
599
|
+
);
|
|
600
|
+
const total = hashed.reduce((sum, [, entry]) => sum + entry.size, 0);
|
|
601
|
+
if (total > MAX_TOTAL_FIXTURE_BYTES) {
|
|
602
|
+
throw new Error(
|
|
603
|
+
`Total fixtures size exceeds limit of ${String(MAX_TOTAL_FIXTURE_BYTES)} bytes`
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
return { ...result, fixtures: Object.fromEntries(hashed) };
|
|
607
|
+
}
|
|
608
|
+
async function hashOneFixture({ fixturesRoot, name }) {
|
|
609
|
+
if (name.includes("..") || path.isAbsolute(name)) {
|
|
610
|
+
throw new Error(`Invalid fixture name "${name}": must be a path under .ripplo/fixtures/`);
|
|
611
|
+
}
|
|
612
|
+
const abs = path.join(fixturesRoot, name);
|
|
613
|
+
const stat = await fs.lstat(abs).catch((error) => {
|
|
614
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
615
|
+
throw new Error(`Fixture "${name}" not found at ${abs}`);
|
|
616
|
+
}
|
|
617
|
+
throw error;
|
|
618
|
+
});
|
|
619
|
+
if (stat.isSymbolicLink()) {
|
|
620
|
+
throw new Error(`Fixture "${name}" is a symlink; symlinks are not allowed`);
|
|
621
|
+
}
|
|
622
|
+
if (!stat.isFile()) {
|
|
623
|
+
throw new Error(`Fixture "${name}" is not a regular file`);
|
|
624
|
+
}
|
|
625
|
+
const bytes = await fs.readFile(abs);
|
|
626
|
+
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
|
627
|
+
return { sha256, size: bytes.byteLength };
|
|
628
|
+
}
|
|
629
|
+
function collectFixtureReferences(result) {
|
|
630
|
+
const names = /* @__PURE__ */ new Set();
|
|
631
|
+
result.tests.forEach((test) => {
|
|
632
|
+
Object.values(test.spec.nodes).forEach((node) => {
|
|
633
|
+
if (node.type === "upload") {
|
|
634
|
+
node.files.forEach((name) => names.add(name));
|
|
635
|
+
}
|
|
636
|
+
});
|
|
637
|
+
});
|
|
638
|
+
return names;
|
|
639
|
+
}
|
|
556
640
|
function compareCompileToLockfile({
|
|
557
641
|
compiled,
|
|
558
642
|
existing
|
|
@@ -589,9 +673,11 @@ function isPlainObject(value) {
|
|
|
589
673
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
590
674
|
}
|
|
591
675
|
export {
|
|
676
|
+
FIXTURES_RELATIVE_PATH,
|
|
592
677
|
LOCKFILE_RELATIVE_PATH,
|
|
593
678
|
compareCompileToLockfile,
|
|
594
679
|
compileResultToLockfile,
|
|
680
|
+
hashFixturesIntoCompileResult,
|
|
595
681
|
lockfileCodec,
|
|
596
682
|
readLockfile,
|
|
597
683
|
serializeLockfile,
|
package/dist/nestjs.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { DynamicModule } from '@nestjs/common';
|
|
2
|
-
import { R as RipploEngine } from './engine-
|
|
3
|
-
import './builder-
|
|
4
|
-
import './types-
|
|
2
|
+
import { R as RipploEngine } from './engine-DVbF4E5A.js';
|
|
3
|
+
import './builder-DiVz3t1D.js';
|
|
4
|
+
import './types-BzZrl65Z.js';
|
|
5
5
|
import './step-De52hTLd.js';
|
|
6
6
|
import '@ripplo/spec';
|
|
7
7
|
|
package/dist/nestjs.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
batchRequestSchema,
|
|
3
|
-
buildSetCookieHeader,
|
|
4
3
|
observerRequestSchema,
|
|
5
4
|
readAdapterWebhookSecret,
|
|
6
|
-
serializeCookie,
|
|
7
5
|
teardownRequestSchema,
|
|
6
|
+
toBatchRunResults,
|
|
7
|
+
toTeardownResults,
|
|
8
8
|
verifyWebhookSignature
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-XO36IU66.js";
|
|
10
10
|
import {
|
|
11
11
|
__decorateClass,
|
|
12
12
|
__decorateParam
|
|
@@ -45,25 +45,15 @@ function createController(path) {
|
|
|
45
45
|
}
|
|
46
46
|
const parsed = batchRequestSchema.safeParse(req.body);
|
|
47
47
|
if (!parsed.success) {
|
|
48
|
-
res.status(400).json({ error: "Invalid request body"
|
|
48
|
+
res.status(400).json({ error: "Invalid request body" });
|
|
49
49
|
return;
|
|
50
50
|
}
|
|
51
51
|
const host = req.get("host") ?? "";
|
|
52
52
|
const proto = req.get("x-forwarded-proto") ?? req.protocol;
|
|
53
53
|
const appUrl = `${proto}://${host}`;
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
});
|
|
57
|
-
result.cookies.forEach((cookie) => {
|
|
58
|
-
res.append("Set-Cookie", buildSetCookieHeader(serializeCookie(cookie)));
|
|
59
|
-
});
|
|
60
|
-
res.status(200).json({
|
|
61
|
-
data: result.data,
|
|
62
|
-
error: result.error,
|
|
63
|
-
executed: result.executed,
|
|
64
|
-
runId: result.runId,
|
|
65
|
-
success: result.success
|
|
66
|
-
});
|
|
54
|
+
const items = parsed.data.batch.map((b) => ({ names: b.preconditions, runId: b.runId }));
|
|
55
|
+
const results = await this.opts.engine.executePreconditions(items, { appUrl });
|
|
56
|
+
res.status(200).json({ results: toBatchRunResults(results) });
|
|
67
57
|
}
|
|
68
58
|
async executeObserver(req, res) {
|
|
69
59
|
if (!guard(req, res, this.opts)) {
|
|
@@ -86,11 +76,16 @@ function createController(path) {
|
|
|
86
76
|
}
|
|
87
77
|
const parsed = teardownRequestSchema.safeParse(req.body);
|
|
88
78
|
if (!parsed.success) {
|
|
89
|
-
res.status(400).json({ error: "Invalid request body"
|
|
79
|
+
res.status(400).json({ error: "Invalid request body" });
|
|
90
80
|
return;
|
|
91
81
|
}
|
|
92
|
-
|
|
93
|
-
|
|
82
|
+
const items = parsed.data.batch.map((b) => ({
|
|
83
|
+
data: b.data,
|
|
84
|
+
names: b.preconditions,
|
|
85
|
+
runId: b.runId
|
|
86
|
+
}));
|
|
87
|
+
const results = await this.opts.engine.teardown(items);
|
|
88
|
+
res.status(200).json({ results: toTeardownResults(results) });
|
|
94
89
|
}
|
|
95
90
|
};
|
|
96
91
|
__decorateClass([
|
package/dist/nextjs.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { R as RipploEngine } from './engine-
|
|
2
|
-
import './builder-
|
|
3
|
-
import './types-
|
|
1
|
+
import { R as RipploEngine } from './engine-DVbF4E5A.js';
|
|
2
|
+
import './builder-DiVz3t1D.js';
|
|
3
|
+
import './types-BzZrl65Z.js';
|
|
4
4
|
import './step-De52hTLd.js';
|
|
5
5
|
import '@ripplo/spec';
|
|
6
6
|
|
package/dist/nextjs.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
batchRequestSchema,
|
|
3
|
-
buildSetCookieHeader,
|
|
4
3
|
observerRequestSchema,
|
|
5
4
|
readAdapterWebhookSecret,
|
|
6
|
-
serializeCookie,
|
|
7
5
|
teardownRequestSchema,
|
|
6
|
+
toBatchRunResults,
|
|
7
|
+
toTeardownResults,
|
|
8
8
|
verifyWebhookSignature
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-XO36IU66.js";
|
|
10
10
|
import "./chunk-4MGIQFAJ.js";
|
|
11
11
|
|
|
12
12
|
// src/adapters/nextjs.ts
|
|
@@ -59,38 +59,31 @@ async function handleExecutePreconditions({
|
|
|
59
59
|
const json = tryParseJson(body);
|
|
60
60
|
const parsed = json == null ? null : batchRequestSchema.safeParse(json);
|
|
61
61
|
if (parsed == null || !parsed.success) {
|
|
62
|
-
return jsonResponse({ error: "Invalid request body"
|
|
62
|
+
return jsonResponse({ error: "Invalid request body" }, 400);
|
|
63
63
|
}
|
|
64
64
|
const host = req.headers.get("host");
|
|
65
65
|
if (host == null || host.length === 0) {
|
|
66
|
-
return jsonResponse({ error: "Missing host header"
|
|
66
|
+
return jsonResponse({ error: "Missing host header" }, 400);
|
|
67
67
|
}
|
|
68
68
|
const proto = req.headers.get("x-forwarded-proto") ?? "http";
|
|
69
69
|
const appUrl = `${proto}://${host}`;
|
|
70
|
-
const
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
headers.append("Set-Cookie", buildSetCookieHeader(serializeCookie(cookie)));
|
|
74
|
-
});
|
|
75
|
-
return new Response(
|
|
76
|
-
JSON.stringify({
|
|
77
|
-
data: result.data,
|
|
78
|
-
error: result.error,
|
|
79
|
-
executed: result.executed,
|
|
80
|
-
runId: result.runId,
|
|
81
|
-
success: result.success
|
|
82
|
-
}),
|
|
83
|
-
{ headers, status: 200 }
|
|
84
|
-
);
|
|
70
|
+
const items = parsed.data.batch.map((b) => ({ names: b.preconditions, runId: b.runId }));
|
|
71
|
+
const results = await engine.executePreconditions(items, { appUrl });
|
|
72
|
+
return jsonResponse({ results: toBatchRunResults(results) }, 200);
|
|
85
73
|
}
|
|
86
74
|
async function handleTeardown({ body, engine }) {
|
|
87
75
|
const json = tryParseJson(body);
|
|
88
76
|
const parsed = json == null ? null : teardownRequestSchema.safeParse(json);
|
|
89
77
|
if (parsed == null || !parsed.success) {
|
|
90
|
-
return jsonResponse({ error: "Invalid request body"
|
|
78
|
+
return jsonResponse({ error: "Invalid request body" }, 400);
|
|
91
79
|
}
|
|
92
|
-
|
|
93
|
-
|
|
80
|
+
const items = parsed.data.batch.map((b) => ({
|
|
81
|
+
data: b.data,
|
|
82
|
+
names: b.preconditions,
|
|
83
|
+
runId: b.runId
|
|
84
|
+
}));
|
|
85
|
+
const results = await engine.teardown(items);
|
|
86
|
+
return jsonResponse({ results: toTeardownResults(results) }, 200);
|
|
94
87
|
}
|
|
95
88
|
async function verifyAndReadBody(req, webhookSecret) {
|
|
96
89
|
if (webhookSecret.length === 0) {
|
|
@@ -40,6 +40,13 @@ interface Precondition<TData extends Record<string, Primitive> = Record<string,
|
|
|
40
40
|
}
|
|
41
41
|
type PreconditionData<T extends Precondition> = T[typeof PRECONDITION_DATA];
|
|
42
42
|
type PreconditionDeps<T extends Precondition> = T[typeof PRECONDITION_DEPS];
|
|
43
|
+
interface PreconditionDefinitionSetupItem {
|
|
44
|
+
readonly ctx: SetupContext;
|
|
45
|
+
readonly deps: Record<string, Record<string, Primitive>>;
|
|
46
|
+
}
|
|
47
|
+
interface PreconditionDefinitionTeardownItem {
|
|
48
|
+
readonly ctx: TeardownContext<Record<string, Primitive>>;
|
|
49
|
+
}
|
|
43
50
|
interface PreconditionDefinition {
|
|
44
51
|
readonly dependsOn: ReadonlyArray<string>;
|
|
45
52
|
readonly depMapping: ReadonlyArray<readonly [string, string]>;
|
|
@@ -47,8 +54,8 @@ interface PreconditionDefinition {
|
|
|
47
54
|
readonly implemented: boolean;
|
|
48
55
|
readonly name: string;
|
|
49
56
|
readonly returns: ReadonlyArray<string>;
|
|
50
|
-
readonly teardown: ((
|
|
51
|
-
readonly setup: (
|
|
57
|
+
readonly teardown: ((items: ReadonlyArray<PreconditionDefinitionTeardownItem>) => Promise<void>) | undefined;
|
|
58
|
+
readonly setup: (items: ReadonlyArray<PreconditionDefinitionSetupItem>) => Promise<ReadonlyArray<Record<string, Primitive>>>;
|
|
52
59
|
}
|
|
53
60
|
type VarsFn<T> = (vars: Record<string, Record<string, Primitive>>) => T;
|
|
54
61
|
interface TestDefinition {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ripplo/testing",
|
|
3
3
|
"description": "TypeScript DSL for defining and running Ripplo e2e workflow tests",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.5.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
7
7
|
"dist"
|
|
@@ -99,8 +99,8 @@
|
|
|
99
99
|
"tsup": "^8.5.1",
|
|
100
100
|
"typescript": "catalog:",
|
|
101
101
|
"vitest": "^4.1.4",
|
|
102
|
-
"@ripplo/
|
|
103
|
-
"@ripplo/
|
|
102
|
+
"@ripplo/eslint-config": "0.0.0",
|
|
103
|
+
"@ripplo/spec": "^0.0.0"
|
|
104
104
|
},
|
|
105
105
|
"peerDependencies": {
|
|
106
106
|
"@nestjs/common": "^10.0.0 || ^11.0.0",
|