Add HTTP headers checker and tests
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
// Import Node's built-in test tools.
|
||||
const test = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
|
||||
// Import the HTTP headers service functions.
|
||||
const {
|
||||
checkHeaders,
|
||||
headersToObject,
|
||||
analyzeSecurityHeaders,
|
||||
isRedirectStatus,
|
||||
SECURITY_HEADERS,
|
||||
} = require("../src/services/headersService");
|
||||
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// isRedirectStatus() tests
|
||||
// ---------------------------------------------------------
|
||||
|
||||
test("recognizes standard redirect status codes", () => {
|
||||
[301, 302, 303, 307, 308].forEach((statusCode) => {
|
||||
assert.equal(isRedirectStatus(statusCode), true);
|
||||
});
|
||||
});
|
||||
|
||||
test("rejects non-redirect status codes", () => {
|
||||
[200, 201, 204, 400, 404, 500].forEach((statusCode) => {
|
||||
assert.equal(isRedirectStatus(statusCode), false);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// headersToObject() tests
|
||||
// ---------------------------------------------------------
|
||||
|
||||
test("converts Fetch Headers into a plain object", () => {
|
||||
const headers = new Headers({
|
||||
"content-type": "text/html",
|
||||
"x-content-type-options": "nosniff",
|
||||
});
|
||||
|
||||
const result = headersToObject(headers);
|
||||
|
||||
assert.deepEqual(result, {
|
||||
"content-type": "text/html",
|
||||
"x-content-type-options": "nosniff",
|
||||
});
|
||||
});
|
||||
|
||||
test("normalizes header names to lowercase", () => {
|
||||
const headers = new Headers({
|
||||
"X-Frame-Options": "DENY",
|
||||
});
|
||||
|
||||
const result = headersToObject(headers);
|
||||
|
||||
assert.equal(result["x-frame-options"], "DENY");
|
||||
});
|
||||
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// analyzeSecurityHeaders() tests
|
||||
// ---------------------------------------------------------
|
||||
|
||||
test("marks present security headers correctly", () => {
|
||||
const headers = {
|
||||
"strict-transport-security": "max-age=31536000",
|
||||
"x-content-type-options": "nosniff",
|
||||
};
|
||||
|
||||
const result = analyzeSecurityHeaders(headers);
|
||||
|
||||
const hsts = result.find(
|
||||
(header) => header.name === "Strict-Transport-Security"
|
||||
);
|
||||
|
||||
const contentType = result.find(
|
||||
(header) => header.name === "X-Content-Type-Options"
|
||||
);
|
||||
|
||||
assert.equal(hsts.present, true);
|
||||
assert.equal(
|
||||
hsts.value,
|
||||
"max-age=31536000"
|
||||
);
|
||||
|
||||
assert.equal(contentType.present, true);
|
||||
assert.equal(contentType.value, "nosniff");
|
||||
});
|
||||
|
||||
test("marks missing security headers correctly", () => {
|
||||
const result = analyzeSecurityHeaders({});
|
||||
|
||||
result.forEach((header) => {
|
||||
assert.equal(header.present, false);
|
||||
assert.equal(header.value, null);
|
||||
});
|
||||
});
|
||||
|
||||
test("returns one result for every configured security header", () => {
|
||||
const result = analyzeSecurityHeaders({});
|
||||
|
||||
assert.equal(
|
||||
result.length,
|
||||
SECURITY_HEADERS.length
|
||||
);
|
||||
});
|
||||
|
||||
test("returns the expected security header labels", () => {
|
||||
const result = analyzeSecurityHeaders({});
|
||||
|
||||
const names = result.map((header) => header.name);
|
||||
|
||||
assert.deepEqual(names, [
|
||||
"Strict-Transport-Security",
|
||||
"Content-Security-Policy",
|
||||
"X-Content-Type-Options",
|
||||
"X-Frame-Options",
|
||||
"Referrer-Policy",
|
||||
"Permissions-Policy",
|
||||
"Cross-Origin-Opener-Policy",
|
||||
"Cross-Origin-Resource-Policy",
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// checkHeaders() integration tests
|
||||
// ---------------------------------------------------------
|
||||
|
||||
test("checks headers for a public website", async () => {
|
||||
const result = await checkHeaders(
|
||||
"https://example.com"
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
result.requestedUrl,
|
||||
"https://example.com"
|
||||
);
|
||||
|
||||
assert.equal(typeof result.finalUrl, "string");
|
||||
assert.equal(typeof result.statusCode, "number");
|
||||
assert.equal(typeof result.statusText, "string");
|
||||
assert.equal(typeof result.redirectCount, "number");
|
||||
assert.equal(typeof result.headers, "object");
|
||||
|
||||
assert.equal(
|
||||
Array.isArray(result.securityHeaders),
|
||||
true
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
result.securityHeaders.length,
|
||||
SECURITY_HEADERS.length
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
typeof result.presentSecurityHeaderCount,
|
||||
"number"
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
result.totalSecurityHeaderCount,
|
||||
SECURITY_HEADERS.length
|
||||
);
|
||||
|
||||
assert.equal(typeof result.checkedAt, "string");
|
||||
});
|
||||
|
||||
test("follows redirects safely", async () => {
|
||||
const result = await checkHeaders(
|
||||
"http://example.com"
|
||||
);
|
||||
|
||||
assert.equal(typeof result.finalUrl, "string");
|
||||
assert.equal(result.redirectCount >= 0, true);
|
||||
});
|
||||
|
||||
test("blocks loopback addresses", async () => {
|
||||
await assert.rejects(
|
||||
checkHeaders("http://127.0.0.1"),
|
||||
/Local and private network addresses are not allowed/
|
||||
);
|
||||
});
|
||||
|
||||
test("blocks private network addresses", async () => {
|
||||
await assert.rejects(
|
||||
checkHeaders("http://192.168.1.1"),
|
||||
/Local and private network addresses are not allowed/
|
||||
);
|
||||
});
|
||||
|
||||
test("rejects malformed URLs", async () => {
|
||||
await assert.rejects(
|
||||
checkHeaders("not a valid URL"),
|
||||
/Enter a valid URL/
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user