slicetest 0.3.0 → 0.4.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 +102 -6
- package/dist/ci.d.ts +29 -0
- package/dist/ci.js +57 -0
- package/dist/cli.js +23 -0
- package/dist/config.d.ts +11 -2
- package/dist/config.js +9 -4
- package/dist/doctor.d.ts +22 -0
- package/dist/doctor.js +190 -0
- package/dist/drivers/driver.d.ts +2 -0
- package/dist/drivers/index.js +2 -0
- package/dist/drivers/sqlite.d.ts +23 -0
- package/dist/drivers/sqlite.js +203 -0
- package/dist/global-setup.js +24 -3
- package/dist/http.d.ts +6 -0
- package/dist/http.js +14 -0
- package/dist/index.d.ts +1 -0
- package/dist/init.js +80 -5
- package/dist/mail.d.ts +58 -0
- package/dist/mail.js +299 -0
- package/dist/matchers.d.ts +4 -0
- package/dist/matchers.js +33 -0
- package/dist/openapi.d.ts +1 -0
- package/dist/openapi.js +11 -1
- package/dist/provided.d.ts +1 -0
- package/dist/record-cli.d.ts +6 -0
- package/dist/record-cli.js +93 -0
- package/dist/record-session.d.ts +1 -0
- package/dist/record-session.js +10 -0
- package/dist/record.d.ts +46 -0
- package/dist/record.js +201 -0
- package/dist/runtime.d.ts +4 -0
- package/dist/runtime.js +24 -3
- package/dist/trace.d.ts +9 -1
- package/dist/trace.js +3 -1
- package/dist/vitest.js +5 -2
- package/dist/yaml-runtime.d.ts +1 -0
- package/dist/yaml-runtime.js +52 -13
- package/dist/yaml.d.ts +24 -1
- package/dist/yaml.js +34 -5
- package/package.json +4 -3
- package/schema/scenario.schema.json +149 -1
package/dist/yaml.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { isMap, isSeq, LineCounter, parseDocument } from "yaml";
|
|
2
2
|
const KINDS = {
|
|
3
3
|
stub: ["on", "when", "reply", "sequence", "networkError", "times", "delay"],
|
|
4
|
-
request: ["headers", "query", "json", "form", "body", "follow", "expect", "capture"],
|
|
4
|
+
request: ["headers", "query", "json", "form", "body", "follow", "concurrency", "expect", "capture"],
|
|
5
5
|
insert: ["rows", "capture"],
|
|
6
6
|
sql: ["params", "expect", "capture", "within"],
|
|
7
7
|
db: ["where", "orderBy", "expect", "capture", "within"],
|
|
@@ -10,17 +10,21 @@ const KINDS = {
|
|
|
10
10
|
log: ["from", "within"],
|
|
11
11
|
checkpoint: [],
|
|
12
12
|
snapshot: ["mask"],
|
|
13
|
+
mail: ["times", "within", "capture"],
|
|
13
14
|
};
|
|
14
15
|
const EXPECT_KEYS = {
|
|
15
|
-
request: ["status", "headers", "json", "text"],
|
|
16
|
+
request: ["status", "statuses", "headers", "json", "text"],
|
|
16
17
|
sql: ["rows", "count"],
|
|
17
18
|
db: ["rows", "count"],
|
|
18
19
|
};
|
|
19
20
|
const CONDITION_KEYS = ["query", "headers", "json", "body"];
|
|
21
|
+
const MAIL_KEYS = ["to", "from", "subject", "text", "html"];
|
|
20
22
|
const CHANGE_KEYS = ["inserted", "updated", "deleted"];
|
|
21
23
|
const RESPONSE_KEYS = ["status", "headers", "body"];
|
|
22
24
|
const SCENARIO_KEYS = ["name", "steps", "each", "skip", "only", "timeout"];
|
|
23
25
|
const CALL = /^([A-Za-z]+|\*)\s+(\/\S*)$/;
|
|
26
|
+
/** A request may also go to a captured URL of the app, e.g. a link from a mail: `GET {{link}}`. */
|
|
27
|
+
const REQUEST = /^[A-Za-z]+\s+(\/\S*|\{\{[^}]+\}\}\S*)$/;
|
|
24
28
|
export class YamlScenarioError extends Error {
|
|
25
29
|
}
|
|
26
30
|
export function parseScenarioFile(text, file) {
|
|
@@ -108,6 +112,14 @@ function parseStep(node, fail, lineOf) {
|
|
|
108
112
|
}
|
|
109
113
|
}
|
|
110
114
|
}
|
|
115
|
+
else if (kind === "mail") {
|
|
116
|
+
const filter = raw.mail;
|
|
117
|
+
if (!filter || typeof filter !== "object" || Array.isArray(filter))
|
|
118
|
+
fail(at(kind), "`mail:` must be a mapping such as { to: alice@example.com, subject: Welcome } ({} for any)");
|
|
119
|
+
for (const k of Object.keys(filter))
|
|
120
|
+
if (!MAIL_KEYS.includes(k))
|
|
121
|
+
fail(at(kind), `unknown key "${k}" in mail (allowed: ${MAIL_KEYS.join(", ")})`);
|
|
122
|
+
}
|
|
111
123
|
else if (kind === "checkpoint") {
|
|
112
124
|
if (raw.checkpoint !== true)
|
|
113
125
|
fail(at(kind), "use `checkpoint: true`");
|
|
@@ -131,8 +143,8 @@ function parseStep(node, fail, lineOf) {
|
|
|
131
143
|
fail(at(key), `unknown key "${k}" in ${key} (allowed: ${allowedKeys.join(", ")})`);
|
|
132
144
|
}
|
|
133
145
|
};
|
|
134
|
-
const call = (key) => {
|
|
135
|
-
if (raw[key] !== undefined && !
|
|
146
|
+
const call = (key, pattern = CALL) => {
|
|
147
|
+
if (raw[key] !== undefined && !pattern.test(String(raw[key])))
|
|
136
148
|
fail(at(key), `\`${key}\` must look like "POST /path", got "${raw[key]}"`);
|
|
137
149
|
};
|
|
138
150
|
if (raw.within !== undefined && (typeof raw.within !== "number" || raw.within <= 0))
|
|
@@ -166,9 +178,23 @@ function parseStep(node, fail, lineOf) {
|
|
|
166
178
|
break;
|
|
167
179
|
}
|
|
168
180
|
case "request":
|
|
169
|
-
call("request");
|
|
181
|
+
call("request", REQUEST);
|
|
170
182
|
if (["json", "form", "body"].filter((k) => raw[k] !== undefined).length > 1)
|
|
171
183
|
fail(node, "use only one of json / form / body");
|
|
184
|
+
if (raw.concurrency !== undefined && !(Number.isInteger(raw.concurrency) && raw.concurrency >= 1))
|
|
185
|
+
fail(at("concurrency"), "`concurrency` must be a positive whole number");
|
|
186
|
+
if (raw.concurrency !== undefined && raw.capture !== undefined)
|
|
187
|
+
fail(at("capture"), "`capture` can't be used with `concurrency`: there is more than one response");
|
|
188
|
+
{
|
|
189
|
+
const statuses = raw.expect?.statuses;
|
|
190
|
+
if (statuses !== undefined) {
|
|
191
|
+
if (raw.concurrency === undefined)
|
|
192
|
+
fail(at("expect"), "`expect.statuses` needs `concurrency`");
|
|
193
|
+
if (!statuses || typeof statuses !== "object" || Object.entries(statuses).some(([k, v]) => !/^\d{3}$/.test(k) || !Number.isInteger(v))) {
|
|
194
|
+
fail(at("expect"), "`expect.statuses` maps status codes to counts, e.g. { 201: 1, 409: 9 }");
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
172
198
|
break;
|
|
173
199
|
case "insert":
|
|
174
200
|
if (!raw.rows || typeof raw.rows !== "object")
|
|
@@ -188,6 +214,9 @@ function parseStep(node, fail, lineOf) {
|
|
|
188
214
|
call("call");
|
|
189
215
|
number("times");
|
|
190
216
|
break;
|
|
217
|
+
case "mail":
|
|
218
|
+
number("times");
|
|
219
|
+
break;
|
|
191
220
|
}
|
|
192
221
|
return { ...raw, line: lineOf(node) };
|
|
193
222
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "slicetest",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Framework-agnostic slice tests: drive a real app process over HTTP with a real Postgres and stubbed outbound APIs, checked against OpenAPI contracts.",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Framework-agnostic slice tests: drive a real app process over HTTP with a real Postgres, MySQL or SQLite and stubbed outbound APIs, checked against OpenAPI contracts.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"testing",
|
|
7
7
|
"integration-testing",
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"api-testing",
|
|
10
10
|
"postgres",
|
|
11
11
|
"mysql",
|
|
12
|
+
"sqlite",
|
|
12
13
|
"stub",
|
|
13
14
|
"mock-server",
|
|
14
15
|
"vitest",
|
|
@@ -55,7 +56,7 @@
|
|
|
55
56
|
"build": "tsc -p tsconfig.build.json",
|
|
56
57
|
"typecheck": "tsc --noEmit",
|
|
57
58
|
"test": "vitest run",
|
|
58
|
-
"test:dist": "npm run build && tsc -p examples/tsconfig.dist.json && vitest run --config examples/vitest.dist.config.ts && node dist/cli.js --config examples/slicetest.config.yaml",
|
|
59
|
+
"test:dist": "npm run build && tsc -p examples/tsconfig.dist.json && vitest run --config examples/vitest.dist.config.ts && node dist/cli.js --config examples/slicetest.config.yaml && node test/record-dist.mjs",
|
|
59
60
|
"prepublishOnly": "npm run typecheck && npm run build"
|
|
60
61
|
},
|
|
61
62
|
"peerDependencies": {
|
|
@@ -180,6 +180,11 @@
|
|
|
180
180
|
"follow": {
|
|
181
181
|
"type": "boolean"
|
|
182
182
|
},
|
|
183
|
+
"concurrency": {
|
|
184
|
+
"type": "integer",
|
|
185
|
+
"minimum": 1,
|
|
186
|
+
"description": "Send the request this many times at once, to provoke races. `expect` applies to every response"
|
|
187
|
+
},
|
|
183
188
|
"expect": {
|
|
184
189
|
"type": "object",
|
|
185
190
|
"additionalProperties": false,
|
|
@@ -196,7 +201,18 @@
|
|
|
196
201
|
"json": {
|
|
197
202
|
"description": "Subset of the response JSON"
|
|
198
203
|
},
|
|
199
|
-
"text": {}
|
|
204
|
+
"text": {},
|
|
205
|
+
"statuses": {
|
|
206
|
+
"type": "object",
|
|
207
|
+
"propertyNames": {
|
|
208
|
+
"pattern": "^\\d{3}$"
|
|
209
|
+
},
|
|
210
|
+
"additionalProperties": {
|
|
211
|
+
"type": "integer",
|
|
212
|
+
"minimum": 0
|
|
213
|
+
},
|
|
214
|
+
"description": "With `concurrency`: how many responses have each status, e.g. { 201: 1, 409: 9 }"
|
|
215
|
+
}
|
|
200
216
|
}
|
|
201
217
|
},
|
|
202
218
|
"capture": {
|
|
@@ -525,6 +541,138 @@
|
|
|
525
541
|
"description": "Label shown when this step fails"
|
|
526
542
|
}
|
|
527
543
|
}
|
|
544
|
+
},
|
|
545
|
+
{
|
|
546
|
+
"type": "object",
|
|
547
|
+
"required": [
|
|
548
|
+
"mail"
|
|
549
|
+
],
|
|
550
|
+
"additionalProperties": false,
|
|
551
|
+
"properties": {
|
|
552
|
+
"mail": {
|
|
553
|
+
"type": "object",
|
|
554
|
+
"additionalProperties": false,
|
|
555
|
+
"description": "The app sent mail matching these fields (needs `mail: true` in the config). to/from are exact addresses; subject/text/html are substrings. {} matches any message",
|
|
556
|
+
"properties": {
|
|
557
|
+
"to": {
|
|
558
|
+
"oneOf": [
|
|
559
|
+
{
|
|
560
|
+
"type": "string"
|
|
561
|
+
},
|
|
562
|
+
{
|
|
563
|
+
"type": "object",
|
|
564
|
+
"required": [
|
|
565
|
+
"$regex"
|
|
566
|
+
],
|
|
567
|
+
"additionalProperties": false,
|
|
568
|
+
"properties": {
|
|
569
|
+
"$regex": {
|
|
570
|
+
"type": "string"
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
]
|
|
575
|
+
},
|
|
576
|
+
"from": {
|
|
577
|
+
"oneOf": [
|
|
578
|
+
{
|
|
579
|
+
"type": "string"
|
|
580
|
+
},
|
|
581
|
+
{
|
|
582
|
+
"type": "object",
|
|
583
|
+
"required": [
|
|
584
|
+
"$regex"
|
|
585
|
+
],
|
|
586
|
+
"additionalProperties": false,
|
|
587
|
+
"properties": {
|
|
588
|
+
"$regex": {
|
|
589
|
+
"type": "string"
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
]
|
|
594
|
+
},
|
|
595
|
+
"subject": {
|
|
596
|
+
"oneOf": [
|
|
597
|
+
{
|
|
598
|
+
"type": "string"
|
|
599
|
+
},
|
|
600
|
+
{
|
|
601
|
+
"type": "object",
|
|
602
|
+
"required": [
|
|
603
|
+
"$regex"
|
|
604
|
+
],
|
|
605
|
+
"additionalProperties": false,
|
|
606
|
+
"properties": {
|
|
607
|
+
"$regex": {
|
|
608
|
+
"type": "string"
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
]
|
|
613
|
+
},
|
|
614
|
+
"text": {
|
|
615
|
+
"oneOf": [
|
|
616
|
+
{
|
|
617
|
+
"type": "string"
|
|
618
|
+
},
|
|
619
|
+
{
|
|
620
|
+
"type": "object",
|
|
621
|
+
"required": [
|
|
622
|
+
"$regex"
|
|
623
|
+
],
|
|
624
|
+
"additionalProperties": false,
|
|
625
|
+
"properties": {
|
|
626
|
+
"$regex": {
|
|
627
|
+
"type": "string"
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
]
|
|
632
|
+
},
|
|
633
|
+
"html": {
|
|
634
|
+
"oneOf": [
|
|
635
|
+
{
|
|
636
|
+
"type": "string"
|
|
637
|
+
},
|
|
638
|
+
{
|
|
639
|
+
"type": "object",
|
|
640
|
+
"required": [
|
|
641
|
+
"$regex"
|
|
642
|
+
],
|
|
643
|
+
"additionalProperties": false,
|
|
644
|
+
"properties": {
|
|
645
|
+
"$regex": {
|
|
646
|
+
"type": "string"
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
]
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
},
|
|
654
|
+
"times": {
|
|
655
|
+
"type": "integer",
|
|
656
|
+
"minimum": 0,
|
|
657
|
+
"description": "Exact number of matching messages. Default: at least one"
|
|
658
|
+
},
|
|
659
|
+
"within": {
|
|
660
|
+
"type": "integer",
|
|
661
|
+
"minimum": 1,
|
|
662
|
+
"description": "Milliseconds to wait. Default 5000"
|
|
663
|
+
},
|
|
664
|
+
"capture": {
|
|
665
|
+
"type": "object",
|
|
666
|
+
"additionalProperties": {
|
|
667
|
+
"type": "string"
|
|
668
|
+
},
|
|
669
|
+
"description": "Values of the last matching message: subject, text, html, from, to.0, links.0"
|
|
670
|
+
},
|
|
671
|
+
"name": {
|
|
672
|
+
"type": "string",
|
|
673
|
+
"description": "Label shown when this step fails"
|
|
674
|
+
}
|
|
675
|
+
}
|
|
528
676
|
}
|
|
529
677
|
]
|
|
530
678
|
},
|