Import tests: backend/tests/e2e/tier1
Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
This commit is contained in:
@@ -0,0 +1,464 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { apiFetch, clearCookieJar } from "./setup.js";
|
||||||
|
import type { User, Website, Domain, DnsRecord } from "../../../src/api/contracts/models.js";
|
||||||
|
|
||||||
|
describe("Tier 1 E2E - Happy Paths", () => {
|
||||||
|
// --- Feature 1: Authentication ---
|
||||||
|
describe("Auth Happy Paths", () => {
|
||||||
|
it("should successfully log in standard credentials", async () => {
|
||||||
|
const sessionId = "tier1_auth_1";
|
||||||
|
clearCookieJar(sessionId);
|
||||||
|
|
||||||
|
const loginRes = await apiFetch<{ user: User; challenge: string | null; sessionExpiresAt: string }>(
|
||||||
|
"/api/auth/login",
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
sessionId,
|
||||||
|
body: JSON.stringify({
|
||||||
|
email: "owner@example.com",
|
||||||
|
password: "correct-password",
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(loginRes.$status).toBe(200);
|
||||||
|
expect(loginRes.error).toBeNull();
|
||||||
|
expect(loginRes.data).toBeDefined();
|
||||||
|
expect(loginRes.data?.user.email).toBe("owner@example.com");
|
||||||
|
expect(loginRes.data?.user.role).toBe("owner");
|
||||||
|
expect(loginRes.data?.challenge).toBeNull();
|
||||||
|
expect(loginRes.data?.sessionExpiresAt).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should trigger a 2FA challenge when logging in with 2FA email", async () => {
|
||||||
|
const sessionId = "tier1_auth_2";
|
||||||
|
clearCookieJar(sessionId);
|
||||||
|
|
||||||
|
const loginRes = await apiFetch<{ user: User | null; challenge: string | null }>(
|
||||||
|
"/api/auth/login",
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
sessionId,
|
||||||
|
body: JSON.stringify({
|
||||||
|
email: "owner-2fa@example.com",
|
||||||
|
password: "2fa-password",
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(loginRes.$status).toBe(200);
|
||||||
|
expect(loginRes.error).toBeNull();
|
||||||
|
expect(loginRes.data?.challenge).toBe("totp");
|
||||||
|
expect(loginRes.data?.user).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should complete login when providing valid 2FA code", async () => {
|
||||||
|
const sessionId = "tier1_auth_3";
|
||||||
|
clearCookieJar(sessionId);
|
||||||
|
|
||||||
|
const loginRes = await apiFetch<{ user: User; challenge: string | null }>(
|
||||||
|
"/api/auth/login",
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
sessionId,
|
||||||
|
body: JSON.stringify({
|
||||||
|
email: "owner-2fa@example.com",
|
||||||
|
password: "2fa-password",
|
||||||
|
code: "123456",
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(loginRes.$status).toBe(200);
|
||||||
|
expect(loginRes.error).toBeNull();
|
||||||
|
expect(loginRes.data?.user).toBeDefined();
|
||||||
|
expect(loginRes.data?.user.twoFactorEnabled).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should successfully fetch profile details via /api/auth/me", async () => {
|
||||||
|
const sessionId = "tier1_auth_4";
|
||||||
|
clearCookieJar(sessionId);
|
||||||
|
|
||||||
|
// Login first
|
||||||
|
await apiFetch("/api/auth/login", {
|
||||||
|
method: "POST",
|
||||||
|
sessionId,
|
||||||
|
body: JSON.stringify({ email: "owner@example.com", password: "correct-password" }),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get profile
|
||||||
|
const meRes = await apiFetch<User>("/api/auth/me", {
|
||||||
|
method: "GET",
|
||||||
|
sessionId,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(meRes.$status).toBe(200);
|
||||||
|
expect(meRes.error).toBeNull();
|
||||||
|
expect(meRes.data?.email).toBe("owner@example.com");
|
||||||
|
expect(meRes.data?.role).toBe("owner");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should successfully log out and invalidate session cookie", async () => {
|
||||||
|
const sessionId = "tier1_auth_5";
|
||||||
|
clearCookieJar(sessionId);
|
||||||
|
|
||||||
|
// Login
|
||||||
|
await apiFetch("/api/auth/login", {
|
||||||
|
method: "POST",
|
||||||
|
sessionId,
|
||||||
|
body: JSON.stringify({ email: "owner@example.com", password: "correct-password" }),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Logout
|
||||||
|
const logoutRes = await apiFetch<{ ok: boolean }>("/api/auth/logout", {
|
||||||
|
method: "POST",
|
||||||
|
sessionId,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(logoutRes.$status).toBe(200);
|
||||||
|
expect(logoutRes.error).toBeNull();
|
||||||
|
expect(logoutRes.data?.ok).toBe(true);
|
||||||
|
|
||||||
|
// Subsequent /api/auth/me should fail
|
||||||
|
const meRes = await apiFetch("/api/auth/me", {
|
||||||
|
method: "GET",
|
||||||
|
sessionId,
|
||||||
|
});
|
||||||
|
expect(meRes.$status).toBe(401);
|
||||||
|
expect(meRes.error?.code).toBe("PERMISSION_DENIED");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Feature 2: Websites CRUD ---
|
||||||
|
describe("Websites Happy Paths", () => {
|
||||||
|
const sessionId = "tier1_websites";
|
||||||
|
|
||||||
|
it("should successfully list websites", async () => {
|
||||||
|
clearCookieJar(sessionId);
|
||||||
|
await apiFetch("/api/auth/login", {
|
||||||
|
method: "POST",
|
||||||
|
sessionId,
|
||||||
|
body: JSON.stringify({ email: "owner@example.com", password: "correct-password" }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const listRes = await apiFetch<Website[]>("/api/websites", {
|
||||||
|
method: "GET",
|
||||||
|
sessionId,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(listRes.$status).toBe(200);
|
||||||
|
expect(listRes.error).toBeNull();
|
||||||
|
expect(listRes.data).toBeInstanceOf(Array);
|
||||||
|
expect(listRes.data!.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should successfully create a new website", async () => {
|
||||||
|
const createRes = await apiFetch<Website>("/api/websites", {
|
||||||
|
method: "POST",
|
||||||
|
sessionId,
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: "Test Happy Site",
|
||||||
|
domain: "happy.com",
|
||||||
|
runtime: "Node.js",
|
||||||
|
environment: "production",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(createRes.$status).toBe(201);
|
||||||
|
expect(createRes.error).toBeNull();
|
||||||
|
expect(createRes.data?.id).toBeDefined();
|
||||||
|
expect(createRes.data?.name).toBe("Test Happy Site");
|
||||||
|
expect(createRes.data?.domain).toBe("happy.com");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should successfully retrieve website details by ID", async () => {
|
||||||
|
// List to get the ID we just created
|
||||||
|
const listRes = await apiFetch<Website[]>("/api/websites", {
|
||||||
|
method: "GET",
|
||||||
|
sessionId,
|
||||||
|
});
|
||||||
|
const created = listRes.data!.find((w) => w.name === "Test Happy Site");
|
||||||
|
expect(created).toBeDefined();
|
||||||
|
|
||||||
|
const getRes = await apiFetch<Website>(`/api/websites/${created!.id}`, {
|
||||||
|
method: "GET",
|
||||||
|
sessionId,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getRes.$status).toBe(200);
|
||||||
|
expect(getRes.error).toBeNull();
|
||||||
|
expect(getRes.data?.name).toBe("Test Happy Site");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should successfully update website settings", async () => {
|
||||||
|
const listRes = await apiFetch<Website[]>("/api/websites", {
|
||||||
|
method: "GET",
|
||||||
|
sessionId,
|
||||||
|
});
|
||||||
|
const created = listRes.data!.find((w) => w.name === "Test Happy Site");
|
||||||
|
|
||||||
|
const updateRes = await apiFetch<Website>(`/api/websites/${created!.id}`, {
|
||||||
|
method: "PUT",
|
||||||
|
sessionId,
|
||||||
|
body: JSON.stringify({
|
||||||
|
runtime: "Docker",
|
||||||
|
environment: "staging",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(updateRes.$status).toBe(200);
|
||||||
|
expect(updateRes.error).toBeNull();
|
||||||
|
expect(updateRes.data?.runtime).toBe("Docker");
|
||||||
|
expect(updateRes.data?.environment).toBe("staging");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should successfully delete a website", async () => {
|
||||||
|
const listRes = await apiFetch<Website[]>("/api/websites", {
|
||||||
|
method: "GET",
|
||||||
|
sessionId,
|
||||||
|
});
|
||||||
|
const created = listRes.data!.find((w) => w.name === "Test Happy Site");
|
||||||
|
|
||||||
|
const deleteRes = await apiFetch<{ deleted: string }>(`/api/websites/${created!.id}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
sessionId,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(deleteRes.$status).toBe(200);
|
||||||
|
expect(deleteRes.error).toBeNull();
|
||||||
|
expect(deleteRes.data?.deleted).toBe(created!.id);
|
||||||
|
|
||||||
|
// Confirm deletion
|
||||||
|
const confirmRes = await apiFetch(`/api/websites/${created!.id}`, {
|
||||||
|
method: "GET",
|
||||||
|
sessionId,
|
||||||
|
});
|
||||||
|
expect(confirmRes.$status).toBe(404);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Feature 3: Domains CRUD ---
|
||||||
|
describe("Domains Happy Paths", () => {
|
||||||
|
const sessionId = "tier1_domains";
|
||||||
|
|
||||||
|
it("should successfully list domains", async () => {
|
||||||
|
clearCookieJar(sessionId);
|
||||||
|
await apiFetch("/api/auth/login", {
|
||||||
|
method: "POST",
|
||||||
|
sessionId,
|
||||||
|
body: JSON.stringify({ email: "owner@example.com", password: "correct-password" }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const listRes = await apiFetch<Domain[]>("/api/domains", {
|
||||||
|
method: "GET",
|
||||||
|
sessionId,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(listRes.$status).toBe(200);
|
||||||
|
expect(listRes.error).toBeNull();
|
||||||
|
expect(listRes.data).toBeInstanceOf(Array);
|
||||||
|
expect(listRes.data!.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should successfully create a new domain", async () => {
|
||||||
|
const createRes = await apiFetch<Domain>("/api/domains", {
|
||||||
|
method: "POST",
|
||||||
|
sessionId,
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: "happydomain.com",
|
||||||
|
type: "primary",
|
||||||
|
documentRoot: "/home/coaxys/happydomain.com",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(createRes.$status).toBe(201);
|
||||||
|
expect(createRes.error).toBeNull();
|
||||||
|
expect(createRes.data?.id).toBeDefined();
|
||||||
|
expect(createRes.data?.name).toBe("happydomain.com");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should successfully update domain properties", async () => {
|
||||||
|
const listRes = await apiFetch<Domain[]>("/api/domains", {
|
||||||
|
method: "GET",
|
||||||
|
sessionId,
|
||||||
|
});
|
||||||
|
const created = listRes.data!.find((d) => d.name === "happydomain.com");
|
||||||
|
|
||||||
|
const updateRes = await apiFetch<Domain>(`/api/domains/${created!.id}`, {
|
||||||
|
method: "PUT",
|
||||||
|
sessionId,
|
||||||
|
body: JSON.stringify({
|
||||||
|
documentRoot: "/home/coaxys/happydomain.com/public",
|
||||||
|
dnssec: true,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(updateRes.$status).toBe(200);
|
||||||
|
expect(updateRes.error).toBeNull();
|
||||||
|
expect(updateRes.data?.documentRoot).toBe("/home/coaxys/happydomain.com/public");
|
||||||
|
expect(updateRes.data?.dnssec).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should successfully list DNS records for a domain", async () => {
|
||||||
|
const listRes = await apiFetch<Domain[]>("/api/domains", {
|
||||||
|
method: "GET",
|
||||||
|
sessionId,
|
||||||
|
});
|
||||||
|
const domain = listRes.data!.find((d) => d.name === "happydomain.com");
|
||||||
|
|
||||||
|
const dnsRes = await apiFetch<DnsRecord[]>(`/api/domains/${domain!.id}/dns`, {
|
||||||
|
method: "GET",
|
||||||
|
sessionId,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(dnsRes.$status).toBe(200);
|
||||||
|
expect(dnsRes.error).toBeNull();
|
||||||
|
expect(dnsRes.data).toBeInstanceOf(Array);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should successfully create, update, and delete a DNS record on a domain", async () => {
|
||||||
|
const listRes = await apiFetch<Domain[]>("/api/domains", {
|
||||||
|
method: "GET",
|
||||||
|
sessionId,
|
||||||
|
});
|
||||||
|
const domain = listRes.data!.find((d) => d.name === "happydomain.com");
|
||||||
|
|
||||||
|
// 1. Create
|
||||||
|
const createDns = await apiFetch<DnsRecord>(`/api/domains/${domain!.id}/dns`, {
|
||||||
|
method: "POST",
|
||||||
|
sessionId,
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: "web",
|
||||||
|
ttl: 3600,
|
||||||
|
type: "A",
|
||||||
|
record: "192.0.2.55",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
expect(createDns.$status).toBe(201);
|
||||||
|
expect(createDns.data?.id).toBeDefined();
|
||||||
|
expect(createDns.data?.record).toBe("192.0.2.55");
|
||||||
|
|
||||||
|
// 2. Update
|
||||||
|
const updateDns = await apiFetch<DnsRecord>(
|
||||||
|
`/api/domains/${domain!.id}/dns/${createDns.data!.id}`,
|
||||||
|
{
|
||||||
|
method: "PUT",
|
||||||
|
sessionId,
|
||||||
|
body: JSON.stringify({
|
||||||
|
record: "192.0.2.56",
|
||||||
|
ttl: 7200,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
expect(updateDns.$status).toBe(200);
|
||||||
|
expect(updateDns.data?.record).toBe("192.0.2.56");
|
||||||
|
expect(updateDns.data?.ttl).toBe(7200);
|
||||||
|
|
||||||
|
// 3. Delete
|
||||||
|
const deleteDns = await apiFetch<{ deleted: string }>(
|
||||||
|
`/api/domains/${domain!.id}/dns/${createDns.data!.id}`,
|
||||||
|
{
|
||||||
|
method: "DELETE",
|
||||||
|
sessionId,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
expect(deleteDns.$status).toBe(200);
|
||||||
|
expect(deleteDns.data?.deleted).toBe(createDns.data!.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should successfully delete a domain", async () => {
|
||||||
|
const listRes = await apiFetch<Domain[]>("/api/domains", {
|
||||||
|
method: "GET",
|
||||||
|
sessionId,
|
||||||
|
});
|
||||||
|
const created = listRes.data!.find((d) => d.name === "happydomain.com");
|
||||||
|
|
||||||
|
const deleteRes = await apiFetch<{ deleted: string }>(`/api/domains/${created!.id}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
sessionId,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(deleteRes.$status).toBe(200);
|
||||||
|
expect(deleteRes.error).toBeNull();
|
||||||
|
|
||||||
|
// Confirm deletion
|
||||||
|
const confirmRes = await apiFetch(`/api/domains/${created!.id}`, {
|
||||||
|
method: "GET",
|
||||||
|
sessionId,
|
||||||
|
});
|
||||||
|
expect(confirmRes.$status).toBe(404);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Feature 4: Envelope & Metadata Validation ---
|
||||||
|
describe("Envelope & Metadata Happy Paths", () => {
|
||||||
|
const sessionId = "tier1_envelope";
|
||||||
|
|
||||||
|
it("should conform to strict envelope formatting on success", async () => {
|
||||||
|
clearCookieJar(sessionId);
|
||||||
|
await apiFetch("/api/auth/login", {
|
||||||
|
method: "POST",
|
||||||
|
sessionId,
|
||||||
|
body: JSON.stringify({ email: "owner@example.com", password: "correct-password" }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await apiFetch("/api/auth/me", { method: "GET", sessionId });
|
||||||
|
|
||||||
|
// Envelope layout validation
|
||||||
|
expect(res).toBeDefined();
|
||||||
|
expect(res).toHaveProperty("data");
|
||||||
|
expect(res).toHaveProperty("meta");
|
||||||
|
expect(res).toHaveProperty("error");
|
||||||
|
expect(res.error).toBeNull();
|
||||||
|
expect(res.data).not.toBeNull();
|
||||||
|
expect(typeof res.meta.requestId).toBe("string");
|
||||||
|
expect(res.meta.requestId.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should conform to strict envelope formatting on failure", async () => {
|
||||||
|
// Bad credentials login
|
||||||
|
const res = await apiFetch("/api/auth/login", {
|
||||||
|
method: "POST",
|
||||||
|
sessionId,
|
||||||
|
body: JSON.stringify({ email: "owner@example.com", password: "wrong-password" }),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Envelope layout validation
|
||||||
|
expect(res).toBeDefined();
|
||||||
|
expect(res).toHaveProperty("data");
|
||||||
|
expect(res).toHaveProperty("meta");
|
||||||
|
expect(res).toHaveProperty("error");
|
||||||
|
expect(res.data).toBeNull();
|
||||||
|
expect(res.error).not.toBeNull();
|
||||||
|
expect(typeof res.error!.code).toBe("string");
|
||||||
|
expect(typeof res.error!.message).toBe("string");
|
||||||
|
expect(typeof res.meta.requestId).toBe("string");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should provide pagination metadata on list endpoints", async () => {
|
||||||
|
const res = await apiFetch<Website[]>("/api/websites?page=1&pageSize=1", {
|
||||||
|
method: "GET",
|
||||||
|
sessionId,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.meta.pagination).toBeDefined();
|
||||||
|
expect(res.meta.pagination?.page).toBe(1);
|
||||||
|
expect(res.meta.pagination?.pageSize).toBe(1);
|
||||||
|
expect(typeof res.meta.pagination?.total).toBe("number");
|
||||||
|
expect(typeof res.meta.pagination?.pageCount).toBe("number");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should include rate limit metadata when appropriate", async () => {
|
||||||
|
// Perform request to rate limited simulator or trigger rate limit headers
|
||||||
|
const res = await apiFetch("/api/websites", {
|
||||||
|
method: "GET",
|
||||||
|
sessionId,
|
||||||
|
headers: { "x-test-rate-limit": "true" },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.$status).toBe(429);
|
||||||
|
expect(res.error?.code).toBe("RATE_LIMIT_ERROR");
|
||||||
|
expect((res.meta as any).rateLimit).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user