diff --git a/src/components/DomainFormModal.test.ts b/src/components/DomainFormModal.test.ts index e36c1d8..3845c27 100644 --- a/src/components/DomainFormModal.test.ts +++ b/src/components/DomainFormModal.test.ts @@ -1,7 +1,16 @@ -import { describe, it, expect, afterEach } from "vitest"; -import { mount, type VueWrapper } from "@vue/test-utils"; +import { describe, it, expect, afterEach, vi } from "vitest"; +import { flushPromises, mount, type VueWrapper } from "@vue/test-utils"; import DomainFormModal from "./DomainFormModal.vue"; import type { DomainConfig } from "@/types"; +import { notificationsApi } from "@/services/api"; + +vi.mock("@/services/api", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + notificationsApi: { ...actual.notificationsApi, getAccessEmailTargets: vi.fn() }, + }; +}); let wrapper: VueWrapper; @@ -21,6 +30,7 @@ const mountModal = (domain?: DomainConfig) => { afterEach(() => { wrapper?.unmount(); document.body.innerHTML = ""; + vi.mocked(notificationsApi.getAccessEmailTargets).mockReset(); }); describe("DomainFormModal routing-only hostnames", () => { @@ -39,7 +49,7 @@ describe("DomainFormModal routing-only hostnames", () => { }); it("emits routing-only hostnames separately from certificate-bearing aliases", async () => { - const wrapper = mountModal({ + mountModal({ id: "d1", service: "web", container_port: 80, @@ -60,7 +70,7 @@ describe("DomainFormModal routing-only hostnames", () => { describe("DomainFormModal static caching", () => { it("emits the static-cache toggle when enabled on the domain", async () => { - const wrapper = mountModal({ + mountModal({ id: "d1", service: "web", container_port: 80, @@ -76,3 +86,87 @@ describe("DomainFormModal static caching", () => { expect(saved.static_cache).toBe(true); }); }); + +describe("DomainFormModal visitor access", () => { + it("loads the deployment's permitted email targets", async () => { + vi.mocked(notificationsApi.getAccessEmailTargets).mockResolvedValue({ + data: { targets: [{ id: "smtp-primary", name: "Primary mail" }] }, + } as Awaited>); + mountModal(); + await flushPromises(); + expect(notificationsApi.getAccessEmailTargets).toHaveBeenCalledWith("shop"); + const accessToggle = Array.from(document.querySelectorAll("label")).find((label) => + label.textContent?.includes("Require email verification"), + ); + if (!accessToggle) throw new Error("Access toggle missing"); + const input = accessToggle.querySelector("input"); + if (!input) throw new Error("Access input missing"); + input.checked = true; + input.dispatchEvent(new Event("change", { bubbles: true })); + await wrapper.vm.$nextTick(); + expect(document.body.textContent).toContain("Primary mail"); + }); + + it("preserves an email allowlist and session settings", async () => { + mountModal({ + id: "d1", + service: "web", + container_port: 80, + domain: "private.example.com", + ssl: { enabled: true, auto_cert: true }, + access: { + enabled: true, + mode: "allowlist", + allowed_emails: ["person@example.com"], + email_target_id: "smtp-primary", + session_hours: 48, + }, + }); + + document.querySelector(".btn-primary")?.click(); + await wrapper.vm.$nextTick(); + + const saved = wrapper.emitted("save")?.[0]?.[0] as DomainConfig; + expect(saved.access).toEqual({ + enabled: true, + mode: "allowlist", + allowed_emails: ["person@example.com"], + email_target_id: "smtp-primary", + session_hours: 48, + }); + }); + + it("normalizes allowlist emails and rejects invalid session lengths", async () => { + mountModal({ + id: "d1", + service: "web", + container_port: 80, + domain: "private.example.com", + ssl: { enabled: true, auto_cert: true }, + access: { + enabled: true, + mode: "allowlist", + allowed_emails: ["Person@Example.com"], + email_target_id: "smtp-primary", + session_hours: 721, + }, + }); + + document.querySelector(".btn-primary")?.click(); + await wrapper.vm.$nextTick(); + expect(wrapper.emitted("save")).toBeUndefined(); + + const sessionInput = Array.from(document.querySelectorAll('input[type="number"]')).find( + (input) => input.max === "720", + ); + if (!sessionInput) throw new Error("Session input missing"); + sessionInput.value = "24"; + sessionInput.dispatchEvent(new Event("input", { bubbles: true })); + await wrapper.vm.$nextTick(); + document.querySelector(".btn-primary")?.click(); + await wrapper.vm.$nextTick(); + + const saved = wrapper.emitted("save")?.[0]?.[0] as DomainConfig; + expect(saved.access?.allowed_emails).toEqual(["person@example.com"]); + }); +}); diff --git a/src/components/DomainFormModal.vue b/src/components/DomainFormModal.vue index 78e4a4a..bfc3ccf 100644 --- a/src/components/DomainFormModal.vue +++ b/src/components/DomainFormModal.vue @@ -84,6 +84,52 @@ +
+

Visitor Access

+ +
+ + Protects this domain and path without changing the application. +
+ + +
+
@@ -143,6 +189,11 @@