express-cache-ctrl 1.1.6 → 1.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,16 @@
1
+ {
2
+ "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3
+ "extends": [
4
+ "config:base"
5
+ ],
6
+ "semanticCommits": "enabled",
7
+ "rangeStrategy": "bump",
8
+ "packageRules": [
9
+ {
10
+ "matchUpdateTypes": [
11
+ "major"
12
+ ],
13
+ "enabled": false
14
+ }
15
+ ]
16
+ }
package/AGENTS.md ADDED
@@ -0,0 +1,106 @@
1
+ # AGENTS.md
2
+
3
+ This file provides guidelines for AI coding agents working on the express-cache-ctrl repository.
4
+
5
+ ## Project Overview
6
+
7
+ An Express.js middleware for managing Cache-Control headers with support for public/private caching, TTL, and OWASP secure caching recommendations.
8
+
9
+ ## Build/Test Commands
10
+
11
+ ```bash
12
+ # Run all tests
13
+ npm test
14
+
15
+ # Run specific test file
16
+ npx mocha 'test/cache.js'
17
+ npx mocha 'test/unit/cache.js'
18
+
19
+ # Run tests matching a pattern (grep)
20
+ npx mocha 'test/**/*.js' --grep "cache disabled"
21
+
22
+ # Run with different reporter
23
+ npx mocha 'test/**/*.js' --reporter spec
24
+ ```
25
+
26
+ Note: This project uses Mocha (test runner) + Chai (assertions). Tests use BDD style with `describe()` and `it()` blocks.
27
+
28
+ ## Code Style Guidelines
29
+
30
+ ### Formatting
31
+ - Indent: 4 spaces for `.js` files, 2 spaces for `package.json` (see `.editorconfig`)
32
+ - Use double quotes for strings
33
+ - Always use semicolons
34
+ - Insert final newline at end of files
35
+ - Trim trailing whitespace
36
+
37
+ ### Language & Imports
38
+ - Use ES5/CommonJS (no ES6 modules)
39
+ - Import style: `const module = require("module");`
40
+ - Use `"use strict";` directive at top of source files
41
+ - Node.js version: >= 12.0
42
+
43
+ ### Naming Conventions
44
+ - Functions: camelCase (e.g., `cacheCustom`, `toTimespan`)
45
+ - Variables: camelCase (e.g., `cacheControl`, `defaultTTL`)
46
+ - Constants: Use descriptive names, may use ALL_CAPS for true constants
47
+ - Files: camelCase for source files (e.g., `cache.js`)
48
+
49
+ ### Functions & Structure
50
+ - Prefer traditional function declarations over arrow functions in source
51
+ - Arrow functions acceptable in tests
52
+ - Group related functions together with clear section comments
53
+ - Export pattern: `exports.functionName = functionName;` at end of file
54
+
55
+ ### Testing Conventions
56
+ - Test files: Located in `test/` directory with `.js` extension
57
+ - Unit tests: Place in `test/unit/` subdirectory
58
+ - Mock objects: Place in `test/mocks/` subdirectory
59
+ - Use Chai's `expect()` assertion style
60
+ - Use descriptive test names that explain behavior
61
+ - Group related tests with nested `describe()` blocks
62
+ - Follow pattern: `it("should do something", function (done) { ... });`
63
+ - Call `done()` callback for async tests
64
+
65
+ ### Error Handling
66
+ - Middleware must call `next()` to pass control
67
+ - Validate inputs with truthy checks: `opts = opts || {};`
68
+ - Use `parseInt()` for numeric conversions
69
+ - Avoid throwing errors for expected edge cases
70
+
71
+ ### Comments
72
+ - Use `//` for single-line comments
73
+ - Add descriptive headers for sections: `// Public functions`, `// Module requires`
74
+ - JSDoc-style comments acceptable but not required
75
+
76
+ ## File Organization
77
+
78
+ ```
79
+ src/
80
+ cache.js # Main middleware implementation
81
+ test/
82
+ cache.js # Integration tests
83
+ unit/
84
+ cache.js # Unit tests for internal functions
85
+ mocks/
86
+ response.js # Mock Express response object
87
+ ```
88
+
89
+ ## Dependencies
90
+
91
+ Production:
92
+ - express: ^4.18.2
93
+ - ms: ^2.1.3
94
+
95
+ Development:
96
+ - mocha: ^10.2.0
97
+ - chai: ^4.3.7
98
+ - mock-res: ^0.6.0
99
+
100
+ ## Important Notes
101
+
102
+ - Default TTL is "1h" (3600 seconds) when not specified
103
+ - Uses `ms` library for human-readable time parsing
104
+ - Middleware follows Express convention: `(req, res, next) => {}`
105
+ - Supports both `res.setHeader()` and `res.set()` methods
106
+ - Always exports internal functions for testability
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "express-cache-ctrl",
3
- "version": "1.1.6",
3
+ "version": "1.1.9",
4
4
  "description": "Express middleware to handle content expiration using Cache-Control header.",
