feat: enforce route-level rate limits for webhook auth and user actions
This commit is contained in:
101
test/app.test.js
101
test/app.test.js
@@ -6,19 +6,43 @@ const { buildApp } = require("../src/app");
|
||||
const { hmacSHA256Hex } = require("../src/lib/signature");
|
||||
|
||||
function createApp(options = {}) {
|
||||
return buildApp({
|
||||
config: {
|
||||
xWebhookSecret: "x-secret",
|
||||
polarWebhookSecret: "polar-secret",
|
||||
credit: {
|
||||
baseCredits: 1,
|
||||
includedChars: 25000,
|
||||
stepChars: 10000,
|
||||
stepCredits: 1,
|
||||
maxCharsPerArticle: 120000,
|
||||
},
|
||||
const baseConfig = {
|
||||
xWebhookSecret: "x-secret",
|
||||
polarWebhookSecret: "polar-secret",
|
||||
rateLimits: {
|
||||
webhookPerMinute: 120,
|
||||
authPerMinute: 30,
|
||||
actionPerMinute: 60,
|
||||
},
|
||||
...options,
|
||||
credit: {
|
||||
baseCredits: 1,
|
||||
includedChars: 25000,
|
||||
stepChars: 10000,
|
||||
stepCredits: 1,
|
||||
maxCharsPerArticle: 120000,
|
||||
},
|
||||
};
|
||||
|
||||
const overrideConfig = options.config || {};
|
||||
const mergedConfig = {
|
||||
...baseConfig,
|
||||
...overrideConfig,
|
||||
rateLimits: {
|
||||
...baseConfig.rateLimits,
|
||||
...(overrideConfig.rateLimits || {}),
|
||||
},
|
||||
credit: {
|
||||
...baseConfig.credit,
|
||||
...(overrideConfig.credit || {}),
|
||||
},
|
||||
};
|
||||
|
||||
const appOptions = { ...options };
|
||||
delete appOptions.config;
|
||||
|
||||
return buildApp({
|
||||
config: mergedConfig,
|
||||
...appOptions,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -224,3 +248,56 @@ test("emits persistence snapshots on mutating actions", () => {
|
||||
assert.equal(typeof latest.updatedAt, "string");
|
||||
assert.equal(typeof latest.engine, "object");
|
||||
});
|
||||
|
||||
test("rate limits repeated webhook calls", () => {
|
||||
const app = createApp({
|
||||
config: {
|
||||
rateLimits: {
|
||||
webhookPerMinute: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const first = call(app, {
|
||||
method: "POST",
|
||||
path: "/api/webhooks/x",
|
||||
headers: { "x-forwarded-for": "1.2.3.4", "x-signature": "sha256=deadbeef" },
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
const second = call(app, {
|
||||
method: "POST",
|
||||
path: "/api/webhooks/x",
|
||||
headers: { "x-forwarded-for": "1.2.3.4", "x-signature": "sha256=deadbeef" },
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
|
||||
assert.equal(first.status, 401);
|
||||
assert.equal(second.status, 429);
|
||||
});
|
||||
|
||||
test("rate limits repeated login attempts from same IP", () => {
|
||||
const app = createApp({
|
||||
config: {
|
||||
rateLimits: {
|
||||
authPerMinute: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const first = call(app, {
|
||||
method: "POST",
|
||||
path: "/auth/dev-login",
|
||||
headers: { "x-forwarded-for": "5.5.5.5" },
|
||||
body: "userId=alice&returnTo=%2Fapp",
|
||||
});
|
||||
const second = call(app, {
|
||||
method: "POST",
|
||||
path: "/auth/dev-login",
|
||||
headers: { "x-forwarded-for": "5.5.5.5" },
|
||||
body: "userId=alice&returnTo=%2Fapp",
|
||||
});
|
||||
|
||||
assert.equal(first.status, 303);
|
||||
assert.equal(second.status, 303);
|
||||
assert.match(second.headers.location, /Too%20many%20requests/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user