Skip to content

Commit 3ea90de

Browse files
authored
Merge pull request #3162 from philippgerard/fix/traefik-host-rule-label-regression-tests
test: add regression tests for Traefik Host rule format
2 parents 6cafb15 + bccef0d commit 3ea90de

File tree

1 file changed

+215
-0
lines changed

1 file changed

+215
-0
lines changed
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
import type { Domain } from "@dokploy/server";
2+
import { createDomainLabels } from "@dokploy/server";
3+
import { parse, stringify } from "yaml";
4+
import { describe, expect, it } from "vitest";
5+
6+
/**
7+
* Regression tests for Traefik Host rule label format.
8+
*
9+
* These tests verify that the Host rule is generated with the correct format:
10+
* - Host(`domain.com`) - with opening and closing parentheses
11+
* - Host(`domain.com`) && PathPrefix(`/path`) - for path-based routing
12+
*
13+
* Issue: https://github.com/Dokploy/dokploy/issues/3161
14+
* The bug caused Host rules to be malformed as Host`domain.com`)
15+
* (missing opening parenthesis) which broke all domain routing.
16+
*/
17+
describe("Host rule format regression tests", () => {
18+
const baseDomain: Domain = {
19+
host: "example.com",
20+
port: 8080,
21+
https: false,
22+
uniqueConfigKey: 1,
23+
customCertResolver: null,
24+
certificateType: "none",
25+
applicationId: "",
26+
composeId: "",
27+
domainType: "compose",
28+
serviceName: "test-app",
29+
domainId: "",
30+
path: "/",
31+
createdAt: "",
32+
previewDeploymentId: "",
33+
internalPath: "/",
34+
stripPath: false,
35+
};
36+
37+
describe("Host rule format validation", () => {
38+
it("should generate Host rule with correct parentheses format", async () => {
39+
const labels = await createDomainLabels("test-app", baseDomain, "web");
40+
const ruleLabel = labels.find((l) => l.includes(".rule="));
41+
42+
expect(ruleLabel).toBeDefined();
43+
// Verify exact format: Host(`domain`)
44+
expect(ruleLabel).toMatch(/Host\(`[^`]+`\)/);
45+
// Ensure opening parenthesis is present after Host
46+
expect(ruleLabel).toContain("Host(`example.com`)");
47+
// Ensure it does NOT have the malformed format
48+
expect(ruleLabel).not.toMatch(/Host`[^`]+`\)/);
49+
});
50+
51+
it("should generate PathPrefix with correct parentheses format", async () => {
52+
const labels = await createDomainLabels(
53+
"test-app",
54+
{ ...baseDomain, path: "/api" },
55+
"web",
56+
);
57+
const ruleLabel = labels.find((l) => l.includes(".rule="));
58+
59+
expect(ruleLabel).toBeDefined();
60+
// Verify PathPrefix format
61+
expect(ruleLabel).toMatch(/PathPrefix\(`[^`]+`\)/);
62+
expect(ruleLabel).toContain("PathPrefix(`/api`)");
63+
// Ensure opening parenthesis is present
64+
expect(ruleLabel).not.toMatch(/PathPrefix`[^`]+`\)/);
65+
});
66+
67+
it("should generate combined Host and PathPrefix with correct format", async () => {
68+
const labels = await createDomainLabels(
69+
"test-app",
70+
{ ...baseDomain, path: "/api/v1" },
71+
"websecure",
72+
);
73+
const ruleLabel = labels.find((l) => l.includes(".rule="));
74+
75+
expect(ruleLabel).toBeDefined();
76+
expect(ruleLabel).toBe(
77+
"traefik.http.routers.test-app-1-websecure.rule=Host(`example.com`) && PathPrefix(`/api/v1`)",
78+
);
79+
});
80+
});
81+
82+
describe("YAML serialization preserves Host rule format", () => {
83+
it("should preserve Host rule format through YAML stringify/parse", async () => {
84+
const labels = await createDomainLabels("test-app", baseDomain, "web");
85+
const ruleLabel = labels.find((l) => l.includes(".rule="));
86+
87+
// Simulate compose file structure
88+
const composeSpec = {
89+
services: {
90+
myapp: {
91+
image: "nginx",
92+
labels: labels,
93+
},
94+
},
95+
};
96+
97+
// Stringify to YAML
98+
const yamlOutput = stringify(composeSpec, { lineWidth: 1000 });
99+
100+
// Parse back
101+
const parsed = parse(yamlOutput) as typeof composeSpec;
102+
const parsedRuleLabel = parsed.services.myapp.labels.find((l: string) =>
103+
l.includes(".rule="),
104+
);
105+
106+
// Verify format is preserved
107+
expect(parsedRuleLabel).toBe(ruleLabel);
108+
expect(parsedRuleLabel).toContain("Host(`example.com`)");
109+
expect(parsedRuleLabel).not.toMatch(/Host`[^`]+`\)/);
110+
});
111+
112+
it("should preserve complex rule format through YAML serialization", async () => {
113+
const labels = await createDomainLabels(
114+
"test-app",
115+
{ ...baseDomain, path: "/api", https: true },
116+
"websecure",
117+
);
118+
119+
const composeSpec = {
120+
services: {
121+
myapp: {
122+
labels: labels,
123+
},
124+
},
125+
};
126+
127+
const yamlOutput = stringify(composeSpec, { lineWidth: 1000 });
128+
const parsed = parse(yamlOutput) as typeof composeSpec;
129+
const parsedRuleLabel = parsed.services.myapp.labels.find((l: string) =>
130+
l.includes(".rule="),
131+
);
132+
133+
expect(parsedRuleLabel).toContain(
134+
"Host(`example.com`) && PathPrefix(`/api`)",
135+
);
136+
});
137+
});
138+
139+
describe("Edge cases for domain names", () => {
140+
const domainCases = [
141+
{ name: "simple domain", host: "example.com" },
142+
{ name: "subdomain", host: "app.example.com" },
143+
{ name: "deep subdomain", host: "api.v1.app.example.com" },
144+
{ name: "numeric domain", host: "123.example.com" },
145+
{ name: "hyphenated domain", host: "my-app.example-host.com" },
146+
{ name: "localhost", host: "localhost" },
147+
{ name: "IP address style", host: "192.168.1.100" },
148+
];
149+
150+
for (const { name, host } of domainCases) {
151+
it(`should generate correct Host rule for ${name}: ${host}`, async () => {
152+
const labels = await createDomainLabels(
153+
"test-app",
154+
{ ...baseDomain, host },
155+
"web",
156+
);
157+
const ruleLabel = labels.find((l) => l.includes(".rule="));
158+
159+
expect(ruleLabel).toBeDefined();
160+
expect(ruleLabel).toContain(`Host(\`${host}\`)`);
161+
// Verify parenthesis is present
162+
expect(ruleLabel).toMatch(
163+
new RegExp(`Host\\(\\\`${host.replace(/\./g, "\\.")}\\\`\\)`),
164+
);
165+
});
166+
}
167+
});
168+
169+
describe("Multiple domains scenario", () => {
170+
it("should generate correct format for both web and websecure entrypoints", async () => {
171+
const webLabels = await createDomainLabels("test-app", baseDomain, "web");
172+
const websecureLabels = await createDomainLabels(
173+
"test-app",
174+
baseDomain,
175+
"websecure",
176+
);
177+
178+
const webRule = webLabels.find((l) => l.includes(".rule="));
179+
const websecureRule = websecureLabels.find((l) => l.includes(".rule="));
180+
181+
// Both should have correct format
182+
expect(webRule).toContain("Host(`example.com`)");
183+
expect(websecureRule).toContain("Host(`example.com`)");
184+
185+
// Neither should have malformed format
186+
expect(webRule).not.toMatch(/Host`[^`]+`\)/);
187+
expect(websecureRule).not.toMatch(/Host`[^`]+`\)/);
188+
});
189+
});
190+
191+
describe("Special characters in paths", () => {
192+
const pathCases = [
193+
{ name: "simple path", path: "/api" },
194+
{ name: "nested path", path: "/api/v1/users" },
195+
{ name: "path with hyphen", path: "/api-v1" },
196+
{ name: "path with underscore", path: "/api_v1" },
197+
];
198+
199+
for (const { name, path } of pathCases) {
200+
it(`should generate correct PathPrefix for ${name}: ${path}`, async () => {
201+
const labels = await createDomainLabels(
202+
"test-app",
203+
{ ...baseDomain, path },
204+
"web",
205+
);
206+
const ruleLabel = labels.find((l) => l.includes(".rule="));
207+
208+
expect(ruleLabel).toBeDefined();
209+
expect(ruleLabel).toContain(`PathPrefix(\`${path}\`)`);
210+
// Verify parenthesis is present
211+
expect(ruleLabel).not.toMatch(/PathPrefix`[^`]+`\)/);
212+
});
213+
}
214+
});
215+
});

0 commit comments

Comments
 (0)