@trieb.work/nextjs-turbo-redis-cache
Version:
Designed for speed, scalability, and optimized performance, nextjs-turbo-redis-cache is your custom cache handler for demanding production environments.
228 lines (188 loc) • 7.31 kB
text/typescript
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { spawn, ChildProcess } from 'child_process';
import { createClient, RedisClientType } from 'redis';
import path from 'path';
const PORT = Number(process.env.CACHE_COMPONENTS_PORT || '3065');
const BASE_URL = `http://localhost:${PORT}`;
describe('Next.js 16 Cache Components Integration', () => {
let nextProcess: ChildProcess;
let redisClient: RedisClientType;
let keyPrefix: string;
async function delay(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
beforeAll(async () => {
// Connect to Redis
redisClient = createClient({
url: process.env.REDIS_URL || 'redis://localhost:6379',
database: 1,
});
await redisClient.connect();
// Generate unique key prefix for this test run
keyPrefix = `cache-components-test-${Math.random().toString(36).substring(7)}`;
process.env.VERCEL_URL = keyPrefix;
// Build and start Next.js app
const appDir = path.join(
__dirname,
'..',
'integration',
'next-app-16-2-3-cache-components',
);
console.log('Installing Next.js app dependencies...');
await new Promise<void>((resolve, reject) => {
const installProcess = spawn('pnpm', ['install'], {
cwd: appDir,
stdio: 'inherit',
});
installProcess.on('close', (code) => {
if (code === 0) resolve();
else reject(new Error(`Install failed with code ${code}`));
});
});
console.log('Building Next.js app...');
await new Promise<void>((resolve, reject) => {
const buildProcess = spawn('pnpm', ['build'], {
cwd: appDir,
stdio: 'inherit',
});
buildProcess.on('close', (code) => {
if (code === 0) resolve();
else reject(new Error(`Build failed with code ${code}`));
});
});
console.log('Starting Next.js app...');
nextProcess = spawn('pnpm', ['start', '-p', PORT.toString()], {
cwd: appDir,
env: { ...process.env, VERCEL_URL: keyPrefix },
});
// Wait for server to be ready
await new Promise((resolve) => setTimeout(resolve, 3000));
}, 120000);
afterAll(async () => {
// Clean up Redis keys
const keys = await redisClient.keys(`${keyPrefix}*`);
if (keys.length > 0) {
await redisClient.del(keys);
}
await redisClient.quit();
// Kill Next.js process
if (nextProcess) {
nextProcess.kill();
}
});
describe('Basic use cache functionality', () => {
it('should cache data and return same counter value on subsequent requests', async () => {
// First request
const res1 = await fetch(`${BASE_URL}/api/cached-static-fetch`);
const data1 = await res1.json();
expect(data1.counter).toBe(1);
// Second request should return cached data
const res2 = await fetch(`${BASE_URL}/api/cached-static-fetch`);
const data2 = await res2.json();
expect(data2.counter).toBe(1); // Same counter value
expect(data2.timestamp).toBe(data1.timestamp); // Same timestamp
});
it('should store cache entry in Redis', async () => {
await fetch(`${BASE_URL}/api/cached-static-fetch`);
// Check Redis for cache keys
const keys = await redisClient.keys(`${keyPrefix}*`);
expect(keys.length).toBeGreaterThan(0);
});
});
describe('cacheTag functionality', () => {
it('should cache data with tags', async () => {
const res1 = await fetch(`${BASE_URL}/api/cached-with-tag`);
const data1 = await res1.json();
expect(data1.counter).toBeDefined();
// Second request should return cached data
const res2 = await fetch(`${BASE_URL}/api/cached-with-tag`);
const data2 = await res2.json();
expect(data2.counter).toBe(data1.counter);
});
it('should invalidate cache when tag is revalidated (Stale while revalidate)', async () => {
// Get initial cached data
const res1 = await fetch(`${BASE_URL}/api/cached-with-tag`);
const data1 = await res1.json();
// Revalidate the tag
await fetch(`${BASE_URL}/api/revalidate-tag`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tag: 'test-tag' }),
});
// The cache should be invalidated - verify by making multiple requests
// until we get fresh data (with retries for async revalidation)
let freshDataReceived = false;
// Next.js tag revalidation can be async and may take longer under some runtimes.
// Use a more tolerant window to avoid flaky failures.
for (let i = 0; i < 60; i++) {
await new Promise((resolve) => setTimeout(resolve, 500));
const res = await fetch(`${BASE_URL}/api/cached-with-tag`);
const data = await res.json();
if (
data.counter !== data1.counter ||
data.timestamp !== data1.timestamp
) {
freshDataReceived = true;
break;
}
}
expect(freshDataReceived).toBe(true);
}, 20_000);
});
describe('cacheLife functionality', () => {
it('should respect expire window and eventually return refreshed data', async () => {
const res1 = await fetch(`${BASE_URL}/api/cached-with-cachelife`);
const data1 = await res1.json();
expect(data1.counter).toBe(1);
const res2 = await fetch(`${BASE_URL}/api/cached-with-cachelife`);
const data2 = await res2.json();
expect(data2.counter).toBe(data1.counter);
expect(data2.timestamp).toBe(data1.timestamp);
await delay(6500);
let refreshedData: any;
for (let i = 0; i < 10; i++) {
const res = await fetch(`${BASE_URL}/api/cached-with-cachelife`);
const data = await res.json();
if (
data.counter !== data1.counter ||
data.timestamp !== data1.timestamp
) {
refreshedData = data;
break;
}
await delay(500);
}
expect(refreshedData).toBeDefined();
expect(refreshedData.counter).toBeGreaterThan(data1.counter);
expect(refreshedData.timestamp).not.toBe(data1.timestamp);
}, 20_000);
});
describe('Redis cache handler integration', () => {
it('should call cache handler get and set methods', async () => {
// Make request to trigger cache (don't clear first)
await fetch(`${BASE_URL}/api/cached-static-fetch`);
// Verify Redis has the cached data
const redisKeys = await redisClient.keys(`${keyPrefix}*`);
expect(redisKeys.length).toBeGreaterThan(0);
// Filter out hash keys (sharedTagsMap) and only check string keys (cache entries)
// Try to get each key and verify at least one is a string value
let foundStringKey = false;
for (const key of redisKeys) {
try {
const type = await redisClient.type(key);
if (type === 'string') {
const cachedValue = await redisClient.get(key);
if (cachedValue) {
foundStringKey = true;
expect(cachedValue).toBeTruthy();
break;
}
}
} catch (e) {
// Skip non-string keys
}
}
expect(foundStringKey).toBe(true);
});
});
});