| | const fs = require('fs'); |
| | const path = require('path'); |
| | const express = require('express'); |
| | const request = require('supertest'); |
| | const zlib = require('zlib'); |
| | const staticCache = require('../staticCache'); |
| |
|
| | describe('staticCache', () => { |
| | let app; |
| | let testDir; |
| | let testFile; |
| | let indexFile; |
| | let manifestFile; |
| | let swFile; |
| |
|
| | beforeAll(() => { |
| | |
| | testDir = path.join(__dirname, 'test-static'); |
| | if (!fs.existsSync(testDir)) { |
| | fs.mkdirSync(testDir, { recursive: true }); |
| | } |
| |
|
| | |
| | testFile = path.join(testDir, 'test.js'); |
| | indexFile = path.join(testDir, 'index.html'); |
| | manifestFile = path.join(testDir, 'manifest.json'); |
| | swFile = path.join(testDir, 'sw.js'); |
| |
|
| | const jsContent = 'console.log("test");'; |
| | const htmlContent = '<html><body>Test</body></html>'; |
| | const jsonContent = '{"name": "test"}'; |
| | const swContent = 'self.addEventListener("install", () => {});'; |
| |
|
| | fs.writeFileSync(testFile, jsContent); |
| | fs.writeFileSync(indexFile, htmlContent); |
| | fs.writeFileSync(manifestFile, jsonContent); |
| | fs.writeFileSync(swFile, swContent); |
| |
|
| | |
| | fs.writeFileSync(testFile + '.gz', zlib.gzipSync(jsContent)); |
| | fs.writeFileSync(path.join(testDir, 'test.css'), 'body { color: red; }'); |
| | fs.writeFileSync(path.join(testDir, 'test.css.gz'), zlib.gzipSync('body { color: red; }')); |
| |
|
| | |
| | fs.writeFileSync( |
| | path.join(testDir, 'only-gzipped.js.gz'), |
| | zlib.gzipSync('console.log("only gzipped");'), |
| | ); |
| |
|
| | |
| | const distImagesDir = path.join(testDir, 'dist', 'images'); |
| | fs.mkdirSync(distImagesDir, { recursive: true }); |
| | fs.writeFileSync(path.join(distImagesDir, 'logo.png'), 'fake-png-data'); |
| | }); |
| |
|
| | afterAll(() => { |
| | |
| | if (fs.existsSync(testDir)) { |
| | fs.rmSync(testDir, { recursive: true, force: true }); |
| | } |
| | }); |
| |
|
| | beforeEach(() => { |
| | app = express(); |
| |
|
| | |
| | delete process.env.NODE_ENV; |
| | delete process.env.STATIC_CACHE_S_MAX_AGE; |
| | delete process.env.STATIC_CACHE_MAX_AGE; |
| | }); |
| | describe('cache headers in production', () => { |
| | beforeEach(() => { |
| | process.env.NODE_ENV = 'production'; |
| | }); |
| |
|
| | it('should set standard cache headers for regular files', async () => { |
| | app.use(staticCache(testDir)); |
| |
|
| | const response = await request(app).get('/test.js').expect(200); |
| |
|
| | expect(response.headers['cache-control']).toBe('public, max-age=172800, s-maxage=86400'); |
| | }); |
| |
|
| | it('should set no-cache headers for index.html', async () => { |
| | app.use(staticCache(testDir)); |
| |
|
| | const response = await request(app).get('/index.html').expect(200); |
| |
|
| | expect(response.headers['cache-control']).toBe('no-store, no-cache, must-revalidate'); |
| | }); |
| |
|
| | it('should set no-cache headers for manifest.json', async () => { |
| | app.use(staticCache(testDir)); |
| |
|
| | const response = await request(app).get('/manifest.json').expect(200); |
| |
|
| | expect(response.headers['cache-control']).toBe('no-store, no-cache, must-revalidate'); |
| | }); |
| |
|
| | it('should set no-cache headers for sw.js', async () => { |
| | app.use(staticCache(testDir)); |
| |
|
| | const response = await request(app).get('/sw.js').expect(200); |
| |
|
| | expect(response.headers['cache-control']).toBe('no-store, no-cache, must-revalidate'); |
| | }); |
| |
|
| | it('should not set cache headers for /dist/images/ files', async () => { |
| | app.use(staticCache(testDir)); |
| |
|
| | const response = await request(app).get('/dist/images/logo.png').expect(200); |
| |
|
| | expect(response.headers['cache-control']).toBe('public, max-age=0'); |
| | }); |
| |
|
| | it('should set no-cache headers when noCache option is true', async () => { |
| | app.use(staticCache(testDir, { noCache: true })); |
| |
|
| | const response = await request(app).get('/test.js').expect(200); |
| |
|
| | expect(response.headers['cache-control']).toBe('no-store, no-cache, must-revalidate'); |
| | }); |
| | }); |
| |
|
| | describe('cache headers in non-production', () => { |
| | beforeEach(() => { |
| | process.env.NODE_ENV = 'development'; |
| | }); |
| |
|
| | it('should not set cache headers in development', async () => { |
| | app.use(staticCache(testDir)); |
| |
|
| | const response = await request(app).get('/test.js').expect(200); |
| |
|
| | |
| | |
| | const cacheControl = response.headers['cache-control']; |
| | expect(cacheControl).toBe('public, max-age=0'); |
| | }); |
| | }); |
| |
|
| | describe('environment variable configuration', () => { |
| | beforeEach(() => { |
| | process.env.NODE_ENV = 'production'; |
| | }); |
| |
|
| | it('should use custom s-maxage from environment', async () => { |
| | process.env.STATIC_CACHE_S_MAX_AGE = '3600'; |
| |
|
| | |
| | jest.resetModules(); |
| | const freshStaticCache = require('../staticCache'); |
| |
|
| | app.use(freshStaticCache(testDir)); |
| |
|
| | const response = await request(app).get('/test.js').expect(200); |
| |
|
| | expect(response.headers['cache-control']).toBe('public, max-age=172800, s-maxage=3600'); |
| | }); |
| |
|
| | it('should use custom max-age from environment', async () => { |
| | process.env.STATIC_CACHE_MAX_AGE = '7200'; |
| |
|
| | |
| | jest.resetModules(); |
| | const freshStaticCache = require('../staticCache'); |
| |
|
| | app.use(freshStaticCache(testDir)); |
| |
|
| | const response = await request(app).get('/test.js').expect(200); |
| |
|
| | expect(response.headers['cache-control']).toBe('public, max-age=7200, s-maxage=86400'); |
| | }); |
| |
|
| | it('should use both custom values from environment', async () => { |
| | process.env.STATIC_CACHE_S_MAX_AGE = '1800'; |
| | process.env.STATIC_CACHE_MAX_AGE = '3600'; |
| |
|
| | |
| | jest.resetModules(); |
| | const freshStaticCache = require('../staticCache'); |
| |
|
| | app.use(freshStaticCache(testDir)); |
| |
|
| | const response = await request(app).get('/test.js').expect(200); |
| |
|
| | expect(response.headers['cache-control']).toBe('public, max-age=3600, s-maxage=1800'); |
| | }); |
| | }); |
| |
|
| | describe('express-static-gzip behavior', () => { |
| | beforeEach(() => { |
| | process.env.NODE_ENV = 'production'; |
| | }); |
| |
|
| | it('should serve gzipped files when client accepts gzip encoding', async () => { |
| | app.use(staticCache(testDir, { skipGzipScan: false })); |
| |
|
| | const response = await request(app) |
| | .get('/test.js') |
| | .set('Accept-Encoding', 'gzip, deflate') |
| | .expect(200); |
| |
|
| | expect(response.headers['content-encoding']).toBe('gzip'); |
| | expect(response.headers['content-type']).toMatch(/javascript/); |
| | expect(response.headers['cache-control']).toBe('public, max-age=172800, s-maxage=86400'); |
| | |
| | expect(response.text).toBe('console.log("test");'); |
| | }); |
| |
|
| | it('should fall back to uncompressed files when client does not accept gzip', async () => { |
| | app.use(staticCache(testDir, { skipGzipScan: false })); |
| |
|
| | const response = await request(app) |
| | .get('/test.js') |
| | .set('Accept-Encoding', 'identity') |
| | .expect(200); |
| |
|
| | expect(response.headers['content-encoding']).toBeUndefined(); |
| | expect(response.headers['content-type']).toMatch(/javascript/); |
| | expect(response.text).toBe('console.log("test");'); |
| | }); |
| |
|
| | it('should serve gzipped CSS files with correct content-type', async () => { |
| | app.use(staticCache(testDir, { skipGzipScan: false })); |
| |
|
| | const response = await request(app) |
| | .get('/test.css') |
| | .set('Accept-Encoding', 'gzip') |
| | .expect(200); |
| |
|
| | expect(response.headers['content-encoding']).toBe('gzip'); |
| | expect(response.headers['content-type']).toMatch(/css/); |
| | expect(response.text).toBe('body { color: red; }'); |
| | }); |
| |
|
| | it('should serve uncompressed files when no gzipped version exists', async () => { |
| | app.use(staticCache(testDir, { skipGzipScan: false })); |
| |
|
| | const response = await request(app) |
| | .get('/manifest.json') |
| | .set('Accept-Encoding', 'gzip') |
| | .expect(200); |
| |
|
| | expect(response.headers['content-encoding']).toBeUndefined(); |
| | expect(response.headers['content-type']).toMatch(/json/); |
| | expect(response.text).toBe('{"name": "test"}'); |
| | }); |
| |
|
| | it('should handle files that only exist in gzipped form', async () => { |
| | app.use(staticCache(testDir, { skipGzipScan: false })); |
| |
|
| | const response = await request(app) |
| | .get('/only-gzipped.js') |
| | .set('Accept-Encoding', 'gzip') |
| | .expect(200); |
| |
|
| | expect(response.headers['content-encoding']).toBe('gzip'); |
| | expect(response.headers['content-type']).toMatch(/javascript/); |
| | expect(response.text).toBe('console.log("only gzipped");'); |
| | }); |
| |
|
| | it('should return 404 for gzip-only files when client does not accept gzip', async () => { |
| | app.use(staticCache(testDir, { skipGzipScan: false })); |
| |
|
| | const response = await request(app) |
| | .get('/only-gzipped.js') |
| | .set('Accept-Encoding', 'identity'); |
| | expect(response.status).toBe(404); |
| | }); |
| |
|
| | it('should handle cache headers correctly for gzipped content', async () => { |
| | app.use(staticCache(testDir, { skipGzipScan: false })); |
| |
|
| | const response = await request(app) |
| | .get('/test.js') |
| | .set('Accept-Encoding', 'gzip') |
| | .expect(200); |
| |
|
| | expect(response.headers['content-encoding']).toBe('gzip'); |
| | expect(response.headers['cache-control']).toBe('public, max-age=172800, s-maxage=86400'); |
| | expect(response.headers['content-type']).toMatch(/javascript/); |
| | }); |
| |
|
| | it('should preserve original MIME types for gzipped files', async () => { |
| | app.use(staticCache(testDir, { skipGzipScan: false })); |
| |
|
| | const jsResponse = await request(app) |
| | .get('/test.js') |
| | .set('Accept-Encoding', 'gzip') |
| | .expect(200); |
| |
|
| | const cssResponse = await request(app) |
| | .get('/test.css') |
| | .set('Accept-Encoding', 'gzip') |
| | .expect(200); |
| |
|
| | expect(jsResponse.headers['content-type']).toMatch(/javascript/); |
| | expect(cssResponse.headers['content-type']).toMatch(/css/); |
| | expect(jsResponse.headers['content-encoding']).toBe('gzip'); |
| | expect(cssResponse.headers['content-encoding']).toBe('gzip'); |
| | }); |
| | }); |
| |
|
| | describe('skipGzipScan option comparison', () => { |
| | beforeEach(() => { |
| | process.env.NODE_ENV = 'production'; |
| | }); |
| |
|
| | it('should use express.static (no gzip) when skipGzipScan is true', async () => { |
| | app.use(staticCache(testDir, { skipGzipScan: true })); |
| |
|
| | const response = await request(app) |
| | .get('/test.js') |
| | .set('Accept-Encoding', 'gzip') |
| | .expect(200); |
| |
|
| | |
| | expect(response.headers['content-encoding']).toBeUndefined(); |
| | expect(response.headers['cache-control']).toBe('public, max-age=172800, s-maxage=86400'); |
| | expect(response.text).toBe('console.log("test");'); |
| | }); |
| |
|
| | it('should use expressStaticGzip when skipGzipScan is false', async () => { |
| | app.use(staticCache(testDir)); |
| |
|
| | const response = await request(app) |
| | .get('/test.js') |
| | .set('Accept-Encoding', 'gzip') |
| | .expect(200); |
| |
|
| | |
| | expect(response.headers['content-encoding']).toBe('gzip'); |
| | expect(response.headers['cache-control']).toBe('public, max-age=172800, s-maxage=86400'); |
| | expect(response.text).toBe('console.log("test");'); |
| | }); |
| | }); |
| |
|
| | describe('file serving', () => { |
| | beforeEach(() => { |
| | process.env.NODE_ENV = 'production'; |
| | }); |
| |
|
| | it('should serve files correctly', async () => { |
| | app.use(staticCache(testDir)); |
| |
|
| | const response = await request(app).get('/test.js').expect(200); |
| |
|
| | expect(response.text).toBe('console.log("test");'); |
| | expect(response.headers['content-type']).toMatch(/javascript|text/); |
| | }); |
| |
|
| | it('should return 404 for non-existent files', async () => { |
| | app.use(staticCache(testDir)); |
| |
|
| | const response = await request(app).get('/nonexistent.js'); |
| | expect(response.status).toBe(404); |
| | }); |
| |
|
| | it('should serve HTML files', async () => { |
| | app.use(staticCache(testDir)); |
| |
|
| | const response = await request(app).get('/index.html').expect(200); |
| |
|
| | expect(response.text).toBe('<html><body>Test</body></html>'); |
| | expect(response.headers['content-type']).toMatch(/html/); |
| | }); |
| | }); |
| |
|
| | describe('edge cases', () => { |
| | beforeEach(() => { |
| | process.env.NODE_ENV = 'production'; |
| | }); |
| |
|
| | it('should handle webmanifest files', async () => { |
| | |
| | const webmanifestFile = path.join(testDir, 'site.webmanifest'); |
| | fs.writeFileSync(webmanifestFile, '{"name": "test app"}'); |
| |
|
| | app.use(staticCache(testDir)); |
| |
|
| | const response = await request(app).get('/site.webmanifest').expect(200); |
| |
|
| | expect(response.headers['cache-control']).toBe('no-store, no-cache, must-revalidate'); |
| |
|
| | |
| | fs.unlinkSync(webmanifestFile); |
| | }); |
| |
|
| | it('should handle files in subdirectories', async () => { |
| | const subDir = path.join(testDir, 'subdir'); |
| | fs.mkdirSync(subDir, { recursive: true }); |
| | const subFile = path.join(subDir, 'nested.js'); |
| | fs.writeFileSync(subFile, 'console.log("nested");'); |
| |
|
| | app.use(staticCache(testDir)); |
| |
|
| | const response = await request(app).get('/subdir/nested.js').expect(200); |
| |
|
| | expect(response.headers['cache-control']).toBe('public, max-age=172800, s-maxage=86400'); |
| | expect(response.text).toBe('console.log("nested");'); |
| |
|
| | |
| | fs.rmSync(subDir, { recursive: true, force: true }); |
| | }); |
| | }); |
| | }); |
| |
|