mirror of
https://github.com/juherr/kill-the-news.git
synced 2026-06-20 22:03:48 +00:00
Testing
This commit is contained in:
+8
-1
@@ -7,7 +7,10 @@
|
|||||||
"build": "wrangler deploy --dry-run --outdir=dist",
|
"build": "wrangler deploy --dry-run --outdir=dist",
|
||||||
"format": "prettier --write '**/*.{js,ts,css,json,md}'",
|
"format": "prettier --write '**/*.{js,ts,css,json,md}'",
|
||||||
"dev": "wrangler dev",
|
"dev": "wrangler dev",
|
||||||
"deploy": "wrangler deploy --env production"
|
"deploy": "wrangler deploy --env production",
|
||||||
|
"test": "vitest run",
|
||||||
|
"test:watch": "vitest",
|
||||||
|
"test:coverage": "vitest run --coverage"
|
||||||
},
|
},
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -15,8 +18,12 @@
|
|||||||
"@cloudflare/workers-types": "^4.20250224.0",
|
"@cloudflare/workers-types": "^4.20250224.0",
|
||||||
"@types/mailparser": "^3.4.5",
|
"@types/mailparser": "^3.4.5",
|
||||||
"@types/rss": "^0.0.32",
|
"@types/rss": "^0.0.32",
|
||||||
|
"@vitest/coverage-v8": "^1.3.1",
|
||||||
|
"happy-dom": "^13.3.8",
|
||||||
|
"msw": "^2.2.1",
|
||||||
"prettier": "^3.5.2",
|
"prettier": "^3.5.2",
|
||||||
"typescript": "^5.7.3",
|
"typescript": "^5.7.3",
|
||||||
|
"vitest": "^1.3.1",
|
||||||
"wrangler": "^3.111.0"
|
"wrangler": "^3.111.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
+21
-7
@@ -96,8 +96,13 @@ app.use('*', async (c, next) => {
|
|||||||
await next();
|
await next();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Webhook security middleware for /api/inbound - verify ForwardEmail.net IP
|
// Group routes by functionality
|
||||||
app.use('/api/inbound', async (c, next) => {
|
const api = new Hono();
|
||||||
|
const rss = new Hono();
|
||||||
|
const admin = new Hono();
|
||||||
|
|
||||||
|
// Webhook security middleware for /inbound - verify ForwardEmail.net IP
|
||||||
|
api.use('/inbound', async (c, next) => {
|
||||||
// Get the client IP
|
// Get the client IP
|
||||||
const clientIP = c.req.header('CF-Connecting-IP') || // Cloudflare-specific header
|
const clientIP = c.req.header('CF-Connecting-IP') || // Cloudflare-specific header
|
||||||
c.req.header('X-Forwarded-For')?.split(',')[0].trim() ||
|
c.req.header('X-Forwarded-For')?.split(',')[0].trim() ||
|
||||||
@@ -113,14 +118,23 @@ app.use('/api/inbound', async (c, next) => {
|
|||||||
return c.text('Unauthorized', 401);
|
return c.text('Unauthorized', 401);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Authorized webhook request from ForwardEmail.net (${clientIP})`);
|
console.log(`Authorized webhook request from ForwardEmail.net (${clientIP}`);
|
||||||
await next();
|
await next();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Route handlers
|
// API routes (inbound webhook)
|
||||||
app.post('/api/inbound', handleInbound);
|
api.post('/inbound', handleInbound);
|
||||||
app.get('/rss/:feedId', handleRSS);
|
|
||||||
app.route('/admin', handleAdmin);
|
// RSS feed routes (public)
|
||||||
|
rss.get('/:feedId', handleRSS);
|
||||||
|
|
||||||
|
// Admin routes (protected)
|
||||||
|
admin.route('/', handleAdmin);
|
||||||
|
|
||||||
|
// Mount the route groups
|
||||||
|
app.route('/api', api);
|
||||||
|
app.route('/rss', rss);
|
||||||
|
app.route('/admin', admin);
|
||||||
|
|
||||||
// Root path redirects to admin dashboard
|
// Root path redirects to admin dashboard
|
||||||
app.get('/', (c) => c.redirect('/admin'));
|
app.get('/', (c) => c.redirect('/admin'));
|
||||||
|
|||||||
@@ -0,0 +1,238 @@
|
|||||||
|
import { describe, it, expect, beforeEach } from 'vitest';
|
||||||
|
import { Hono } from 'hono';
|
||||||
|
import app from './admin';
|
||||||
|
import { createMockEnv } from '../test/setup';
|
||||||
|
import { Env } from '../types';
|
||||||
|
|
||||||
|
describe('Admin Routes', () => {
|
||||||
|
let testApp: Hono;
|
||||||
|
let mockEnv: Env;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockEnv = createMockEnv();
|
||||||
|
testApp = new Hono();
|
||||||
|
testApp.route('/admin', app);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Authentication', () => {
|
||||||
|
it('should redirect to login page when not authenticated', async () => {
|
||||||
|
const res = await testApp.request('/admin', {
|
||||||
|
env: mockEnv
|
||||||
|
});
|
||||||
|
expect(res.status).toBe(302);
|
||||||
|
expect(res.headers.get('Location')).toBe('/admin/login');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow access to login page without authentication', async () => {
|
||||||
|
const res = await testApp.request('/admin/login', {
|
||||||
|
env: mockEnv
|
||||||
|
});
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(res.headers.get('Content-Type')).toContain('text/html');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set auth cookie and redirect on successful login', async () => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('password', 'test-password');
|
||||||
|
|
||||||
|
const res = await testApp.request('/admin/login', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
env: mockEnv
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
const cookie = res.headers.get('Set-Cookie');
|
||||||
|
expect(cookie).toContain('admin_auth=true');
|
||||||
|
expect(cookie).toContain('HttpOnly');
|
||||||
|
expect(cookie).toContain('SameSite=Strict');
|
||||||
|
expect(cookie).toContain('Path=/');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject login with incorrect password', async () => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('password', 'wrong-password');
|
||||||
|
|
||||||
|
const res = await testApp.request('/admin/login', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
env: mockEnv
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.status).toBe(302);
|
||||||
|
expect(res.headers.get('Location')).toBe('/admin/login?error=invalid');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject login with missing password', async () => {
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
const res = await testApp.request('/admin/login', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
env: mockEnv
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.status).toBe(302);
|
||||||
|
expect(res.headers.get('Location')).toBe('/admin/login?error=invalid');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Protected Routes', () => {
|
||||||
|
const authCookie = 'admin_auth=true';
|
||||||
|
|
||||||
|
it('should allow access to dashboard with valid auth cookie', async () => {
|
||||||
|
const res = await testApp.request('/admin', {
|
||||||
|
headers: {
|
||||||
|
Cookie: authCookie
|
||||||
|
},
|
||||||
|
env: mockEnv
|
||||||
|
});
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(res.headers.get('Content-Type')).toContain('text/html');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Feed Creation', () => {
|
||||||
|
it('should prevent feed creation without authentication', async () => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('title', 'Test Feed');
|
||||||
|
formData.append('description', 'Test Description');
|
||||||
|
|
||||||
|
const res = await testApp.request('/admin/feeds/create', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
env: mockEnv
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.status).toBe(302);
|
||||||
|
expect(res.headers.get('Location')).toBe('/admin/login');
|
||||||
|
|
||||||
|
// Verify no feed was created
|
||||||
|
const feedList = await mockEnv.EMAIL_STORAGE.get('feeds', 'json');
|
||||||
|
expect(feedList).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow feed creation with valid authentication', async () => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('title', 'Test Feed');
|
||||||
|
formData.append('description', 'Test Description');
|
||||||
|
|
||||||
|
const res = await testApp.request('/admin/feeds/create', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Cookie: authCookie
|
||||||
|
},
|
||||||
|
body: formData,
|
||||||
|
env: mockEnv
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.status).toBe(302); // Redirects back to dashboard
|
||||||
|
expect(res.headers.get('Location')).toBe('/admin');
|
||||||
|
|
||||||
|
// Verify feed was created in KV
|
||||||
|
const feedList = await mockEnv.EMAIL_STORAGE.get('feeds', 'json');
|
||||||
|
expect(feedList).toBeTruthy();
|
||||||
|
expect(feedList.length).toBe(1);
|
||||||
|
expect(feedList[0].title).toBe('Test Feed');
|
||||||
|
|
||||||
|
// Verify feed config was created
|
||||||
|
const feedId = feedList[0].id;
|
||||||
|
const feedConfig = await mockEnv.EMAIL_STORAGE.get(`feed:${feedId}:config`, 'json');
|
||||||
|
expect(feedConfig).toBeTruthy();
|
||||||
|
expect(feedConfig.title).toBe('Test Feed');
|
||||||
|
expect(feedConfig.description).toBe('Test Description');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject feed creation with missing title', async () => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('description', 'Test Description');
|
||||||
|
|
||||||
|
const res = await testApp.request('/admin/feeds/create', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Cookie: authCookie
|
||||||
|
},
|
||||||
|
body: formData,
|
||||||
|
env: mockEnv
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.status).toBe(400);
|
||||||
|
|
||||||
|
// Verify no feed was created
|
||||||
|
const feedList = await mockEnv.EMAIL_STORAGE.get('feeds', 'json');
|
||||||
|
expect(feedList).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Feed Management', () => {
|
||||||
|
it('should prevent feed deletion without authentication', async () => {
|
||||||
|
const res = await testApp.request('/admin/feeds/test-feed/delete', {
|
||||||
|
method: 'POST',
|
||||||
|
env: mockEnv
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.status).toBe(302);
|
||||||
|
expect(res.headers.get('Location')).toBe('/admin/login');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should prevent API feed updates without authentication', async () => {
|
||||||
|
const res = await testApp.request('/admin/api/feeds/test-feed/update', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
title: 'Updated Title',
|
||||||
|
description: 'Updated Description'
|
||||||
|
}),
|
||||||
|
env: mockEnv
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.status).toBe(302);
|
||||||
|
expect(res.headers.get('Location')).toBe('/admin/login');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow feed deletion with valid authentication', async () => {
|
||||||
|
// First create a feed
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('title', 'Test Feed');
|
||||||
|
formData.append('description', 'Test Description');
|
||||||
|
|
||||||
|
const createRes = await testApp.request('/admin/feeds/create', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Cookie: authCookie
|
||||||
|
},
|
||||||
|
body: formData,
|
||||||
|
env: mockEnv
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(createRes.status).toBe(302);
|
||||||
|
|
||||||
|
// Get the feed ID
|
||||||
|
const feedList = await mockEnv.EMAIL_STORAGE.get('feeds', 'json');
|
||||||
|
const feedId = feedList[0].id;
|
||||||
|
|
||||||
|
// Now delete it
|
||||||
|
const deleteRes = await testApp.request(`/admin/feeds/${feedId}/delete`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Cookie: authCookie
|
||||||
|
},
|
||||||
|
env: mockEnv
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(deleteRes.status).toBe(302);
|
||||||
|
expect(deleteRes.headers.get('Location')).toBe('/admin');
|
||||||
|
|
||||||
|
// Verify feed was deleted
|
||||||
|
const updatedFeedList = await mockEnv.EMAIL_STORAGE.get('feeds', 'json');
|
||||||
|
expect(updatedFeedList).toBeTruthy();
|
||||||
|
expect(updatedFeedList.length).toBe(0);
|
||||||
|
|
||||||
|
// Verify feed config was deleted
|
||||||
|
const feedConfig = await mockEnv.EMAIL_STORAGE.get(`feed:${feedId}:config`, 'json');
|
||||||
|
expect(feedConfig).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
+32
-2
@@ -1,4 +1,4 @@
|
|||||||
import { Hono } from 'hono';
|
import { Context, Hono } from 'hono';
|
||||||
import { html, raw } from 'hono/html';
|
import { html, raw } from 'hono/html';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { Env, FeedConfig, FeedList, FeedMetadata, EmailMetadata, EmailData, FeedListItem } from '../types';
|
import { Env, FeedConfig, FeedList, FeedMetadata, EmailMetadata, EmailData, FeedListItem } from '../types';
|
||||||
@@ -6,9 +6,39 @@ import { generateFeedId } from '../utils/id-generator';
|
|||||||
import { designSystem } from '../styles/index';
|
import { designSystem } from '../styles/index';
|
||||||
import { interactiveScripts, authHelpers } from '../scripts/index';
|
import { interactiveScripts, authHelpers } from '../scripts/index';
|
||||||
|
|
||||||
// Create a Hono app for admin routes
|
/**
|
||||||
|
* Admin routes handler for Email-to-RSS
|
||||||
|
* Provides a secure interface for managing RSS feeds and viewing emails
|
||||||
|
*
|
||||||
|
* Security:
|
||||||
|
* - All routes except /login are protected by server-side cookie authentication
|
||||||
|
* - Uses HttpOnly cookies to prevent XSS attacks
|
||||||
|
* - Implements SameSite=Strict to prevent CSRF attacks
|
||||||
|
*/
|
||||||
const app = new Hono();
|
const app = new Hono();
|
||||||
|
|
||||||
|
// Export for testing
|
||||||
|
export default app;
|
||||||
|
|
||||||
|
// Authentication middleware for admin routes
|
||||||
|
async function authMiddleware(c: Context, next: () => Promise<void>) {
|
||||||
|
const path = new URL(c.req.url).pathname;
|
||||||
|
// Skip auth check for login page - note that path includes /admin prefix
|
||||||
|
if (path === '/admin/login') {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
const authCookie = c.req.cookie('admin_auth');
|
||||||
|
if (!authCookie || authCookie !== 'true') {
|
||||||
|
return c.redirect('/admin/login');
|
||||||
|
}
|
||||||
|
|
||||||
|
await next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply auth middleware to all admin routes
|
||||||
|
app.use('*', authMiddleware);
|
||||||
|
|
||||||
// Schema for feed creation
|
// Schema for feed creation
|
||||||
const createFeedSchema = z.object({
|
const createFeedSchema = z.object({
|
||||||
title: z.string().min(1, 'Title is required'),
|
title: z.string().min(1, 'Title is required'),
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
import { beforeAll, afterAll, afterEach } from 'vitest';
|
||||||
|
import { setupServer } from 'msw/node';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock implementation of Cloudflare Workers runtime environment
|
||||||
|
* Based on: https://developers.cloudflare.com/workers/testing/
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Define Cloudflare Workers runtime globals
|
||||||
|
declare global {
|
||||||
|
// CF Worker specific globals
|
||||||
|
var caches: CacheStorage;
|
||||||
|
var crypto: Crypto;
|
||||||
|
var Response: typeof Response;
|
||||||
|
var Request: typeof Request;
|
||||||
|
var URLSearchParams: typeof URLSearchParams;
|
||||||
|
var URL: typeof URL;
|
||||||
|
var Headers: typeof Headers;
|
||||||
|
var FormData: typeof FormData;
|
||||||
|
var Blob: typeof Blob;
|
||||||
|
var atob: (data: string) => string;
|
||||||
|
var btoa: (data: string) => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock KV namespace implementation
|
||||||
|
* Simulates Cloudflare Workers KV storage using an in-memory Map
|
||||||
|
*/
|
||||||
|
class MockKV {
|
||||||
|
private store: Map<string, any> = new Map();
|
||||||
|
|
||||||
|
async get(key: string, type: 'text' | 'json' | 'arrayBuffer' | 'stream' = 'text') {
|
||||||
|
const value = this.store.get(key);
|
||||||
|
if (!value) return null;
|
||||||
|
return type === 'json' ? JSON.parse(value) : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
async put(key: string, value: any) {
|
||||||
|
this.store.set(key, typeof value === 'string' ? value : JSON.stringify(value));
|
||||||
|
return undefined; // Match CF Workers KV behavior
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(key: string) {
|
||||||
|
this.store.delete(key);
|
||||||
|
return undefined; // Match CF Workers KV behavior
|
||||||
|
}
|
||||||
|
|
||||||
|
async list(options?: { prefix?: string; cursor?: string; limit?: number }) {
|
||||||
|
const keys = Array.from(this.store.keys())
|
||||||
|
.filter(key => !options?.prefix || key.startsWith(options.prefix))
|
||||||
|
.slice(0, options?.limit || undefined)
|
||||||
|
.map(name => ({ name }));
|
||||||
|
|
||||||
|
return {
|
||||||
|
keys,
|
||||||
|
list_complete: true,
|
||||||
|
cursor: ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock Cache implementation
|
||||||
|
* Simulates Cloudflare Workers Cache API using an in-memory Map
|
||||||
|
*/
|
||||||
|
class MockCache implements Cache {
|
||||||
|
private store: Map<string, Response> = new Map();
|
||||||
|
|
||||||
|
async put(request: RequestInfo, response: Response): Promise<undefined> {
|
||||||
|
const key = request instanceof Request ? request.url : request;
|
||||||
|
this.store.set(key, response.clone());
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async match(request: RequestInfo, options?: CacheQueryOptions): Promise<Response | undefined> {
|
||||||
|
const key = request instanceof Request ? request.url : request;
|
||||||
|
const response = this.store.get(key);
|
||||||
|
return response?.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(request: RequestInfo, options?: CacheQueryOptions): Promise<boolean> {
|
||||||
|
const key = request instanceof Request ? request.url : request;
|
||||||
|
return this.store.delete(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Required Cache interface methods with minimal implementations
|
||||||
|
async add(): Promise<void> { throw new Error('Not implemented'); }
|
||||||
|
async addAll(): Promise<void> { throw new Error('Not implemented'); }
|
||||||
|
async keys(): Promise<Request[]> { return []; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create MSW server for mocking external requests
|
||||||
|
export const server = setupServer();
|
||||||
|
|
||||||
|
// Setup before tests
|
||||||
|
beforeAll(() => {
|
||||||
|
// Setup MSW server
|
||||||
|
server.listen({ onUnhandledRequest: 'error' });
|
||||||
|
|
||||||
|
// Mock Cloudflare Workers runtime globals
|
||||||
|
global.caches = {
|
||||||
|
default: new MockCache(),
|
||||||
|
open: async () => new MockCache()
|
||||||
|
} as unknown as CacheStorage;
|
||||||
|
|
||||||
|
// Mock crypto for generating random values
|
||||||
|
if (!global.crypto) {
|
||||||
|
global.crypto = require('crypto').webcrypto;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure other required globals are available
|
||||||
|
if (!global.FormData) {
|
||||||
|
const { FormData } = require('undici');
|
||||||
|
global.FormData = FormData;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!global.Headers) {
|
||||||
|
const { Headers } = require('undici');
|
||||||
|
global.Headers = Headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!global.Request) {
|
||||||
|
const { Request } = require('undici');
|
||||||
|
global.Request = Request;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!global.Response) {
|
||||||
|
const { Response } = require('undici');
|
||||||
|
global.Response = Response;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clean up after tests
|
||||||
|
afterAll(() => {
|
||||||
|
server.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
server.resetHandlers();
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a mock environment for testing
|
||||||
|
* @returns Mock environment with KV storage and configuration
|
||||||
|
*/
|
||||||
|
export const createMockEnv = () => ({
|
||||||
|
EMAIL_STORAGE: new MockKV(),
|
||||||
|
DOMAIN: 'test.getmynews.app',
|
||||||
|
ADMIN_PASSWORD: 'test-password',
|
||||||
|
});
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { defineConfig } from 'vitest/config';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
// Use happy-dom for browser API simulation
|
||||||
|
environment: 'happy-dom',
|
||||||
|
|
||||||
|
// Include source files for coverage
|
||||||
|
include: ['src/**/*.{test,spec}.{js,ts}'],
|
||||||
|
|
||||||
|
// Coverage configuration
|
||||||
|
coverage: {
|
||||||
|
provider: 'v8',
|
||||||
|
reporter: ['text', 'json', 'html'],
|
||||||
|
include: ['src/**/*.ts'],
|
||||||
|
exclude: [
|
||||||
|
'src/**/*.{test,spec}.ts',
|
||||||
|
'src/types/**',
|
||||||
|
'src/scripts/**',
|
||||||
|
'src/styles/**'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// Global setup files
|
||||||
|
setupFiles: ['src/test/setup.ts'],
|
||||||
|
|
||||||
|
// Mock Cloudflare Workers runtime
|
||||||
|
globals: true,
|
||||||
|
|
||||||
|
// Timeouts
|
||||||
|
testTimeout: 10000,
|
||||||
|
hookTimeout: 10000
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user