test(auth): hermetically mock SQL and Valkey rate limiter to prevent connection leaks
This commit is contained in:
parent
11d4b80014
commit
a8111f6dc3
@ -57,7 +57,7 @@ export const publicRateLimiter = async (
|
|||||||
) => {
|
) => {
|
||||||
const ip = getClientIp(c);
|
const ip = getClientIp(c);
|
||||||
const key = `ratelimit:public:${ip}`;
|
const key = `ratelimit:public:${ip}`;
|
||||||
const allowed = await checkRateLimit(key, 10, 60000);
|
const allowed = await rateLimitWrapper.checkRateLimit(key, 10, 60000);
|
||||||
if (!allowed) {
|
if (!allowed) {
|
||||||
return c.json({ error: "Too Many Requests" }, 429);
|
return c.json({ error: "Too Many Requests" }, 429);
|
||||||
}
|
}
|
||||||
@ -74,7 +74,7 @@ export const adminRateLimiter = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const key = `ratelimit:admin:${auth.userId}`;
|
const key = `ratelimit:admin:${auth.userId}`;
|
||||||
const allowed = await checkRateLimit(key, 60, 60000);
|
const allowed = await rateLimitWrapper.checkRateLimit(key, 60, 60000);
|
||||||
if (!allowed) {
|
if (!allowed) {
|
||||||
return c.json({ error: "Too Many Requests" }, 429);
|
return c.json({ error: "Too Many Requests" }, 429);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,9 @@ import { test } from "jsr:@std/testing/bdd";
|
|||||||
import { expect } from "jsr:@std/expect";
|
import { expect } from "jsr:@std/expect";
|
||||||
import { Hono } from "jsr:@hono/hono@4";
|
import { Hono } from "jsr:@hono/hono@4";
|
||||||
import { authRoutes } from "./routes.tsx";
|
import { authRoutes } from "./routes.tsx";
|
||||||
|
import { sqlWrapper } from "../../core/db.ts";
|
||||||
|
import { rateLimitWrapper } from "../../core/middleware.ts";
|
||||||
|
import { valkey } from "../../core/valkey.ts";
|
||||||
|
|
||||||
test("auth slice UI endpoints return HTML", async () => {
|
test("auth slice UI endpoints return HTML", async () => {
|
||||||
const app = new Hono();
|
const app = new Hono();
|
||||||
@ -20,7 +23,46 @@ test("auth slice UI endpoints return HTML", async () => {
|
|||||||
expect(res.headers.get("content-type")).toContain("text/html");
|
expect(res.headers.get("content-type")).toContain("text/html");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("login challenge requires JSON payload", async () => {
|
test("login challenge generates valid WebAuthn options for user with passkey", async () => {
|
||||||
|
const originalSql = sqlWrapper.sql;
|
||||||
|
const originalRateLimit = rateLimitWrapper.checkRateLimit;
|
||||||
|
const originalValkeySetex = valkey.setex;
|
||||||
|
const originalRpId = Deno.env.get("RP_ID");
|
||||||
|
|
||||||
|
try {
|
||||||
|
Deno.env.set("RP_ID", "localhost");
|
||||||
|
|
||||||
|
// Hermetic mocks
|
||||||
|
rateLimitWrapper.checkRateLimit = () => Promise.resolve(true);
|
||||||
|
valkey.setex = (() => Promise.resolve("OK")) as any;
|
||||||
|
|
||||||
|
const mockUser = {
|
||||||
|
id: "mock-user-123",
|
||||||
|
username: "alice",
|
||||||
|
account_status: "active",
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockPasskey = {
|
||||||
|
id: "mock-passkey-1",
|
||||||
|
credential_id: "Y3JlZGVudGlhbC0x",
|
||||||
|
public_key: new Uint8Array([1, 2, 3]),
|
||||||
|
counter: 0,
|
||||||
|
transports: ["internal"],
|
||||||
|
user_id: "mock-user-123",
|
||||||
|
prf_enabled: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
sqlWrapper.sql = ((strings: any, ..._values: any[]) => {
|
||||||
|
const query = strings.join("?");
|
||||||
|
if (query.includes("SELECT id, account_status FROM users WHERE username =")) {
|
||||||
|
return Promise.resolve([mockUser]);
|
||||||
|
}
|
||||||
|
if (query.includes("FROM passkeys WHERE user_id =")) {
|
||||||
|
return Promise.resolve([mockPasskey]);
|
||||||
|
}
|
||||||
|
return Promise.resolve([]);
|
||||||
|
}) as any;
|
||||||
|
|
||||||
const app = new Hono();
|
const app = new Hono();
|
||||||
app.route("/", authRoutes);
|
app.route("/", authRoutes);
|
||||||
|
|
||||||
@ -29,9 +71,21 @@ test("login challenge requires JSON payload", async () => {
|
|||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ username: "test" }),
|
body: JSON.stringify({ username: "alice" }),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Mock DB isn't loaded so it might fail with 500, but we just verify it routed
|
expect(res.status).toBe(200);
|
||||||
expect(res.status).not.toBe(404);
|
const data = await res.json();
|
||||||
|
expect(data.options).toBeDefined();
|
||||||
|
expect(data.options.challenge).toBeDefined();
|
||||||
|
expect(data.options.allowCredentials).toBeDefined();
|
||||||
|
expect(data.options.allowCredentials.length).toBe(1);
|
||||||
|
} finally {
|
||||||
|
sqlWrapper.sql = originalSql;
|
||||||
|
rateLimitWrapper.checkRateLimit = originalRateLimit;
|
||||||
|
valkey.setex = originalValkeySetex;
|
||||||
|
if (originalRpId) {
|
||||||
|
Deno.env.set("RP_ID", originalRpId);
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user