5
5
  "main": "src/cache.js",
6
6
  "scripts": {
7
- "test": "mocha test/"
7
+ "test": "mocha 'test/**/*.js'"
8
8
  },
9
9
  "repository": {
10
10
  "type": "git",
@@ -22,13 +22,13 @@
22
22
  "author": "Carlos Luis Castro Márquez <carlosluiscastro@gmail.com>",
23
23
  "license": "MIT",
24
24
  "dependencies": {
25
- "express": "^4.18.2",
25
+ "express": "^4.22.1",
26
26
  "ms": "^2.1.3"
27
27
  },
28
28
  "devDependencies": {
29
- "chai": "^4.3.7",
30
- "mocha": "^10.2.0",
29
+ "chai": "^4.5.0",
30
+ "mocha": "^10.8.2",
31
31
  "mock-res": "^0.6.0",
32
- "morgan": "^1.10.0"
32
+ "morgan": "^1.10.1"
33
33
  }
34
34
  }
package/src/cache.js CHANGED
@@ -120,3 +120,4 @@ exports.secure = cacheSecure;
120
120
  exports.public = cachePublic;
121
121
  exports.private = cachePrivate;
122
122
  exports.toTimespan = toTimespan;
123
+ exports.generateHeader = generateHeader;
package/test/cache.js CHANGED
@@ -154,6 +154,123 @@ describe("Cache Middleware", function () {
154
154
  });
155
155
  });
156
156
 
