mirror of
https://github.com/tale/headplane.git
synced 2026-07-26 07:48:14 +00:00
test: add docker and proc e2e testing
This commit is contained in:
+2
-2
@@ -11,7 +11,7 @@
|
||||
"start": "node build/server/index.js",
|
||||
"typecheck": "react-router typegen && tsgo",
|
||||
"test:unit": "vitest run --project unit",
|
||||
"test:integration": "vitest run --project integration",
|
||||
"test:integration": "vitest run --project integration:*",
|
||||
"docs:dev": "vitepress dev docs",
|
||||
"docs:build": "vitepress build docs",
|
||||
"docs:preview": "vitepress preview docs",
|
||||
@@ -87,7 +87,7 @@
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^8.0.0",
|
||||
"vitepress": "next",
|
||||
"vitest": "^4.0.18"
|
||||
"vitest": "^4.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24.2 <25",
|
||||
|
||||
Generated
+59
-61
@@ -210,8 +210,8 @@ importers:
|
||||
specifier: next
|
||||
version: 2.0.0-alpha.12(@types/node@25.3.1)(jiti@2.6.1)(lightningcss@1.32.0)(postcss@8.5.6)(terser@5.39.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2)
|
||||
vitest:
|
||||
specifier: ^4.0.18
|
||||
version: 4.0.18(@types/node@25.3.1)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||
specifier: ^4.1.0
|
||||
version: 4.1.0(@types/node@25.3.1)(vite@8.0.0(@types/node@25.3.1)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.2))
|
||||
|
||||
packages:
|
||||
|
||||
@@ -2679,34 +2679,34 @@ packages:
|
||||
vite: ^5.0.0 || ^6.0.0 || ^7.0.0
|
||||
vue: ^3.2.25
|
||||
|
||||
'@vitest/expect@4.0.18':
|
||||
resolution: {integrity: sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==}
|
||||
'@vitest/expect@4.1.0':
|
||||
resolution: {integrity: sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA==}
|
||||
|
||||
'@vitest/mocker@4.0.18':
|
||||
resolution: {integrity: sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==}
|
||||
'@vitest/mocker@4.1.0':
|
||||
resolution: {integrity: sha512-evxREh+Hork43+Y4IOhTo+h5lGmVRyjqI739Rz4RlUPqwrkFFDF6EMvOOYjTx4E8Tl6gyCLRL8Mu7Ry12a13Tw==}
|
||||
peerDependencies:
|
||||
msw: ^2.4.9
|
||||
vite: ^6.0.0 || ^7.0.0-0
|
||||
vite: ^6.0.0 || ^7.0.0 || ^8.0.0-0
|
||||
peerDependenciesMeta:
|
||||
msw:
|
||||
optional: true
|
||||
vite:
|
||||
optional: true
|
||||
|
||||
'@vitest/pretty-format@4.0.18':
|
||||
resolution: {integrity: sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==}
|
||||
'@vitest/pretty-format@4.1.0':
|
||||
resolution: {integrity: sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A==}
|
||||
|
||||
'@vitest/runner@4.0.18':
|
||||
resolution: {integrity: sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==}
|
||||
'@vitest/runner@4.1.0':
|
||||
resolution: {integrity: sha512-Duvx2OzQ7d6OjchL+trw+aSrb9idh7pnNfxrklo14p3zmNL4qPCDeIJAK+eBKYjkIwG96Bc6vYuxhqDXQOWpoQ==}
|
||||
|
||||
'@vitest/snapshot@4.0.18':
|
||||
resolution: {integrity: sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==}
|
||||
'@vitest/snapshot@4.1.0':
|
||||
resolution: {integrity: sha512-0Vy9euT1kgsnj1CHttwi9i9o+4rRLEaPRSOJ5gyv579GJkNpgJK+B4HSv/rAWixx2wdAFci1X4CEPjiu2bXIMg==}
|
||||
|
||||
'@vitest/spy@4.0.18':
|
||||
resolution: {integrity: sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==}
|
||||
'@vitest/spy@4.1.0':
|
||||
resolution: {integrity: sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw==}
|
||||
|
||||
'@vitest/utils@4.0.18':
|
||||
resolution: {integrity: sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==}
|
||||
'@vitest/utils@4.1.0':
|
||||
resolution: {integrity: sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw==}
|
||||
|
||||
'@vue/compiler-core@3.5.18':
|
||||
resolution: {integrity: sha512-3slwjQrrV1TO8MoXgy3aynDQ7lslj5UqDxuHnrzHtpON5CBinhWjJETciPngpin/T3OuW3tXUf86tEurusnztw==}
|
||||
@@ -3372,6 +3372,9 @@ packages:
|
||||
es-module-lexer@1.7.0:
|
||||
resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==}
|
||||
|
||||
es-module-lexer@2.0.0:
|
||||
resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==}
|
||||
|
||||
es-object-atoms@1.1.1:
|
||||
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -4478,8 +4481,8 @@ packages:
|
||||
stackback@0.0.2:
|
||||
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
|
||||
|
||||
std-env@3.10.0:
|
||||
resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==}
|
||||
std-env@4.0.0:
|
||||
resolution: {integrity: sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==}
|
||||
|
||||
stream-buffers@3.0.3:
|
||||
resolution: {integrity: sha512-pqMqwQCso0PBJt2PQmDO0cFj0lyqmiwOMiMSkVtRokl7e+ZTRYgDHKnuZNbqjiJXgsg4nuqtD/zxuo9KqTp0Yw==}
|
||||
@@ -4813,20 +4816,21 @@ packages:
|
||||
postcss:
|
||||
optional: true
|
||||
|
||||
vitest@4.0.18:
|
||||
resolution: {integrity: sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==}
|
||||
vitest@4.1.0:
|
||||
resolution: {integrity: sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw==}
|
||||
engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@edge-runtime/vm': '*'
|
||||
'@opentelemetry/api': ^1.9.0
|
||||
'@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0
|
||||
'@vitest/browser-playwright': 4.0.18
|
||||
'@vitest/browser-preview': 4.0.18
|
||||
'@vitest/browser-webdriverio': 4.0.18
|
||||
'@vitest/ui': 4.0.18
|
||||
'@vitest/browser-playwright': 4.1.0
|
||||
'@vitest/browser-preview': 4.1.0
|
||||
'@vitest/browser-webdriverio': 4.1.0
|
||||
'@vitest/ui': 4.1.0
|
||||
happy-dom: '*'
|
||||
jsdom: '*'
|
||||
vite: ^6.0.0 || ^7.0.0 || ^8.0.0-0
|
||||
peerDependenciesMeta:
|
||||
'@edge-runtime/vm':
|
||||
optional: true
|
||||
@@ -7664,43 +7668,45 @@ snapshots:
|
||||
vite: 7.3.1(@types/node@25.3.1)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||
vue: 3.5.18(typescript@5.9.3)
|
||||
|
||||
'@vitest/expect@4.0.18':
|
||||
'@vitest/expect@4.1.0':
|
||||
dependencies:
|
||||
'@standard-schema/spec': 1.1.0
|
||||
'@types/chai': 5.2.3
|
||||
'@vitest/spy': 4.0.18
|
||||
'@vitest/utils': 4.0.18
|
||||
'@vitest/spy': 4.1.0
|
||||
'@vitest/utils': 4.1.0
|
||||
chai: 6.2.2
|
||||
tinyrainbow: 3.0.3
|
||||
|
||||
'@vitest/mocker@4.0.18(vite@7.3.1(@types/node@25.3.1)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.2))':
|
||||
'@vitest/mocker@4.1.0(vite@8.0.0(@types/node@25.3.1)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.2))':
|
||||
dependencies:
|
||||
'@vitest/spy': 4.0.18
|
||||
'@vitest/spy': 4.1.0
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 0.30.21
|
||||
optionalDependencies:
|
||||
vite: 7.3.1(@types/node@25.3.1)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||
vite: 8.0.0(@types/node@25.3.1)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||
|
||||
'@vitest/pretty-format@4.0.18':
|
||||
'@vitest/pretty-format@4.1.0':
|
||||
dependencies:
|
||||
tinyrainbow: 3.0.3
|
||||
|
||||
'@vitest/runner@4.0.18':
|
||||
'@vitest/runner@4.1.0':
|
||||
dependencies:
|
||||
'@vitest/utils': 4.0.18
|
||||
'@vitest/utils': 4.1.0
|
||||
pathe: 2.0.3
|
||||
|
||||
'@vitest/snapshot@4.0.18':
|
||||
'@vitest/snapshot@4.1.0':
|
||||
dependencies:
|
||||
'@vitest/pretty-format': 4.0.18
|
||||
'@vitest/pretty-format': 4.1.0
|
||||
'@vitest/utils': 4.1.0
|
||||
magic-string: 0.30.21
|
||||
pathe: 2.0.3
|
||||
|
||||
'@vitest/spy@4.0.18': {}
|
||||
'@vitest/spy@4.1.0': {}
|
||||
|
||||
'@vitest/utils@4.0.18':
|
||||
'@vitest/utils@4.1.0':
|
||||
dependencies:
|
||||
'@vitest/pretty-format': 4.0.18
|
||||
'@vitest/pretty-format': 4.1.0
|
||||
convert-source-map: 2.0.0
|
||||
tinyrainbow: 3.0.3
|
||||
|
||||
'@vue/compiler-core@3.5.18':
|
||||
@@ -8251,6 +8257,8 @@ snapshots:
|
||||
|
||||
es-module-lexer@1.7.0: {}
|
||||
|
||||
es-module-lexer@2.0.0: {}
|
||||
|
||||
es-object-atoms@1.1.1:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
@@ -9492,7 +9500,7 @@ snapshots:
|
||||
|
||||
stackback@0.0.2: {}
|
||||
|
||||
std-env@3.10.0: {}
|
||||
std-env@4.0.0: {}
|
||||
|
||||
stream-buffers@3.0.3: {}
|
||||
|
||||
@@ -9883,42 +9891,32 @@ snapshots:
|
||||
- universal-cookie
|
||||
- yaml
|
||||
|
||||
vitest@4.0.18(@types/node@25.3.1)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.2):
|
||||
vitest@4.1.0(@types/node@25.3.1)(vite@8.0.0(@types/node@25.3.1)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.2)):
|
||||
dependencies:
|
||||
'@vitest/expect': 4.0.18
|
||||
'@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@25.3.1)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.2))
|
||||
'@vitest/pretty-format': 4.0.18
|
||||
'@vitest/runner': 4.0.18
|
||||
'@vitest/snapshot': 4.0.18
|
||||
'@vitest/spy': 4.0.18
|
||||
'@vitest/utils': 4.0.18
|
||||
es-module-lexer: 1.7.0
|
||||
'@vitest/expect': 4.1.0
|
||||
'@vitest/mocker': 4.1.0(vite@8.0.0(@types/node@25.3.1)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.2))
|
||||
'@vitest/pretty-format': 4.1.0
|
||||
'@vitest/runner': 4.1.0
|
||||
'@vitest/snapshot': 4.1.0
|
||||
'@vitest/spy': 4.1.0
|
||||
'@vitest/utils': 4.1.0
|
||||
es-module-lexer: 2.0.0
|
||||
expect-type: 1.3.0
|
||||
magic-string: 0.30.21
|
||||
obug: 2.1.1
|
||||
pathe: 2.0.3
|
||||
picomatch: 4.0.3
|
||||
std-env: 3.10.0
|
||||
std-env: 4.0.0
|
||||
tinybench: 2.9.0
|
||||
tinyexec: 1.0.2
|
||||
tinyglobby: 0.2.15
|
||||
tinyrainbow: 3.0.3
|
||||
vite: 7.3.1(@types/node@25.3.1)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||
vite: 8.0.0(@types/node@25.3.1)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||
why-is-node-running: 2.3.0
|
||||
optionalDependencies:
|
||||
'@types/node': 25.3.1
|
||||
transitivePeerDependencies:
|
||||
- jiti
|
||||
- less
|
||||
- lightningcss
|
||||
- msw
|
||||
- sass
|
||||
- sass-embedded
|
||||
- stylus
|
||||
- sugarss
|
||||
- terser
|
||||
- tsx
|
||||
- yaml
|
||||
|
||||
vue@3.5.18(typescript@5.9.3):
|
||||
dependencies:
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import { getRuntimeClient, HS_VERSIONS } from './setup/env';
|
||||
|
||||
describe.for(HS_VERSIONS)('Headscale %s: API Keys', (version) => {
|
||||
test('api keys can be fetched', async () => {
|
||||
const client = await getRuntimeClient(version);
|
||||
const apiKeys = await client.getApiKeys();
|
||||
expect(Array.isArray(apiKeys)).toBe(true);
|
||||
expect(apiKeys.length).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import { getRuntimeClient, HS_VERSIONS } from "../setup/env";
|
||||
|
||||
describe.for(HS_VERSIONS)("Headscale %s: API Keys", (version) => {
|
||||
test("api keys can be fetched", async () => {
|
||||
const client = await getRuntimeClient(version);
|
||||
const apiKeys = await client.getApiKeys();
|
||||
expect(Array.isArray(apiKeys)).toBe(true);
|
||||
expect(apiKeys.length).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import { getBootstrapClient, getNode, getRuntimeClient, HS_VERSIONS } from "./setup/env";
|
||||
import { getBootstrapClient, getNode, getRuntimeClient, HS_VERSIONS } from "../setup/env";
|
||||
|
||||
describe.sequential.for(HS_VERSIONS)("Headscale %s: Users", (version) => {
|
||||
let workingNodeId: string;
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import { getBootstrapClient, getIsAtLeast, getRuntimeClient, HS_VERSIONS } from "./setup/env";
|
||||
import { getBootstrapClient, getIsAtLeast, getRuntimeClient, HS_VERSIONS } from "../setup/env";
|
||||
|
||||
describe.sequential.for(HS_VERSIONS)("Headscale %s: Pre-auth Keys", (version) => {
|
||||
test("pre-auth keys can be created", async () => {
|
||||
@@ -0,0 +1,74 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import { getRuntimeClient, HS_VERSIONS } from "../setup/env";
|
||||
|
||||
describe.sequential.for(HS_VERSIONS)("Headscale %s: Users", (version) => {
|
||||
test("users can be created", async () => {
|
||||
const client = await getRuntimeClient(version);
|
||||
const user = await client.createUser("tale@");
|
||||
expect(user).toBeDefined();
|
||||
expect(user.name).toBe("tale@");
|
||||
});
|
||||
|
||||
test("users can be created with attributes", async () => {
|
||||
const client = await getRuntimeClient(version);
|
||||
const user = await client.createUser(
|
||||
"test-user@",
|
||||
"test-user@example.com",
|
||||
"Test User",
|
||||
"https://github.com/tale.png",
|
||||
);
|
||||
|
||||
expect(user).toBeDefined();
|
||||
expect(user.name).toBe("test-user@");
|
||||
expect(user.email).toBe("test-user@example.com");
|
||||
expect(user.displayName).toBe("Test User");
|
||||
expect(user.profilePicUrl).toBe("https://github.com/tale.png");
|
||||
});
|
||||
|
||||
test("users can be listed", async () => {
|
||||
const client = await getRuntimeClient(version);
|
||||
const users = await client.getUsers();
|
||||
expect(Array.isArray(users)).toBe(true);
|
||||
expect(users.length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
test("users can be listed by name", async () => {
|
||||
const client = await getRuntimeClient(version);
|
||||
const users = await client.getUsers(undefined, "tale@");
|
||||
expect(Array.isArray(users)).toBe(true);
|
||||
expect(users.length).toBe(1);
|
||||
expect(users[0].name).toBe("tale@");
|
||||
});
|
||||
|
||||
test("users can be listed by email", async () => {
|
||||
const client = await getRuntimeClient(version);
|
||||
const users = await client.getUsers(undefined, undefined, "test-user@example.com");
|
||||
expect(Array.isArray(users)).toBe(true);
|
||||
expect(users.length).toBe(1);
|
||||
expect(users[0].email).toBe("test-user@example.com");
|
||||
});
|
||||
|
||||
test("users can be renamed", async () => {
|
||||
const client = await getRuntimeClient(version);
|
||||
const usersBefore = await client.getUsers(undefined, "tale@");
|
||||
expect(usersBefore.length).toBe(1);
|
||||
const user = usersBefore[0];
|
||||
|
||||
await client.renameUser(user.id, "renamed-user@");
|
||||
const usersAfter = await client.getUsers(undefined, "renamed-user@");
|
||||
expect(usersAfter.length).toBe(1);
|
||||
expect(usersAfter[0].id).toBe(user.id);
|
||||
});
|
||||
|
||||
test("users can be deleted", async () => {
|
||||
const client = await getRuntimeClient(version);
|
||||
const usersBefore = await client.getUsers(undefined, "test-user@");
|
||||
expect(usersBefore.length).toBe(1);
|
||||
const user = usersBefore[0];
|
||||
|
||||
await client.deleteUser(user.id);
|
||||
const usersAfter = await client.getUsers(undefined, "test-user@");
|
||||
expect(usersAfter.length).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import canonicals from "~/openapi-canonical-families.json";
|
||||
|
||||
import { getBootstrapClient, getRuntimeClient, HS_VERSIONS, Version } from "../setup/env";
|
||||
|
||||
function getCanonicalVersion(version: Version) {
|
||||
const canonical = Object.entries(canonicals).find(([_, family]) =>
|
||||
family.includes(version),
|
||||
)?.[0] as Version | undefined;
|
||||
|
||||
if (!canonical) {
|
||||
return version;
|
||||
}
|
||||
|
||||
return canonical;
|
||||
}
|
||||
|
||||
describe.for(HS_VERSIONS)("Headscale %s: Runtime Client", (version) => {
|
||||
test("the runtime client is usable", async () => {
|
||||
const bootstrapper = await getBootstrapClient(version);
|
||||
const runtimeClient = bootstrapper.getRuntimeClient("test-api-key");
|
||||
expect(runtimeClient).toBeDefined();
|
||||
});
|
||||
|
||||
test("the runtime client has the correct canonical API version", async () => {
|
||||
const bootstrapper = await getBootstrapClient(version);
|
||||
expect(bootstrapper.apiVersion).toBe(getCanonicalVersion(version));
|
||||
});
|
||||
|
||||
test("the health check endpoint works", async () => {
|
||||
const client = await getRuntimeClient(version);
|
||||
const health = await client.isHealthy();
|
||||
expect(health).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,104 @@
|
||||
import { Client } from "undici";
|
||||
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
||||
|
||||
import DockerIntegration from "~/server/config/integration/docker";
|
||||
|
||||
import { type HeadscaleEnv, startHeadscale } from "../setup/start-headscale";
|
||||
|
||||
const TEST_LABEL_KEY = "me.tale.headplane.integration-test";
|
||||
const TEST_LABEL_VALUE = `docker-${Date.now()}`;
|
||||
|
||||
describe("DockerIntegration", () => {
|
||||
let env: HeadscaleEnv;
|
||||
let dockerSocket: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
dockerSocket = process.env.DOCKER_HOST ?? "unix:///var/run/docker.sock";
|
||||
env = await startHeadscale("0.25.1", {
|
||||
labels: { [TEST_LABEL_KEY]: TEST_LABEL_VALUE },
|
||||
});
|
||||
}, 60_000);
|
||||
|
||||
afterAll(async () => {
|
||||
await env?.container.stop({ remove: true, removeVolumes: true });
|
||||
});
|
||||
|
||||
test("isAvailable finds the headscale container by label", async () => {
|
||||
const integration = new DockerIntegration({
|
||||
enabled: true,
|
||||
container_label: `${TEST_LABEL_KEY}=${TEST_LABEL_VALUE}`,
|
||||
socket: dockerSocket,
|
||||
});
|
||||
|
||||
expect(await integration.isAvailable()).toBe(true);
|
||||
});
|
||||
|
||||
test("isAvailable finds the headscale container by name", async () => {
|
||||
const name = env.container.getName().replace(/^\//, "");
|
||||
const integration = new DockerIntegration({
|
||||
enabled: true,
|
||||
container_name: name,
|
||||
container_label: "unused=unused",
|
||||
socket: dockerSocket,
|
||||
});
|
||||
|
||||
expect(await integration.isAvailable()).toBe(true);
|
||||
});
|
||||
|
||||
test("isAvailable returns false for a non-existent label", async () => {
|
||||
const integration = new DockerIntegration({
|
||||
enabled: true,
|
||||
container_label: "me.tale.headplane.nonexistent=true",
|
||||
socket: dockerSocket,
|
||||
});
|
||||
|
||||
expect(await integration.isAvailable()).toBe(false);
|
||||
});
|
||||
|
||||
test("isAvailable returns false for an invalid socket", async () => {
|
||||
const integration = new DockerIntegration({
|
||||
enabled: true,
|
||||
container_label: `${TEST_LABEL_KEY}=${TEST_LABEL_VALUE}`,
|
||||
socket: "unix:///tmp/nonexistent-docker.sock",
|
||||
});
|
||||
|
||||
expect(await integration.isAvailable()).toBe(false);
|
||||
});
|
||||
|
||||
test("onConfigChange restarts the container", async () => {
|
||||
const integration = new DockerIntegration({
|
||||
enabled: true,
|
||||
container_label: `${TEST_LABEL_KEY}=${TEST_LABEL_VALUE}`,
|
||||
socket: dockerSocket,
|
||||
});
|
||||
|
||||
expect(await integration.isAvailable()).toBe(true);
|
||||
|
||||
// Health check goes through the Docker socket to avoid stale port
|
||||
// mappings after container restart.
|
||||
const containerId = env.container.getId();
|
||||
const dockerClient = new Client("http://localhost", {
|
||||
socketPath: "/var/run/docker.sock",
|
||||
});
|
||||
|
||||
const mockClient = {
|
||||
isHealthy: async () => {
|
||||
try {
|
||||
const res = await dockerClient.request({
|
||||
method: "GET",
|
||||
path: `/v1.44/containers/${containerId}/json`,
|
||||
});
|
||||
const info = (await res.body.json()) as any;
|
||||
return info.State?.Running === true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
} as any;
|
||||
|
||||
await integration.onConfigChange(mockClient);
|
||||
|
||||
const healthy = await mockClient.isHealthy();
|
||||
expect(healthy).toBe(true);
|
||||
}, 60_000);
|
||||
});
|
||||
@@ -0,0 +1,103 @@
|
||||
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import tc from "testcontainers";
|
||||
import { build } from "vite";
|
||||
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
||||
|
||||
// This is a little shim file that we use to execute the real proc-helper that
|
||||
// is bundled through Vite in order to test against a real Linux /proc FS.
|
||||
const testRunner = `
|
||||
const { findHeadscaleServe } = require("./proc-helper.js");
|
||||
|
||||
async function main() {
|
||||
const pid = await findHeadscaleServe();
|
||||
console.log(JSON.stringify({
|
||||
pid,
|
||||
found: typeof pid === "number" && pid > 0,
|
||||
}));
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.log(JSON.stringify({ error: e.message }));
|
||||
process.exit(1);
|
||||
});
|
||||
`;
|
||||
|
||||
const root = fileURLToPath(new URL("../../..", import.meta.url));
|
||||
const procHelper = join(root, "app/server/config/integration/proc-helper.ts");
|
||||
const appDir = join(root, "app");
|
||||
|
||||
describe("ProcIntegration", () => {
|
||||
let container: tc.StartedTestContainer;
|
||||
let tmpDir: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
tmpDir = await mkdtemp(join(tmpdir(), "headplane-proc-test-"));
|
||||
const bundlePath = join(tmpDir, "proc-helper.js");
|
||||
|
||||
await build({
|
||||
configFile: false,
|
||||
logLevel: "silent",
|
||||
build: {
|
||||
lib: {
|
||||
entry: procHelper,
|
||||
formats: ["cjs"],
|
||||
fileName: () => "proc-helper.js",
|
||||
},
|
||||
outDir: tmpDir,
|
||||
emptyOutDir: false,
|
||||
rollupOptions: {
|
||||
external: (id) => id.startsWith("node:"),
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: { "~": appDir },
|
||||
},
|
||||
});
|
||||
|
||||
const runnerPath = join(tmpDir, "test-runner.js");
|
||||
await writeFile(runnerPath, testRunner);
|
||||
|
||||
// Build a container image that has both headscale and Node.js using
|
||||
// a multi-stage Dockerfile. The headscale image is distroless (no shell)
|
||||
// so we pull the binary into node:alpine via COPY --from.
|
||||
await writeFile(
|
||||
join(tmpDir, "Dockerfile"),
|
||||
[
|
||||
"FROM headscale/headscale:0.25.1-debug AS headscale",
|
||||
"FROM node:24-alpine",
|
||||
"COPY --from=headscale /ko-app/headscale /usr/local/bin/headscale",
|
||||
].join("\n"),
|
||||
);
|
||||
|
||||
const configPath = join(root, "tests/integration/setup/config.yaml");
|
||||
const image = await tc.GenericContainer.fromDockerfile(tmpDir).build();
|
||||
|
||||
container = await image
|
||||
.withCopyFilesToContainer([
|
||||
{ source: configPath, target: "/etc/headscale/config.yaml" },
|
||||
{ source: bundlePath, target: "/test/proc-helper.js" },
|
||||
{ source: runnerPath, target: "/test/test-runner.js" },
|
||||
])
|
||||
.withCommand(["sh", "-c", "headscale serve & sleep 2 && sleep infinity"])
|
||||
.withWaitStrategy(tc.Wait.forLogMessage("headscale", 1).withStartupTimeout(30_000))
|
||||
.start();
|
||||
}, 120_000);
|
||||
|
||||
afterAll(async () => {
|
||||
await container?.stop({ remove: true, removeVolumes: true });
|
||||
await rm(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("findHeadscaleServe finds the real headscale process", async () => {
|
||||
const result = await container.exec(["node", "/test/test-runner.js"]);
|
||||
const output = JSON.parse(result.stdout.trim());
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(output.found).toBe(true);
|
||||
expect(output.pid).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
@@ -1,51 +1,54 @@
|
||||
import { join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import tc from 'testcontainers';
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import tc from "testcontainers";
|
||||
|
||||
export interface HeadscaleEnv {
|
||||
container: tc.StartedTestContainer;
|
||||
apiUrl: string;
|
||||
apiKey: string;
|
||||
container: tc.StartedTestContainer;
|
||||
apiUrl: string;
|
||||
apiKey: string;
|
||||
}
|
||||
|
||||
export interface HeadscaleOptions {
|
||||
labels?: Record<string, string>;
|
||||
}
|
||||
|
||||
const cwd = fileURLToPath(import.meta.url);
|
||||
const config = join(cwd, '..', 'config.yaml');
|
||||
const config = join(cwd, "..", "config.yaml");
|
||||
|
||||
export async function startHeadscale(version: string): Promise<HeadscaleEnv> {
|
||||
const container = await new tc.GenericContainer(
|
||||
`headscale/headscale:${version}`,
|
||||
)
|
||||
.withExposedPorts(8080)
|
||||
.withWaitStrategy(
|
||||
tc.Wait.forHttp('/health', 8080)
|
||||
.withStartupTimeout(30_000)
|
||||
.forStatusCode(200),
|
||||
)
|
||||
.withCopyFilesToContainer([
|
||||
{
|
||||
source: config,
|
||||
target: '/etc/headscale/config.yaml',
|
||||
},
|
||||
])
|
||||
.withCommand(['serve'])
|
||||
.start();
|
||||
export async function startHeadscale(
|
||||
version: string,
|
||||
options?: HeadscaleOptions,
|
||||
): Promise<HeadscaleEnv> {
|
||||
let builder = new tc.GenericContainer(`headscale/headscale:${version}`)
|
||||
.withExposedPorts(8080)
|
||||
.withWaitStrategy(
|
||||
tc.Wait.forHttp("/health", 8080).withStartupTimeout(30_000).forStatusCode(200),
|
||||
)
|
||||
.withCopyFilesToContainer([
|
||||
{
|
||||
source: config,
|
||||
target: "/etc/headscale/config.yaml",
|
||||
},
|
||||
])
|
||||
.withCommand(["serve"]);
|
||||
|
||||
const host = container.getHost();
|
||||
const port = container.getMappedPort(8080);
|
||||
const apiUrl = `http://${host}:${port}`;
|
||||
if (options?.labels) {
|
||||
builder = builder.withLabels(options.labels);
|
||||
}
|
||||
|
||||
const exec = await container.exec([
|
||||
'headscale',
|
||||
'apikeys',
|
||||
'create',
|
||||
'-o',
|
||||
'json',
|
||||
]);
|
||||
const container = await builder.start();
|
||||
|
||||
if (exec.exitCode !== 0) {
|
||||
throw new Error(`headscale apikeys create failed:\n${exec.stderr}`);
|
||||
}
|
||||
const host = container.getHost();
|
||||
const port = container.getMappedPort(8080);
|
||||
const apiUrl = `http://${host}:${port}`;
|
||||
|
||||
const apiKey = JSON.parse(exec.stdout.toString());
|
||||
return { container, apiUrl, apiKey };
|
||||
const exec = await container.exec(["headscale", "apikeys", "create", "-o", "json"]);
|
||||
|
||||
if (exec.exitCode !== 0) {
|
||||
throw new Error(`headscale apikeys create failed:\n${exec.stderr}`);
|
||||
}
|
||||
|
||||
const apiKey = JSON.parse(exec.stdout.toString());
|
||||
return { container, apiUrl, apiKey };
|
||||
}
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import { getRuntimeClient, HS_VERSIONS } from './setup/env';
|
||||
|
||||
describe.sequential.for(HS_VERSIONS)('Headscale %s: Users', (version) => {
|
||||
test('users can be created', async () => {
|
||||
const client = await getRuntimeClient(version);
|
||||
const user = await client.createUser('tale@');
|
||||
expect(user).toBeDefined();
|
||||
expect(user.name).toBe('tale@');
|
||||
});
|
||||
|
||||
test('users can be created with attributes', async () => {
|
||||
const client = await getRuntimeClient(version);
|
||||
const user = await client.createUser(
|
||||
'test-user@',
|
||||
'test-user@example.com',
|
||||
'Test User',
|
||||
'https://github.com/tale.png',
|
||||
);
|
||||
|
||||
expect(user).toBeDefined();
|
||||
expect(user.name).toBe('test-user@');
|
||||
expect(user.email).toBe('test-user@example.com');
|
||||
expect(user.displayName).toBe('Test User');
|
||||
expect(user.profilePicUrl).toBe('https://github.com/tale.png');
|
||||
});
|
||||
|
||||
test('users can be listed', async () => {
|
||||
const client = await getRuntimeClient(version);
|
||||
const users = await client.getUsers();
|
||||
expect(Array.isArray(users)).toBe(true);
|
||||
expect(users.length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
test('users can be listed by name', async () => {
|
||||
const client = await getRuntimeClient(version);
|
||||
const users = await client.getUsers(undefined, 'tale@');
|
||||
expect(Array.isArray(users)).toBe(true);
|
||||
expect(users.length).toBe(1);
|
||||
expect(users[0].name).toBe('tale@');
|
||||
});
|
||||
|
||||
test('users can be listed by email', async () => {
|
||||
const client = await getRuntimeClient(version);
|
||||
const users = await client.getUsers(
|
||||
undefined,
|
||||
undefined,
|
||||
'test-user@example.com',
|
||||
);
|
||||
expect(Array.isArray(users)).toBe(true);
|
||||
expect(users.length).toBe(1);
|
||||
expect(users[0].email).toBe('test-user@example.com');
|
||||
});
|
||||
|
||||
test('users can be renamed', async () => {
|
||||
const client = await getRuntimeClient(version);
|
||||
const usersBefore = await client.getUsers(undefined, 'tale@');
|
||||
expect(usersBefore.length).toBe(1);
|
||||
const user = usersBefore[0];
|
||||
|
||||
await client.renameUser(user.id, 'renamed-user@');
|
||||
const usersAfter = await client.getUsers(undefined, 'renamed-user@');
|
||||
expect(usersAfter.length).toBe(1);
|
||||
expect(usersAfter[0].id).toBe(user.id);
|
||||
});
|
||||
|
||||
test('users can be deleted', async () => {
|
||||
const client = await getRuntimeClient(version);
|
||||
const usersBefore = await client.getUsers(undefined, 'test-user@');
|
||||
expect(usersBefore.length).toBe(1);
|
||||
const user = usersBefore[0];
|
||||
|
||||
await client.deleteUser(user.id);
|
||||
const usersAfter = await client.getUsers(undefined, 'test-user@');
|
||||
expect(usersAfter.length).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -1,39 +0,0 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import canonicals from '~/openapi-canonical-families.json';
|
||||
import {
|
||||
getBootstrapClient,
|
||||
getRuntimeClient,
|
||||
HS_VERSIONS,
|
||||
Version,
|
||||
} from './setup/env';
|
||||
|
||||
function getCanonicalVersion(version: Version) {
|
||||
const canonical = Object.entries(canonicals).find(([_, family]) =>
|
||||
family.includes(version),
|
||||
)?.[0] as Version | undefined;
|
||||
|
||||
if (!canonical) {
|
||||
return version;
|
||||
}
|
||||
|
||||
return canonical;
|
||||
}
|
||||
|
||||
describe.for(HS_VERSIONS)('Headscale %s: Runtime Client', (version) => {
|
||||
test('the runtime client is usable', async () => {
|
||||
const bootstrapper = await getBootstrapClient(version);
|
||||
const runtimeClient = bootstrapper.getRuntimeClient('test-api-key');
|
||||
expect(runtimeClient).toBeDefined();
|
||||
});
|
||||
|
||||
test('the runtime client has the correct canonical API version', async () => {
|
||||
const bootstrapper = await getBootstrapClient(version);
|
||||
expect(bootstrapper.apiVersion).toBe(getCanonicalVersion(version));
|
||||
});
|
||||
|
||||
test('the health check endpoint works', async () => {
|
||||
const client = await getRuntimeClient(version);
|
||||
const health = await client.isHealthy();
|
||||
expect(health).toBe(true);
|
||||
});
|
||||
});
|
||||
+53
-44
@@ -1,54 +1,63 @@
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { resolve } from 'node:path';
|
||||
import { defineConfig } from 'vitest/config';
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
const prefix = process.env.__INTERNAL_PREFIX || '/admin';
|
||||
if (prefix.endsWith('/')) {
|
||||
throw new Error('Prefix must not end with a slash');
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
const prefix = process.env.__INTERNAL_PREFIX || "/admin";
|
||||
if (prefix.endsWith("/")) {
|
||||
throw new Error("Prefix must not end with a slash");
|
||||
}
|
||||
|
||||
// Load the version via package.json
|
||||
const pkg = await readFile('package.json', 'utf-8');
|
||||
const isNext = process.env.IMAGE_TAG?.includes('next');
|
||||
const pkg = await readFile("package.json", "utf-8");
|
||||
const isNext = process.env.IMAGE_TAG?.includes("next");
|
||||
const { version } = JSON.parse(pkg);
|
||||
if (!version) {
|
||||
throw new Error('Unable to read version from package.json');
|
||||
throw new Error("Unable to read version from package.json");
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
projects: [
|
||||
{
|
||||
extends: true,
|
||||
test: {
|
||||
name: 'unit',
|
||||
include: ['tests/unit/**/*.test.ts'],
|
||||
setupFiles: ['tests/unit/setup/overlay-fs.ts'],
|
||||
},
|
||||
},
|
||||
{
|
||||
extends: true,
|
||||
test: {
|
||||
name: 'integration',
|
||||
include: ['tests/integration/*.test.ts'],
|
||||
setupFiles: ['tests/integration/setup/vitest-hook.ts'],
|
||||
testTimeout: 15_000,
|
||||
},
|
||||
},
|
||||
],
|
||||
env: {
|
||||
HEADPLANE_DEBUG_LOG: 'true',
|
||||
},
|
||||
environment: 'node',
|
||||
exclude: ['node_modules/**', 'build/**'],
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'~': resolve(__dirname, './app'),
|
||||
},
|
||||
},
|
||||
define: {
|
||||
__VERSION__: JSON.stringify(isNext ? `${version}-next` : version),
|
||||
__PREFIX__: JSON.stringify(prefix),
|
||||
},
|
||||
test: {
|
||||
projects: [
|
||||
{
|
||||
extends: true,
|
||||
test: {
|
||||
name: "unit",
|
||||
include: ["tests/unit/**/*.test.ts"],
|
||||
setupFiles: ["tests/unit/setup/overlay-fs.ts"],
|
||||
},
|
||||
},
|
||||
{
|
||||
extends: true,
|
||||
test: {
|
||||
name: "integration:api",
|
||||
include: ["tests/integration/api/**/*.test.ts"],
|
||||
setupFiles: ["tests/integration/setup/vitest-hook.ts"],
|
||||
testTimeout: 15_000,
|
||||
},
|
||||
},
|
||||
{
|
||||
extends: true,
|
||||
test: {
|
||||
name: "integration:platform",
|
||||
include: ["tests/integration/platform/**/*.test.ts"],
|
||||
testTimeout: 60_000,
|
||||
},
|
||||
},
|
||||
],
|
||||
env: {
|
||||
HEADPLANE_DEBUG_LOG: "true",
|
||||
},
|
||||
environment: "node",
|
||||
exclude: ["node_modules/**", "build/**"],
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
"~": resolve(__dirname, "./app"),
|
||||
},
|
||||
},
|
||||
define: {
|
||||
__VERSION__: JSON.stringify(isNext ? `${version}-next` : version),
|
||||
__PREFIX__: JSON.stringify(prefix),
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user