import type { ApplicationNested, Domain, Redirect } from "@dokploy/server"; import { createRouterConfig } from "@dokploy/server"; import { expect, test } from "vitest"; const baseApp: ApplicationNested = { railpackVersion: "0.15.4", rollbackActive: false, applicationId: "", previewLabels: [], createEnvFile: true, bitbucketRepositorySlug: "", herokuVersion: "", giteaRepository: "", giteaOwner: "", giteaBranch: "", buildServerId: "", buildRegistryId: "", buildRegistry: null, giteaBuildPath: "", giteaId: "", args: [], rollbackRegistryId: "", rollbackRegistry: null, deployments: [], cleanCache: false, applicationStatus: "done", endpointSpecSwarm: null, appName: "", autoDeploy: true, enableSubmodules: false, previewRequireCollaboratorPermissions: false, serverId: "", branch: null, dockerBuildStage: "", registryUrl: "", watchPaths: [], buildArgs: null, buildSecrets: null, isPreviewDeploymentsActive: false, previewBuildArgs: null, previewBuildSecrets: null, triggerType: "push", previewCertificateType: "none", previewEnv: null, previewHttps: false, previewPath: "/", previewPort: 3000, previewLimit: 0, previewCustomCertResolver: null, previewWildcard: "", environmentId: "", environment: { env: "", isDefault: false, environmentId: "", name: "", createdAt: "", description: "", projectId: "", project: { env: "", organizationId: "", name: "", description: "", createdAt: "", projectId: "", }, }, buildPath: "/", gitlabPathNamespace: "", buildType: "nixpacks", bitbucketBranch: "", bitbucketBuildPath: "", bitbucketId: "", bitbucketRepository: "", bitbucketOwner: "", githubId: "", gitlabProjectId: 0, gitlabBranch: "", gitlabBuildPath: "", gitlabId: "", gitlabRepository: "", gitlabOwner: "", command: null, cpuLimit: null, cpuReservation: null, createdAt: "", customGitBranch: "", customGitBuildPath: "", customGitSSHKeyId: null, customGitUrl: "", description: "", dockerfile: null, dockerImage: null, dropBuildPath: null, enabled: null, env: null, icon: null, healthCheckSwarm: null, labelsSwarm: null, memoryLimit: null, memoryReservation: null, modeSwarm: null, mounts: [], name: "", networkSwarm: null, owner: null, password: null, placementSwarm: null, ports: [], publishDirectory: null, isStaticSpa: null, redirects: [], refreshToken: "", registry: null, registryId: null, replicas: 1, repository: null, restartPolicySwarm: null, rollbackConfigSwarm: null, security: [], sourceType: "git", subtitle: null, title: null, updateConfigSwarm: null, username: null, dockerContextPath: null, stopGracePeriodSwarm: null, ulimitsSwarm: null, }; const baseDomain: Domain = { applicationId: "", certificateType: "none", createdAt: "", domainId: "", host: "", https: false, path: null, port: null, customEntrypoint: null, serviceName: "", composeId: "", customCertResolver: null, domainType: "application", uniqueConfigKey: 1, previewDeploymentId: "", internalPath: "/", stripPath: false, middlewares: null, }; const baseRedirect: Redirect = { redirectId: "", regex: "", replacement: "", permanent: false, uniqueConfigKey: 1, createdAt: "", applicationId: "", }; /** Middlewares */ test("Web entrypoint on http domain", async () => { const router = await createRouterConfig( baseApp, { ...baseDomain, https: false }, "web", ); expect(router.middlewares).not.toContain("redirect-to-https"); expect(router.rule).not.toContain("PathPrefix"); }); test("Web entrypoint on http domain with custom path", async () => { const router = await createRouterConfig( baseApp, { ...baseDomain, path: "/foo", https: false }, "web", ); expect(router.rule).toContain("PathPrefix(`/foo`)"); }); test("Web entrypoint on http domain with redirect", async () => { const router = await createRouterConfig( { ...baseApp, appName: "test", redirects: [{ ...baseRedirect, uniqueConfigKey: 1 }], }, { ...baseDomain, https: false }, "web", ); expect(router.middlewares).not.toContain("redirect-to-https"); expect(router.middlewares).toContain("redirect-test-1"); }); test("Web entrypoint on http domain with multiple redirect", async () => { const router = await createRouterConfig( { ...baseApp, appName: "test", redirects: [ { ...baseRedirect, uniqueConfigKey: 1 }, { ...baseRedirect, uniqueConfigKey: 2 }, ], }, { ...baseDomain, https: false }, "web", ); expect(router.middlewares).not.toContain("redirect-to-https"); expect(router.middlewares).toContain("redirect-test-1"); expect(router.middlewares).toContain("redirect-test-2"); }); test("Web entrypoint on https domain", async () => { const router = await createRouterConfig( baseApp, { ...baseDomain, https: true }, "web", ); expect(router.middlewares).toContain("redirect-to-https"); }); test("Web entrypoint on https domain with redirect", async () => { const router = await createRouterConfig( { ...baseApp, appName: "test", redirects: [{ ...baseRedirect, uniqueConfigKey: 1 }], }, { ...baseDomain, https: true }, "web", ); expect(router.middlewares).toContain("redirect-to-https"); expect(router.middlewares).not.toContain("redirect-test-1"); }); test("Websecure entrypoint on https domain", async () => { const router = await createRouterConfig( baseApp, { ...baseDomain, https: true }, "websecure", ); expect(router.middlewares).not.toContain("redirect-to-https"); }); test("Websecure entrypoint on https domain with redirect", async () => { const router = await createRouterConfig( { ...baseApp, appName: "test", redirects: [{ ...baseRedirect, uniqueConfigKey: 1 }], }, { ...baseDomain, https: true }, "websecure", ); expect(router.middlewares).not.toContain("redirect-to-https"); expect(router.middlewares).toContain("redirect-test-1"); }); /** Custom Middlewares */ test("Web entrypoint with single custom middleware", async () => { const router = await createRouterConfig( baseApp, { ...baseDomain, middlewares: ["auth@file"] }, "web", ); expect(router.middlewares).toContain("auth@file"); }); test("Web entrypoint with multiple custom middlewares", async () => { const router = await createRouterConfig( baseApp, { ...baseDomain, middlewares: ["auth@file", "rate-limit@file"] }, "web", ); expect(router.middlewares).toContain("auth@file"); expect(router.middlewares).toContain("rate-limit@file"); }); test("Web entrypoint on https domain with custom middleware", async () => { const router = await createRouterConfig( baseApp, { ...baseDomain, https: true, middlewares: ["auth@file"] }, "web", ); // Should only have HTTPS redirect - custom middleware applies on websecure expect(router.middlewares).toContain("redirect-to-https"); expect(router.middlewares).not.toContain("auth@file"); }); test("Websecure entrypoint with custom middleware", async () => { const router = await createRouterConfig( baseApp, { ...baseDomain, https: true, middlewares: ["auth@file"] }, "websecure", ); // Should have custom middleware but not HTTPS redirect expect(router.middlewares).not.toContain("redirect-to-https"); expect(router.middlewares).toContain("auth@file"); }); test("Web entrypoint with redirect and custom middleware", async () => { const router = await createRouterConfig( { ...baseApp, appName: "test", redirects: [{ ...baseRedirect, uniqueConfigKey: 1 }], }, { ...baseDomain, middlewares: ["auth@file"] }, "web", ); // Should have both redirect middleware and custom middleware expect(router.middlewares).toContain("redirect-test-1"); expect(router.middlewares).toContain("auth@file"); }); test("Web entrypoint with empty middlewares array", async () => { const router = await createRouterConfig( baseApp, { ...baseDomain, https: false, middlewares: [] }, "web", ); // Should behave same as no middlewares - no redirect for http expect(router.middlewares).not.toContain("redirect-to-https"); }); /** Certificates */ test("CertificateType on websecure entrypoint", async () => { const router = await createRouterConfig( baseApp, { ...baseDomain, certificateType: "letsencrypt" }, "websecure", ); expect(router.tls?.certResolver).toBe("letsencrypt"); }); test("Custom entrypoint on http domain", async () => { const router = await createRouterConfig( baseApp, { ...baseDomain, https: false, customEntrypoint: "custom" }, "custom", ); expect(router.entryPoints).toEqual(["custom"]); expect(router.middlewares).not.toContain("redirect-to-https"); expect(router.tls).toBeUndefined(); }); test("Custom entrypoint on https domain", async () => { const router = await createRouterConfig( baseApp, { ...baseDomain, https: true, customEntrypoint: "custom", certificateType: "letsencrypt", }, "custom", ); expect(router.entryPoints).toEqual(["custom"]); expect(router.middlewares).not.toContain("redirect-to-https"); expect(router.tls?.certResolver).toBe("letsencrypt"); }); test("Custom entrypoint with path includes PathPrefix in rule", async () => { const router = await createRouterConfig( baseApp, { ...baseDomain, customEntrypoint: "custom", path: "/api" }, "custom", ); expect(router.rule).toContain("PathPrefix(`/api`)"); expect(router.entryPoints).toEqual(["custom"]); }); test("Custom entrypoint with stripPath adds stripprefix middleware", async () => { const router = await createRouterConfig( baseApp, { ...baseDomain, customEntrypoint: "custom", path: "/api", stripPath: true, }, "custom", ); expect(router.middlewares).toContain("stripprefix--1"); expect(router.entryPoints).toEqual(["custom"]); }); test("Custom entrypoint with internalPath adds addprefix middleware", async () => { const router = await createRouterConfig( baseApp, { ...baseDomain, customEntrypoint: "custom", internalPath: "/hello", }, "custom", ); expect(router.middlewares).toContain("addprefix--1"); expect(router.entryPoints).toEqual(["custom"]); }); test("stripPath and internalPath together: stripprefix must come before addprefix", async () => { const router = await createRouterConfig( baseApp, { ...baseDomain, path: "/public", stripPath: true, internalPath: "/app/v2", }, "web", ); const stripIndex = router.middlewares?.indexOf("stripprefix--1") ?? -1; const addIndex = router.middlewares?.indexOf("addprefix--1") ?? -1; expect(stripIndex).toBeGreaterThanOrEqual(0); expect(addIndex).toBeGreaterThanOrEqual(0); expect(stripIndex).toBeLessThan(addIndex); }); test("Custom entrypoint with https and custom cert resolver", async () => { const router = await createRouterConfig( baseApp, { ...baseDomain, https: true, customEntrypoint: "custom", certificateType: "custom", customCertResolver: "myresolver", }, "custom", ); expect(router.entryPoints).toEqual(["custom"]); expect(router.tls?.certResolver).toBe("myresolver"); }); test("Custom entrypoint without https should not have tls", async () => { const router = await createRouterConfig( baseApp, { ...baseDomain, https: false, customEntrypoint: "custom", certificateType: "letsencrypt", }, "custom", ); expect(router.entryPoints).toEqual(["custom"]); expect(router.tls).toBeUndefined(); }); /** IDN/Punycode */ test("Internationalized domain name is converted to punycode", async () => { const router = await createRouterConfig( baseApp, { ...baseDomain, host: "тест.рф" }, "web", ); // тест.рф in punycode is xn--e1aybc.xn--p1ai expect(router.rule).toContain("Host(`xn--e1aybc.xn--p1ai`)"); expect(router.rule).not.toContain("тест.рф"); }); test("ASCII domain remains unchanged", async () => { const router = await createRouterConfig( baseApp, { ...baseDomain, host: "example.com" }, "web", ); expect(router.rule).toContain("Host(`example.com`)"); }); test("Russian Cyrillic label with .ru TLD is converted to punycode", async () => { const router = await createRouterConfig( baseApp, { ...baseDomain, host: "сайт.ru" }, "web", ); // сайт in punycode is xn--80aswg expect(router.rule).toContain("Host(`xn--80aswg.ru`)"); expect(router.rule).not.toContain("сайт"); }); test("Subdomain with Russian IDN TLD converts non-ASCII part to punycode", async () => { const router = await createRouterConfig( baseApp, { ...baseDomain, host: "app.тест.рф" }, "web", ); // app stays ASCII, тест.рф becomes xn--e1aybc.xn--p1ai expect(router.rule).toContain("Host(`app.xn--e1aybc.xn--p1ai`)"); expect(router.rule).not.toContain("тест.рф"); });