157
+ describe("Edge Cases", function () {
158
+ it("cache private with TTL = 0 defaults to defaultTTL", function (done) {
159
+ const middleware = cache.private(0);
160
+ runMiddleware(middleware, function (res) {
161
+ const cacheControl = res.get("Cache-Control");
162
+ const controls = parseCacheControl(cacheControl);
163
+ const defaultTtlSeconds = cache.toTimespan(cache.defaultTTL);
164
+ // 0 is falsy, so it defaults to defaultTTL (1h = 3600)
165
+ expect(controls).to.property("max-age").and.to.equal(defaultTtlSeconds);
166
+ done();
167
+ });
168
+ });
169
+
170
+ it("cache public with string TTL '1d'", function (done) {
171
+ const middleware = cache.public("1d");
172
+ runMiddleware(middleware, function (res) {
173
+ const cacheControl = res.get("Cache-Control");
174
+ const controls = parseCacheControl(cacheControl);
175
+ expect(controls).to.property("max-age").and.to.equal(86400);
176
+ expect(controls).to.property("s-maxage").and.to.equal(86400);
177
+ done();
178
+ });
179
+ });
180
+
181
+ it("cache public with string TTL '2h'", function (done) {
182
+ const middleware = cache.public("2h");
183
+ runMiddleware(middleware, function (res) {
184
+ const cacheControl = res.get("Cache-Control");
185
+ const controls = parseCacheControl(cacheControl);
186
+ expect(controls).to.property("max-age").and.to.equal(7200);
187
+ expect(controls).to.property("s-maxage").and.to.equal(7200);
188
+ done();
189
+ });
190
+ });
191
+
192
+ it("cache public with string TTL '30m'", function (done) {
193
+ const middleware = cache.public("30m");
194
+ runMiddleware(middleware, function (res) {
195
+ const cacheControl = res.get("Cache-Control");
196
+ const controls = parseCacheControl(cacheControl);
197
+ expect(controls).to.property("max-age").and.to.equal(1800);
198
+ expect(controls).to.property("s-maxage").and.to.equal(1800);
199
+ done();
200
+ });
201
+ });
202
+
203
+ it("cache custom with no options", function (done) {
204
+ const middleware = cache.custom();
205
+ runMiddleware(middleware, function (res) {
206
+ const cacheControl = res.get("Cache-Control");
207
+ expect(cacheControl).to.not.be.null;
208
+ expect(cacheControl).to.be.a("string");
209
+ done();
210
+ });
211
+ });
212
+
213
+ it("cache custom with empty options", function (done) {
214
+ const middleware = cache.custom({});
215
+ runMiddleware(middleware, function (res) {
216
+ const cacheControl = res.get("Cache-Control");
217
+ expect(cacheControl).to.not.be.null;
218
+ const controls = parseCacheControl(cacheControl);
219
+ expect(controls).to.property("private");
220
+ done();
221
+ });
222
+ });
223
+
224
+ it("should remove Pragma header when noCache is false", function (done) {
225
+ const middleware = cache.private(3600);
226
+ const res = new Response();
227
+ // Set Pragma header first
228
+ res.setHeader("Pragma", "no-cache");
229
+ middleware.call(middleware, {}, res, function () {
230
+ const pragma = res.get("Pragma");
231
+ expect(pragma).to.be.undefined;
232
+ done();
233
+ });
234
+ });
235
+
236
+ it("should call next() callback", function (done) {
237
+ const middleware = cache.private(3600);
238
+ const res = new Response();
239
+ let nextCalled = false;
240
+ middleware.call(middleware, {}, res, function () {
241
+ nextCalled = true;
242
+ expect(nextCalled).to.be.true;
243
+ done();
244
+ });
245
+ });
246
+
247
+ it("cache public with 1 week TTL", function (done) {
248
+ const middleware = cache.public("1w");
249
+ runMiddleware(middleware, function (res) {
250
+ const cacheControl = res.get("Cache-Control");
251
+ const controls = parseCacheControl(cacheControl);
252
+ expect(controls).to.property("max-age").and.to.equal(604800);
253
+ expect(controls).to.property("s-maxage").and.to.equal(604800);
254
+ done();
255
+ });
256
+ });
257
+
258
+ it("cache custom with different max-age and s-maxage", function (done) {
259
+ const middleware = cache.custom({
260
+ scope: "public",
261
+ ttl: "2h",
262
+ sttl: "1h",
263
+ });
264
+ runMiddleware(middleware, function (res) {
265
+ const cacheControl = res.get("Cache-Control");
266
+ const controls = parseCacheControl(cacheControl);
267
+ expect(controls).to.property("max-age").and.to.equal(7200);
268
+ expect(controls).to.property("s-maxage").and.to.equal(3600);
269
+ done();
270
+ });
271
+ });
272
+ });
273
+
157
274
  function runMiddleware(middleware, callback) {
158
275
  const res = new Response();
159
276
  middleware.call(middleware, {}, res, function () {
@@ -0,0 +1,253 @@
1
+ const { expect } = require("chai");
2
+ const cache = require("../../src/cache");
3
+
4
+ describe("Cache Internal Functions", function () {
5
+ describe("toTimespan()", function () {
6
+ describe("numeric inputs", function () {
7
+ it("should return numeric value as-is for positive integers", function () {
8
+ expect(cache.toTimespan(3600)).to.equal(3600);
9
+ expect(cache.toTimespan(60)).to.equal(60);
10
+ expect(cache.toTimespan(1)).to.equal(1);
11
+ });
12
+
13
+ it("should return numeric value for large numbers", function () {
14
+ expect(cache.toTimespan(86400)).to.equal(86400);
15
+ expect(cache.toTimespan(31536000)).to.equal(31536000);
16
+ });
17
+
18
+ it("should return 0 for zero value", function () {
19
+ expect(cache.toTimespan(0)).to.equal(0);
20
+ });
21
+ });
22
+
23
+ describe("string inputs", function () {
24
+ it("should convert days to seconds", function () {
25
+ expect(cache.toTimespan("1d")).to.equal(86400);
26
+ expect(cache.toTimespan("2d")).to.equal(172800);
27
+ expect(cache.toTimespan("7d")).to.equal(604800);
28
+ });
29
+
30
+ it("should convert hours to seconds", function () {
31
+ expect(cache.toTimespan("1h")).to.equal(3600);
32
+ expect(cache.toTimespan("2h")).to.equal(7200);
33
+ expect(cache.toTimespan("24h")).to.equal(86400);
34
+ });
35
+
36
+ it("should convert minutes to seconds", function () {
37
+ expect(cache.toTimespan("1m")).to.equal(60);
38
+ expect(cache.toTimespan("30m")).to.equal(1800);
39
+ expect(cache.toTimespan("60m")).to.equal(3600);
40
+ });
41
+
42
+ it("should convert seconds to seconds", function () {
43
+ expect(cache.toTimespan("1s")).to.equal(1);
44
+ expect(cache.toTimespan("30s")).to.equal(30);
45
+ expect(cache.toTimespan("60s")).to.equal(60);
46
+ });
47
+
48
+ it("should convert weeks to seconds", function () {
49
+ expect(cache.toTimespan("1w")).to.equal(604800);
50
+ expect(cache.toTimespan("2w")).to.equal(1209600);
51
+ });
52
+
53
+ it("should convert milliseconds to seconds", function () {
54
+ expect(cache.toTimespan("1000ms")).to.equal(1);
55
+ expect(cache.toTimespan("60000ms")).to.equal(60);
56
+ });
57
+
58
+ it("should return NaN for compound time strings (not supported)", function () {
59
+ // The ms library doesn't support compound time strings
60
+ expect(cache.toTimespan("1h30m")).to.be.NaN;
61
+ expect(cache.toTimespan("1d12h")).to.be.NaN;
62
+ });
63
+ });
64
+
65
+ describe("edge cases", function () {
66
+ it("should handle negative numbers", function () {
67
+ expect(cache.toTimespan(-3600)).to.equal(-3600);
68
+ });
69
+
70
+ it("should return NaN for invalid string input", function () {
71
+ expect(cache.toTimespan("invalid")).to.be.NaN;
72
+ });
73
+
74
+ it("should handle floating point numbers", function () {
75
+ expect(cache.toTimespan(3600.5)).to.equal(3600);
76
+ });
77
+ });
78
+ });
79
+
80
+ describe("generateHeader()", function () {
81
+ describe("scope handling", function () {
82
+ it("should add public scope when specified", function () {
83
+ const result = cache.generateHeader({ scope: "public" });
84
+ expect(result).to.include("public");
85
+ });
86
+
87
+ it("should add private scope when specified", function () {
88
+ const result = cache.generateHeader({ scope: "private" });
89
+ expect(result).to.include("private");
90
+ });
91
+
92
+ it("should default to private when no scope is specified and not no-cache", function () {
93
+ const result = cache.generateHeader({ ttl: 3600 });
94
+ expect(result).to.include("private");
95
+ });
96
+ });
97
+
98
+ describe("noCache handling", function () {
99
+ it("should add no-cache and no-store when noCache is true", function () {
100
+ const result = cache.generateHeader({ noCache: true });
101
+ expect(result).to.include("no-cache");
102
+ expect(result).to.include("no-store");
103
+ });
104
+
105
+ it("should not add max-age when noCache is true", function () {
106
+ const result = cache.generateHeader({ noCache: true, ttl: 3600 });
107
+ const hasMaxAge = result.some(item => item.startsWith("max-age="));
108
+ expect(hasMaxAge).to.be.false;
109
+ });
110
+
111
+ it("should not add private scope when noCache is true without explicit scope", function () {
112
+ const result = cache.generateHeader({ noCache: true });
113
+ expect(result).to.not.include("private");
114
+ });
115
+
116
+ it("should respect explicit scope even with noCache", function () {
117
+ const result = cache.generateHeader({ noCache: true, scope: "public" });
118
+ expect(result).to.include("public");
119
+ });
120
+ });
121
+
122
+ describe("TTL handling", function () {
123
+ it("should add max-age with numeric TTL", function () {
124
+ const result = cache.generateHeader({ ttl: 3600 });
125
+ expect(result).to.include("max-age=3600");
126
+ });
127
+
128
+ it("should add max-age with string TTL", function () {
129
+ const result = cache.generateHeader({ ttl: "1h" });
130
+ expect(result).to.include("max-age=3600");
131
+ });
132
+
133
+ it("should use default TTL when not specified", function () {
134
+ const result = cache.generateHeader({});
135
+ const defaultTtlSeconds = cache.toTimespan(cache.defaultTTL);
136
+ expect(result).to.include(`max-age=${defaultTtlSeconds}`);
137
+ });
138
+
139
+ it("should add s-maxage when sttl is specified", function () {
140
+ const result = cache.generateHeader({ ttl: 3600, sttl: 7200 });
141
+ expect(result).to.include("s-maxage=7200");
142
+ });
143
+
144
+ it("should add s-maxage with string sttl", function () {
145
+ const result = cache.generateHeader({ ttl: 3600, sttl: "2h" });
146
+ expect(result).to.include("s-maxage=7200");
147
+ });
148
+ });
149
+
150
+ describe("revalidation flags", function () {
151
+ it("should add must-revalidate when specified", function () {
152
+ const result = cache.generateHeader({ mustRevalidate: true });
153
+ expect(result).to.include("must-revalidate");
154
+ });
155
+
156
+ it("should add proxy-revalidate when specified", function () {
157
+ const result = cache.generateHeader({ proxyRevalidate: true });
158
+ expect(result).to.include("proxy-revalidate");
159
+ });
160
+
161
+ it("should add both revalidation flags when both specified", function () {
162
+ const result = cache.generateHeader({
163
+ mustRevalidate: true,
164
+ proxyRevalidate: true,
165
+ });
166
+ expect(result).to.include("must-revalidate");
167
+ expect(result).to.include("proxy-revalidate");
168
+ });
169
+
170
+ it("should not add revalidation flags when not specified", function () {
171
+ const result = cache.generateHeader({ ttl: 3600 });
172
+ expect(result).to.not.include("must-revalidate");
173
+ expect(result).to.not.include("proxy-revalidate");
174
+ });
175
+ });
176
+
177
+ describe("no-transform flag", function () {
178
+ it("should add no-transform when specified", function () {
179
+ const result = cache.generateHeader({ noTransform: true });
180
+ expect(result).to.include("no-transform");
181
+ });
182
+
183
+ it("should not add no-transform when not specified", function () {
184
+ const result = cache.generateHeader({ ttl: 3600 });
185
+ expect(result).to.not.include("no-transform");
186
+ });
187
+ });
188
+
189
+ describe("complex scenarios", function () {
190
+ it("should generate correct header for public cache with all options", function () {
191
+ const result = cache.generateHeader({
192
+ scope: "public",
193
+ ttl: 3600,
194
+ sttl: 7200,
195
+ mustRevalidate: true,
196
+ proxyRevalidate: true,
197
+ noTransform: true,
198
+ });
199
+
200
+ expect(result).to.include("public");
201
+ expect(result).to.include("max-age=3600");
202
+ expect(result).to.include("s-maxage=7200");
203
+ expect(result).to.include("must-revalidate");
204
+ expect(result).to.include("proxy-revalidate");
205
+ expect(result).to.include("no-transform");
206
+ });
207
+
208
+ it("should generate correct header for disabled cache", function () {
209
+ const result = cache.generateHeader({
210
+ noCache: true,
211
+ mustRevalidate: true,
212
+ proxyRevalidate: true,
213
+ });
214
+
215
+ expect(result).to.include("no-cache");
216
+ expect(result).to.include("no-store");
217
+ expect(result).to.include("must-revalidate");
218
+ expect(result).to.include("proxy-revalidate");
219
+ });
220
+
221
+ it("should generate correct header for secure cache", function () {
222
+ const result = cache.generateHeader({
223
+ scope: "private",
224
+ noCache: true,
225
+ mustRevalidate: true,
226
+ noTransform: true,
227
+ });
228
+
229
+ expect(result).to.include("private");
230
+ expect(result).to.include("no-cache");
231
+ expect(result).to.include("no-store");
232
+ expect(result).to.include("must-revalidate");
233
+ expect(result).to.include("no-transform");
234
+ });
235
+
236
+ it("should handle empty options object", function () {
237
+ const result = cache.generateHeader({});
238
+ expect(result).to.be.an("array");
239
+ expect(result.length).to.be.greaterThan(0);
240
+ });
241
+
242
+ it("should throw error for undefined options", function () {
243
+ // The code doesn't handle undefined, which is expected behavior
244
+ expect(() => cache.generateHeader(undefined)).to.throw(TypeError);
245
+ });
246
+
247
+ it("should throw error for null options", function () {
248
+ // The code doesn't handle null, which is expected behavior
249
+ expect(() => cache.generateHeader(null)).to.throw(TypeError);
250
+ });
251
+ });
252
+ });
253
+ });