@ajnart/self-host-testkit 0.0.0-stage → 1.3.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/LICENSE +202 -0
- package/README.md +370 -2
- package/binary.mjs +52 -0
- package/examples/homarr-sonarr.mjs +66 -0
- package/examples/sonarr.mjs +15 -0
- package/index.d.mts +102 -0
- package/index.mjs +837 -0
- package/package.json +56 -5
- package/reasons.d.mts +15 -0
- package/reasons.mjs +14 -0
- package/setup.mjs +109 -0
package/package.json
CHANGED
|
@@ -1,6 +1,57 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
"name": "@ajnart/self-host-testkit",
|
|
3
|
+
"version": "1.3.0",
|
|
4
|
+
"description": "Real, isolated Docker App fixtures for Node.js and TypeScript integration tests",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./index.mjs",
|
|
7
|
+
"types": "./index.d.mts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./index.d.mts",
|
|
11
|
+
"import": "./index.mjs"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"bin": { "self-host-testkit-setup": "./setup.mjs" },
|
|
15
|
+
"files": [
|
|
16
|
+
"index.mjs",
|
|
17
|
+
"index.d.mts",
|
|
18
|
+
"binary.mjs",
|
|
19
|
+
"reasons.mjs",
|
|
20
|
+
"reasons.d.mts",
|
|
21
|
+
"setup.mjs",
|
|
22
|
+
"examples/sonarr.mjs",
|
|
23
|
+
"examples/homarr-sonarr.mjs",
|
|
24
|
+
"README.md",
|
|
25
|
+
"LICENSE"
|
|
26
|
+
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsc -p tsconfig.json && cp ../../LICENSE LICENSE",
|
|
29
|
+
"check": "tsc -p tsconfig.json --noEmit",
|
|
30
|
+
"lint": "biome check .",
|
|
31
|
+
"test": "bun test ./tests",
|
|
32
|
+
"prepack": "npm run build"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@biomejs/biome": "1.9.4",
|
|
36
|
+
"@types/node": "^26.6.4",
|
|
37
|
+
"typescript": "^6.0.3"
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": { "access": "public" },
|
|
40
|
+
"keywords": [
|
|
41
|
+
"docker",
|
|
42
|
+
"compose",
|
|
43
|
+
"integration-testing",
|
|
44
|
+
"fixtures",
|
|
45
|
+
"self-hosted",
|
|
46
|
+
"typescript"
|
|
47
|
+
],
|
|
48
|
+
"homepage": "https://github.com/ajnart/self-host/tree/main/testkit",
|
|
49
|
+
"bugs": "https://github.com/ajnart/self-host/issues",
|
|
50
|
+
"engines": { "node": ">=22" },
|
|
51
|
+
"license": "Apache-2.0",
|
|
52
|
+
"repository": {
|
|
53
|
+
"type": "git",
|
|
54
|
+
"url": "https://github.com/ajnart/self-host.git",
|
|
55
|
+
"directory": "testkit/node"
|
|
56
|
+
}
|
|
57
|
+
}
|
package/reasons.d.mts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare const startupReasons: {
|
|
2
|
+
readonly APP: "Choose one supported Catalog App.";
|
|
3
|
+
readonly CAPABILITIES: "Docker must support memory, CPU quota and PID limits.";
|
|
4
|
+
readonly CONTAINER: "The selected App container is absent.";
|
|
5
|
+
readonly DOCKER: "Check the local Unix Docker socket, daemon and Compose plugin.";
|
|
6
|
+
readonly LIMITS: "Container resource caps or selected image could not be verified.";
|
|
7
|
+
readonly LOCK: "The private startup lock is unavailable.";
|
|
8
|
+
readonly OPTIONS: "Check the selected image and lifetime options.";
|
|
9
|
+
readonly OWNERSHIP: "Fixture ownership or isolation could not be verified.";
|
|
10
|
+
readonly PORT: "A required local port could not be reserved.";
|
|
11
|
+
readonly SETUP: "App Setup failed or requires a Manual step.";
|
|
12
|
+
readonly STORAGE: "Private fixture storage or the recovery manifest is unavailable.";
|
|
13
|
+
readonly UNSUPPORTED: "This App's Compose configuration or operations are unsupported.";
|
|
14
|
+
};
|
|
15
|
+
export type TestkitStartupReason = keyof typeof startupReasons;
|
package/reasons.mjs
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export const startupReasons = {
|
|
2
|
+
APP: "Choose one supported Catalog App.",
|
|
3
|
+
CAPABILITIES: "Docker must support memory, CPU quota and PID limits.",
|
|
4
|
+
CONTAINER: "The selected App container is absent.",
|
|
5
|
+
DOCKER: "Check the local Unix Docker socket, daemon and Compose plugin.",
|
|
6
|
+
LIMITS: "Container resource caps or selected image could not be verified.",
|
|
7
|
+
LOCK: "The private startup lock is unavailable.",
|
|
8
|
+
OPTIONS: "Check the selected image and lifetime options.",
|
|
9
|
+
OWNERSHIP: "Fixture ownership or isolation could not be verified.",
|
|
10
|
+
PORT: "A required local port could not be reserved.",
|
|
11
|
+
SETUP: "App Setup failed or requires a Manual step.",
|
|
12
|
+
STORAGE: "Private fixture storage or the recovery manifest is unavailable.",
|
|
13
|
+
UNSUPPORTED: "This App's Compose configuration or operations are unsupported.",
|
|
14
|
+
};
|
package/setup.mjs
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { chmodSync, existsSync, lstatSync, mkdirSync, mkdtempSync, realpathSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
5
|
+
import { isAbsolute, join } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { binaryDirectory, installedBinary, privateDirectory, version, } from "./binary.mjs";
|
|
8
|
+
export class SetupError extends Error {
|
|
9
|
+
}
|
|
10
|
+
async function download(url, limit) {
|
|
11
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(60_000) });
|
|
12
|
+
if (!response.ok || !response.body)
|
|
13
|
+
throw new SetupError("Release download failed; check connectivity and that the matching release exists.");
|
|
14
|
+
const chunks = [];
|
|
15
|
+
let size = 0;
|
|
16
|
+
for await (const chunk of response.body) {
|
|
17
|
+
size += chunk.length;
|
|
18
|
+
if (size > limit) {
|
|
19
|
+
throw new SetupError("Release download exceeds the size limit.");
|
|
20
|
+
}
|
|
21
|
+
chunks.push(chunk);
|
|
22
|
+
}
|
|
23
|
+
return Buffer.concat(chunks);
|
|
24
|
+
}
|
|
25
|
+
async function install(exactVersion, directory) {
|
|
26
|
+
if (!(process.platform === "linux" || process.platform === "darwin") ||
|
|
27
|
+
!(process.arch === "x64" || process.arch === "arm64")) {
|
|
28
|
+
throw new SetupError("Testkit supports Linux and macOS on x64 or arm64. On Windows, use WSL2 with a local Unix Docker socket.");
|
|
29
|
+
}
|
|
30
|
+
if (!isAbsolute(directory) || /[\0\r\n]/.test(directory))
|
|
31
|
+
throw new SetupError("Testkit helper directory must be an absolute path.");
|
|
32
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
33
|
+
if (!privateDirectory(directory))
|
|
34
|
+
throw new SetupError("Testkit helper cache must be private and owned by the current user.");
|
|
35
|
+
const existing = installedBinary(directory);
|
|
36
|
+
if (existing !== "self-host-testkit") {
|
|
37
|
+
console.log(`Testkit helper ${exactVersion} is already installed.`);
|
|
38
|
+
return existing;
|
|
39
|
+
}
|
|
40
|
+
let arch = process.arch;
|
|
41
|
+
if (arch === "x64")
|
|
42
|
+
arch = "amd64";
|
|
43
|
+
const archive = `self-host_${exactVersion}_${process.platform}_${arch}.tar.gz`;
|
|
44
|
+
const base = `https://github.com/ajnart/self-host/releases/download/v${exactVersion}`;
|
|
45
|
+
console.log(`Downloading and verifying testkit helper ${exactVersion} for ${process.platform}/${arch}…`);
|
|
46
|
+
const checksums = (await download(`${base}/checksums.txt`, 1024 * 1024)).toString("utf8");
|
|
47
|
+
const matches = checksums
|
|
48
|
+
.split("\n")
|
|
49
|
+
.map((line) => line.trim().split(/\s+/))
|
|
50
|
+
.filter((entry) => entry[1] === archive);
|
|
51
|
+
if (matches.length !== 1 || !/^[a-f0-9]{64}$/.test(matches[0]?.[0] ?? ""))
|
|
52
|
+
throw new SetupError("Release lacks an unambiguous helper checksum.");
|
|
53
|
+
const data = await download(`${base}/${archive}`, 128 * 1024 * 1024);
|
|
54
|
+
if (createHash("sha256").update(data).digest("hex") !== matches[0]?.[0])
|
|
55
|
+
throw new SetupError("Helper checksum mismatch; nothing installed.");
|
|
56
|
+
const staging = mkdtempSync(join(directory, ".download-"));
|
|
57
|
+
try {
|
|
58
|
+
const path = join(staging, archive);
|
|
59
|
+
writeFileSync(path, data, { mode: 0o600 });
|
|
60
|
+
const result = spawnSync("tar", ["-xzf", path, "-C", staging, "self-host-testkit"], { stdio: "ignore", timeout: 30_000, killSignal: "SIGKILL" });
|
|
61
|
+
if (result.status !== 0)
|
|
62
|
+
throw new SetupError("Helper extraction failed; make sure tar is installed.");
|
|
63
|
+
const binary = join(staging, "self-host-testkit");
|
|
64
|
+
if (!lstatSync(binary).isFile())
|
|
65
|
+
throw new SetupError("Release did not contain a regular helper binary.");
|
|
66
|
+
chmodSync(binary, 0o700);
|
|
67
|
+
renameSync(binary, join(directory, "self-host-testkit"));
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
rmSync(staging, { recursive: true, force: true });
|
|
71
|
+
}
|
|
72
|
+
console.log("Helper ready. createSelfHost() will find it automatically; no PATH or environment changes needed.");
|
|
73
|
+
return installedBinary(directory);
|
|
74
|
+
}
|
|
75
|
+
export async function setup(options = {}) {
|
|
76
|
+
try {
|
|
77
|
+
const exactVersion = options.version ?? version;
|
|
78
|
+
const defaultDirectory = binaryDirectory(exactVersion);
|
|
79
|
+
return await install(exactVersion, options.directory ?? defaultDirectory);
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
if (error instanceof SetupError)
|
|
83
|
+
throw error;
|
|
84
|
+
throw new SetupError("Testkit helper setup failed; check connectivity, tar, and private cache permissions.");
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const invoked = process.argv[1];
|
|
88
|
+
if (invoked &&
|
|
89
|
+
existsSync(invoked) &&
|
|
90
|
+
realpathSync(invoked) === fileURLToPath(import.meta.url)) {
|
|
91
|
+
try {
|
|
92
|
+
if (process.argv.length === 3 && process.argv[2] === "--help") {
|
|
93
|
+
console.log("self-host-testkit-setup: download and verify the matching helper for Linux/macOS x64/arm64. Requires tar. Does not start Docker or modify PATH.");
|
|
94
|
+
}
|
|
95
|
+
else if (process.argv.length !== 2) {
|
|
96
|
+
throw new SetupError("Usage: self-host-testkit-setup [--help]");
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
await setup();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
let message = "Unknown setup failure.";
|
|
104
|
+
if (error instanceof SetupError)
|
|
105
|
+
message = error.message;
|
|
106
|
+
console.error("Testkit setup failed.", message);
|
|
107
|
+
process.exitCode = 1;
|
|
108
|
+
}
|
|
109
|
+
}
|