import { beforeEach, describe, expect, it, vi } from "vitest"; vi.mock("~/utils/db.server", () => ({ db: { ipAddress: { findUnique: vi.fn(), upsert: vi.fn(), }, ipAddressLog: { create: vi.fn(), }, }, })); vi.mock("~/utils/cache.server", () => ({ cachified: vi.fn(({ getFreshValue }) => getFreshValue()), clearCacheKey: vi.fn(), })); vi.mock("~/utils/server/IpUtils", () => ({ getClientIPAddress: vi.fn(() => "203.0.113.10") })); vi.mock("~/utils/db/blacklist.db.server", () => ({ addBlacklistAttempt: vi.fn(), addToBlacklist: vi.fn(), findInBlacklist: vi.fn(() => null), })); // AD-22: GeoIP/Hashing wird hermetisch gemockt — hashIp ist deterministisch, Land-Auflösung ein No-op. vi.mock("./geoip.server", () => ({ hashIp: (ip: string) => `hash(${ip})`, lookupCountry: vi.fn(async () => null), })); import IpAddressServiceServer from "./IpAddressService.server"; import { db } from "~/utils/db.server"; import { addBlacklistAttempt, findInBlacklist } from "~/utils/db/blacklist.db.server"; const RAW_IP = "203.0.113.10"; const HASHED_IP = `hash(${RAW_IP})`; const dbMock = db as unknown as { ipAddress: { findUnique: ReturnType; upsert: ReturnType; }; ipAddressLog: { create: ReturnType; }; }; const findInBlacklistMock = vi.mocked(findInBlacklist); const addBlacklistAttemptMock = vi.mocked(addBlacklistAttempt); describe("IpAddressServiceServer", () => { beforeEach(() => { vi.clearAllMocks(); findInBlacklistMock.mockResolvedValue(null); dbMock.ipAddress.findUnique.mockResolvedValue(null); dbMock.ipAddress.upsert.mockResolvedValue({ id: "ip_1", ip: HASHED_IP, provider: "", type: "", countryCode: "", countryName: "", regionCode: "", regionName: "", city: "", zipCode: "", latitude: null, longitude: null, metadata: "null", createdAt: new Date("2026-01-01T00:00:00.000Z"), updatedAt: new Date("2026-01-01T00:00:00.000Z"), }); dbMock.ipAddressLog.create.mockResolvedValue({ id: "log_1" }); }); it("speichert nur den IP-Hash, nie die Klartext-IP (AD-22)", async () => { const request = new Request("https://admin.boiler.saas-ctrl.com/login"); await expect( IpAddressServiceServer.log(request, { action: "login", description: "admin@example.com" }) ).resolves.toBeUndefined(); expect(dbMock.ipAddress.upsert).toHaveBeenCalledWith({ where: { ip: HASHED_IP }, update: {}, create: expect.objectContaining({ ip: HASHED_IP }), }); const logArgs = dbMock.ipAddressLog.create.mock.calls[0][0]; expect(logArgs.data.ip).toBe(HASHED_IP); expect(logArgs.data).toMatchObject({ ipAddressId: "ip_1", url: "/login", action: "login", success: true }); }); it("ersetzt die Request-IP nicht durch eine fixe Dev-Adresse", async () => { const request = new Request("https://admin.boiler.saas-ctrl.com/login"); await expect( IpAddressServiceServer.log(request, { action: "login", description: "admin@example.com" }) ).resolves.toBeUndefined(); expect(dbMock.ipAddress.upsert).toHaveBeenCalledWith(expect.objectContaining({ where: { ip: HASHED_IP } })); expect(dbMock.ipAddress.upsert).not.toHaveBeenCalledWith(expect.objectContaining({ where: { ip: "hash(189.203.100.82)" } })); }); it("speichert stabile blacklisted-Fehlercodes mit gehashter IP", async () => { findInBlacklistMock.mockResolvedValue({ id: "blacklist_1" } as never); const request = new Request("https://admin.boiler.saas-ctrl.com/login"); await expect( IpAddressServiceServer.log(request, { action: "login", description: "admin@example.com" }) ).rejects.toThrow("blacklisted"); expect(addBlacklistAttemptMock).toHaveBeenCalledWith({ id: "blacklist_1" }); expect(dbMock.ipAddressLog.create).toHaveBeenCalledWith({ data: expect.objectContaining({ ip: HASHED_IP, error: "blacklisted", success: false, }), }); }); it("speichert stabile unauthorized-Fehlercodes für provider-abgelehnte IP-Log-Zeilen", async () => { dbMock.ipAddress.findUnique.mockResolvedValue({ id: "ip_1", ip: HASHED_IP, provider: "ipapi.is", type: "", countryCode: "", countryName: "", regionCode: "", regionName: "", city: "", zipCode: "", latitude: null, longitude: null, metadata: JSON.stringify({ is_crawler: true }), createdAt: new Date("2026-01-01T00:00:00.000Z"), updatedAt: new Date("2026-01-01T00:00:00.000Z"), }); const request = new Request("https://admin.boiler.saas-ctrl.com/login"); await expect( IpAddressServiceServer.log(request, { action: "login", description: "admin@example.com" }) ).rejects.toThrow("unauthorized"); expect(dbMock.ipAddressLog.create).toHaveBeenCalledWith({ data: expect.objectContaining({ ip: HASHED_IP, error: "unauthorized", success: false, }), }); }); });