mirror of
https://github.com/tale/headplane.git
synced 2026-08-27 15:37:04 +00:00
feat: replace openapi hashing system with /version
Apparently I didn't use my brain cells and rely on the /version endpoint that Headscale has exposed since 0.26 (our lowest supported version). Switching to that significantly simplifies the API surface.
This commit is contained in:
@@ -5,7 +5,7 @@ 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();
|
||||
const apiKeys = await client.apiKeys.list();
|
||||
expect(Array.isArray(apiKeys)).toBe(true);
|
||||
expect(apiKeys.length).toBe(1);
|
||||
});
|
||||
|
||||
@@ -9,8 +9,8 @@ describe.sequential.for(HS_VERSIONS)("Headscale %s: Users", (version) => {
|
||||
const client = await getRuntimeClient(version);
|
||||
const tailnetNode = await getNode(version);
|
||||
|
||||
const user = await client.createUser("node-reg@");
|
||||
const node = await client.registerNode(user.name, tailnetNode.authCode);
|
||||
const user = await client.users.create({ name: "node-reg@" });
|
||||
const node = await client.nodes.register(user.name, tailnetNode.authCode);
|
||||
expect(node).toBeDefined();
|
||||
expect(node.registerMethod).toBe("REGISTER_METHOD_CLI");
|
||||
expect(node.name).toBe(tailnetNode.nodeName);
|
||||
@@ -19,12 +19,12 @@ describe.sequential.for(HS_VERSIONS)("Headscale %s: Users", (version) => {
|
||||
test("nodes can be retrieved", async () => {
|
||||
const client = await getRuntimeClient(version);
|
||||
const { nodeName } = await getNode(version);
|
||||
const nodes = await client.getNodes();
|
||||
const nodes = await client.nodes.list();
|
||||
const node = nodes.find((n) => n.name === nodeName);
|
||||
expect(node).toBeDefined();
|
||||
expect(node?.name).toBe(nodeName);
|
||||
|
||||
const fetchedNode = await client.getNode(node!.id);
|
||||
const fetchedNode = await client.nodes.get(node!.id);
|
||||
expect(fetchedNode).toBeDefined();
|
||||
expect(fetchedNode.id).toBe(node!.id);
|
||||
workingNodeId = node!.id;
|
||||
@@ -35,41 +35,43 @@ describe.sequential.for(HS_VERSIONS)("Headscale %s: Users", (version) => {
|
||||
const { nodeName } = await getNode(version);
|
||||
const newName = `${nodeName}-renamed`;
|
||||
|
||||
await client.renameNode(workingNodeId, newName);
|
||||
const renamedNode = await client.getNode(workingNodeId);
|
||||
await client.nodes.rename(workingNodeId, newName);
|
||||
const renamedNode = await client.nodes.get(workingNodeId);
|
||||
expect(renamedNode).toBeDefined();
|
||||
expect(renamedNode.givenName).toBe(newName);
|
||||
});
|
||||
|
||||
test("nodes can be reassigned to another user", async (context) => {
|
||||
const bootstrap = await getBootstrapClient(version);
|
||||
if (bootstrap.clientHelpers.isAtleast("0.28.0")) {
|
||||
// Reassigning a node owner was removed in 0.28.
|
||||
if (bootstrap.capabilities.nodeOwnerIsImmutable) {
|
||||
context.skip();
|
||||
}
|
||||
|
||||
const client = await getRuntimeClient(version);
|
||||
const user = await client.createUser("node-reassign@");
|
||||
const user = await client.users.create({ name: "node-reassign@" });
|
||||
|
||||
await client.setNodeUser(workingNodeId, user.id);
|
||||
const reassignedNode = await client.getNode(workingNodeId);
|
||||
// reassignUser is only defined on pre-0.28 clients, hence the guard above.
|
||||
await client.nodes.reassignUser!(workingNodeId, user.id);
|
||||
const reassignedNode = await client.nodes.get(workingNodeId);
|
||||
expect(reassignedNode).toBeDefined();
|
||||
expect(reassignedNode.user.name).toBe(user.name);
|
||||
expect(reassignedNode.user?.name).toBe(user.name);
|
||||
});
|
||||
|
||||
test("nodes can be expired", async () => {
|
||||
const client = await getRuntimeClient(version);
|
||||
await client.expireNode(workingNodeId);
|
||||
await client.nodes.expire(workingNodeId);
|
||||
|
||||
const expiredNode = await client.getNode(workingNodeId);
|
||||
const expiredNode = await client.nodes.get(workingNodeId);
|
||||
expect(expiredNode).toBeDefined();
|
||||
expect(expiredNode.expiry).toBeDefined();
|
||||
});
|
||||
|
||||
test("nodes can be deleted", async () => {
|
||||
const client = await getRuntimeClient(version);
|
||||
await client.deleteNode(workingNodeId);
|
||||
await client.nodes.delete(workingNodeId);
|
||||
|
||||
const nodes = await client.getNodes();
|
||||
const nodes = await client.nodes.list();
|
||||
const node = nodes.find((n) => n.id === workingNodeId);
|
||||
expect(node).toBeUndefined();
|
||||
});
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import { getBootstrapClient, getIsAtLeast, getRuntimeClient, HS_VERSIONS } from "../setup/env";
|
||||
import { getBootstrapClient, 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 () => {
|
||||
const client = await getRuntimeClient(version);
|
||||
const preAuthKeyUser = await client.createUser("preauthkeyuser@");
|
||||
const preAuthKeyUser = await client.users.create({ name: "preauthkeyuser@" });
|
||||
expect(preAuthKeyUser).toBeDefined();
|
||||
expect(preAuthKeyUser.name).toBe("preauthkeyuser@");
|
||||
|
||||
const expiry = new Date(Date.now() + 3600 * 1000);
|
||||
const preAuthKey = await client.createPreAuthKey(preAuthKeyUser.id, false, false, expiry, null);
|
||||
const preAuthKey = await client.preAuthKeys.create({
|
||||
user: preAuthKeyUser.id,
|
||||
ephemeral: false,
|
||||
reusable: false,
|
||||
expiration: expiry,
|
||||
aclTags: null,
|
||||
});
|
||||
|
||||
expect(preAuthKey).toBeDefined();
|
||||
expect(preAuthKey.user?.id).toBe(preAuthKeyUser.id);
|
||||
@@ -22,12 +28,18 @@ describe.sequential.for(HS_VERSIONS)("Headscale %s: Pre-auth Keys", (version) =>
|
||||
|
||||
test("pre-auth keys can be created with ACL tags", async () => {
|
||||
const client = await getRuntimeClient(version);
|
||||
const [preAuthKeyUser] = await client.getUsers(undefined, "preauthkeyuser@");
|
||||
const [preAuthKeyUser] = await client.users.list({ name: "preauthkeyuser@" });
|
||||
expect(preAuthKeyUser).toBeDefined();
|
||||
expect(preAuthKeyUser.name).toBe("preauthkeyuser@");
|
||||
|
||||
const aclTags = ["tag:test1", "tag:test2"];
|
||||
const preAuthKey = await client.createPreAuthKey(preAuthKeyUser.id, true, true, null, aclTags);
|
||||
const preAuthKey = await client.preAuthKeys.create({
|
||||
user: preAuthKeyUser.id,
|
||||
ephemeral: true,
|
||||
reusable: true,
|
||||
expiration: null,
|
||||
aclTags,
|
||||
});
|
||||
|
||||
expect(preAuthKey).toBeDefined();
|
||||
expect(preAuthKey.user?.id).toBe(preAuthKeyUser.id);
|
||||
@@ -38,13 +50,19 @@ describe.sequential.for(HS_VERSIONS)("Headscale %s: Pre-auth Keys", (version) =>
|
||||
|
||||
test("tag-only pre-auth keys (0.28+)", async (context) => {
|
||||
const bootstrap = await getBootstrapClient(version);
|
||||
if (!bootstrap.clientHelpers.isAtleast("0.28.0")) {
|
||||
if (!bootstrap.capabilities.preAuthKeysHaveStableIds) {
|
||||
context.skip();
|
||||
}
|
||||
|
||||
const client = await getRuntimeClient(version);
|
||||
const aclTags = ["tag:server", "tag:prod"];
|
||||
const preAuthKey = await client.createPreAuthKey(null, false, true, null, aclTags);
|
||||
const preAuthKey = await client.preAuthKeys.create({
|
||||
user: null,
|
||||
ephemeral: false,
|
||||
reusable: true,
|
||||
expiration: null,
|
||||
aclTags,
|
||||
});
|
||||
|
||||
expect(preAuthKey).toBeDefined();
|
||||
expect(preAuthKey.user).toBeNull();
|
||||
@@ -55,44 +73,44 @@ describe.sequential.for(HS_VERSIONS)("Headscale %s: Pre-auth Keys", (version) =>
|
||||
|
||||
test("pre-auth keys can be listed", async () => {
|
||||
const client = await getRuntimeClient(version);
|
||||
const [preAuthKeyUser] = await client.getUsers(undefined, "preauthkeyuser@");
|
||||
const [preAuthKeyUser] = await client.users.list({ name: "preauthkeyuser@" });
|
||||
expect(preAuthKeyUser).toBeDefined();
|
||||
expect(preAuthKeyUser.name).toBe("preauthkeyuser@");
|
||||
|
||||
const preAuthKeys = await client.getPreAuthKeys(preAuthKeyUser.id);
|
||||
const preAuthKeys = await client.preAuthKeys.listForUser(preAuthKeyUser.id);
|
||||
expect(Array.isArray(preAuthKeys)).toBe(true);
|
||||
expect(preAuthKeys.length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
test("all pre-auth keys can be listed without user filter (0.28+)", async (context) => {
|
||||
const isAtLeast = await getIsAtLeast(version);
|
||||
if (!isAtLeast("0.28.0")) {
|
||||
const bootstrap = await getBootstrapClient(version);
|
||||
if (!bootstrap.capabilities.preAuthKeysHaveStableIds) {
|
||||
context.skip();
|
||||
}
|
||||
|
||||
const client = await getRuntimeClient(version);
|
||||
const [preAuthKeyUser] = await client.getUsers(undefined, "preauthkeyuser@");
|
||||
const [preAuthKeyUser] = await client.users.list({ name: "preauthkeyuser@" });
|
||||
expect(preAuthKeyUser).toBeDefined();
|
||||
|
||||
const allKeys = await client.getAllPreAuthKeys();
|
||||
const allKeys = await client.preAuthKeys.listAll!();
|
||||
expect(Array.isArray(allKeys)).toBe(true);
|
||||
expect(allKeys.length).toBeGreaterThanOrEqual(2);
|
||||
|
||||
const userSpecificKeys = await client.getPreAuthKeys(preAuthKeyUser.id);
|
||||
const userSpecificKeys = await client.preAuthKeys.listForUser(preAuthKeyUser.id);
|
||||
for (const userKey of userSpecificKeys) {
|
||||
const found = allKeys.find((k) => k.key === userKey.key);
|
||||
expect(found).toBeDefined();
|
||||
}
|
||||
});
|
||||
|
||||
test("getAllPreAuthKeys returns keys with correct structure (0.28+)", async (context) => {
|
||||
const isAtLeast = await getIsAtLeast(version);
|
||||
if (!isAtLeast("0.28.0")) {
|
||||
test("listAll returns keys with correct structure (0.28+)", async (context) => {
|
||||
const bootstrap = await getBootstrapClient(version);
|
||||
if (!bootstrap.capabilities.preAuthKeysHaveStableIds) {
|
||||
context.skip();
|
||||
}
|
||||
|
||||
const client = await getRuntimeClient(version);
|
||||
const allKeys = await client.getAllPreAuthKeys();
|
||||
const allKeys = await client.preAuthKeys.listAll!();
|
||||
|
||||
for (const key of allKeys) {
|
||||
expect(key.id).toBeDefined();
|
||||
@@ -107,16 +125,16 @@ describe.sequential.for(HS_VERSIONS)("Headscale %s: Pre-auth Keys", (version) =>
|
||||
|
||||
test("pre-auth keys can be expired", async () => {
|
||||
const client = await getRuntimeClient(version);
|
||||
const [preAuthKeyUser] = await client.getUsers(undefined, "preauthkeyuser@");
|
||||
const [preAuthKeyUser] = await client.users.list({ name: "preauthkeyuser@" });
|
||||
expect(preAuthKeyUser).toBeDefined();
|
||||
expect(preAuthKeyUser.name).toBe("preauthkeyuser@");
|
||||
|
||||
const preAuthKeys = await client.getPreAuthKeys(preAuthKeyUser.id);
|
||||
const preAuthKeys = await client.preAuthKeys.listForUser(preAuthKeyUser.id);
|
||||
expect(preAuthKeys.length).toBeGreaterThanOrEqual(2);
|
||||
const preAuthKeyToExpire = preAuthKeys[0];
|
||||
await client.expirePreAuthKey(preAuthKeyUser.id, preAuthKeyToExpire);
|
||||
await client.preAuthKeys.expire(preAuthKeyToExpire);
|
||||
|
||||
const preAuthKeysAfterExpire = await client.getPreAuthKeys(preAuthKeyUser.id);
|
||||
const preAuthKeysAfterExpire = await client.preAuthKeys.listForUser(preAuthKeyUser.id);
|
||||
const expiredKey = preAuthKeysAfterExpire.find((key) => key.key === preAuthKeyToExpire.key);
|
||||
expect(expiredKey).toBeDefined();
|
||||
});
|
||||
|
||||
@@ -5,19 +5,19 @@ 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@");
|
||||
const user = await client.users.create({ name: "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",
|
||||
);
|
||||
const user = await client.users.create({
|
||||
name: "test-user@",
|
||||
email: "test-user@example.com",
|
||||
displayName: "Test User",
|
||||
pictureUrl: "https://github.com/tale.png",
|
||||
});
|
||||
|
||||
expect(user).toBeDefined();
|
||||
expect(user.name).toBe("test-user@");
|
||||
@@ -28,14 +28,14 @@ describe.sequential.for(HS_VERSIONS)("Headscale %s: Users", (version) => {
|
||||
|
||||
test("users can be listed", async () => {
|
||||
const client = await getRuntimeClient(version);
|
||||
const users = await client.getUsers();
|
||||
const users = await client.users.list();
|
||||
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@");
|
||||
const users = await client.users.list({ name: "tale@" });
|
||||
expect(Array.isArray(users)).toBe(true);
|
||||
expect(users.length).toBe(1);
|
||||
expect(users[0].name).toBe("tale@");
|
||||
@@ -43,7 +43,7 @@ describe.sequential.for(HS_VERSIONS)("Headscale %s: Users", (version) => {
|
||||
|
||||
test("users can be listed by email", async () => {
|
||||
const client = await getRuntimeClient(version);
|
||||
const users = await client.getUsers(undefined, undefined, "test-user@example.com");
|
||||
const users = await client.users.list({ email: "test-user@example.com" });
|
||||
expect(Array.isArray(users)).toBe(true);
|
||||
expect(users.length).toBe(1);
|
||||
expect(users[0].email).toBe("test-user@example.com");
|
||||
@@ -51,24 +51,24 @@ describe.sequential.for(HS_VERSIONS)("Headscale %s: Users", (version) => {
|
||||
|
||||
test("users can be renamed", async () => {
|
||||
const client = await getRuntimeClient(version);
|
||||
const usersBefore = await client.getUsers(undefined, "tale@");
|
||||
const usersBefore = await client.users.list({ name: "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@");
|
||||
await client.users.rename(user.id, "renamed-user@");
|
||||
const usersAfter = await client.users.list({ name: "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@");
|
||||
const usersBefore = await client.users.list({ name: "test-user@" });
|
||||
expect(usersBefore.length).toBe(1);
|
||||
const user = usersBefore[0];
|
||||
|
||||
await client.deleteUser(user.id);
|
||||
const usersAfter = await client.getUsers(undefined, "test-user@");
|
||||
await client.users.delete(user.id);
|
||||
const usersAfter = await client.users.list({ name: "test-user@" });
|
||||
expect(usersAfter.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,31 +1,36 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import canonicals from "~/openapi-canonical-families.json";
|
||||
import { gte } from "~/server/headscale/api/server-version";
|
||||
|
||||
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");
|
||||
const runtimeClient = bootstrapper.client("test-api-key");
|
||||
expect(runtimeClient).toBeDefined();
|
||||
});
|
||||
|
||||
test("the runtime client has the correct canonical API version", async () => {
|
||||
test("the server version reported by /version matches the running container", async () => {
|
||||
const bootstrapper = await getBootstrapClient(version);
|
||||
expect(bootstrapper.apiVersion).toBe(getCanonicalVersion(version));
|
||||
expect(bootstrapper.version.unknown).toBe(false);
|
||||
const reported = `${bootstrapper.version.major}.${bootstrapper.version.minor}.${bootstrapper.version.patch}`;
|
||||
expect(reported).toBe(version);
|
||||
});
|
||||
|
||||
test("capabilities are derived correctly from the detected version", async (context) => {
|
||||
const bootstrapper = await getBootstrapClient(version);
|
||||
const v = bootstrapper.version;
|
||||
expect(bootstrapper.capabilities.preAuthKeysHaveStableIds).toBe(gte(v, "0.28.0"));
|
||||
expect(bootstrapper.capabilities.nodeTagsAreFlat).toBe(gte(v, "0.28.0"));
|
||||
expect(bootstrapper.capabilities.nodeOwnerIsImmutable).toBe(gte(v, "0.28.0"));
|
||||
expect(bootstrapper.capabilities.policyErrorsUseModernFormat).toBe(gte(v, "0.27.0"));
|
||||
// The known version table only has 0.26+; if a future version is added
|
||||
// before this test is updated, surface that explicitly rather than passing.
|
||||
const known: Version[] = ["0.26.1", "0.27.0", "0.27.1", "0.28.0"];
|
||||
if (!known.includes(version)) {
|
||||
context.skip();
|
||||
}
|
||||
});
|
||||
|
||||
test("the health check endpoint works", async () => {
|
||||
|
||||
Reference in New Issue
Block a user