@curl-runner/cli 1.10.0 → 1.12.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/package.json +1 -1
- package/src/cli.ts +92 -0
- package/src/executor/request-executor.ts +51 -1
- package/src/snapshot/index.ts +3 -0
- package/src/snapshot/snapshot-differ.test.ts +358 -0
- package/src/snapshot/snapshot-differ.ts +296 -0
- package/src/snapshot/snapshot-formatter.ts +170 -0
- package/src/snapshot/snapshot-manager.test.ts +204 -0
- package/src/snapshot/snapshot-manager.ts +342 -0
- package/src/types/config.ts +190 -0
- package/src/utils/condition-evaluator.test.ts +415 -0
- package/src/utils/condition-evaluator.ts +327 -0
- package/src/utils/logger.ts +67 -4
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -166,6 +166,38 @@ class CurlRunnerCLI {
|
|
|
166
166
|
};
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
+
// Snapshot configuration
|
|
170
|
+
if (process.env.CURL_RUNNER_SNAPSHOT) {
|
|
171
|
+
envConfig.snapshot = {
|
|
172
|
+
...envConfig.snapshot,
|
|
173
|
+
enabled: process.env.CURL_RUNNER_SNAPSHOT.toLowerCase() === 'true',
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (process.env.CURL_RUNNER_SNAPSHOT_UPDATE) {
|
|
178
|
+
const mode = process.env.CURL_RUNNER_SNAPSHOT_UPDATE.toLowerCase();
|
|
179
|
+
if (['none', 'all', 'failing'].includes(mode)) {
|
|
180
|
+
envConfig.snapshot = {
|
|
181
|
+
...envConfig.snapshot,
|
|
182
|
+
updateMode: mode as 'none' | 'all' | 'failing',
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (process.env.CURL_RUNNER_SNAPSHOT_DIR) {
|
|
188
|
+
envConfig.snapshot = {
|
|
189
|
+
...envConfig.snapshot,
|
|
190
|
+
dir: process.env.CURL_RUNNER_SNAPSHOT_DIR,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (process.env.CURL_RUNNER_SNAPSHOT_CI) {
|
|
195
|
+
envConfig.snapshot = {
|
|
196
|
+
...envConfig.snapshot,
|
|
197
|
+
ci: process.env.CURL_RUNNER_SNAPSHOT_CI.toLowerCase() === 'true',
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
169
201
|
return envConfig;
|
|
170
202
|
}
|
|
171
203
|
|
|
@@ -311,6 +343,33 @@ class CurlRunnerCLI {
|
|
|
311
343
|
};
|
|
312
344
|
}
|
|
313
345
|
|
|
346
|
+
// Apply snapshot options
|
|
347
|
+
if (options.snapshot !== undefined) {
|
|
348
|
+
globalConfig.snapshot = {
|
|
349
|
+
...globalConfig.snapshot,
|
|
350
|
+
enabled: options.snapshot as boolean,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
if (options.snapshotUpdate !== undefined) {
|
|
354
|
+
globalConfig.snapshot = {
|
|
355
|
+
...globalConfig.snapshot,
|
|
356
|
+
enabled: true,
|
|
357
|
+
updateMode: options.snapshotUpdate as 'none' | 'all' | 'failing',
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
if (options.snapshotDir !== undefined) {
|
|
361
|
+
globalConfig.snapshot = {
|
|
362
|
+
...globalConfig.snapshot,
|
|
363
|
+
dir: options.snapshotDir as string,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
if (options.snapshotCi !== undefined) {
|
|
367
|
+
globalConfig.snapshot = {
|
|
368
|
+
...globalConfig.snapshot,
|
|
369
|
+
ci: options.snapshotCi as boolean,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
314
373
|
if (allRequests.length === 0) {
|
|
315
374
|
this.logger.logError('No requests found in YAML files');
|
|
316
375
|
process.exit(1);
|
|
@@ -450,6 +509,14 @@ class CurlRunnerCLI {
|
|
|
450
509
|
options.watchClear = true;
|
|
451
510
|
} else if (key === 'no-watch-clear') {
|
|
452
511
|
options.watchClear = false;
|
|
512
|
+
} else if (key === 'snapshot') {
|
|
513
|
+
options.snapshot = true;
|
|
514
|
+
} else if (key === 'update-snapshots') {
|
|
515
|
+
options.snapshotUpdate = 'all';
|
|
516
|
+
} else if (key === 'update-failing') {
|
|
517
|
+
options.snapshotUpdate = 'failing';
|
|
518
|
+
} else if (key === 'ci-snapshot') {
|
|
519
|
+
options.snapshotCi = true;
|
|
453
520
|
} else if (nextArg && !nextArg.startsWith('--')) {
|
|
454
521
|
if (key === 'continue-on-error') {
|
|
455
522
|
options.continueOnError = nextArg === 'true';
|
|
@@ -483,6 +550,8 @@ class CurlRunnerCLI {
|
|
|
483
550
|
}
|
|
484
551
|
} else if (key === 'watch-debounce') {
|
|
485
552
|
options.watchDebounce = Number.parseInt(nextArg, 10);
|
|
553
|
+
} else if (key === 'snapshot-dir') {
|
|
554
|
+
options.snapshotDir = nextArg;
|
|
486
555
|
} else {
|
|
487
556
|
options[key] = nextArg;
|
|
488
557
|
}
|
|
@@ -512,6 +581,12 @@ class CurlRunnerCLI {
|
|
|
512
581
|
case 'w':
|
|
513
582
|
options.watch = true;
|
|
514
583
|
break;
|
|
584
|
+
case 's':
|
|
585
|
+
options.snapshot = true;
|
|
586
|
+
break;
|
|
587
|
+
case 'u':
|
|
588
|
+
options.snapshotUpdate = 'all';
|
|
589
|
+
break;
|
|
515
590
|
case 'o': {
|
|
516
591
|
// Handle -o flag for output file
|
|
517
592
|
const outputArg = args[i + 1];
|
|
@@ -641,6 +716,7 @@ class CurlRunnerCLI {
|
|
|
641
716
|
defaults: { ...base.defaults, ...override.defaults },
|
|
642
717
|
ci: { ...base.ci, ...override.ci },
|
|
643
718
|
watch: { ...base.watch, ...override.watch },
|
|
719
|
+
snapshot: { ...base.snapshot, ...override.snapshot },
|
|
644
720
|
};
|
|
645
721
|
}
|
|
646
722
|
|
|
@@ -733,6 +809,13 @@ ${this.logger.color('CI/CD OPTIONS:', 'yellow')}
|
|
|
733
809
|
--fail-on <count> Exit with code 1 if failures exceed this count
|
|
734
810
|
--fail-on-percentage <pct> Exit with code 1 if failure percentage exceeds this value
|
|
735
811
|
|
|
812
|
+
${this.logger.color('SNAPSHOT OPTIONS:', 'yellow')}
|
|
813
|
+
-s, --snapshot Enable snapshot testing
|
|
814
|
+
-u, --update-snapshots Update all snapshots
|
|
815
|
+
--update-failing Update only failing snapshots
|
|
816
|
+
--snapshot-dir <dir> Custom snapshot directory (default: __snapshots__)
|
|
817
|
+
--ci-snapshot Fail if snapshot is missing (CI mode)
|
|
818
|
+
|
|
736
819
|
${this.logger.color('EXAMPLES:', 'yellow')}
|
|
737
820
|
# Run all YAML files in current directory
|
|
738
821
|
curl-runner
|
|
@@ -782,6 +865,15 @@ ${this.logger.color('EXAMPLES:', 'yellow')}
|
|
|
782
865
|
# Watch with custom debounce
|
|
783
866
|
curl-runner tests/ -w --watch-debounce 500
|
|
784
867
|
|
|
868
|
+
# Snapshot testing - save and compare responses
|
|
869
|
+
curl-runner api.yaml --snapshot
|
|
870
|
+
|
|
871
|
+
# Update all snapshots
|
|
872
|
+
curl-runner api.yaml -su
|
|
873
|
+
|
|
874
|
+
# CI mode - fail if snapshot missing
|
|
875
|
+
curl-runner api.yaml --snapshot --ci-snapshot
|
|
876
|
+
|
|
785
877
|
${this.logger.color('YAML STRUCTURE:', 'yellow')}
|
|
786
878
|
Single request:
|
|
787
879
|
request:
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { YamlParser } from '../parser/yaml';
|
|
2
|
+
import { SnapshotManager } from '../snapshot/snapshot-manager';
|
|
2
3
|
import type {
|
|
3
4
|
ExecutionResult,
|
|
4
5
|
ExecutionSummary,
|
|
@@ -8,7 +9,9 @@ import type {
|
|
|
8
9
|
JsonValue,
|
|
9
10
|
RequestConfig,
|
|
10
11
|
ResponseStoreContext,
|
|
12
|
+
SnapshotConfig,
|
|
11
13
|
} from '../types/config';
|
|
14
|
+
import { evaluateCondition } from '../utils/condition-evaluator';
|
|
12
15
|
import { CurlBuilder } from '../utils/curl-builder';
|
|
13
16
|
import { Logger } from '../utils/logger';
|
|
14
17
|
import { createStoreContext, extractStoreValues } from '../utils/response-store';
|
|
@@ -16,10 +19,12 @@ import { createStoreContext, extractStoreValues } from '../utils/response-store'
|
|
|
16
19
|
export class RequestExecutor {
|
|
17
20
|
private logger: Logger;
|
|
18
21
|
private globalConfig: GlobalConfig;
|
|
22
|
+
private snapshotManager: SnapshotManager;
|
|
19
23
|
|
|
20
24
|
constructor(globalConfig: GlobalConfig = {}) {
|
|
21
25
|
this.globalConfig = globalConfig;
|
|
22
26
|
this.logger = new Logger(globalConfig.output);
|
|
27
|
+
this.snapshotManager = new SnapshotManager(globalConfig.snapshot);
|
|
23
28
|
}
|
|
24
29
|
|
|
25
30
|
private mergeOutputConfig(config: RequestConfig): GlobalConfig['output'] {
|
|
@@ -30,6 +35,13 @@ export class RequestExecutor {
|
|
|
30
35
|
};
|
|
31
36
|
}
|
|
32
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Gets the effective snapshot config for a request.
|
|
40
|
+
*/
|
|
41
|
+
private getSnapshotConfig(config: RequestConfig): SnapshotConfig | null {
|
|
42
|
+
return SnapshotManager.mergeConfig(this.globalConfig.snapshot, config.snapshot);
|
|
43
|
+
}
|
|
44
|
+
|
|
33
45
|
/**
|
|
34
46
|
* Checks if a form field value is a file attachment.
|
|
35
47
|
*/
|
|
@@ -140,6 +152,25 @@ export class RequestExecutor {
|
|
|
140
152
|
}
|
|
141
153
|
}
|
|
142
154
|
|
|
155
|
+
// Snapshot testing
|
|
156
|
+
const snapshotConfig = this.getSnapshotConfig(config);
|
|
157
|
+
if (snapshotConfig && config.sourceFile) {
|
|
158
|
+
const snapshotResult = await this.snapshotManager.compareAndUpdate(
|
|
159
|
+
config.sourceFile,
|
|
160
|
+
config.name || 'Request',
|
|
161
|
+
executionResult,
|
|
162
|
+
snapshotConfig,
|
|
163
|
+
);
|
|
164
|
+
executionResult.snapshotResult = snapshotResult;
|
|
165
|
+
|
|
166
|
+
if (!snapshotResult.match && !snapshotResult.updated) {
|
|
167
|
+
executionResult.success = false;
|
|
168
|
+
if (!executionResult.error) {
|
|
169
|
+
executionResult.error = 'Snapshot mismatch';
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
143
174
|
requestLogger.logRequestComplete(executionResult);
|
|
144
175
|
return executionResult;
|
|
145
176
|
}
|
|
@@ -486,6 +517,23 @@ export class RequestExecutor {
|
|
|
486
517
|
for (let i = 0; i < requests.length; i++) {
|
|
487
518
|
// Interpolate store variables before execution
|
|
488
519
|
const interpolatedRequest = this.interpolateStoreVariables(requests[i], storeContext);
|
|
520
|
+
|
|
521
|
+
// Evaluate `when` condition if present
|
|
522
|
+
if (interpolatedRequest.when) {
|
|
523
|
+
const conditionResult = evaluateCondition(interpolatedRequest.when, storeContext);
|
|
524
|
+
if (!conditionResult.shouldRun) {
|
|
525
|
+
const skippedResult: ExecutionResult = {
|
|
526
|
+
request: interpolatedRequest,
|
|
527
|
+
success: true, // Skipped requests are not failures
|
|
528
|
+
skipped: true,
|
|
529
|
+
skipReason: conditionResult.reason,
|
|
530
|
+
};
|
|
531
|
+
results.push(skippedResult);
|
|
532
|
+
this.logger.logSkipped(interpolatedRequest, i + 1, conditionResult.reason);
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
489
537
|
const result = await this.executeRequest(interpolatedRequest, i + 1);
|
|
490
538
|
results.push(result);
|
|
491
539
|
|
|
@@ -588,13 +636,15 @@ export class RequestExecutor {
|
|
|
588
636
|
}
|
|
589
637
|
|
|
590
638
|
private createSummary(results: ExecutionResult[], duration: number): ExecutionSummary {
|
|
591
|
-
const
|
|
639
|
+
const skipped = results.filter((r) => r.skipped).length;
|
|
640
|
+
const successful = results.filter((r) => r.success && !r.skipped).length;
|
|
592
641
|
const failed = results.filter((r) => !r.success).length;
|
|
593
642
|
|
|
594
643
|
return {
|
|
595
644
|
total: results.length,
|
|
596
645
|
successful,
|
|
597
646
|
failed,
|
|
647
|
+
skipped,
|
|
598
648
|
duration,
|
|
599
649
|
results,
|
|
600
650
|
};
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import type { Snapshot } from '../types/config';
|
|
3
|
+
import { SnapshotDiffer } from './snapshot-differ';
|
|
4
|
+
|
|
5
|
+
describe('SnapshotDiffer', () => {
|
|
6
|
+
describe('basic comparison', () => {
|
|
7
|
+
test('should match identical snapshots', () => {
|
|
8
|
+
const differ = new SnapshotDiffer({});
|
|
9
|
+
const snapshot: Snapshot = {
|
|
10
|
+
status: 200,
|
|
11
|
+
body: { id: 1, name: 'test' },
|
|
12
|
+
hash: 'abc123',
|
|
13
|
+
updatedAt: '2024-01-01',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const result = differ.compare(snapshot, snapshot);
|
|
17
|
+
expect(result.match).toBe(true);
|
|
18
|
+
expect(result.differences).toHaveLength(0);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('should detect status changes', () => {
|
|
22
|
+
const differ = new SnapshotDiffer({});
|
|
23
|
+
const expected: Snapshot = { status: 200, hash: 'a', updatedAt: '' };
|
|
24
|
+
const received: Snapshot = { status: 201, hash: 'b', updatedAt: '' };
|
|
25
|
+
|
|
26
|
+
const result = differ.compare(expected, received);
|
|
27
|
+
expect(result.match).toBe(false);
|
|
28
|
+
expect(result.differences).toHaveLength(1);
|
|
29
|
+
expect(result.differences[0]).toEqual({
|
|
30
|
+
path: 'status',
|
|
31
|
+
expected: 200,
|
|
32
|
+
received: 201,
|
|
33
|
+
type: 'changed',
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('should detect body value changes', () => {
|
|
38
|
+
const differ = new SnapshotDiffer({});
|
|
39
|
+
const expected: Snapshot = {
|
|
40
|
+
body: { name: 'old' },
|
|
41
|
+
hash: 'a',
|
|
42
|
+
updatedAt: '',
|
|
43
|
+
};
|
|
44
|
+
const received: Snapshot = {
|
|
45
|
+
body: { name: 'new' },
|
|
46
|
+
hash: 'b',
|
|
47
|
+
updatedAt: '',
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const result = differ.compare(expected, received);
|
|
51
|
+
expect(result.match).toBe(false);
|
|
52
|
+
expect(result.differences[0].path).toBe('body.name');
|
|
53
|
+
expect(result.differences[0].type).toBe('changed');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('should detect added fields', () => {
|
|
57
|
+
const differ = new SnapshotDiffer({});
|
|
58
|
+
const expected: Snapshot = {
|
|
59
|
+
body: { id: 1 },
|
|
60
|
+
hash: 'a',
|
|
61
|
+
updatedAt: '',
|
|
62
|
+
};
|
|
63
|
+
const received: Snapshot = {
|
|
64
|
+
body: { id: 1, newField: 'value' },
|
|
65
|
+
hash: 'b',
|
|
66
|
+
updatedAt: '',
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const result = differ.compare(expected, received);
|
|
70
|
+
expect(result.match).toBe(false);
|
|
71
|
+
expect(result.differences[0].path).toBe('body.newField');
|
|
72
|
+
expect(result.differences[0].type).toBe('added');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('should detect removed fields', () => {
|
|
76
|
+
const differ = new SnapshotDiffer({});
|
|
77
|
+
const expected: Snapshot = {
|
|
78
|
+
body: { id: 1, oldField: 'value' },
|
|
79
|
+
hash: 'a',
|
|
80
|
+
updatedAt: '',
|
|
81
|
+
};
|
|
82
|
+
const received: Snapshot = {
|
|
83
|
+
body: { id: 1 },
|
|
84
|
+
hash: 'b',
|
|
85
|
+
updatedAt: '',
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const result = differ.compare(expected, received);
|
|
89
|
+
expect(result.match).toBe(false);
|
|
90
|
+
expect(result.differences[0].path).toBe('body.oldField');
|
|
91
|
+
expect(result.differences[0].type).toBe('removed');
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('array comparison', () => {
|
|
96
|
+
test('should compare array elements', () => {
|
|
97
|
+
const differ = new SnapshotDiffer({});
|
|
98
|
+
const expected: Snapshot = {
|
|
99
|
+
body: { items: [1, 2, 3] },
|
|
100
|
+
hash: 'a',
|
|
101
|
+
updatedAt: '',
|
|
102
|
+
};
|
|
103
|
+
const received: Snapshot = {
|
|
104
|
+
body: { items: [1, 2, 4] },
|
|
105
|
+
hash: 'b',
|
|
106
|
+
updatedAt: '',
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const result = differ.compare(expected, received);
|
|
110
|
+
expect(result.match).toBe(false);
|
|
111
|
+
expect(result.differences[0].path).toBe('body.items[2]');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('should detect added array elements', () => {
|
|
115
|
+
const differ = new SnapshotDiffer({});
|
|
116
|
+
const expected: Snapshot = {
|
|
117
|
+
body: { items: [1, 2] },
|
|
118
|
+
hash: 'a',
|
|
119
|
+
updatedAt: '',
|
|
120
|
+
};
|
|
121
|
+
const received: Snapshot = {
|
|
122
|
+
body: { items: [1, 2, 3] },
|
|
123
|
+
hash: 'b',
|
|
124
|
+
updatedAt: '',
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const result = differ.compare(expected, received);
|
|
128
|
+
expect(result.match).toBe(false);
|
|
129
|
+
expect(result.differences[0].path).toBe('body.items[2]');
|
|
130
|
+
expect(result.differences[0].type).toBe('added');
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe('exclusions', () => {
|
|
135
|
+
test('should exclude exact paths', () => {
|
|
136
|
+
const differ = new SnapshotDiffer({
|
|
137
|
+
exclude: ['body.timestamp'],
|
|
138
|
+
});
|
|
139
|
+
const expected: Snapshot = {
|
|
140
|
+
body: { id: 1, timestamp: '2024-01-01' },
|
|
141
|
+
hash: 'a',
|
|
142
|
+
updatedAt: '',
|
|
143
|
+
};
|
|
144
|
+
const received: Snapshot = {
|
|
145
|
+
body: { id: 1, timestamp: '2024-12-31' },
|
|
146
|
+
hash: 'b',
|
|
147
|
+
updatedAt: '',
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const result = differ.compare(expected, received);
|
|
151
|
+
expect(result.match).toBe(true);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test('should exclude wildcard paths (*.field)', () => {
|
|
155
|
+
const differ = new SnapshotDiffer({
|
|
156
|
+
exclude: ['*.createdAt'],
|
|
157
|
+
});
|
|
158
|
+
const expected: Snapshot = {
|
|
159
|
+
body: { user: { createdAt: '2024-01-01' }, post: { createdAt: '2024-01-01' } },
|
|
160
|
+
hash: 'a',
|
|
161
|
+
updatedAt: '',
|
|
162
|
+
};
|
|
163
|
+
const received: Snapshot = {
|
|
164
|
+
body: { user: { createdAt: '2024-12-31' }, post: { createdAt: '2024-12-31' } },
|
|
165
|
+
hash: 'b',
|
|
166
|
+
updatedAt: '',
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const result = differ.compare(expected, received);
|
|
170
|
+
expect(result.match).toBe(true);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test('should exclude array wildcard paths (body[*].id)', () => {
|
|
174
|
+
const differ = new SnapshotDiffer({
|
|
175
|
+
exclude: ['body.items[*].id'],
|
|
176
|
+
});
|
|
177
|
+
const expected: Snapshot = {
|
|
178
|
+
body: {
|
|
179
|
+
items: [
|
|
180
|
+
{ id: 1, name: 'a' },
|
|
181
|
+
{ id: 2, name: 'b' },
|
|
182
|
+
],
|
|
183
|
+
},
|
|
184
|
+
hash: 'a',
|
|
185
|
+
updatedAt: '',
|
|
186
|
+
};
|
|
187
|
+
const received: Snapshot = {
|
|
188
|
+
body: {
|
|
189
|
+
items: [
|
|
190
|
+
{ id: 99, name: 'a' },
|
|
191
|
+
{ id: 100, name: 'b' },
|
|
192
|
+
],
|
|
193
|
+
},
|
|
194
|
+
hash: 'b',
|
|
195
|
+
updatedAt: '',
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const result = differ.compare(expected, received);
|
|
199
|
+
expect(result.match).toBe(true);
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
describe('match rules', () => {
|
|
204
|
+
test('should accept any value with wildcard (*)', () => {
|
|
205
|
+
const differ = new SnapshotDiffer({
|
|
206
|
+
match: { 'body.id': '*' },
|
|
207
|
+
});
|
|
208
|
+
const expected: Snapshot = {
|
|
209
|
+
body: { id: 1 },
|
|
210
|
+
hash: 'a',
|
|
211
|
+
updatedAt: '',
|
|
212
|
+
};
|
|
213
|
+
const received: Snapshot = {
|
|
214
|
+
body: { id: 999 },
|
|
215
|
+
hash: 'b',
|
|
216
|
+
updatedAt: '',
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const result = differ.compare(expected, received);
|
|
220
|
+
expect(result.match).toBe(true);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test('should match regex patterns', () => {
|
|
224
|
+
const differ = new SnapshotDiffer({
|
|
225
|
+
match: { 'body.version': 'regex:^v\\d+\\.\\d+' },
|
|
226
|
+
});
|
|
227
|
+
const expected: Snapshot = {
|
|
228
|
+
body: { version: 'v1.0.0' },
|
|
229
|
+
hash: 'a',
|
|
230
|
+
updatedAt: '',
|
|
231
|
+
};
|
|
232
|
+
const received: Snapshot = {
|
|
233
|
+
body: { version: 'v2.5.3' },
|
|
234
|
+
hash: 'b',
|
|
235
|
+
updatedAt: '',
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const result = differ.compare(expected, received);
|
|
239
|
+
expect(result.match).toBe(true);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test('should fail on non-matching regex', () => {
|
|
243
|
+
const differ = new SnapshotDiffer({
|
|
244
|
+
match: { 'body.version': 'regex:^v\\d+\\.\\d+' },
|
|
245
|
+
});
|
|
246
|
+
const expected: Snapshot = {
|
|
247
|
+
body: { version: 'v1.0' },
|
|
248
|
+
hash: 'a',
|
|
249
|
+
updatedAt: '',
|
|
250
|
+
};
|
|
251
|
+
const received: Snapshot = {
|
|
252
|
+
body: { version: 'invalid' },
|
|
253
|
+
hash: 'b',
|
|
254
|
+
updatedAt: '',
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const result = differ.compare(expected, received);
|
|
258
|
+
expect(result.match).toBe(false);
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
describe('type mismatches', () => {
|
|
263
|
+
test('should detect type changes', () => {
|
|
264
|
+
const differ = new SnapshotDiffer({});
|
|
265
|
+
const expected: Snapshot = {
|
|
266
|
+
body: { count: 42 },
|
|
267
|
+
hash: 'a',
|
|
268
|
+
updatedAt: '',
|
|
269
|
+
};
|
|
270
|
+
const received: Snapshot = {
|
|
271
|
+
body: { count: '42' },
|
|
272
|
+
hash: 'b',
|
|
273
|
+
updatedAt: '',
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const result = differ.compare(expected, received);
|
|
277
|
+
expect(result.match).toBe(false);
|
|
278
|
+
expect(result.differences[0].type).toBe('type_mismatch');
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test('should detect object to array change', () => {
|
|
282
|
+
const differ = new SnapshotDiffer({});
|
|
283
|
+
const expected: Snapshot = {
|
|
284
|
+
body: { data: { key: 'value' } },
|
|
285
|
+
hash: 'a',
|
|
286
|
+
updatedAt: '',
|
|
287
|
+
};
|
|
288
|
+
const received: Snapshot = {
|
|
289
|
+
body: { data: ['value'] },
|
|
290
|
+
hash: 'b',
|
|
291
|
+
updatedAt: '',
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
const result = differ.compare(expected, received);
|
|
295
|
+
expect(result.match).toBe(false);
|
|
296
|
+
expect(result.differences[0].type).toBe('type_mismatch');
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
describe('nested objects', () => {
|
|
301
|
+
test('should compare deeply nested values', () => {
|
|
302
|
+
const differ = new SnapshotDiffer({});
|
|
303
|
+
const expected: Snapshot = {
|
|
304
|
+
body: { level1: { level2: { level3: { value: 'old' } } } },
|
|
305
|
+
hash: 'a',
|
|
306
|
+
updatedAt: '',
|
|
307
|
+
};
|
|
308
|
+
const received: Snapshot = {
|
|
309
|
+
body: { level1: { level2: { level3: { value: 'new' } } } },
|
|
310
|
+
hash: 'b',
|
|
311
|
+
updatedAt: '',
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
const result = differ.compare(expected, received);
|
|
315
|
+
expect(result.match).toBe(false);
|
|
316
|
+
expect(result.differences[0].path).toBe('body.level1.level2.level3.value');
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
describe('header comparison', () => {
|
|
321
|
+
test('should compare headers', () => {
|
|
322
|
+
const differ = new SnapshotDiffer({});
|
|
323
|
+
const expected: Snapshot = {
|
|
324
|
+
headers: { 'content-type': 'application/json' },
|
|
325
|
+
hash: 'a',
|
|
326
|
+
updatedAt: '',
|
|
327
|
+
};
|
|
328
|
+
const received: Snapshot = {
|
|
329
|
+
headers: { 'content-type': 'text/html' },
|
|
330
|
+
hash: 'b',
|
|
331
|
+
updatedAt: '',
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
const result = differ.compare(expected, received);
|
|
335
|
+
expect(result.match).toBe(false);
|
|
336
|
+
expect(result.differences[0].path).toBe('headers.content-type');
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test('should exclude headers', () => {
|
|
340
|
+
const differ = new SnapshotDiffer({
|
|
341
|
+
exclude: ['headers.date', 'headers.x-request-id'],
|
|
342
|
+
});
|
|
343
|
+
const expected: Snapshot = {
|
|
344
|
+
headers: { 'content-type': 'application/json', date: '2024-01-01' },
|
|
345
|
+
hash: 'a',
|
|
346
|
+
updatedAt: '',
|
|
347
|
+
};
|
|
348
|
+
const received: Snapshot = {
|
|
349
|
+
headers: { 'content-type': 'application/json', date: '2024-12-31' },
|
|
350
|
+
hash: 'b',
|
|
351
|
+
updatedAt: '',
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
const result = differ.compare(expected, received);
|
|
355
|
+
expect(result.match).toBe(true);
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
});
|