IKHSAN
EN Read in EnglishID

homeio

Publik

Fork dari doctor-io/homeio

Branch
main
Commit terakhir
958d74616 Oktober 2026

route.test.ts

Buka raw
import { NextResponse } from "next/server";
import { beforeEach, describe, expect, it, vi } from "vitest";

vi.mock("@/lib/server/modules/auth/totp-service", () => ({
  TotpServiceError: class TotpServiceError extends Error {
    code: string;
    statusCode: number;
    publicMessage: string;

    constructor(
      message: string,
      options: {
        code: string;
        statusCode: number;
        publicMessage?: string;
      },
    ) {
      super(message);
      this.name = "TotpServiceError";
      this.code = options.code;
      this.statusCode = options.statusCode;
      this.publicMessage = options.publicMessage ?? message;
    }
  },
  completeTotpEnrollment: vi.fn(),
}));

import { POST } from "@/app/api/v1/auth/2fa/verify/route";
import { requireApiSession } from "@/lib/server/modules/auth/api";
import {
  TotpServiceError,
  completeTotpEnrollment,
} from "@/lib/server/modules/auth/totp-service";

const buildRequest = (body: unknown) =>
  new Request("http://localhost/api/v1/auth/2fa/verify", {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: typeof body === "string" ? body : JSON.stringify(body),
  });

const VALID_BODY = { code: "123456" };

beforeEach(() => {
  vi.mocked(completeTotpEnrollment).mockReset();
  vi.mocked(requireApiSession).mockImplementation(async () => ({
    session: {
      sessionId: "test-session",
      userId: "test-user",
      username: "admin",
      passwordHash: "test-password-hash",
      expiresAt: new Date(Date.now() + 3600_000),
    },
    response: null,
  }));
});

describe("POST /api/v1/auth/2fa/verify", () => {
  it("returns 401 when the session cannot be authenticated", async () => {
    vi.mocked(requireApiSession).mockResolvedValueOnce({
      session: null,
      response: NextResponse.json({ error: "Unauthorized" }, { status: 401 }),
    });

    const response = await POST(buildRequest(VALID_BODY));
    expect(response.status).toBe(401);
    expect(vi.mocked(completeTotpEnrollment)).not.toHaveBeenCalled();
  });

  it("returns 400 for a malformed JSON body", async () => {
    const response = await POST(buildRequest("{not json"));
    expect(response.status).toBe(400);
    expect(vi.mocked(completeTotpEnrollment)).not.toHaveBeenCalled();
  });

  it("returns 400 when the body is missing 'code'", async () => {
    const response = await POST(buildRequest({}));
    expect(response.status).toBe(400);
    expect(vi.mocked(completeTotpEnrollment)).not.toHaveBeenCalled();
  });

  it("returns 200 with the backup codes on success", async () => {
    vi.mocked(completeTotpEnrollment).mockResolvedValueOnce({
      enabled: true,
      enrolledAt: "2026-05-23T13:00:00.000Z",
      backupCodes: ["AAAAA11111", "BBBBB22222"],
    });

    const response = await POST(buildRequest(VALID_BODY));
    expect(response.status).toBe(200);

    const json = (await response.json()) as {
      data: {
        enabled: boolean;
        enrolledAt: string;
        backupCodes: string[];
      };
    };
    expect(json.data.enabled).toBe(true);
    expect(json.data.backupCodes).toEqual(["AAAAA11111", "BBBBB22222"]);

    expect(vi.mocked(completeTotpEnrollment)).toHaveBeenCalledWith({
      userId: "test-user",
      code: "123456",
    });
  });

  it("maps invalid_totp to 400 with the code in the body", async () => {
    vi.mocked(completeTotpEnrollment).mockRejectedValueOnce(
      new TotpServiceError("Invalid verification code", {
        code: "invalid_totp",
        statusCode: 400,
      }),
    );

    const response = await POST(buildRequest(VALID_BODY));
    expect(response.status).toBe(400);

    const json = (await response.json()) as { error: string; code: string };
    expect(json.code).toBe("invalid_totp");
  });

  it("maps already_enabled to 409", async () => {
    vi.mocked(completeTotpEnrollment).mockRejectedValueOnce(
      new TotpServiceError("Already enabled", {
        code: "already_enabled",
        statusCode: 409,
      }),
    );

    const response = await POST(buildRequest(VALID_BODY));
    expect(response.status).toBe(409);

    const json = (await response.json()) as { code: string };
    expect(json.code).toBe("already_enabled");
  });

  it("maps no_pending_enrollment to 409", async () => {
    vi.mocked(completeTotpEnrollment).mockRejectedValueOnce(
      new TotpServiceError("No pending enrolment", {
        code: "no_pending_enrollment",
        statusCode: 409,
      }),
    );

    const response = await POST(buildRequest(VALID_BODY));
    expect(response.status).toBe(409);

    const json = (await response.json()) as { code: string };
    expect(json.code).toBe("no_pending_enrollment");
  });

  it("returns 500 with a generic message for unexpected errors", async () => {
    vi.mocked(completeTotpEnrollment).mockRejectedValueOnce(
      new Error("database is down"),
    );

    const response = await POST(buildRequest(VALID_BODY));
    expect(response.status).toBe(500);

    const json = (await response.json()) as { error: string; code?: string };
    expect(json.error).not.toContain("database is down");
    expect(json.code).toBeUndefined();
  });
});
byte
5.162
baris
166
Jenis
ts
Commit terakhir
16 Oktober 2026