chore: init
This commit is contained in:
commit
e59d95bce0
18 changed files with 8015 additions and 0 deletions
11
.gitignore
vendored
Normal file
11
.gitignore
vendored
Normal file
|
@ -0,0 +1,11 @@
|
|||
# Build output
|
||||
/dist/
|
||||
|
||||
# SVR.JS
|
||||
/svrjs/
|
||||
|
||||
# Dependencies
|
||||
node_modules
|
||||
|
||||
# ESLint cache
|
||||
.eslintcache
|
2
.husky/commit-msg
Executable file
2
.husky/commit-msg
Executable file
|
@ -0,0 +1,2 @@
|
|||
#!/bin/sh
|
||||
npx --no -- commitlint --edit "$1"
|
2
.husky/pre-commit
Executable file
2
.husky/pre-commit
Executable file
|
@ -0,0 +1,2 @@
|
|||
#!/bin/sh
|
||||
npx lint-staged
|
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2018-2024 SVR.JS
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
29
README
Normal file
29
README
Normal file
|
@ -0,0 +1,29 @@
|
|||
This repository contains SVR.JS mod starter code and its build system
|
||||
The mod will work for SVR.JS Nightly-GitNext.
|
||||
|
||||
Before doing anything, run "npm install".
|
||||
To build SVR.JS mod, run "npm run build".
|
||||
To check SVR.JS mod code for errors with ESLint, run "npm run lint".
|
||||
To fix and beautify SVR.JS mod code with ESLint and Prettier, run "npm run lint:fix".
|
||||
To perform unit tests with Jest, run "npm test".
|
||||
|
||||
To test the mod:
|
||||
1. Clone the SVR.JS repository with "git clone https://git.svrjs.org/svrjs/svrjs.git" command.
|
||||
2. Change the working directory to "svrjs" using "cd svrjs".
|
||||
3. Build SVR.JS by first running "npm install" and then running "npm run build".
|
||||
4. Copy the mod into mods directory in the dist directory using "cp ../dist/mod.js dist/mods" (GNU/Linux, Unix, BSD) or "copy ..\dist\mod.js dist\mods" (Windows).
|
||||
5. Do the necessary mod configuration if the mod requires it.
|
||||
6. Run SVR.JS by running "npm start".
|
||||
7. Do some requests to the endpoints covered by the mod.
|
||||
|
||||
Structure:
|
||||
- dist - contains the built SVR.JS mod
|
||||
- src - contains SVR.JS mod source code
|
||||
- index.js - entry point
|
||||
- utils - utility functions
|
||||
- tests - Jest unit tests
|
||||
- utils - unit tests for utility functions
|
||||
- esbuild.config.js - the build script
|
||||
- eslint.config.js - ESLint configuration
|
||||
- jest.config.js - Jest configuration
|
||||
- modInfo.json - SVR.JS mod name and version
|
3
commitlint.config.js
Normal file
3
commitlint.config.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
module.exports = {
|
||||
extends: ["@commitlint/config-conventional"]
|
||||
};
|
11
esbuild.config.js
Normal file
11
esbuild.config.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
const esbuild = require("esbuild");
|
||||
|
||||
esbuild.build({
|
||||
entryPoints: ["src/index.js"],
|
||||
bundle: true,
|
||||
outfile: "dist/mod.js", // Mod output file
|
||||
platform: "node",
|
||||
target: "es2017",
|
||||
}).catch((err) => {
|
||||
throw err;
|
||||
});
|
30
eslint.config.js
Normal file
30
eslint.config.js
Normal file
|
@ -0,0 +1,30 @@
|
|||
const globals = require("globals");
|
||||
const pluginJs = require("@eslint/js");
|
||||
const eslintPluginPrettierRecommended = require("eslint-plugin-prettier/recommended");
|
||||
const jest = require("eslint-plugin-jest");
|
||||
|
||||
module.exports = [
|
||||
{
|
||||
files: ["**/*.js"],
|
||||
languageOptions: {
|
||||
sourceType: "commonjs"
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ["tests/*.test.js", "tests/**/*.test.js"],
|
||||
...jest.configs['flat/recommended'],
|
||||
rules: {
|
||||
...jest.configs['flat/recommended'].rules,
|
||||
'jest/prefer-expect-assertions': 'off',
|
||||
}
|
||||
},
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node
|
||||
}
|
||||
}
|
||||
},
|
||||
pluginJs.configs.recommended,
|
||||
eslintPluginPrettierRecommended
|
||||
];
|
5
jest.config.js
Normal file
5
jest.config.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
module.exports = {
|
||||
testEnvironment: 'node',
|
||||
testMatch: ['**/tests/**/*.test.js'],
|
||||
verbose: true,
|
||||
};
|
4
lint-staged.config.js
Normal file
4
lint-staged.config.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
module.exports = {
|
||||
"tests/**/*.js": "eslint --cache --fix",
|
||||
"src/**/*.js": "eslint --cache --fix"
|
||||
};
|
4
modInfo.json
Normal file
4
modInfo.json
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"name": "SVR.JS Cache mod",
|
||||
"version": "Nightly-GitMain"
|
||||
}
|
7308
package-lock.json
generated
Normal file
7308
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
35
package.json
Normal file
35
package.json
Normal file
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"name": "svrjs-mod-starter",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "node esbuild.config.js",
|
||||
"cz": "cz",
|
||||
"lint": "eslint --no-error-on-unmatched-pattern src/**/*.js src/*.js tests/**/*.test.js tests/**/*.js",
|
||||
"lint:fix": "npm run lint -- --fix",
|
||||
"prepare": "husky",
|
||||
"test": "jest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^19.6.1",
|
||||
"@commitlint/config-conventional": "^19.6.0",
|
||||
"@eslint/js": "^9.9.1",
|
||||
"commitizen": "^4.3.1",
|
||||
"cz-conventional-changelog": "^3.3.0",
|
||||
"esbuild": "^0.23.1",
|
||||
"eslint": "^9.9.1",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-jest": "^28.8.0",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"globals": "^15.9.0",
|
||||
"husky": "^9.1.7",
|
||||
"jest": "^29.7.0",
|
||||
"lint-staged": "^15.2.11",
|
||||
"prettier": "^3.3.3"
|
||||
},
|
||||
"config": {
|
||||
"commitizen": {
|
||||
"path": "./node_modules/cz-conventional-changelog"
|
||||
}
|
||||
}
|
||||
}
|
15
prettier.config.js
Normal file
15
prettier.config.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
// prettier.config.js, .prettierrc.js, prettier.config.cjs, or .prettierrc.cjs
|
||||
|
||||
/**
|
||||
* @see https://prettier.io/docs/en/configuration.html
|
||||
* @type {import("prettier").Config}
|
||||
*/
|
||||
const config = {
|
||||
trailingComma: "none",
|
||||
tabWidth: 2,
|
||||
semi: true,
|
||||
singleQuote: false,
|
||||
endOfLine: "lf"
|
||||
};
|
||||
|
||||
module.exports = config;
|
219
src/index.js
Normal file
219
src/index.js
Normal file
|
@ -0,0 +1,219 @@
|
|||
const {
|
||||
parseCacheControl,
|
||||
parseVary,
|
||||
shouldCacheResponse,
|
||||
isCacheValid
|
||||
} = require("./utils/cacheControlUtils.js");
|
||||
const modInfo = require("../modInfo.json");
|
||||
|
||||
// Simple in-memory cache
|
||||
const cache = new Map();
|
||||
const varyCache = new Map();
|
||||
|
||||
module.exports = function (req, res, logFacilities, config, next) {
|
||||
// Cache configuration
|
||||
const cacheVaryHeadersConfigured = config.cacheVaryHeaders
|
||||
? config.cacheVaryHeaders
|
||||
: [];
|
||||
const maximumCachedResponseSize = config.maximumCachedResponseSize
|
||||
? config.maximumCachedResponseSize
|
||||
: null;
|
||||
|
||||
const cacheKey =
|
||||
req.method +
|
||||
" " +
|
||||
(req.socket.encrypted ? "https://" : "http://") +
|
||||
(req.headers.host ? req.headers.host : "") +
|
||||
req.url;
|
||||
const requestCacheControl = parseCacheControl(req.headers["cache-control"]);
|
||||
|
||||
if (
|
||||
(req.method != "GET" && req.method != "HEAD") ||
|
||||
requestCacheControl["no-store"]
|
||||
) {
|
||||
res.setHeader("X-SVRJS-Cache", "BYPASS");
|
||||
return next(); // Skip cache and proceed to the next middleware
|
||||
}
|
||||
|
||||
// Check cache
|
||||
if (!requestCacheControl["no-cache"] && varyCache.has(cacheKey)) {
|
||||
const processedVary = varyCache.get(cacheKey);
|
||||
const cacheKeyWithVary =
|
||||
cacheKey +
|
||||
"\n" +
|
||||
processedVary
|
||||
.map((headerName) =>
|
||||
req.headers[headerName]
|
||||
? `${headerName}: ${req.headers[headerName]}`
|
||||
: ""
|
||||
)
|
||||
.join("\n");
|
||||
varyCache.set(cacheKey, processedVary);
|
||||
|
||||
if (cache.has(cacheKeyWithVary)) {
|
||||
const cachedEntry = cache.get(cacheKeyWithVary);
|
||||
if (isCacheValid(cachedEntry, req.headers)) {
|
||||
logFacilities.resmessage("The response is cached.");
|
||||
res.getHeaderNames().forEach((headerName) => {
|
||||
res.removeHeader(headerName);
|
||||
});
|
||||
res.setHeader("X-SVRJS-Cache", "HIT");
|
||||
res.writeHead(cachedEntry.statusCode, cachedEntry.headers);
|
||||
res.end(Buffer.from(cachedEntry.body, "latin1"));
|
||||
return; // Serve cached response and stop further execution
|
||||
} else {
|
||||
cache.delete(cacheKey); // Cache expired
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Capture the response
|
||||
const originalWriteHead = res.writeHead.bind(res);
|
||||
const originalEnd = res.end.bind(res);
|
||||
let writtenHeaders = res.getHeaders();
|
||||
let writtenStatusCode = 200;
|
||||
let responseBody = "";
|
||||
let maximumCachedResponseSizeExceeded = false;
|
||||
|
||||
res.writeHead = function (statusCode, statusCodeDescription, headers) {
|
||||
writtenHeaders = Object.assign(
|
||||
writtenHeaders,
|
||||
headers ? headers : statusCodeDescription
|
||||
);
|
||||
writtenStatusCode = statusCode;
|
||||
res.setHeader("X-SVRJS-Cache", "MISS");
|
||||
if (headers) {
|
||||
originalWriteHead(
|
||||
writtenStatusCode,
|
||||
statusCodeDescription,
|
||||
writtenHeaders
|
||||
);
|
||||
} else {
|
||||
originalWriteHead(writtenStatusCode, writtenHeaders);
|
||||
}
|
||||
};
|
||||
|
||||
res.end = function (chunk, encoding, callback) {
|
||||
if (req.method != "HEAD" && chunk && !maximumCachedResponseSizeExceeded) {
|
||||
const processedChunk = Buffer.from(
|
||||
chunk,
|
||||
typeof encoding === "string" ? encoding : undefined
|
||||
).toString("latin1");
|
||||
if (
|
||||
maximumCachedResponseSize !== null &&
|
||||
maximumCachedResponseSize !== undefined &&
|
||||
responseBody.length + processedChunk.length > maximumCachedResponseSize
|
||||
) {
|
||||
maximumCachedResponseSizeExceeded = true;
|
||||
} else {
|
||||
try {
|
||||
responseBody += processedChunk;
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
} catch (err) {
|
||||
maximumCachedResponseSizeExceeded = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const responseCacheControl = parseCacheControl(
|
||||
writtenHeaders[
|
||||
Object.keys(writtenHeaders).find(
|
||||
(key) => key.toLowerCase() == "cache-control"
|
||||
)
|
||||
]
|
||||
);
|
||||
|
||||
if (
|
||||
!maximumCachedResponseSizeExceeded &&
|
||||
shouldCacheResponse(
|
||||
responseCacheControl,
|
||||
req.headers.authentication !== undefined
|
||||
)
|
||||
) {
|
||||
if (!responseCacheControl["max-age"])
|
||||
responseCacheControl["max-age"] = "300"; // Set the default max-age to 300 seconds (5 minutes)
|
||||
|
||||
const responseVary = parseVary(
|
||||
writtenHeaders[
|
||||
Object.keys(writtenHeaders).find((key) => key.toLowerCase() == "vary")
|
||||
]
|
||||
);
|
||||
const processedVary = [
|
||||
...new Set(
|
||||
[...cacheVaryHeadersConfigured, ...responseVary].map((headerName) =>
|
||||
headerName.toLowerCase()
|
||||
)
|
||||
)
|
||||
];
|
||||
if (!responseVary.find((headerName) => headerName == "*")) {
|
||||
const cacheKeyWithVary =
|
||||
cacheKey +
|
||||
"\n" +
|
||||
processedVary
|
||||
.map((headerName) =>
|
||||
req.headers[headerName]
|
||||
? `${headerName}: ${req.headers[headerName]}`
|
||||
: ""
|
||||
)
|
||||
.join("\n");
|
||||
|
||||
varyCache.set(cacheKey, processedVary);
|
||||
cache.set(cacheKeyWithVary, {
|
||||
body: responseBody,
|
||||
headers: writtenHeaders,
|
||||
timestamp: Date.now(),
|
||||
statusCode: writtenStatusCode,
|
||||
cacheControl: responseCacheControl
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
originalEnd(chunk, encoding, callback);
|
||||
};
|
||||
|
||||
if (req.method != "HEAD") {
|
||||
res.on("pipe", (src) => {
|
||||
src.on("data", (chunk) => {
|
||||
if (!maximumCachedResponseSizeExceeded) {
|
||||
const processedChunk = Buffer.from(chunk).toString("latin1");
|
||||
if (
|
||||
maximumCachedResponseSize !== null &&
|
||||
maximumCachedResponseSize !== undefined &&
|
||||
responseBody.length + processedChunk.length >
|
||||
maximumCachedResponseSize
|
||||
) {
|
||||
maximumCachedResponseSizeExceeded = true;
|
||||
} else {
|
||||
try {
|
||||
responseBody += processedChunk;
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
} catch (err) {
|
||||
maximumCachedResponseSizeExceeded = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
next(); // Continue with normal processing
|
||||
};
|
||||
|
||||
module.exports.commands = {
|
||||
purgecache: (args, log) => {
|
||||
// All commands are executed on workers
|
||||
cache.clear();
|
||||
varyCache.clear();
|
||||
log("Cache cleared successfully.");
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.configValidators = {
|
||||
cacheVaryHeaders: (value) =>
|
||||
Array.isArray(value) &&
|
||||
value.every((element) => typeof element === "string"),
|
||||
maximumCachedResponseSize: (value) =>
|
||||
typeof value === "number" || value === null
|
||||
};
|
||||
|
||||
module.exports.modInfo = modInfo;
|
48
src/utils/cacheControlUtils.js
Normal file
48
src/utils/cacheControlUtils.js
Normal file
|
@ -0,0 +1,48 @@
|
|||
function parseCacheControl(header) {
|
||||
const directives = {};
|
||||
if (!header) return directives;
|
||||
header.split(",").forEach((directive) => {
|
||||
const [key, value] = directive.trim().split("=");
|
||||
directives[key.toLowerCase().trim()] = value ? value.trim() : true;
|
||||
});
|
||||
return directives;
|
||||
}
|
||||
|
||||
function parseVary(header) {
|
||||
if (!header) return [];
|
||||
return header.split(",").map((headerName) => headerName.trim());
|
||||
}
|
||||
|
||||
function shouldCacheResponse(cacheControl, isAuthenticated) {
|
||||
if (cacheControl["no-store"] || cacheControl["private"]) {
|
||||
return false;
|
||||
}
|
||||
if (cacheControl["public"]) {
|
||||
return true;
|
||||
}
|
||||
return !isAuthenticated && cacheControl["max-age"] !== undefined;
|
||||
}
|
||||
|
||||
function isCacheValid(entry, requestHeaders) {
|
||||
const { timestamp, cacheControl } = entry;
|
||||
const maxAge = parseInt(cacheControl["max-age"], 10);
|
||||
if (Date.now() - timestamp > maxAge * 1000) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
requestHeaders["cache-control"] &&
|
||||
requestHeaders["cache-control"].includes("no-cache")
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
parseCacheControl: parseCacheControl,
|
||||
parseVary: parseVary,
|
||||
shouldCacheResponse: shouldCacheResponse,
|
||||
isCacheValid: isCacheValid
|
||||
};
|
151
tests/index.test.js
Normal file
151
tests/index.test.js
Normal file
|
@ -0,0 +1,151 @@
|
|||
const mod = require("../src/index.js");
|
||||
const {
|
||||
parseCacheControl,
|
||||
parseVary,
|
||||
shouldCacheResponse,
|
||||
isCacheValid
|
||||
} = require("../src/utils/cacheControlUtils.js");
|
||||
|
||||
jest.mock("../src/utils/cacheControlUtils.js", () => ({
|
||||
parseCacheControl: jest.fn(),
|
||||
parseVary: jest.fn(),
|
||||
shouldCacheResponse: jest.fn(),
|
||||
isCacheValid: jest.fn()
|
||||
}));
|
||||
|
||||
describe("SVR.JS Cache mod", () => {
|
||||
let req, res, logFacilities, config, next, resWriteHead, resEnd;
|
||||
|
||||
beforeEach(() => {
|
||||
resWriteHead = jest.fn();
|
||||
resEnd = jest.fn();
|
||||
|
||||
req = {
|
||||
method: "GET",
|
||||
headers: {},
|
||||
url: "/test",
|
||||
socket: { encrypted: false }
|
||||
};
|
||||
|
||||
res = {
|
||||
headers: {},
|
||||
writeHead: resWriteHead,
|
||||
end: resEnd,
|
||||
setHeader: jest.fn(),
|
||||
getHeaderNames: jest.fn(() => []),
|
||||
getHeaders: jest.fn(() => ({})),
|
||||
removeHeader: jest.fn(),
|
||||
on: jest.fn()
|
||||
};
|
||||
|
||||
logFacilities = { resmessage: jest.fn() };
|
||||
|
||||
config = {
|
||||
cacheVaryHeaders: ["accept"],
|
||||
maximumCachedResponseSize: 1024
|
||||
};
|
||||
|
||||
next = jest.fn();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test("should bypass caching for non-GET requests", () => {
|
||||
req.method = "POST";
|
||||
|
||||
mod(req, res, logFacilities, config, next);
|
||||
|
||||
expect(res.setHeader).toHaveBeenCalledWith("X-SVRJS-Cache", "BYPASS");
|
||||
expect(next).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should bypass caching if Cache-Control contains no-store", () => {
|
||||
req.headers["cache-control"] = "no-store";
|
||||
parseCacheControl.mockReturnValue({ "no-store": true });
|
||||
|
||||
mod(req, res, logFacilities, config, next);
|
||||
|
||||
expect(res.setHeader).toHaveBeenCalledWith("X-SVRJS-Cache", "BYPASS");
|
||||
expect(next).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should cache the response and serve it on subsequent requests", () => {
|
||||
req.headers.host = "test.com";
|
||||
req.headers.accept = "application/json";
|
||||
|
||||
parseCacheControl.mockReturnValue({});
|
||||
parseVary.mockReturnValue(["accept"]);
|
||||
shouldCacheResponse.mockReturnValue(true);
|
||||
|
||||
// Mock cache-control headers
|
||||
res.getHeaders.mockReturnValue({ "cache-control": "max-age=300" });
|
||||
|
||||
// First request: cache the response
|
||||
mod(req, res, logFacilities, config, next);
|
||||
|
||||
// Simulate the first response
|
||||
res.writeHead(200, { "content-type": "application/json" });
|
||||
res.end("cached response body");
|
||||
|
||||
// Assertions for the first request
|
||||
expect(next).toHaveBeenCalled(); // Proceed to next middleware during first request
|
||||
|
||||
// Reset mocks for the second invocation
|
||||
jest.clearAllMocks();
|
||||
next.mockReset();
|
||||
|
||||
// Second request: retrieve from cache
|
||||
parseCacheControl.mockReturnValue({});
|
||||
isCacheValid.mockReturnValue(true); // Simulate a valid cache entry
|
||||
|
||||
mod(req, res, logFacilities, config, next);
|
||||
|
||||
// Assertions for the second request
|
||||
expect(logFacilities.resmessage).toHaveBeenCalledWith(
|
||||
"The response is cached."
|
||||
);
|
||||
expect(res.setHeader).toHaveBeenCalledWith("X-SVRJS-Cache", "HIT");
|
||||
expect(resWriteHead).toHaveBeenCalledWith(200, {
|
||||
"cache-control": "max-age=300",
|
||||
"content-type": "application/json"
|
||||
});
|
||||
expect(resEnd).toHaveBeenCalledWith(
|
||||
Buffer.from("cached response body", "latin1"),
|
||||
undefined,
|
||||
undefined
|
||||
);
|
||||
expect(next).not.toHaveBeenCalled(); // No middleware should be called
|
||||
});
|
||||
|
||||
test("should validate config values correctly", () => {
|
||||
const validConfig = {
|
||||
cacheVaryHeaders: ["accept", "user-agent"],
|
||||
maximumCachedResponseSize: 2048
|
||||
};
|
||||
|
||||
expect(
|
||||
mod.configValidators.cacheVaryHeaders(validConfig.cacheVaryHeaders)
|
||||
).toBe(true);
|
||||
expect(
|
||||
mod.configValidators.maximumCachedResponseSize(
|
||||
validConfig.maximumCachedResponseSize
|
||||
)
|
||||
).toBe(true);
|
||||
|
||||
const invalidConfig = {
|
||||
cacheVaryHeaders: "invalid",
|
||||
maximumCachedResponseSize: "invalid"
|
||||
};
|
||||
|
||||
expect(
|
||||
mod.configValidators.cacheVaryHeaders(invalidConfig.cacheVaryHeaders)
|
||||
).toBe(false);
|
||||
expect(
|
||||
mod.configValidators.maximumCachedResponseSize(
|
||||
invalidConfig.maximumCachedResponseSize
|
||||
)
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
117
tests/utils/cacheControlUtils.test.js
Normal file
117
tests/utils/cacheControlUtils.test.js
Normal file
|
@ -0,0 +1,117 @@
|
|||
const {
|
||||
parseCacheControl,
|
||||
parseVary,
|
||||
shouldCacheResponse,
|
||||
isCacheValid
|
||||
} = require("../../src/utils/cacheControlUtils.js");
|
||||
|
||||
describe("parseCacheControl", () => {
|
||||
test("should return an empty object if header is null or undefined", () => {
|
||||
expect(parseCacheControl(null)).toEqual({});
|
||||
expect(parseCacheControl(undefined)).toEqual({});
|
||||
});
|
||||
|
||||
test("should parse cache-control header correctly", () => {
|
||||
const header = "max-age=3600, no-cache, private";
|
||||
expect(parseCacheControl(header)).toEqual({
|
||||
"max-age": "3600",
|
||||
"no-cache": true,
|
||||
private: true
|
||||
});
|
||||
});
|
||||
|
||||
test("should handle directives without values", () => {
|
||||
const header = "no-store, public";
|
||||
expect(parseCacheControl(header)).toEqual({
|
||||
"no-store": true,
|
||||
public: true
|
||||
});
|
||||
});
|
||||
|
||||
test("should trim whitespace correctly", () => {
|
||||
const header = " max-age = 3600 , no-cache , private ";
|
||||
expect(parseCacheControl(header)).toEqual({
|
||||
"max-age": "3600",
|
||||
"no-cache": true,
|
||||
private: true
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseVary", () => {
|
||||
test("should return an empty array if header is null or undefined", () => {
|
||||
expect(parseVary(null)).toEqual([]);
|
||||
expect(parseVary(undefined)).toEqual([]);
|
||||
});
|
||||
|
||||
test("should parse vary header correctly", () => {
|
||||
const header = "Accept-Encoding, User-Agent";
|
||||
expect(parseVary(header)).toEqual(["Accept-Encoding", "User-Agent"]);
|
||||
});
|
||||
|
||||
test("should trim whitespace correctly", () => {
|
||||
const header = " Accept-Encoding , User-Agent ";
|
||||
expect(parseVary(header)).toEqual(["Accept-Encoding", "User-Agent"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("shouldCacheResponse", () => {
|
||||
test("should return false if no-store is present", () => {
|
||||
const cacheControl = { "no-store": true };
|
||||
expect(shouldCacheResponse(cacheControl, false)).toBe(false);
|
||||
});
|
||||
|
||||
test("should return false if private is present", () => {
|
||||
const cacheControl = { private: true };
|
||||
expect(shouldCacheResponse(cacheControl, false)).toBe(false);
|
||||
});
|
||||
|
||||
test("should return true if public is present", () => {
|
||||
const cacheControl = { public: true };
|
||||
expect(shouldCacheResponse(cacheControl, false)).toBe(true);
|
||||
});
|
||||
|
||||
test("should return true if max-age is present and not authenticated", () => {
|
||||
const cacheControl = { "max-age": "3600" };
|
||||
expect(shouldCacheResponse(cacheControl, false)).toBe(true);
|
||||
});
|
||||
|
||||
test("should return false if max-age is present and authenticated", () => {
|
||||
const cacheControl = { "max-age": "3600" };
|
||||
expect(shouldCacheResponse(cacheControl, true)).toBe(false);
|
||||
});
|
||||
|
||||
test("should return false if no relevant directives are present", () => {
|
||||
const cacheControl = {};
|
||||
expect(shouldCacheResponse(cacheControl, false)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isCacheValid", () => {
|
||||
test("should return false if cache is expired", () => {
|
||||
const entry = {
|
||||
timestamp: Date.now() - 4000,
|
||||
cacheControl: { "max-age": "3" }
|
||||
};
|
||||
const requestHeaders = {};
|
||||
expect(isCacheValid(entry, requestHeaders)).toBe(false);
|
||||
});
|
||||
|
||||
test("should return false if no-cache is present in request headers", () => {
|
||||
const entry = {
|
||||
timestamp: Date.now(),
|
||||
cacheControl: { "max-age": "3600" }
|
||||
};
|
||||
const requestHeaders = { "cache-control": "no-cache" };
|
||||
expect(isCacheValid(entry, requestHeaders)).toBe(false);
|
||||
});
|
||||
|
||||
test("should return true if cache is valid", () => {
|
||||
const entry = {
|
||||
timestamp: Date.now(),
|
||||
cacheControl: { "max-age": "3600" }
|
||||
};
|
||||
const requestHeaders = {};
|
||||
expect(isCacheValid(entry, requestHeaders)).toBe(true);
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue