mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-11-01 08:05:23 +00:00
feat: move acme certificate automation into a plugin (C7)
This commit is contained in:
1 parent
4dbce08379
commit
8c1e02af89
123 files changed
+4808
-1912
No files matched your search
@@ -0,0 +1,102 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { PluginFetchInit } from "@termix/plugin-sdk/backend";
|
||||
import { createFetchAdapter } from "../../src/backend/acme.js";
|
||||
import { createTxtRecord } from "../../src/backend/cloudflare.js";
|
||||
|
||||
describe("createFetchAdapter", () => {
|
||||
it("sends acme-client requests through ctx.fetch", async () => {
|
||||
const fetch = vi.fn(
|
||||
async (_url: string, _init?: PluginFetchInit) =>
|
||||
new Response('{"ok":true}', {
|
||||
status: 201,
|
||||
headers: { "replay-nonce": "n1", location: "https://ca/acct/1" },
|
||||
}),
|
||||
);
|
||||
const adapter = createFetchAdapter(fetch, ["ca.internal"]);
|
||||
const result = await adapter({
|
||||
url: "https://ca.internal/new-acct",
|
||||
method: "post",
|
||||
headers: { "Content-Type": "application/jose+json" },
|
||||
data: '{"payload":"x"}',
|
||||
validateStatus: null,
|
||||
});
|
||||
|
||||
expect(fetch).toHaveBeenCalledWith(
|
||||
"https://ca.internal/new-acct",
|
||||
expect.objectContaining({
|
||||
method: "POST",
|
||||
body: '{"payload":"x"}',
|
||||
allowPrivateHosts: ["ca.internal"],
|
||||
headers: { "Content-Type": "application/jose+json" },
|
||||
}),
|
||||
);
|
||||
expect(result.status).toBe(201);
|
||||
expect(result.data).toBe('{"ok":true}');
|
||||
expect(result.headers["replay-nonce"]).toBe("n1");
|
||||
});
|
||||
|
||||
it("rejects with the response attached when validateStatus says no", async () => {
|
||||
const adapter = createFetchAdapter(
|
||||
async () => new Response("slow down", { status: 429 }),
|
||||
);
|
||||
await expect(
|
||||
adapter({ url: "https://ca/x", validateStatus: () => false }),
|
||||
).rejects.toMatchObject({
|
||||
code: "ERR_BAD_REQUEST",
|
||||
response: { status: 429, data: "slow down" },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("createTxtRecord", () => {
|
||||
it("finds the zone by parent domain, creates the record and deletes it", async () => {
|
||||
const calls: Array<{ url: string; method: string; body?: string }> = [];
|
||||
const fetch = vi.fn(async (url: string, init?: PluginFetchInit) => {
|
||||
calls.push({ url, method: init?.method ?? "GET", body: init?.body });
|
||||
if (url.includes("/zones?name=example.com")) {
|
||||
return Response.json({
|
||||
success: true,
|
||||
result: [{ id: "zone-1", name: "example.com" }],
|
||||
});
|
||||
}
|
||||
if (url.includes("/zones?name=")) {
|
||||
return Response.json({ success: true, result: [] });
|
||||
}
|
||||
if (init?.method === "POST") {
|
||||
return Response.json({ success: true, result: { id: "rec-1" } });
|
||||
}
|
||||
return Response.json({ success: true, result: {} });
|
||||
});
|
||||
|
||||
const remove = await createTxtRecord(
|
||||
fetch,
|
||||
"token",
|
||||
"_acme-challenge.termix.example.com",
|
||||
"digest",
|
||||
);
|
||||
const post = calls.find((call) => call.method === "POST")!;
|
||||
expect(post.url).toContain("/zones/zone-1/dns_records");
|
||||
expect(JSON.parse(post.body!)).toMatchObject({
|
||||
type: "TXT",
|
||||
name: "_acme-challenge.termix.example.com",
|
||||
content: "digest",
|
||||
});
|
||||
|
||||
await remove();
|
||||
expect(calls.at(-1)).toMatchObject({
|
||||
method: "DELETE",
|
||||
url: expect.stringContaining("/zones/zone-1/dns_records/rec-1"),
|
||||
});
|
||||
});
|
||||
|
||||
it("surfaces the Cloudflare error message", async () => {
|
||||
const fetch = async () =>
|
||||
Response.json(
|
||||
{ success: false, errors: [{ message: "Invalid token" }] },
|
||||
{ status: 403 },
|
||||
);
|
||||
await expect(
|
||||
createTxtRecord(fetch, "bad", "_acme-challenge.a.example", "d"),
|
||||
).rejects.toThrow(/Invalid token/);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user