198 lines
4.8 KiB
JavaScript
198 lines
4.8 KiB
JavaScript
// 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/
|
|
);
|
|
}); |