Files
zitadel/apps/login/src/lib/server/host.test.ts
T
b1c8781516 fix(login): lru-cache compatibility, improve session error handling (#12099)
# Which Problems Are Solved

- Logo and heading overlap on login pages due to a -mb-4 negative margin
on the logo container
- The /login route returns a 500 when `listSessions` fails (e.g. stale
session cookies), because loadSessions has no error handling.
- The proxy logs `"fetch() returned undefined"` because
`fetchIframeOrigins` returns undefined, which lru-cache's forceFetch
treats as a fetch failure.

# How the Problems Are Solved

- Remove `-mb-4` from the logo container in DynamicTheme.
- Wrap `loadSessions` in a try/catch — failures fall through as empty
sessions instead of crashing.
- Return `null` instead of `undefined` from `fetchIframeOrigins` so
lru-cache caches it normally.

---------

Co-authored-by: Livio Spring <[email protected]>
Co-authored-by: conblem <[email protected]>
2026-04-30 15:27:44 +02:00

282 lines
9.6 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { getInstanceHost, getPublicHost, getPublicHostWithProtocol } from "./host";
describe("Host utility functions", () => {
beforeEach(() => {
vi.clearAllMocks();
});
afterEach(() => {
vi.restoreAllMocks();
});
describe("getInstanceHost", () => {
test("should use x-zitadel-instance-host when available", () => {
const mockHeaders = {
get: vi.fn((key: string) => {
if (key === "x-zitadel-instance-host") return "instance.zitadel.cloud";
if (key === "x-zitadel-forward-host") return "forward.zitadel.cloud";
return null;
}),
} as any;
const result = getInstanceHost(mockHeaders);
expect(result).toBe("instance.zitadel.cloud");
expect(mockHeaders.get).toHaveBeenCalledWith("x-zitadel-instance-host");
});
test("should use x-zitadel-forward-host when x-zitadel-instance-host is not available", () => {
const mockHeaders = {
get: vi.fn((key: string) => {
if (key === "x-zitadel-instance-host") return null;
if (key === "x-zitadel-forward-host") return "forward.zitadel.cloud";
return null;
}),
} as any;
const result = getInstanceHost(mockHeaders);
expect(result).toBe("forward.zitadel.cloud");
expect(mockHeaders.get).toHaveBeenCalledWith("x-zitadel-instance-host");
expect(mockHeaders.get).toHaveBeenCalledWith("x-zitadel-forward-host");
});
});
describe("getPublicHostWithProtocol", () => {
test("should return https for production domain", () => {
const mockHeaders = {
get: vi.fn(() => "zitadel.com"),
} as any;
const result = getPublicHostWithProtocol(mockHeaders);
expect(result).toBe("https://zitadel.com");
});
test("should return http for localhost", () => {
const mockHeaders = {
get: vi.fn(() => "localhost:3000"),
} as any;
const result = getPublicHostWithProtocol(mockHeaders);
expect(result).toBe("http://localhost:3000");
});
test("should return http for localhost without port", () => {
const mockHeaders = {
get: vi.fn(() => "localhost"),
} as any;
const result = getPublicHostWithProtocol(mockHeaders);
expect(result).toBe("http://localhost");
});
test("should return https for custom domain", () => {
const mockHeaders = {
get: vi.fn(() => "auth.company.com"),
} as any;
const result = getPublicHostWithProtocol(mockHeaders);
expect(result).toBe("https://auth.company.com");
});
});
describe("Real-world scenarios", () => {
test("should handle Vercel rewrite scenario", () => {
const mockHeaders = {
get: vi.fn((key: string) => {
// Simulate Vercel rewrite: zitadel.com/login -> login-zitadel-qa.vercel.app
if (key === "x-forwarded-host") return "zitadel.com";
if (key === "host") return "login-zitadel-qa.vercel.app";
return null;
}),
} as any;
const result = getPublicHostWithProtocol(mockHeaders);
expect(result).toBe("https://zitadel.com");
});
test("should handle CloudFlare proxy scenario", () => {
const mockHeaders = {
get: vi.fn((key: string) => {
if (key === "x-forwarded-host") return "auth.company.com";
if (key === "x-original-host") return null;
if (key === "host") return "cloudflare-worker.workers.dev";
return null;
}),
} as any;
const result = getPublicHost(mockHeaders);
expect(result).toBe("auth.company.com");
});
test("should handle development environment", () => {
const mockHeaders = {
get: vi.fn((key: string) => {
if (key === "host") return "localhost:3000";
return null;
}),
} as any;
const result = getPublicHostWithProtocol(mockHeaders);
expect(result).toBe("http://localhost:3000");
});
test("should handle staging environment with subdomain", () => {
const mockHeaders = {
get: vi.fn((key: string) => {
if (key === "x-forwarded-host") return "staging-auth.company.com";
if (key === "host") return "staging-internal.vercel.app";
return null;
}),
} as any;
const result = getPublicHostWithProtocol(mockHeaders);
expect(result).toBe("https://staging-auth.company.com");
});
test("should prioritize x-zitadel-instance-host in multi-tenant scenario", () => {
const mockHeaders = {
get: vi.fn((key: string) => {
if (key === "x-zitadel-instance-host") return "customer.zitadel.cloud";
if (key === "x-forwarded-host") return "accounts.company.com";
if (key === "host") return "internal.vercel.app";
return null;
}),
} as any;
const result = getInstanceHost(mockHeaders);
expect(result).toBe("customer.zitadel.cloud");
});
});
describe("Edge cases", () => {
test("should handle IPv4 addresses", () => {
const mockHeaders = {
get: vi.fn(() => "192.168.1.100:3000"),
} as any;
const result = getPublicHostWithProtocol(mockHeaders);
expect(result).toBe("https://192.168.1.100:3000");
});
test("should handle IPv6 addresses", () => {
const mockHeaders = {
get: vi.fn(() => "[::1]:3000"),
} as any;
const result = getPublicHostWithProtocol(mockHeaders);
expect(result).toBe("https://[::1]:3000");
});
test("should handle hosts with ports", () => {
const mockHeaders = {
get: vi.fn(() => "zitadel.com:8080"),
} as any;
const result = getPublicHostWithProtocol(mockHeaders);
expect(result).toBe("https://zitadel.com:8080");
});
test("should handle localhost with different ports", () => {
const mockHeaders = {
get: vi.fn(() => "localhost:8080"),
} as any;
const result = getPublicHostWithProtocol(mockHeaders);
expect(result).toBe("http://localhost:8080");
});
});
describe("getPublicHost", () => {
test("should use x-zitadel-public-host when available", () => {
const mockHeaders = {
get: vi.fn((key: string) => {
if (key === "x-zitadel-public-host") return "public.zitadel.cloud";
if (key === "x-forwarded-host") return "accounts.company.com";
return null;
}),
} as any;
const result = getPublicHost(mockHeaders);
expect(result).toBe("public.zitadel.cloud");
expect(mockHeaders.get).toHaveBeenCalledWith("x-zitadel-public-host");
});
test("should use x-zitadel-forward-host when x-zitadel-public-host is not available", () => {
const mockHeaders = {
get: vi.fn((key: string) => {
if (key === "x-zitadel-public-host") return null;
if (key === "x-zitadel-forward-host") return "forward.zitadel.cloud";
if (key === "x-forwarded-host") return "accounts.company.com";
return null;
}),
} as any;
const result = getPublicHost(mockHeaders);
expect(result).toBe("forward.zitadel.cloud");
expect(mockHeaders.get).toHaveBeenCalledWith("x-zitadel-public-host");
expect(mockHeaders.get).toHaveBeenCalledWith("x-zitadel-forward-host");
});
test("should use x-forwarded-host when neither x-zitadel-public-host nor x-zitadel-forward-host is available", () => {
const mockHeaders = {
get: vi.fn((key: string) => {
if (key === "x-zitadel-public-host") return null;
if (key === "x-zitadel-forward-host") return null;
if (key === "x-forwarded-host") return "accounts.company.com";
if (key === "host") return "internal.server";
return null;
}),
} as any;
const result = getPublicHost(mockHeaders);
expect(result).toBe("accounts.company.com");
expect(mockHeaders.get).toHaveBeenCalledWith("x-zitadel-public-host");
expect(mockHeaders.get).toHaveBeenCalledWith("x-zitadel-forward-host");
expect(mockHeaders.get).toHaveBeenCalledWith("x-forwarded-host");
});
test("should fall back to host when x-forwarded-host is not available", () => {
const mockHeaders = {
get: vi.fn((key: string) => {
if (key === "x-zitadel-public-host") return null;
if (key === "x-zitadel-forward-host") return null;
if (key === "x-forwarded-host") return null;
if (key === "host") return "localhost:3000";
return null;
}),
} as any;
const result = getPublicHost(mockHeaders);
expect(result).toBe("localhost:3000");
expect(mockHeaders.get).toHaveBeenCalledWith("x-forwarded-host");
expect(mockHeaders.get).toHaveBeenCalledWith("host");
});
test("should throw error when no host is found", () => {
const mockHeaders = {
get: vi.fn(() => null),
} as any;
expect(() => getPublicHost(mockHeaders)).toThrow("No host found in headers");
});
test("should differ from getInstanceHost when x-zitadel-instance-host is present", () => {
const mockHeaders = {
get: vi.fn((key: string) => {
if (key === "x-zitadel-instance-host") return "instance.zitadel.cloud";
if (key === "x-forwarded-host") return "accounts.company.com";
if (key === "host") return "internal.server";
return null;
}),
} as any;
const instanceHost = getInstanceHost(mockHeaders);
const publicHost = getPublicHost(mockHeaders);
expect(instanceHost).toBe("instance.zitadel.cloud");
expect(publicHost).toBe("accounts.company.com");
expect(instanceHost).not.toBe(publicHost);
});
});
});