import type { Domain } from "@dokploy/server"; import { createDomainLabels } from "@dokploy/server"; import { describe, expect, it } from "vitest"; import { parse, stringify } from "yaml"; /** * Regression tests for Traefik Host rule label format. * * These tests verify that the Host rule is generated with the correct format: * - Host(`domain.com`) - with opening and closing parentheses * - Host(`domain.com`) && PathPrefix(`/path`) - for path-based routing * * Issue: https://github.com/Dokploy/dokploy/issues/3161 * The bug caused Host rules to be malformed as Host`domain.com`) * (missing opening parenthesis) which broke all domain routing. */ describe("Host rule format regression tests", () => { const baseDomain: Domain = { host: "example.com", port: 8080, https: false, uniqueConfigKey: 1, customCertResolver: null, certificateType: "none", applicationId: "", composeId: "", domainType: "compose", serviceName: "test-app", domainId: "", path: "/", createdAt: "", previewDeploymentId: "", internalPath: "/", stripPath: false, customEntrypoint: null, middlewares: null, }; describe("Host rule format validation", () => { it("should generate Host rule with correct parentheses format", async () => { const labels = await createDomainLabels("test-app", baseDomain, "web"); const ruleLabel = labels.find((l) => l.includes(".rule=")); expect(ruleLabel).toBeDefined(); // Verify exact format: Host(`domain`) expect(ruleLabel).toMatch(/Host\(`[^`]+`\)/); // Ensure opening parenthesis is present after Host expect(ruleLabel).toContain("Host(`example.com`)"); // Ensure it does NOT have the malformed format expect(ruleLabel).not.toMatch(/Host`[^`]+`\)/); }); it("should generate PathPrefix with correct parentheses format", async () => { const labels = await createDomainLabels( "test-app", { ...baseDomain, path: "/api" }, "web", ); const ruleLabel = labels.find((l) => l.includes(".rule=")); expect(ruleLabel).toBeDefined(); // Verify PathPrefix format expect(ruleLabel).toMatch(/PathPrefix\(`[^`]+`\)/); expect(ruleLabel).toContain("PathPrefix(`/api`)"); // Ensure opening parenthesis is present expect(ruleLabel).not.toMatch(/PathPrefix`[^`]+`\)/); }); it("should generate combined Host and PathPrefix with correct format", async () => { const labels = await createDomainLabels( "test-app", { ...baseDomain, path: "/api/v1" }, "websecure", ); const ruleLabel = labels.find((l) => l.includes(".rule=")); expect(ruleLabel).toBeDefined(); expect(ruleLabel).toBe( "traefik.http.routers.test-app-1-websecure.rule=Host(`example.com`) && PathPrefix(`/api/v1`)", ); }); }); describe("YAML serialization preserves Host rule format", () => { it("should preserve Host rule format through YAML stringify/parse", async () => { const labels = await createDomainLabels("test-app", baseDomain, "web"); const ruleLabel = labels.find((l) => l.includes(".rule=")); // Simulate compose file structure const composeSpec = { services: { myapp: { image: "nginx", labels: labels, }, }, }; // Stringify to YAML const yamlOutput = stringify(composeSpec, { lineWidth: 1000 }); // Parse back const parsed = parse(yamlOutput) as typeof composeSpec; const parsedRuleLabel = parsed.services.myapp.labels.find((l: string) => l.includes(".rule="), ); // Verify format is preserved expect(parsedRuleLabel).toBe(ruleLabel); expect(parsedRuleLabel).toContain("Host(`example.com`)"); expect(parsedRuleLabel).not.toMatch(/Host`[^`]+`\)/); }); it("should preserve complex rule format through YAML serialization", async () => { const labels = await createDomainLabels( "test-app", { ...baseDomain, path: "/api", https: true }, "websecure", ); const composeSpec = { services: { myapp: { labels: labels, }, }, }; const yamlOutput = stringify(composeSpec, { lineWidth: 1000 }); const parsed = parse(yamlOutput) as typeof composeSpec; const parsedRuleLabel = parsed.services.myapp.labels.find((l: string) => l.includes(".rule="), ); expect(parsedRuleLabel).toContain( "Host(`example.com`) && PathPrefix(`/api`)", ); }); }); describe("Edge cases for domain names", () => { const domainCases = [ { name: "simple domain", host: "example.com" }, { name: "subdomain", host: "app.example.com" }, { name: "deep subdomain", host: "api.v1.app.example.com" }, { name: "numeric domain", host: "123.example.com" }, { name: "hyphenated domain", host: "my-app.example-host.com" }, { name: "localhost", host: "localhost" }, { name: "IP address style", host: "192.168.1.100" }, ]; for (const { name, host } of domainCases) { it(`should generate correct Host rule for ${name}: ${host}`, async () => { const labels = await createDomainLabels( "test-app", { ...baseDomain, host }, "web", ); const ruleLabel = labels.find((l) => l.includes(".rule=")); expect(ruleLabel).toBeDefined(); expect(ruleLabel).toContain(`Host(\`${host}\`)`); // Verify parenthesis is present expect(ruleLabel).toMatch( new RegExp(`Host\\(\\\`${host.replace(/\./g, "\\.")}\\\`\\)`), ); }); } }); describe("Multiple domains scenario", () => { it("should generate correct format for both web and websecure entrypoints", async () => { const webLabels = await createDomainLabels("test-app", baseDomain, "web"); const websecureLabels = await createDomainLabels( "test-app", baseDomain, "websecure", ); const webRule = webLabels.find((l) => l.includes(".rule=")); const websecureRule = websecureLabels.find((l) => l.includes(".rule=")); // Both should have correct format expect(webRule).toContain("Host(`example.com`)"); expect(websecureRule).toContain("Host(`example.com`)"); // Neither should have malformed format expect(webRule).not.toMatch(/Host`[^`]+`\)/); expect(websecureRule).not.toMatch(/Host`[^`]+`\)/); }); }); describe("Special characters in paths", () => { const pathCases = [ { name: "simple path", path: "/api" }, { name: "nested path", path: "/api/v1/users" }, { name: "path with hyphen", path: "/api-v1" }, { name: "path with underscore", path: "/api_v1" }, ]; for (const { name, path } of pathCases) { it(`should generate correct PathPrefix for ${name}: ${path}`, async () => { const labels = await createDomainLabels( "test-app", { ...baseDomain, path }, "web", ); const ruleLabel = labels.find((l) => l.includes(".rule=")); expect(ruleLabel).toBeDefined(); expect(ruleLabel).toContain(`PathPrefix(\`${path}\`)`); // Verify parenthesis is present expect(ruleLabel).not.toMatch(/PathPrefix`[^`]+`\)/); }); } }); });