@jterrazz/test 5.0.0 → 5.2.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 +25 -12
- package/dist/index.cjs +383 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +136 -5
- package/dist/index.d.ts +136 -5
- package/dist/index.js +384 -17
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import MockDatePackage from "mockdate";
|
|
2
2
|
import { mockDeep } from "vitest-mock-extended";
|
|
3
|
-
import { cpSync, existsSync, mkdtempSync, readFileSync } from "node:fs";
|
|
4
|
-
import { dirname, isAbsolute, resolve } from "node:path";
|
|
3
|
+
import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync } from "node:fs";
|
|
4
|
+
import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
5
5
|
import { execSync, spawn } from "node:child_process";
|
|
6
6
|
import { parse } from "yaml";
|
|
7
7
|
import { Client } from "pg";
|
|
8
8
|
import { tmpdir } from "node:os";
|
|
9
|
+
import { readdir } from "node:fs/promises";
|
|
9
10
|
//#region src/mocking/mock-of-date.ts
|
|
10
11
|
const mockOfDate = MockDatePackage;
|
|
11
12
|
//#endregion
|
|
@@ -268,6 +269,47 @@ function formatResponseDiff(file, expected, actual) {
|
|
|
268
269
|
}
|
|
269
270
|
return lines.join("\n");
|
|
270
271
|
}
|
|
272
|
+
function formatDirectoryDiff(fixtureName, diff, hint) {
|
|
273
|
+
const lines = [];
|
|
274
|
+
const total = diff.added.length + diff.removed.length + diff.changed.length;
|
|
275
|
+
lines.push(`Directory mismatch: ${BOLD}${fixtureName}${RESET}`);
|
|
276
|
+
lines.push(`${DIM} ${total} difference${total === 1 ? "" : "s"}: ${diff.added.length} added, ${diff.removed.length} removed, ${diff.changed.length} changed${RESET}`);
|
|
277
|
+
lines.push("");
|
|
278
|
+
lines.push(`${GREEN}- Expected (fixture)${RESET}`);
|
|
279
|
+
lines.push(`${RED}+ Received (generated)${RESET}`);
|
|
280
|
+
lines.push("");
|
|
281
|
+
for (const path of diff.added) lines.push(`${RED}+ added ${path}${RESET} ${DIM}(not in fixture)${RESET}`);
|
|
282
|
+
for (const path of diff.removed) lines.push(`${GREEN}- removed ${path}${RESET} ${DIM}(in fixture, not generated)${RESET}`);
|
|
283
|
+
for (const { path, expected, actual } of diff.changed) {
|
|
284
|
+
const expectedLines = expected.split("\n");
|
|
285
|
+
const actualLines = actual.split("\n");
|
|
286
|
+
const changedCount = countLineDifferences(expectedLines, actualLines);
|
|
287
|
+
lines.push(`${BOLD}~ changed ${path}${RESET} ${DIM}(${changedCount} line${changedCount === 1 ? "" : "s"} differ)${RESET}`);
|
|
288
|
+
let shown = 0;
|
|
289
|
+
const maxShown = 5;
|
|
290
|
+
const maxLines = Math.max(expectedLines.length, actualLines.length);
|
|
291
|
+
for (let i = 0; i < maxLines && shown < maxShown; i++) {
|
|
292
|
+
const exp = expectedLines[i];
|
|
293
|
+
const act = actualLines[i];
|
|
294
|
+
if (exp !== act) {
|
|
295
|
+
lines.push(`${DIM} line ${i + 1}:${RESET}`);
|
|
296
|
+
if (exp !== void 0) lines.push(` ${GREEN}- ${exp}${RESET}`);
|
|
297
|
+
if (act !== void 0) lines.push(` ${RED}+ ${act}${RESET}`);
|
|
298
|
+
shown++;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if (changedCount > maxShown) lines.push(` ${DIM}... ${changedCount - maxShown} more line(s)${RESET}`);
|
|
302
|
+
}
|
|
303
|
+
lines.push("");
|
|
304
|
+
lines.push(`${DIM}${hint}${RESET}`);
|
|
305
|
+
return lines.join("\n");
|
|
306
|
+
}
|
|
307
|
+
function countLineDifferences(expected, actual) {
|
|
308
|
+
let count = 0;
|
|
309
|
+
const max = Math.max(expected.length, actual.length);
|
|
310
|
+
for (let i = 0; i < max; i++) if (expected[i] !== actual[i]) count++;
|
|
311
|
+
return count;
|
|
312
|
+
}
|
|
271
313
|
function rowLabel(n) {
|
|
272
314
|
return n === 1 ? "1 row" : `${n} rows`;
|
|
273
315
|
}
|
|
@@ -619,6 +661,19 @@ var Orchestrator = class {
|
|
|
619
661
|
//#endregion
|
|
620
662
|
//#region src/specification/adapters/exec.adapter.ts
|
|
621
663
|
/**
|
|
664
|
+
* Build a child-process env from the parent env plus user overrides.
|
|
665
|
+
* `null` overrides delete keys (e.g. `INIT_CWD: null`).
|
|
666
|
+
*/
|
|
667
|
+
function buildEnv(extra) {
|
|
668
|
+
const env = {
|
|
669
|
+
...process.env,
|
|
670
|
+
INIT_CWD: void 0
|
|
671
|
+
};
|
|
672
|
+
if (extra) for (const [key, value] of Object.entries(extra)) if (value === null) delete env[key];
|
|
673
|
+
else env[key] = value;
|
|
674
|
+
return env;
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
622
677
|
* Executes CLI commands via execSync (blocking) or spawn (long-running).
|
|
623
678
|
* Used by cli() for local command execution.
|
|
624
679
|
*/
|
|
@@ -627,11 +682,8 @@ var ExecAdapter = class {
|
|
|
627
682
|
constructor(command) {
|
|
628
683
|
this.command = command;
|
|
629
684
|
}
|
|
630
|
-
async exec(args, cwd) {
|
|
631
|
-
const env =
|
|
632
|
-
...process.env,
|
|
633
|
-
INIT_CWD: void 0
|
|
634
|
-
};
|
|
685
|
+
async exec(args, cwd, extraEnv) {
|
|
686
|
+
const env = buildEnv(extraEnv);
|
|
635
687
|
try {
|
|
636
688
|
return {
|
|
637
689
|
exitCode: 0,
|
|
@@ -655,11 +707,8 @@ var ExecAdapter = class {
|
|
|
655
707
|
};
|
|
656
708
|
}
|
|
657
709
|
}
|
|
658
|
-
async spawn(args, cwd, options) {
|
|
659
|
-
const env =
|
|
660
|
-
...process.env,
|
|
661
|
-
INIT_CWD: void 0
|
|
662
|
-
};
|
|
710
|
+
async spawn(args, cwd, options, extraEnv) {
|
|
711
|
+
const env = buildEnv(extraEnv);
|
|
663
712
|
return new Promise((resolve) => {
|
|
664
713
|
let stdout = "";
|
|
665
714
|
let stderr = "";
|
|
@@ -766,6 +815,75 @@ var HonoAdapter = class {
|
|
|
766
815
|
}
|
|
767
816
|
};
|
|
768
817
|
//#endregion
|
|
818
|
+
//#region src/specification/directory.ts
|
|
819
|
+
/**
|
|
820
|
+
* Default ignore patterns — paths that should never appear in a tracked snapshot.
|
|
821
|
+
* Each entry is matched against any path segment OR a path prefix.
|
|
822
|
+
*/
|
|
823
|
+
const DEFAULT_IGNORES = [
|
|
824
|
+
".git",
|
|
825
|
+
".DS_Store",
|
|
826
|
+
"node_modules",
|
|
827
|
+
".next",
|
|
828
|
+
"dist",
|
|
829
|
+
".turbo",
|
|
830
|
+
".cache"
|
|
831
|
+
];
|
|
832
|
+
/**
|
|
833
|
+
* Recursively walk a directory, returning sorted relative paths of files only.
|
|
834
|
+
* Ignored entries (default + caller-supplied) are skipped.
|
|
835
|
+
*/
|
|
836
|
+
async function walkDirectory(root, options = {}) {
|
|
837
|
+
const ignores = new Set([...DEFAULT_IGNORES, ...options.ignore ?? []]);
|
|
838
|
+
const out = [];
|
|
839
|
+
async function walk(current) {
|
|
840
|
+
let entries;
|
|
841
|
+
try {
|
|
842
|
+
entries = await readdir(current);
|
|
843
|
+
} catch {
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
for (const entry of entries) {
|
|
847
|
+
if (ignores.has(entry)) continue;
|
|
848
|
+
const abs = resolve(current, entry);
|
|
849
|
+
const stat = statSync(abs);
|
|
850
|
+
if (stat.isDirectory()) await walk(abs);
|
|
851
|
+
else if (stat.isFile()) out.push(relative(root, abs).split(sep).join("/"));
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
await walk(root);
|
|
855
|
+
out.sort();
|
|
856
|
+
return out;
|
|
857
|
+
}
|
|
858
|
+
/**
|
|
859
|
+
* Compare two directory trees file-by-file.
|
|
860
|
+
* Binary files are compared by byte equality but reported without inline diff.
|
|
861
|
+
*/
|
|
862
|
+
async function diffDirectories(expectedRoot, actualRoot, options = {}) {
|
|
863
|
+
const expectedFiles = await walkDirectory(expectedRoot, options);
|
|
864
|
+
const actualFiles = await walkDirectory(actualRoot, options);
|
|
865
|
+
const expectedSet = new Set(expectedFiles);
|
|
866
|
+
const actualSet = new Set(actualFiles);
|
|
867
|
+
const added = actualFiles.filter((f) => !expectedSet.has(f));
|
|
868
|
+
const removed = expectedFiles.filter((f) => !actualSet.has(f));
|
|
869
|
+
const changed = [];
|
|
870
|
+
for (const file of expectedFiles) {
|
|
871
|
+
if (!actualSet.has(file)) continue;
|
|
872
|
+
const expected = readFileSync(resolve(expectedRoot, file), "utf8");
|
|
873
|
+
const actual = readFileSync(resolve(actualRoot, file), "utf8");
|
|
874
|
+
if (expected !== actual) changed.push({
|
|
875
|
+
actual,
|
|
876
|
+
expected,
|
|
877
|
+
path: file
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
return {
|
|
881
|
+
added,
|
|
882
|
+
changed,
|
|
883
|
+
removed
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
//#endregion
|
|
769
887
|
//#region src/specification/specification.ts
|
|
770
888
|
var TableAssertion = class {
|
|
771
889
|
tableName;
|
|
@@ -783,6 +901,54 @@ var TableAssertion = class {
|
|
|
783
901
|
if (actual.length !== 0) throw new Error(`Expected table "${this.tableName}" to be empty, but it has ${actual.length} rows`);
|
|
784
902
|
}
|
|
785
903
|
};
|
|
904
|
+
/**
|
|
905
|
+
* Detect whether the user wants to update snapshots — `true` for any of:
|
|
906
|
+
* - vitest run with `-u` / `--update`
|
|
907
|
+
* - JTERRAZZ_TEST_UPDATE=1
|
|
908
|
+
* - UPDATE_SNAPSHOTS=1
|
|
909
|
+
*/
|
|
910
|
+
function shouldUpdateSnapshots() {
|
|
911
|
+
if (process.env.JTERRAZZ_TEST_UPDATE === "1") return true;
|
|
912
|
+
if (process.env.UPDATE_SNAPSHOTS === "1") return true;
|
|
913
|
+
if (process.argv.includes("-u") || process.argv.includes("--update")) return true;
|
|
914
|
+
return false;
|
|
915
|
+
}
|
|
916
|
+
var DirectoryAccessor = class {
|
|
917
|
+
absPath;
|
|
918
|
+
testDir;
|
|
919
|
+
constructor(absPath, testDir) {
|
|
920
|
+
this.absPath = absPath;
|
|
921
|
+
this.testDir = testDir;
|
|
922
|
+
}
|
|
923
|
+
/**
|
|
924
|
+
* Compare the directory tree against `expected/{name}/` (relative to the test file).
|
|
925
|
+
* On mismatch, throws with a structured diff. With update mode enabled, the
|
|
926
|
+
* fixture is overwritten with the current contents instead.
|
|
927
|
+
*/
|
|
928
|
+
async toMatchFixture(name, options = {}) {
|
|
929
|
+
const fixtureDir = resolve(this.testDir, "expected", name);
|
|
930
|
+
if (options.update ?? shouldUpdateSnapshots()) {
|
|
931
|
+
rmSync(fixtureDir, {
|
|
932
|
+
force: true,
|
|
933
|
+
recursive: true
|
|
934
|
+
});
|
|
935
|
+
mkdirSync(fixtureDir, { recursive: true });
|
|
936
|
+
cpSync(this.absPath, fixtureDir, { recursive: true });
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
if (!existsSync(fixtureDir)) throw new Error(`Directory fixture "${name}" does not exist at ${fixtureDir}.\nRun with JTERRAZZ_TEST_UPDATE=1 (or vitest -u) to create it.`);
|
|
940
|
+
const diff = await diffDirectories(fixtureDir, this.absPath, { ignore: options.ignore });
|
|
941
|
+
if (diff.added.length === 0 && diff.removed.length === 0 && diff.changed.length === 0) return;
|
|
942
|
+
throw new Error(formatDirectoryDiff(name, diff, "Run with JTERRAZZ_TEST_UPDATE=1 to update the fixture."));
|
|
943
|
+
}
|
|
944
|
+
/**
|
|
945
|
+
* List all files in the directory (recursive, sorted, ignoring defaults).
|
|
946
|
+
* Useful for ad-hoc assertions when you don't want a full snapshot.
|
|
947
|
+
*/
|
|
948
|
+
async files(options = {}) {
|
|
949
|
+
return walkDirectory(this.absPath, options);
|
|
950
|
+
}
|
|
951
|
+
};
|
|
786
952
|
var ResponseAccessor = class {
|
|
787
953
|
body;
|
|
788
954
|
testDir;
|
|
@@ -830,6 +996,9 @@ var SpecificationResult = class {
|
|
|
830
996
|
if (!this.responseData) throw new Error(".response requires an HTTP action (.get(), .post(), etc.)");
|
|
831
997
|
return new ResponseAccessor(this.responseData.body, this.testDir);
|
|
832
998
|
}
|
|
999
|
+
directory(path = ".") {
|
|
1000
|
+
return new DirectoryAccessor(resolve(this.workDir ?? this.testDir, path), this.testDir);
|
|
1001
|
+
}
|
|
833
1002
|
file(path) {
|
|
834
1003
|
const resolvedPath = resolve(this.workDir ?? this.testDir, path);
|
|
835
1004
|
const exists = existsSync(resolvedPath);
|
|
@@ -853,6 +1022,7 @@ var SpecificationResult = class {
|
|
|
853
1022
|
};
|
|
854
1023
|
var SpecificationBuilder = class {
|
|
855
1024
|
commandArgs = null;
|
|
1025
|
+
commandEnv = {};
|
|
856
1026
|
config;
|
|
857
1027
|
fixtures = [];
|
|
858
1028
|
label;
|
|
@@ -886,6 +1056,23 @@ var SpecificationBuilder = class {
|
|
|
886
1056
|
this.mocks.push({ file });
|
|
887
1057
|
return this;
|
|
888
1058
|
}
|
|
1059
|
+
/**
|
|
1060
|
+
* Set environment variables for the CLI process. Merged on top of process.env.
|
|
1061
|
+
* Use `null` to unset a variable. Multiple calls merge.
|
|
1062
|
+
*
|
|
1063
|
+
* The token `$WORKDIR` (in any value) is replaced with the actual working
|
|
1064
|
+
* directory at run-time — useful for tests that need a fully isolated `HOME`.
|
|
1065
|
+
*
|
|
1066
|
+
* @example
|
|
1067
|
+
* spec("...").env({ HOME: "$WORKDIR", TZ: "UTC" }).exec("status").run();
|
|
1068
|
+
*/
|
|
1069
|
+
env(env) {
|
|
1070
|
+
this.commandEnv = {
|
|
1071
|
+
...this.commandEnv,
|
|
1072
|
+
...env
|
|
1073
|
+
};
|
|
1074
|
+
return this;
|
|
1075
|
+
}
|
|
889
1076
|
get(path) {
|
|
890
1077
|
this.request = {
|
|
891
1078
|
method: "GET",
|
|
@@ -951,8 +1138,17 @@ var SpecificationBuilder = class {
|
|
|
951
1138
|
if (hasHttpAction) return this.runHttpAction();
|
|
952
1139
|
return this.runCliAction(workDir);
|
|
953
1140
|
}
|
|
1141
|
+
resolveEnv(workDir) {
|
|
1142
|
+
const keys = Object.keys(this.commandEnv);
|
|
1143
|
+
if (keys.length === 0) return;
|
|
1144
|
+
const resolved = {};
|
|
1145
|
+
for (const key of keys) {
|
|
1146
|
+
const value = this.commandEnv[key];
|
|
1147
|
+
resolved[key] = typeof value === "string" ? value.replace(/\$WORKDIR/g, workDir) : value;
|
|
1148
|
+
}
|
|
1149
|
+
return resolved;
|
|
1150
|
+
}
|
|
954
1151
|
prepareWorkDir() {
|
|
955
|
-
if (!this.projectName && this.fixtures.length === 0) return this.config.fixturesRoot ?? process.cwd();
|
|
956
1152
|
const tempDir = mkdtempSync(resolve(tmpdir(), "spec-cli-"));
|
|
957
1153
|
if (this.projectName && this.config.fixturesRoot) {
|
|
958
1154
|
const projectDir = resolve(this.config.fixturesRoot, this.projectName);
|
|
@@ -979,8 +1175,9 @@ var SpecificationBuilder = class {
|
|
|
979
1175
|
}
|
|
980
1176
|
async runCliAction(workDir) {
|
|
981
1177
|
if (!this.config.command) throw new Error("CLI actions require a command adapter (use cli())");
|
|
1178
|
+
const env = this.resolveEnv(workDir);
|
|
982
1179
|
let commandResult;
|
|
983
|
-
if (this.spawnConfig) commandResult = await this.config.command.spawn(this.spawnConfig.args, workDir, this.spawnConfig.options);
|
|
1180
|
+
if (this.spawnConfig) commandResult = await this.config.command.spawn(this.spawnConfig.args, workDir, this.spawnConfig.options, env);
|
|
984
1181
|
else if (Array.isArray(this.commandArgs)) {
|
|
985
1182
|
commandResult = {
|
|
986
1183
|
exitCode: 0,
|
|
@@ -988,10 +1185,10 @@ var SpecificationBuilder = class {
|
|
|
988
1185
|
stdout: ""
|
|
989
1186
|
};
|
|
990
1187
|
for (const args of this.commandArgs) {
|
|
991
|
-
commandResult = await this.config.command.exec(args, workDir);
|
|
1188
|
+
commandResult = await this.config.command.exec(args, workDir, env);
|
|
992
1189
|
if (commandResult.exitCode !== 0) break;
|
|
993
1190
|
}
|
|
994
|
-
} else commandResult = await this.config.command.exec(this.commandArgs, workDir);
|
|
1191
|
+
} else commandResult = await this.config.command.exec(this.commandArgs, workDir, env);
|
|
995
1192
|
return new SpecificationResult({
|
|
996
1193
|
commandResult,
|
|
997
1194
|
config: this.config,
|
|
@@ -1152,6 +1349,176 @@ async function cli(options) {
|
|
|
1152
1349
|
return runner;
|
|
1153
1350
|
}
|
|
1154
1351
|
//#endregion
|
|
1155
|
-
|
|
1352
|
+
//#region src/infrastructure/docker/docker-adapter.ts
|
|
1353
|
+
var DockerAdapter = class {
|
|
1354
|
+
containerId;
|
|
1355
|
+
constructor(containerId) {
|
|
1356
|
+
this.containerId = containerId;
|
|
1357
|
+
}
|
|
1358
|
+
async exec(cmd) {
|
|
1359
|
+
return execSync(`docker exec ${this.containerId} ${cmd.map((c) => `'${c}'`).join(" ")}`, {
|
|
1360
|
+
encoding: "utf8",
|
|
1361
|
+
timeout: 1e4
|
|
1362
|
+
}).trim();
|
|
1363
|
+
}
|
|
1364
|
+
async file(path) {
|
|
1365
|
+
try {
|
|
1366
|
+
return {
|
|
1367
|
+
exists: true,
|
|
1368
|
+
content: await this.exec(["cat", path])
|
|
1369
|
+
};
|
|
1370
|
+
} catch {
|
|
1371
|
+
return {
|
|
1372
|
+
exists: false,
|
|
1373
|
+
content: ""
|
|
1374
|
+
};
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
async isRunning() {
|
|
1378
|
+
try {
|
|
1379
|
+
return execSync(`docker inspect --format='{{.State.Running}}' ${this.containerId}`, {
|
|
1380
|
+
encoding: "utf8",
|
|
1381
|
+
timeout: 5e3
|
|
1382
|
+
}).trim() === "true";
|
|
1383
|
+
} catch {
|
|
1384
|
+
return false;
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
async logs(tail) {
|
|
1388
|
+
return execSync(`docker logs ${tail ? `--tail ${tail}` : ""} ${this.containerId}`, {
|
|
1389
|
+
encoding: "utf8",
|
|
1390
|
+
timeout: 1e4
|
|
1391
|
+
});
|
|
1392
|
+
}
|
|
1393
|
+
async inspect() {
|
|
1394
|
+
const raw = execSync(`docker inspect ${this.containerId}`, {
|
|
1395
|
+
encoding: "utf8",
|
|
1396
|
+
timeout: 5e3
|
|
1397
|
+
});
|
|
1398
|
+
const data = JSON.parse(raw)[0];
|
|
1399
|
+
return {
|
|
1400
|
+
id: data.Id,
|
|
1401
|
+
name: data.Name,
|
|
1402
|
+
state: {
|
|
1403
|
+
running: data.State.Running,
|
|
1404
|
+
exitCode: data.State.ExitCode,
|
|
1405
|
+
status: data.State.Status
|
|
1406
|
+
},
|
|
1407
|
+
config: {
|
|
1408
|
+
image: data.Config.Image,
|
|
1409
|
+
env: data.Config.Env || []
|
|
1410
|
+
},
|
|
1411
|
+
hostConfig: {
|
|
1412
|
+
memory: data.HostConfig.Memory || 0,
|
|
1413
|
+
cpuQuota: data.HostConfig.CpuQuota || 0,
|
|
1414
|
+
networkMode: data.HostConfig.NetworkMode || "",
|
|
1415
|
+
mounts: (data.Mounts || []).map((m) => ({
|
|
1416
|
+
source: m.Source,
|
|
1417
|
+
destination: m.Destination,
|
|
1418
|
+
type: m.Type
|
|
1419
|
+
}))
|
|
1420
|
+
},
|
|
1421
|
+
networkSettings: { networks: Object.fromEntries(Object.entries(data.NetworkSettings.Networks || {}).map(([name, net]) => [name, {
|
|
1422
|
+
gateway: net.Gateway,
|
|
1423
|
+
ipAddress: net.IPAddress
|
|
1424
|
+
}])) }
|
|
1425
|
+
};
|
|
1426
|
+
}
|
|
1427
|
+
async exists(path) {
|
|
1428
|
+
try {
|
|
1429
|
+
await this.exec([
|
|
1430
|
+
"test",
|
|
1431
|
+
"-e",
|
|
1432
|
+
path
|
|
1433
|
+
]);
|
|
1434
|
+
return true;
|
|
1435
|
+
} catch {
|
|
1436
|
+
return false;
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
};
|
|
1440
|
+
/** Create a Docker container port for an existing container */
|
|
1441
|
+
function dockerContainer(containerId) {
|
|
1442
|
+
return new DockerAdapter(containerId);
|
|
1443
|
+
}
|
|
1444
|
+
//#endregion
|
|
1445
|
+
//#region src/infrastructure/docker/docker-assertion.ts
|
|
1446
|
+
/** Fluent assertion builder for Docker containers */
|
|
1447
|
+
var DockerAssertion = class {
|
|
1448
|
+
container;
|
|
1449
|
+
constructor(container) {
|
|
1450
|
+
this.container = container;
|
|
1451
|
+
}
|
|
1452
|
+
/** Assert the container is running */
|
|
1453
|
+
async toBeRunning() {
|
|
1454
|
+
if (!await this.container.isRunning()) throw new Error("Expected container to be running");
|
|
1455
|
+
return this;
|
|
1456
|
+
}
|
|
1457
|
+
/** Assert the container is NOT running / doesn't exist */
|
|
1458
|
+
async toNotExist() {
|
|
1459
|
+
if (await this.container.isRunning()) throw new Error("Expected container to not exist or not be running");
|
|
1460
|
+
return this;
|
|
1461
|
+
}
|
|
1462
|
+
/** Assert a file exists inside the container */
|
|
1463
|
+
async toHaveFile(path, opts) {
|
|
1464
|
+
const file = await this.container.file(path);
|
|
1465
|
+
if (!file.exists) throw new Error(`Expected file ${path} to exist in container`);
|
|
1466
|
+
if (opts?.containing && !file.content.includes(opts.containing)) throw new Error(`Expected file ${path} to contain "${opts.containing}", got: ${file.content.slice(0, 200)}`);
|
|
1467
|
+
return this;
|
|
1468
|
+
}
|
|
1469
|
+
/** Assert a file does NOT exist */
|
|
1470
|
+
async toNotHaveFile(path) {
|
|
1471
|
+
if (await this.container.exists(path)) throw new Error(`Expected ${path} to not exist in container`);
|
|
1472
|
+
return this;
|
|
1473
|
+
}
|
|
1474
|
+
/** Assert a directory exists */
|
|
1475
|
+
async toHaveDirectory(path) {
|
|
1476
|
+
if (!await this.container.exists(path)) throw new Error(`Expected directory ${path} to exist in container`);
|
|
1477
|
+
return this;
|
|
1478
|
+
}
|
|
1479
|
+
/** Assert a mount exists */
|
|
1480
|
+
async toHaveMount(destination) {
|
|
1481
|
+
const info = await this.container.inspect();
|
|
1482
|
+
if (!info.hostConfig.mounts.find((m) => m.destination === destination)) {
|
|
1483
|
+
const available = info.hostConfig.mounts.map((m) => m.destination).join(", ");
|
|
1484
|
+
throw new Error(`Expected mount at ${destination}, found: [${available}]`);
|
|
1485
|
+
}
|
|
1486
|
+
return this;
|
|
1487
|
+
}
|
|
1488
|
+
/** Assert network mode */
|
|
1489
|
+
async toHaveNetwork(mode) {
|
|
1490
|
+
const info = await this.container.inspect();
|
|
1491
|
+
if (!info.hostConfig.networkMode.includes(mode)) throw new Error(`Expected network mode "${mode}", got "${info.hostConfig.networkMode}"`);
|
|
1492
|
+
return this;
|
|
1493
|
+
}
|
|
1494
|
+
/** Assert memory limit */
|
|
1495
|
+
async toHaveMemoryLimit(bytes) {
|
|
1496
|
+
const info = await this.container.inspect();
|
|
1497
|
+
if (info.hostConfig.memory !== bytes) throw new Error(`Expected memory limit ${bytes}, got ${info.hostConfig.memory}`);
|
|
1498
|
+
return this;
|
|
1499
|
+
}
|
|
1500
|
+
/** Assert CPU quota */
|
|
1501
|
+
async toHaveCpuQuota(quota) {
|
|
1502
|
+
const info = await this.container.inspect();
|
|
1503
|
+
if (info.hostConfig.cpuQuota !== quota) throw new Error(`Expected CPU quota ${quota}, got ${info.hostConfig.cpuQuota}`);
|
|
1504
|
+
return this;
|
|
1505
|
+
}
|
|
1506
|
+
/** Execute a command and return output for custom assertions */
|
|
1507
|
+
async exec(cmd) {
|
|
1508
|
+
return this.container.exec(cmd);
|
|
1509
|
+
}
|
|
1510
|
+
/** Read a file for custom assertions */
|
|
1511
|
+
async readFile(path) {
|
|
1512
|
+
const file = await this.container.file(path);
|
|
1513
|
+
if (!file.exists) throw new Error(`File ${path} does not exist`);
|
|
1514
|
+
return file.content;
|
|
1515
|
+
}
|
|
1516
|
+
/** Get logs for custom assertions */
|
|
1517
|
+
async getLogs(tail) {
|
|
1518
|
+
return this.container.logs(tail);
|
|
1519
|
+
}
|
|
1520
|
+
};
|
|
1521
|
+
//#endregion
|
|
1522
|
+
export { DockerAssertion, ExecAdapter, FetchAdapter, HonoAdapter, Orchestrator, cli, dockerContainer, e2e, grep, integration, mockOf, mockOfDate, normalizeOutput, postgres, redis, stripAnsi };
|
|
1156
1523
|
|
|
1157
1524
|
//# sourceMappingURL=index.js.map
|