diff --git a/.cursor/rules/unit-testing.mdc b/.cursor/rules/unit-testing.mdc index 41a9698..feae76d 100644 --- a/.cursor/rules/unit-testing.mdc +++ b/.cursor/rules/unit-testing.mdc @@ -3,320 +3,563 @@ description: globs: alwaysApply: false --- -# Unit Testing Guidelines +# Testing Guidelines for Sink ## Overview -Unit testing is implemented exclusively for the `server/api` directory, focusing on API endpoint functionality, data validation, and business logic. We use Vitest as our testing framework with Nuxt Test Utils for API testing. +This project implements comprehensive testing for the backend API layer using Vitest with Cloudflare Workers Pool. Testing focuses exclusively on API endpoint functionality, data validation, authentication, and business logic for the URL shortening service. + +## Testing Stack + +- **Test Framework**: [Vitest](https://vitest.dev/) with [`@cloudflare/vitest-pool-workers`](https://github.com/cloudflare/workers-sdk/tree/main/packages/vitest-pool-workers) +- **Mock Generation**: [`@anatine/zod-mock`](https://github.com/anatine/zod-plugins/tree/main/packages/zod-mock) for generating realistic test data +- **Fake Data**: [`@faker-js/faker`](https://fakerjs.dev/) for additional test data generation +- **Environment**: Cloudflare Workers runtime with isolated storage ## Testing Scope -### What to Test +### What We Test - **API Endpoints**: All routes in [server/api/](mdc:server/api/) - - Link management APIs in [server/api/link/](mdc:server/api/link/) - - Statistics APIs in [server/api/stats/](mdc:server/api/stats/) - - Authentication endpoints like [verify.ts](mdc:server/api/verify.ts) - - Location services like [location.ts](mdc:server/api/location.ts) -- **Input Validation**: Zod schema validation -- **Error Handling**: Error responses and status codes -- **Business Logic**: Core functionality and edge cases -- **Authentication**: Token validation and authorization + - Link management APIs: [create.post.ts](mdc:server/api/link/create.post.ts), [edit.put.ts](mdc:server/api/link/edit.put.ts), [delete.post.ts](mdc:server/api/link/delete.post.ts) + - Link retrieval APIs: [list.get.ts](mdc:server/api/link/list.get.ts), [query.get.ts](mdc:server/api/link/query.get.ts), [search.get.ts](mdc:server/api/link/search.get.ts) + - Advanced features: [upsert.post.ts](mdc:server/api/link/upsert.post.ts), [ai.get.ts](mdc:server/api/link/ai.get.ts) + - Statistics endpoints: [views.get.ts](mdc:server/api/stats/views.get.ts), [counters.get.ts](mdc:server/api/stats/counters.get.ts), [metrics.get.ts](mdc:server/api/stats/metrics.get.ts) + - Utility endpoints: [location.ts](mdc:server/api/location.ts), [verify.ts](mdc:server/api/verify.ts) +- **Authentication**: Bearer token validation and authorization flows +- **Input Validation**: Zod schema validation for all API inputs +- **Error Handling**: HTTP status codes and error response structures +- **Business Logic**: URL shortening, analytics, and link management features -### What NOT to Test -- Frontend components (Vue components, pages, layouts) -- Third-party library internals -- Cloudflare Workers runtime specifics -- Database connection details +### What We Don't Test +- Frontend components, pages, or layouts (Vue.js application layer) +- Third-party service integrations (external API dependencies) +- Cloudflare Workers runtime internals +- Database connection layer specifics +- Build and deployment processes -## Test Structure +## Project Structure -### File Organization +### Current Test Organization +``` +tests/ +├── api/ +│ ├── link.spec.ts # All link-related endpoint tests +│ ├── location.spec.ts # Location API tests +│ └── verify.spec.ts # Authentication verification tests +├── setup.ts # Global test setup and configuration +├── utils.ts # Test utility functions and helpers +└── sink.spec.ts # End-to-end integration tests +``` + +### Recommended Test Structure (for expansion) ``` tests/ ├── api/ │ ├── link/ -│ │ ├── create.test.ts -│ │ ├── edit.test.ts -│ │ ├── delete.test.ts -│ │ ├── list.test.ts -│ │ ├── query.test.ts -│ │ ├── search.test.ts -│ │ ├── upsert.test.ts -│ │ └── ai.test.ts +│ │ ├── create.test.ts # POST /api/link/create +│ │ ├── edit.test.ts # PUT /api/link/edit +│ │ ├── delete.test.ts # POST /api/link/delete +│ │ ├── list.test.ts # GET /api/link/list +│ │ ├── query.test.ts # GET /api/link/query +│ │ ├── search.test.ts # GET /api/link/search +│ │ ├── upsert.test.ts # POST /api/link/upsert +│ │ └── ai.test.ts # GET /api/link/ai │ ├── stats/ -│ │ ├── views.test.ts -│ │ ├── counters.test.ts -│ │ └── metrics.test.ts -│ ├── location.test.ts -│ ├── verify.test.ts +│ │ ├── views.test.ts # GET /api/stats/views +│ │ ├── counters.test.ts # GET /api/stats/counters +│ │ └── metrics.test.ts # GET /api/stats/metrics +│ ├── location.test.ts # GET /api/location +│ ├── verify.test.ts # GET /api/verify │ └── helpers/ -│ ├── fixtures.ts -│ ├── mocks.ts -│ └── test-utils.ts -└── sink.spec.ts (existing e2e test) +│ ├── fixtures.ts # Test data fixtures +│ ├── mocks.ts # Mock implementations +│ └── assertions.ts # Custom assertion helpers +├── setup.ts +├── utils.ts +└── integration/ + └── sink.spec.ts # End-to-end workflow tests ``` -### Naming Conventions -- Test files: `*.test.ts` for unit tests -- Spec files: `*.spec.ts` for integration/e2e tests -- Test descriptions: Use descriptive, behavior-focused names - ## Testing Patterns -### API Endpoint Testing Template +### Standard API Test Template ```typescript -import { describe, it, expect, beforeEach, vi } from 'vitest' -import { $fetch, setup } from '@nuxt/test-utils/e2e' +import { describe, it, expect } from 'vitest' +import { generateMock } from '@anatine/zod-mock' +import { z } from 'zod' +import { fetch, fetchWithAuth } from '../utils' + +// Define schema for test data generation +const linkSchema = z.object({ + url: z.string().url(), + slug: z.string().min(1).max(50), +}) describe('/api/link/create', () => { - beforeEach(async () => { - await setup({ - // Test configuration + it('creates a new link with valid data', async () => { + const payload = generateMock(linkSchema) + + const response = await fetchWithAuth('/api/link/create', { + method: 'POST', + body: JSON.stringify(payload), + headers: { 'Content-Type': 'application/json' }, }) + + expect(response.status).toBe(201) + + const data = await response.json() + expect(data.link).toBeDefined() + expect(data.link.url).toBe(payload.url) + expect(data.link.slug).toBe(payload.slug) + expect(data.shortLink).toContain(payload.slug) }) - describe('POST requests', () => { - it('should create a new link with valid data', async () => { - const payload = { - url: 'https://example.com', - slug: 'test-slug' - } + it('returns 409 when slug already exists', async () => { + const payload = generateMock(linkSchema) - const response = await $fetch('/api/link/create', { - method: 'POST', - body: payload - }) - - expect(response.success).toBe(true) - expect(response.data).toHaveProperty('slug', 'test-slug') - expect(response.data).toHaveProperty('url', 'https://example.com') + // Create the link first + await fetchWithAuth('/api/link/create', { + method: 'POST', + body: JSON.stringify(payload), + headers: { 'Content-Type': 'application/json' }, }) - it('should return validation error for invalid URL', async () => { - const payload = { - url: 'invalid-url', - slug: 'test-slug' - } - - await expect($fetch('/api/link/create', { - method: 'POST', - body: payload - })).rejects.toThrow() + // Attempt to create duplicate + const duplicateResponse = await fetchWithAuth('/api/link/create', { + method: 'POST', + body: JSON.stringify(payload), + headers: { 'Content-Type': 'application/json' }, }) - it('should handle missing required fields', async () => { - const payload = { - slug: 'test-slug' - // Missing URL - } - - await expect($fetch('/api/link/create', { - method: 'POST', - body: payload - })).rejects.toThrow() - }) + expect(duplicateResponse.status).toBe(409) }) - describe('Authentication', () => { - it('should require valid authentication token', async () => { - const payload = { - url: 'https://example.com', - slug: 'test-slug' - } + it('returns 401 without authentication', async () => { + const payload = generateMock(linkSchema) - await expect($fetch('/api/link/create', { - method: 'POST', - body: payload, - headers: { - Authorization: 'Bearer invalid-token' - } - })).rejects.toThrow() + const response = await fetch('/api/link/create', { + method: 'POST', + body: JSON.stringify(payload), + headers: { 'Content-Type': 'application/json' }, }) + + expect(response.status).toBe(401) + }) + + it('validates required fields', async () => { + const invalidPayload = { slug: 'test' } // Missing URL + + const response = await fetchWithAuth('/api/link/create', { + method: 'POST', + body: JSON.stringify(invalidPayload), + headers: { 'Content-Type': 'application/json' }, + }) + + expect(response.status).toBe(400) }) }) ``` -### Validation Testing +### Authentication Testing Pattern +```typescript +describe('Authentication', () => { + it('accepts valid bearer token', async () => { + const response = await fetchWithAuth('/api/link/list') + expect(response.status).toBe(200) + }) + + it('rejects missing authorization header', async () => { + const response = await fetch('/api/link/list') + expect(response.status).toBe(401) + }) + + it('rejects invalid bearer token', async () => { + const response = await fetch('/api/link/list', { + headers: { Authorization: 'Bearer invalid-token' } + }) + expect(response.status).toBe(401) + }) + + it('rejects malformed authorization header', async () => { + const response = await fetch('/api/link/list', { + headers: { Authorization: 'InvalidFormat' } + }) + expect(response.status).toBe(401) + }) +}) +``` + +### Input Validation Testing ```typescript import { z } from 'zod' -import { describe, it, expect } from 'vitest' -describe('Link creation validation', () => { +describe('Link validation', () => { const createLinkSchema = z.object({ url: z.string().url(), slug: z.string().min(1).max(50).optional(), - expiresAt: z.string().datetime().optional() + expiresAt: z.string().datetime().optional(), }) - it('should validate correct link data', () => { - const validData = { + it('accepts valid URL formats', () => { + const testCases = [ + 'https://example.com', + 'http://sub.domain.com/path', + 'https://site.com/path?query=value#anchor' + ] + + testCases.forEach(url => { + expect(() => createLinkSchema.parse({ url })).not.toThrow() + }) + }) + + it('rejects invalid URL formats', () => { + const testCases = [ + 'not-a-url', + 'ftp://invalid-protocol.com', + 'javascript:alert("xss")', + '' + ] + + testCases.forEach(url => { + expect(() => createLinkSchema.parse({ url })).toThrow() + }) + }) + + it('validates slug constraints', () => { + // Valid slugs + expect(() => createLinkSchema.parse({ url: 'https://example.com', - slug: 'test-slug' - } + slug: 'valid-slug' + })).not.toThrow() - expect(() => createLinkSchema.parse(validData)).not.toThrow() - }) - - it('should reject invalid URL format', () => { - const invalidData = { - url: 'not-a-url', - slug: 'test-slug' - } - - expect(() => createLinkSchema.parse(invalidData)).toThrow() - }) - - it('should reject slug that is too long', () => { - const invalidData = { + // Invalid slugs + expect(() => createLinkSchema.parse({ url: 'https://example.com', - slug: 'a'.repeat(51) // Exceeds 50 character limit - } + slug: '' + })).toThrow() - expect(() => createLinkSchema.parse(invalidData)).toThrow() + expect(() => createLinkSchema.parse({ + url: 'https://example.com', + slug: 'a'.repeat(51) + })).toThrow() }) }) ``` -### Mock Data and Fixtures +### Statistics and Analytics Testing ```typescript -// tests/api/helpers/fixtures.ts -export const mockLinkData = { - valid: { - url: 'https://example.com', - slug: 'test-slug', - createdAt: new Date('2024-01-01T00:00:00Z'), - isActive: true - }, - withExpiry: { - url: 'https://example.com', - slug: 'expiring-link', - expiresAt: new Date('2024-12-31T23:59:59Z') - } -} - -export const mockStatsData = { - views: { - total: 100, - unique: 85, - timeframe: '7d' - }, - countries: [ - { code: 'US', count: 45 }, - { code: 'UK', count: 20 } - ] -} -``` - -### Error Handling Tests -```typescript -describe('Error handling', () => { - it('should return 400 for malformed request body', async () => { - await expect($fetch('/api/link/create', { +describe('/api/stats/views', () => { + it('returns view statistics for valid slug', async () => { + // First create a link to get stats for + const linkPayload = generateMock(linkSchema) + await fetchWithAuth('/api/link/create', { method: 'POST', - body: 'invalid-json' - })).rejects.toMatchObject({ - statusCode: 400 + body: JSON.stringify(linkPayload), + headers: { 'Content-Type': 'application/json' }, }) + + const response = await fetchWithAuth( + `/api/stats/views?slug=${linkPayload.slug}&timeframe=7d` + ) + + expect(response.status).toBe(200) + + const data = await response.json() + expect(data).toHaveProperty('total') + expect(data).toHaveProperty('unique') + expect(data).toHaveProperty('timeframe') + expect(typeof data.total).toBe('number') + expect(typeof data.unique).toBe('number') }) - it('should return 401 for missing authentication', async () => { - await expect($fetch('/api/link/delete', { - method: 'POST', - body: { slug: 'test-slug' } - })).rejects.toMatchObject({ - statusCode: 401 - }) - }) + it('handles non-existent slug gracefully', async () => { + const response = await fetchWithAuth('/api/stats/views?slug=non-existent') - it('should return 404 for non-existent resources', async () => { - await expect($fetch('/api/link/query', { - query: { slug: 'non-existent-slug' } - })).rejects.toMatchObject({ - statusCode: 404 - }) + // Should either return 404 or empty stats + expect([200, 404]).toContain(response.status) + + if (response.status === 200) { + const data = await response.json() + expect(data.total).toBe(0) + expect(data.unique).toBe(0) + } }) }) ``` ## Test Configuration -### Vitest Setup -Ensure your `vitest.config.ts` includes: -```typescript -import { defineVitestConfig } from '@nuxt/test-utils/config' +### Vitest Configuration +The [vitest.config.ts](mdc:vitest.config.ts) uses Cloudflare Workers pool: -export default defineVitestConfig({ +```typescript +import { defineWorkersConfig } from '@cloudflare/vitest-pool-workers/config' +import { loadEnv } from 'vite' + +export default defineWorkersConfig(({ mode }) => ({ test: { - environment: 'nuxt', - include: ['tests/**/*.{test,spec}.ts'], - coverage: { - provider: 'v8', - include: ['server/api/**/*.ts'], - exclude: ['**/*.d.ts', '**/*.config.*'] - } + env: loadEnv(mode, process.cwd(), ''), + globalSetup: './tests/setup.ts', + poolOptions: { + workers: { + singleWorker: true, + isolatedStorage: false, + wrangler: { + configPath: './wrangler.jsonc', + }, + miniflare: { + cf: true, + }, + }, + }, + }, +})) +``` + +### Test Utilities +The [tests/utils.ts](mdc:tests/utils.ts) provides essential helpers: + +```typescript +import { SELF } from 'cloudflare:test' + +export async function fetchWithAuth(path: string, options?: RequestInit) { + return SELF.fetch(`http://localhost${path}`, { + ...options, + headers: { + ...options?.headers, + Authorization: `Bearer ${import.meta.env.NUXT_SITE_TOKEN}`, + }, + }) +} + +export async function fetch(path: string, options?: RequestInit) { + return SELF.fetch(`http://localhost${path}`, options) +} +``` + +### Environment Setup +Configure test environment variables in [tests/setup.ts](mdc:tests/setup.ts): + +```typescript +// Global test setup for environment variables and shared state +export default function setup() { + // Set up test-specific environment variables + process.env.NUXT_SITE_TOKEN = 'test-token-value' + // Additional setup as needed +} +``` + +## Test Data Management + +### Using Zod Mock for Realistic Data +```typescript +import { generateMock } from '@anatine/zod-mock' +import { z } from 'zod' + +// Schema-based mock generation +const linkSchema = z.object({ + url: z.string().url(), + slug: z.string().min(1).max(50), + description: z.string().optional(), +}) + +const testLink = generateMock(linkSchema) +// Generates: { url: "https://example.com", slug: "abc123", description: "..." } +``` + +### Creating Test Fixtures +```typescript +// tests/api/helpers/fixtures.ts +import { faker } from '@faker-js/faker' + +export const linkFixtures = { + valid: { + url: 'https://example.com', + slug: 'test-link', + description: 'A test link for unit testing' + }, + + withExpiry: { + url: 'https://temporary.com', + slug: 'temp-link', + expiresAt: new Date(Date.now() + 86400000).toISOString() // 24 hours + }, + + longUrl: { + url: 'https://very-long-domain-name.example.com/very/long/path/with/many/segments', + slug: 'long-url' } +} + +export const statsFixtures = { + basicStats: { + total: 150, + unique: 123, + timeframe: '7d' + }, + + geoStats: [ + { country: 'US', count: 45 }, + { country: 'UK', count: 30 }, + { country: 'DE', count: 25 } + ] +} + +// Dynamic fixture generation +export function createRandomLink() { + return { + url: faker.internet.url(), + slug: faker.internet.domainWord(), + description: faker.lorem.sentence() + } +} +``` + +## Error Testing Patterns + +### HTTP Status Code Testing +```typescript +describe('Error handling', () => { + it('returns 400 for malformed JSON', async () => { + const response = await fetchWithAuth('/api/link/create', { + method: 'POST', + body: 'invalid-json{', + headers: { 'Content-Type': 'application/json' }, + }) + + expect(response.status).toBe(400) + }) + + it('returns 422 for validation errors', async () => { + const response = await fetchWithAuth('/api/link/create', { + method: 'POST', + body: JSON.stringify({ url: 'not-a-url' }), + headers: { 'Content-Type': 'application/json' }, + }) + + expect(response.status).toBe(422) + }) + + it('returns 404 for non-existent resources', async () => { + const response = await fetchWithAuth('/api/link/query?slug=non-existent') + expect(response.status).toBe(404) + }) + + it('returns 405 for unsupported methods', async () => { + const response = await fetchWithAuth('/api/link/create', { + method: 'GET', // Should be POST + }) + + expect(response.status).toBe(405) + }) }) ``` -### Environment Variables -Set up test-specific environment variables: +### Rate Limiting and Performance ```typescript -// tests/setup.ts -import { vi } from 'vitest' +describe('Performance and limits', () => { + it('handles multiple concurrent requests', async () => { + const requests = Array.from({ length: 10 }, (_, i) => + fetchWithAuth('/api/link/create', { + method: 'POST', + body: JSON.stringify({ + url: `https://example-${i}.com`, + slug: `test-${i}` + }), + headers: { 'Content-Type': 'application/json' }, + }) + ) -// Mock environment variables for testing -vi.stubEnv('SITE_TOKEN', 'test-token') -vi.stubEnv('KV_NAMESPACE', 'test-kv') + const responses = await Promise.all(requests) + + // All requests should succeed + responses.forEach(response => { + expect(response.status).toBe(201) + }) + }) +}) ``` ## Best Practices -### Test Independence -- Each test should be independent and not rely on other tests -- Use `beforeEach` to set up clean state -- Clean up after tests when necessary +### Test Organization +- **Sequential Testing**: Use `describe.sequential()` for tests that modify shared state +- **Isolation**: Each test should be independent and not rely on others +- **Cleanup**: Clean up created resources when necessary +- **Descriptive Names**: Use clear, behavior-focused test descriptions -### Test Data -- Use meaningful test data that reflects real-world scenarios -- Create reusable fixtures for common test data -- Avoid hardcoded values; use constants +### Data Management +- **Mock Generation**: Use `@anatine/zod-mock` for schema-consistent test data +- **Realistic Data**: Generate realistic URLs, slugs, and user inputs +- **Edge Cases**: Test boundary conditions, empty values, and extreme inputs +- **State Management**: Be mindful of shared KV storage in sequential tests -### Assertions -- Test both positive and negative cases -- Verify response structure and data types -- Check error messages and status codes +### Performance Considerations +- **Focused Tests**: Keep tests fast and focused on specific functionality +- **Parallel Execution**: Use parallel execution where state doesn't conflict +- **Resource Usage**: Monitor test execution time and resource consumption +- **Mock External Services**: Avoid real external API calls in tests -### Performance -- Keep tests fast and focused -- Use mocking to avoid external dependencies -- Group related tests using `describe` blocks - -### Coverage Goals -- Aim for high coverage of critical API endpoints -- Focus on business logic and error scenarios -- Don't chase 100% coverage at the expense of test quality +### Assertion Strategies +- **Comprehensive Checks**: Verify response status, structure, and data +- **Type Safety**: Ensure response data matches expected TypeScript types +- **Error Messages**: Test error response content and structure +- **Business Logic**: Verify core URL shortening and analytics logic ## Running Tests -### Commands +### Available Commands ```bash # Run all tests npm run test -# Run tests in watch mode -npm run test:watch - -# Run tests with coverage -npm run test:coverage +# Run tests in watch mode (for development) +npm run test -- --watch # Run specific test file -npm run test tests/api/link/create.test.ts +npm run test tests/api/link.spec.ts + +# Run tests with verbose output +npm run test -- --reporter=verbose # Run tests matching pattern npm run test -- --grep "authentication" + +# Run tests for specific API group +npm run test tests/api/link/ tests/api/stats/ +``` + +### Debugging Tests +```bash +# Run tests with debugging +npm run test -- --inspect-brk + +# Run single test with detailed output +npm run test -- --run --reporter=verbose tests/api/link.spec.ts ``` ### CI/CD Integration -- Tests should run on every pull request -- Enforce minimum coverage thresholds -- Block merges if tests fail +- Tests run automatically on every pull request +- Enforce test passage before merging +- Consider adding coverage reporting and thresholds +- Use test results to gate deployments + +## Coverage Goals + +### Target Areas +- **API Endpoints**: 100% coverage of all public endpoints +- **Authentication**: Complete coverage of auth flows and edge cases +- **Validation**: All input validation paths and error scenarios +- **Business Logic**: Core URL shortening and analytics functionality + +### Coverage Exclusions +- Configuration files and build scripts +- Type definitions and interfaces +- Third-party integrations (mock instead) +- Development-only utilities + +### Monitoring +- Track coverage trends over time +- Identify uncovered critical paths +- Balance coverage percentage with test quality +- Focus on meaningful coverage over arbitrary numbers diff --git a/.gitignore b/.gitignore index 479f83d..e0338e5 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,4 @@ node_modules site cache public/world.json +.dev.* diff --git a/package.json b/package.json index d835caf..2623d23 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "deploy": "npm run deploy:pages", "deploy:pages": "wrangler pages deploy dist", "deploy:worker": "wrangler deploy", - "preview": "wrangler dev --port 8888 --var $(cat .env | sed 's/=/:/g')", + "preview": "wrangler dev --var $(cat .env | sed 's/=/:/g')", "test": "vitest" }, "dependencies": { @@ -52,10 +52,12 @@ "zod": "^3.25.20" }, "devDependencies": { + "@anatine/zod-mock": "^3.14.0", "@antfu/eslint-config": "^4.13.2", + "@cloudflare/vitest-pool-workers": "^0.8.33", + "@faker-js/faker": "^9.8.0", "@nuxt/eslint": "^1.4.1", "@nuxt/eslint-config": "^1.4.1", - "@nuxt/test-utils": "^3.19.1", "@nuxthub/core": "^0.8.27", "@nuxtjs/color-mode": "^3.5.2", "@nuxtjs/i18n": "^9.5.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 020f60a..7a68052 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -87,18 +87,24 @@ importers: specifier: ^3.25.20 version: 3.25.20 devDependencies: + '@anatine/zod-mock': + specifier: ^3.14.0 + version: 3.14.0(@faker-js/faker@9.8.0)(zod@3.25.20) '@antfu/eslint-config': specifier: ^4.13.2 version: 4.13.2(@vue/compiler-sfc@3.5.14)(eslint@9.27.0(jiti@2.4.2))(typescript@5.7.2)(vitest@3.1.4(@types/debug@4.1.12)(@types/node@20.12.11)(jiti@2.4.2)(terser@5.31.0)(yaml@2.8.0)) + '@cloudflare/vitest-pool-workers': + specifier: ^0.8.33 + version: 0.8.33(@vitest/runner@3.1.4)(@vitest/snapshot@3.1.4)(vitest@3.1.4(@types/debug@4.1.12)(@types/node@20.12.11)(jiti@2.4.2)(terser@5.31.0)(yaml@2.8.0)) + '@faker-js/faker': + specifier: ^9.8.0 + version: 9.8.0 '@nuxt/eslint': specifier: ^1.4.1 version: 1.4.1(@vue/compiler-sfc@3.5.14)(eslint@9.27.0(jiti@2.4.2))(magicast@0.3.5)(typescript@5.7.2)(vite@6.3.5(@types/node@20.12.11)(jiti@2.4.2)(terser@5.31.0)(yaml@2.8.0)) '@nuxt/eslint-config': specifier: ^1.4.1 version: 1.4.1(@vue/compiler-sfc@3.5.14)(eslint@9.27.0(jiti@2.4.2))(typescript@5.7.2) - '@nuxt/test-utils': - specifier: ^3.19.1 - version: 3.19.1(@types/node@20.12.11)(jiti@2.4.2)(magicast@0.3.5)(terser@5.31.0)(typescript@5.7.2)(vitest@3.1.4(@types/debug@4.1.12)(@types/node@20.12.11)(jiti@2.4.2)(terser@5.31.0)(yaml@2.8.0))(yaml@2.8.0) '@nuxthub/core': specifier: ^0.8.27 version: 0.8.27(db0@0.3.2)(ioredis@5.6.1)(magicast@0.3.5)(vite@6.3.5(@types/node@20.12.11)(jiti@2.4.2)(terser@5.31.0)(yaml@2.8.0)) @@ -149,7 +155,7 @@ importers: version: 2.2.10(typescript@5.7.2) wrangler: specifier: ^4.16.1 - version: 4.16.1(@cloudflare/workers-types@4.20250520.0) + version: 4.16.1 packages: @@ -161,6 +167,12 @@ packages: resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} + '@anatine/zod-mock@3.14.0': + resolution: {integrity: sha512-pTYyn9dlszUl5gMGWtE7DBRvx1EcqpBJ1iJ73rlJYCZr9lgIEdZ6PCZQqoXIdlC8DYIwuT1M3/N8arkJa7A10Q==} + peerDependencies: + '@faker-js/faker': ^7.0.0 || ^8.0.0 || ^9.0.0 + zod: ^3.21.4 + '@antfu/eslint-config@4.13.2': resolution: {integrity: sha512-F+IVIQUCfw6eW4H06c9a9USJ3UOnoBx4I0qsTL3kO6GcyJB6mwk+nawFf95DfHKT3fJKv58YPPz0XCmsY/w0XA==} hasBin: true @@ -353,36 +365,73 @@ packages: workerd: optional: true + '@cloudflare/vitest-pool-workers@0.8.33': + resolution: {integrity: sha512-Joh/kJhwMXP7uq0zsK2HweD+N9X2rHv51GOyaJPwwgFjfn4cRZ9flIbn9xBAMgGoNseuS38aI+cVOWFG7xlBfg==} + peerDependencies: + '@vitest/runner': 2.0.x - 3.1.x + '@vitest/snapshot': 2.0.x - 3.1.x + vitest: 2.0.x - 3.1.x + '@cloudflare/workerd-darwin-64@1.20250508.0': resolution: {integrity: sha512-9x09MrA9Y5RQs3zqWvWns8xHgM2pVNXWpeJ+3hQYu4PrwPFZXtTD6b/iMmOnlYKzINlREq1RGeEybMFyWEUlUg==} engines: {node: '>=16'} cpu: [x64] os: [darwin] + '@cloudflare/workerd-darwin-64@1.20250523.0': + resolution: {integrity: sha512-/K7vKkPDx9idJ7hJtqYXYsKkHX9XQ6awyDyBZ4RwbaQ/o3fyS/tgHaej2rUO6zkb7CfUxiaeAB7Z6i7KltMY5Q==} + engines: {node: '>=16'} + cpu: [x64] + os: [darwin] + '@cloudflare/workerd-darwin-arm64@1.20250508.0': resolution: {integrity: sha512-0Ili+nE2LLRzYue/yPc1pepSyNNg6LxR3/ng/rlQzVQUxPXIXldHFkJ/ynsYwQnAcf6OxasSi/kbTm6yvDoSAQ==} engines: {node: '>=16'} cpu: [arm64] os: [darwin] + '@cloudflare/workerd-darwin-arm64@1.20250523.0': + resolution: {integrity: sha512-tVQqStt245KzkrCT6DBXoMNHaJgh/8hQy3fsG+4gHfqw/JdKEgXigkc9hWdC6BoS5DiGK+dGVJo2MnWHFC7XlQ==} + engines: {node: '>=16'} + cpu: [arm64] + os: [darwin] + '@cloudflare/workerd-linux-64@1.20250508.0': resolution: {integrity: sha512-5saVrZ3uVwYxvBa7BaonXjeqB6X0YF3ak05qvBaWcmZ3FNmnarMm2W8842cnbhnckDVBpB/iDo51Sy6Y7y1jcw==} engines: {node: '>=16'} cpu: [x64] os: [linux] + '@cloudflare/workerd-linux-64@1.20250523.0': + resolution: {integrity: sha512-PCPWBlwiKr9Es2TP93JVygXRPwx+AkygUMV2gFOPerVrdXUd13A4dJ68Qjpmh3O0xqmVIRV6PSogM3wNvwnw5Q==} + engines: {node: '>=16'} + cpu: [x64] + os: [linux] + '@cloudflare/workerd-linux-arm64@1.20250508.0': resolution: {integrity: sha512-muQe1pkxRi3eaq1Q417xvfGd2SlktbLTzNhT5Yftsx8OecWrYuB8i4ttR6Nr5ER06bfEj0FqQjqJJhcp6wLLUQ==} engines: {node: '>=16'} cpu: [arm64] os: [linux] + '@cloudflare/workerd-linux-arm64@1.20250523.0': + resolution: {integrity: sha512-uKa/L9W1AzT+yE0wNxFZPlMXms5xmGaaOmTAK0wuLPW6qmKj1zyBidjHqQXVZ+eK/fLy3CNeyB9EBtR0/8FH7A==} + engines: {node: '>=16'} + cpu: [arm64] + os: [linux] + '@cloudflare/workerd-windows-64@1.20250508.0': resolution: {integrity: sha512-EJj8iTWFMqjgvZUxxNvzK7frA1JMFi3y/9eDIdZPL/OaQh3cmk5Lai5DCXsKYUxfooMBZWYTp53zOLrvuJI8VQ==} engines: {node: '>=16'} cpu: [x64] os: [win32] + '@cloudflare/workerd-windows-64@1.20250523.0': + resolution: {integrity: sha512-H5ggClWrskRs7pj2Fd+iJpjFMrh7DZqAfhJT3IloTW85lCEY2+y/yfXEGyDsc0UTLuTS0znldcUrVCRjSiSOkw==} + engines: {node: '>=16'} + cpu: [x64] + os: [win32] + '@cloudflare/workers-types@4.20250520.0': resolution: {integrity: sha512-bMPrpZREctlSaKtqIp3mdRjgyRSgioPQDUlhOeCUYRIo9DzWUACvw/u5kq3GQMmE4V1/nVCGHb7a1Ie8f5wG5g==} @@ -681,6 +730,10 @@ packages: resolution: {integrity: sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@faker-js/faker@9.8.0': + resolution: {integrity: sha512-U9wpuSrJC93jZBxx/Qq2wPjCuYISBueyVUGK7qqdmj7r/nxaxwW8AQDCLeRO7wZnjj94sh3p246cAYjUKuqgfg==} + engines: {node: '>=18.0.0', npm: '>=9.0.0'} + '@fastify/busboy@2.1.1': resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} engines: {node: '>=14'} @@ -1149,42 +1202,6 @@ packages: engines: {node: '>=18.12.0'} hasBin: true - '@nuxt/test-utils@3.19.1': - resolution: {integrity: sha512-qq2ioRgPCM7JwPIeJO2OzzqCWr8NR5eQINoskX2NEXTHzucvb8N9mt2UB2+NUe8OL9yNjGDZA+oA51GUKNhqhg==} - engines: {node: ^18.20.5 || ^20.9.0 || ^22.0.0 || >=23.0.0} - peerDependencies: - '@cucumber/cucumber': ^10.3.1 || ^11.0.0 - '@jest/globals': ^29.5.0 - '@playwright/test': ^1.43.1 - '@testing-library/vue': ^7.0.0 || ^8.0.1 - '@vitest/ui': '*' - '@vue/test-utils': ^2.4.2 - happy-dom: ^9.10.9 || ^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - jsdom: ^22.0.0 || ^23.0.0 || ^24.0.0 || ^25.0.0 || ^26.0.0 - playwright-core: ^1.43.1 - vitest: ^0.34.6 || ^1.0.0 || ^2.0.0 || ^3.0.0 - peerDependenciesMeta: - '@cucumber/cucumber': - optional: true - '@jest/globals': - optional: true - '@playwright/test': - optional: true - '@testing-library/vue': - optional: true - '@vitest/ui': - optional: true - '@vue/test-utils': - optional: true - happy-dom: - optional: true - jsdom: - optional: true - playwright-core: - optional: true - vitest: - optional: true - '@nuxt/vite-builder@3.17.4': resolution: {integrity: sha512-MRcGe02nEDpu+MnRJcmgVfHdzgt9tWvxVdJbhfd6oyX19plw/CANjgHedlpUNUxqeWXC6CQfGvoVJXn3bQlEqA==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0.0} @@ -2560,6 +2577,9 @@ packages: bindings@1.5.0: resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + birpc@0.2.14: + resolution: {integrity: sha512-37FHE8rqsYM5JEKCnXFyHpBCzvgHEExwVVTq+nUmloInU7l8ezD1TpOhKpS8oe1DTYFqEK27rFZVKG43oTqXRA==} + birpc@2.3.0: resolution: {integrity: sha512-ijbtkn/F3Pvzb6jHypHRyve2QApOCZDR25D/VnkY2G/lBNcXCTsnsCxgY4k4PkVB7zfwzYbY3O9Lcqe3xufS5g==} @@ -2707,6 +2727,9 @@ packages: citty@0.1.6: resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} + cjs-module-lexer@1.4.3: + resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} + class-variance-authority@0.7.1: resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} @@ -3306,6 +3329,9 @@ packages: resolution: {integrity: sha512-ARFxjzizOhPqs1fYC/2NMC3N4jrQ6HvVflnXBTRqNEqJuXwyKLRr9CrJwkRcV/SnZt1sNXgsF6FPm0x57Tq0rw==} engines: {node: ^14.14.0 || >=16.0.0} + devalue@4.3.3: + resolution: {integrity: sha512-UH8EL6H2ifcY8TbD2QsxwCC/pr5xSwPvv85LrLXVihmHVC3T3YqTCIwnR5ak0yO1KYqlxrPVOA/JVZJYPy2ATg==} + devalue@5.1.1: resolution: {integrity: sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==} @@ -3347,6 +3373,10 @@ packages: resolution: {integrity: sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==} engines: {node: '>=12'} + drange@1.1.1: + resolution: {integrity: sha512-pYxfDYpued//QpnLIm4Avk7rsNtAtQkUES2cwAYSvD/wd2pKD71gN2Ebj3e7klzXwjocvE8c5vx/1fxwpqmSxA==} + engines: {node: '>=4'} + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -3718,10 +3748,6 @@ packages: engines: {node: '>= 10.17.0'} hasBin: true - fake-indexeddb@6.0.1: - resolution: {integrity: sha512-He2AjQGHe46svIFq5+L2Nx/eHDTI1oKgoevBP+TthnjymXiKkeJQ3+ITeWey99Y5+2OaPFbI1qEsx/5RsGtWnQ==} - engines: {node: '>=18'} - fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -4776,6 +4802,11 @@ packages: engines: {node: '>=18.0.0'} hasBin: true + miniflare@4.20250523.0: + resolution: {integrity: sha512-g4F1AC5xi66rB2eQNo2Fx7EffaXhMdgUSRl/ivgb4LMALMpxghG98oC4twqVwDLWIFSVFjtL1YEuYrPO8044mg==} + engines: {node: '>=18.0.0'} + hasBin: true + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -5578,6 +5609,10 @@ packages: radix3@1.1.2: resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==} + randexp@0.5.3: + resolution: {integrity: sha512-U+5l2KrcMNOUPYvazA3h5ekF80FHTUG+87SEAmHZmolh1M+i/WyTCxVzmi+tidIa1tM4BSe8g2Y/D3loWDjj+w==} + engines: {node: '>=4'} + randombytes@2.1.0: resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} @@ -5698,6 +5733,10 @@ packages: resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} engines: {node: '>=18'} + ret@0.2.2: + resolution: {integrity: sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==} + engines: {node: '>=4'} + reusify@1.0.4: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -6597,9 +6636,6 @@ packages: yaml: optional: true - vitest-environment-nuxt@1.0.1: - resolution: {integrity: sha512-eBCwtIQriXW5/M49FjqNKfnlJYlG2LWMSNFsRVKomc8CaMqmhQPBS5LZ9DlgYL9T8xIVsiA6RZn2lk7vxov3Ow==} - vitest@3.1.4: resolution: {integrity: sha512-Ta56rT7uWxCSJXlBtKgIlApJnT6e6IGmTYxYcmxjJ4ujuZDI59GUQgVDObXXJujOmPDBYXHK1qmaGtneu6TNIQ==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -6740,6 +6776,11 @@ packages: engines: {node: '>=16'} hasBin: true + workerd@1.20250523.0: + resolution: {integrity: sha512-OClsq9ZzZZNdkY8/JTBjf+/A6F1q/SOn3/RQWCR0kDoclxecHS6Nq80jY6NP0ubJBKnqrUggA9WOWBgwWWOGUA==} + engines: {node: '>=16'} + hasBin: true + wrangler@4.16.1: resolution: {integrity: sha512-YiLdWXcaQva2K/bqokpsZbySPmoT8TJFyJPsQPZumnkgimM9+/g/yoXArByA+pf+xU8jhw7ybQ8X1yBGXv731g==} engines: {node: '>=18.0.0'} @@ -6750,6 +6791,16 @@ packages: '@cloudflare/workers-types': optional: true + wrangler@4.17.0: + resolution: {integrity: sha512-FIOriw2Z7aNALAtnt4hTojDuU44n8pGJl62id0ig0s45Mej/Clg07vpmz+QCLTT7huiaSSyA1wthYOwtp0+K6A==} + engines: {node: '>=18.0.0'} + hasBin: true + peerDependencies: + '@cloudflare/workers-types': ^4.20250523.0 + peerDependenciesMeta: + '@cloudflare/workers-types': + optional: true + wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -6884,6 +6935,12 @@ snapshots: '@jridgewell/gen-mapping': 0.3.5 '@jridgewell/trace-mapping': 0.3.25 + '@anatine/zod-mock@3.14.0(@faker-js/faker@9.8.0)(zod@3.25.20)': + dependencies: + '@faker-js/faker': 9.8.0 + randexp: 0.5.3 + zod: 3.25.20 + '@antfu/eslint-config@4.13.2(@vue/compiler-sfc@3.5.14)(eslint@9.27.0(jiti@2.4.2))(typescript@5.7.2)(vitest@3.1.4(@types/debug@4.1.12)(@types/node@20.12.11)(jiti@2.4.2)(terser@5.31.0)(yaml@2.8.0))': dependencies: '@antfu/install-pkg': 1.1.0 @@ -7131,21 +7188,59 @@ snapshots: optionalDependencies: workerd: 1.20250508.0 + '@cloudflare/unenv-preset@2.3.2(unenv@2.0.0-rc.17)(workerd@1.20250523.0)': + dependencies: + unenv: 2.0.0-rc.17 + optionalDependencies: + workerd: 1.20250523.0 + + '@cloudflare/vitest-pool-workers@0.8.33(@vitest/runner@3.1.4)(@vitest/snapshot@3.1.4)(vitest@3.1.4(@types/debug@4.1.12)(@types/node@20.12.11)(jiti@2.4.2)(terser@5.31.0)(yaml@2.8.0))': + dependencies: + '@vitest/runner': 3.1.4 + '@vitest/snapshot': 3.1.4 + birpc: 0.2.14 + cjs-module-lexer: 1.4.3 + devalue: 4.3.3 + miniflare: 4.20250523.0 + semver: 7.7.2 + vitest: 3.1.4(@types/debug@4.1.12)(@types/node@20.12.11)(jiti@2.4.2)(terser@5.31.0)(yaml@2.8.0) + wrangler: 4.17.0 + zod: 3.25.20 + transitivePeerDependencies: + - '@cloudflare/workers-types' + - bufferutil + - utf-8-validate + '@cloudflare/workerd-darwin-64@1.20250508.0': optional: true + '@cloudflare/workerd-darwin-64@1.20250523.0': + optional: true + '@cloudflare/workerd-darwin-arm64@1.20250508.0': optional: true + '@cloudflare/workerd-darwin-arm64@1.20250523.0': + optional: true + '@cloudflare/workerd-linux-64@1.20250508.0': optional: true + '@cloudflare/workerd-linux-64@1.20250523.0': + optional: true + '@cloudflare/workerd-linux-arm64@1.20250508.0': optional: true + '@cloudflare/workerd-linux-arm64@1.20250523.0': + optional: true + '@cloudflare/workerd-windows-64@1.20250508.0': optional: true + '@cloudflare/workerd-windows-64@1.20250523.0': + optional: true + '@cloudflare/workers-types@4.20250520.0': {} '@colors/colors@1.6.0': {} @@ -7428,6 +7523,8 @@ snapshots: '@eslint/core': 0.14.0 levn: 0.4.1 + '@faker-js/faker@9.8.0': {} + '@fastify/busboy@2.1.1': {} '@fastify/busboy@3.1.1': {} @@ -8091,51 +8188,6 @@ snapshots: transitivePeerDependencies: - magicast - '@nuxt/test-utils@3.19.1(@types/node@20.12.11)(jiti@2.4.2)(magicast@0.3.5)(terser@5.31.0)(typescript@5.7.2)(vitest@3.1.4(@types/debug@4.1.12)(@types/node@20.12.11)(jiti@2.4.2)(terser@5.31.0)(yaml@2.8.0))(yaml@2.8.0)': - dependencies: - '@nuxt/kit': 3.17.4(magicast@0.3.5) - '@nuxt/schema': 3.17.4 - c12: 3.0.4(magicast@0.3.5) - consola: 3.4.2 - defu: 6.1.4 - destr: 2.0.5 - estree-walker: 3.0.3 - fake-indexeddb: 6.0.1 - get-port-please: 3.1.2 - h3: 1.15.3 - local-pkg: 1.1.1 - magic-string: 0.30.17 - node-fetch-native: 1.6.6 - node-mock-http: 1.0.0 - ofetch: 1.4.1 - pathe: 2.0.3 - perfect-debounce: 1.0.0 - radix3: 1.1.2 - scule: 1.3.0 - std-env: 3.9.0 - tinyexec: 1.0.1 - ufo: 1.6.1 - unplugin: 2.3.4 - vite: 6.3.5(@types/node@20.12.11)(jiti@2.4.2)(terser@5.31.0)(yaml@2.8.0) - vitest-environment-nuxt: 1.0.1(@types/node@20.12.11)(jiti@2.4.2)(magicast@0.3.5)(terser@5.31.0)(typescript@5.7.2)(vitest@3.1.4(@types/debug@4.1.12)(@types/node@20.12.11)(jiti@2.4.2)(terser@5.31.0)(yaml@2.8.0))(yaml@2.8.0) - vue: 3.5.14(typescript@5.7.2) - optionalDependencies: - vitest: 3.1.4(@types/debug@4.1.12)(@types/node@20.12.11)(jiti@2.4.2)(terser@5.31.0)(yaml@2.8.0) - transitivePeerDependencies: - - '@types/node' - - jiti - - less - - lightningcss - - magicast - - sass - - sass-embedded - - stylus - - sugarss - - terser - - tsx - - typescript - - yaml - '@nuxt/vite-builder@3.17.4(@types/node@20.12.11)(eslint@9.27.0(jiti@2.4.2))(magicast@0.3.5)(optionator@0.9.4)(rollup@4.41.0)(terser@5.31.0)(typescript@5.7.2)(vue-tsc@2.2.10(typescript@5.7.2))(vue@3.5.14(typescript@5.7.2))(yaml@2.8.0)': dependencies: '@nuxt/kit': 3.17.4(magicast@0.3.5) @@ -9679,6 +9731,8 @@ snapshots: dependencies: file-uri-to-path: 1.0.0 + birpc@0.2.14: {} + birpc@2.3.0: {} bl@4.1.0: @@ -9836,6 +9890,8 @@ snapshots: dependencies: consola: 3.4.2 + cjs-module-lexer@1.4.3: {} + class-variance-authority@0.7.1: dependencies: clsx: 2.1.1 @@ -10420,6 +10476,8 @@ snapshots: transitivePeerDependencies: - supports-color + devalue@4.3.3: {} + devalue@5.1.1: {} devlop@1.1.0: @@ -10460,6 +10518,8 @@ snapshots: dotenv@16.5.0: {} + drange@1.1.1: {} + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -10939,8 +10999,6 @@ snapshots: transitivePeerDependencies: - supports-color - fake-indexeddb@6.0.1: {} - fast-deep-equal@3.1.3: {} fast-fifo@1.3.2: {} @@ -12216,6 +12274,24 @@ snapshots: - bufferutil - utf-8-validate + miniflare@4.20250523.0: + dependencies: + '@cspotcode/source-map-support': 0.8.1 + acorn: 8.14.0 + acorn-walk: 8.3.2 + exit-hook: 2.2.1 + glob-to-regexp: 0.4.1 + sharp: 0.33.5 + stoppable: 1.1.0 + undici: 5.29.0 + workerd: 1.20250523.0 + ws: 8.18.0 + youch: 3.3.4 + zod: 3.22.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + minimatch@3.1.2: dependencies: brace-expansion: 1.1.11 @@ -13185,6 +13261,11 @@ snapshots: radix3@1.1.2: {} + randexp@0.5.3: + dependencies: + drange: 1.1.1 + ret: 0.2.2 + randombytes@2.1.0: dependencies: safe-buffer: 5.2.1 @@ -13332,6 +13413,8 @@ snapshots: onetime: 7.0.0 signal-exit: 4.1.0 + ret@0.2.2: {} + reusify@1.0.4: {} rfdc@1.4.1: {} @@ -14303,34 +14386,6 @@ snapshots: terser: 5.31.0 yaml: 2.8.0 - vitest-environment-nuxt@1.0.1(@types/node@20.12.11)(jiti@2.4.2)(magicast@0.3.5)(terser@5.31.0)(typescript@5.7.2)(vitest@3.1.4(@types/debug@4.1.12)(@types/node@20.12.11)(jiti@2.4.2)(terser@5.31.0)(yaml@2.8.0))(yaml@2.8.0): - dependencies: - '@nuxt/test-utils': 3.19.1(@types/node@20.12.11)(jiti@2.4.2)(magicast@0.3.5)(terser@5.31.0)(typescript@5.7.2)(vitest@3.1.4(@types/debug@4.1.12)(@types/node@20.12.11)(jiti@2.4.2)(terser@5.31.0)(yaml@2.8.0))(yaml@2.8.0) - transitivePeerDependencies: - - '@cucumber/cucumber' - - '@jest/globals' - - '@playwright/test' - - '@testing-library/vue' - - '@types/node' - - '@vitest/ui' - - '@vue/test-utils' - - happy-dom - - jiti - - jsdom - - less - - lightningcss - - magicast - - playwright-core - - sass - - sass-embedded - - stylus - - sugarss - - terser - - tsx - - typescript - - vitest - - yaml - vitest@3.1.4(@types/debug@4.1.12)(@types/node@20.12.11)(jiti@2.4.2)(terser@5.31.0)(yaml@2.8.0): dependencies: '@vitest/expect': 3.1.4 @@ -14500,7 +14555,15 @@ snapshots: '@cloudflare/workerd-linux-arm64': 1.20250508.0 '@cloudflare/workerd-windows-64': 1.20250508.0 - wrangler@4.16.1(@cloudflare/workers-types@4.20250520.0): + workerd@1.20250523.0: + optionalDependencies: + '@cloudflare/workerd-darwin-64': 1.20250523.0 + '@cloudflare/workerd-darwin-arm64': 1.20250523.0 + '@cloudflare/workerd-linux-64': 1.20250523.0 + '@cloudflare/workerd-linux-arm64': 1.20250523.0 + '@cloudflare/workerd-windows-64': 1.20250523.0 + + wrangler@4.16.1: dependencies: '@cloudflare/kv-asset-handler': 0.4.0 '@cloudflare/unenv-preset': 2.3.2(unenv@2.0.0-rc.17)(workerd@1.20250508.0) @@ -14511,13 +14574,28 @@ snapshots: unenv: 2.0.0-rc.17 workerd: 1.20250508.0 optionalDependencies: - '@cloudflare/workers-types': 4.20250520.0 fsevents: 2.3.3 sharp: 0.33.5 transitivePeerDependencies: - bufferutil - utf-8-validate + wrangler@4.17.0: + dependencies: + '@cloudflare/kv-asset-handler': 0.4.0 + '@cloudflare/unenv-preset': 2.3.2(unenv@2.0.0-rc.17)(workerd@1.20250523.0) + blake3-wasm: 2.1.5 + esbuild: 0.25.4 + miniflare: 4.20250523.0 + path-to-regexp: 6.3.0 + unenv: 2.0.0-rc.17 + workerd: 1.20250523.0 + optionalDependencies: + fsevents: 2.3.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 diff --git a/schemas/link.ts b/schemas/link.ts index c27397e..f9a31c7 100644 --- a/schemas/link.ts +++ b/schemas/link.ts @@ -16,7 +16,7 @@ export const LinkSchema = z.object({ updatedAt: z.number().int().safe().default(() => Math.floor(Date.now() / 1000)), expiration: z.number().int().safe().refine(expiration => expiration > Math.floor(Date.now() / 1000), { message: 'expiration must be greater than current time', - path: ['expiration'], // 这里指定错误消息关联到哪个字段 + path: ['expiration'], }).optional(), title: z.string().trim().max(2048).optional(), description: z.string().trim().max(2048).optional(), diff --git a/server/api/location.ts b/server/api/location.ts index 507f061..27d2507 100644 --- a/server/api/location.ts +++ b/server/api/location.ts @@ -12,7 +12,7 @@ defineRouteMeta({ export default eventHandler((event) => { const { request: { cf } } = event.context.cloudflare return { - latitude: cf.latitude, - longitude: cf.longitude, + latitude: cf?.latitude, + longitude: cf?.longitude, } }) diff --git a/tests/api/link.spec.ts b/tests/api/link.spec.ts new file mode 100644 index 0000000..9b67dcf --- /dev/null +++ b/tests/api/link.spec.ts @@ -0,0 +1,227 @@ +import { generateMock } from '@anatine/zod-mock' +import { describe, expect, it } from 'vitest' +import { z } from 'zod' +import { fetch, fetchWithAuth } from '../utils' + +const testLinkPayload = generateMock(z.object({ + url: z.string().url(), + slug: z.string().min(1).max(50), +})) + +describe('/api/link/ai', () => { + // it('generates AI Slug for valid URL', async () => { + // const response = await fetchWithAuth(`/api/link/ai?url=${encodeURIComponent('https://sink.cool')}`) + + // expect(response.status).toBe(200) + + // const data = await response.json() + // console.log(data) + // }) + + it('returns 401 when accessing without auth', async () => { + const response = await fetch('/api/link/ai') + + expect(response.status).toBe(401) + }) +}) + +describe.sequential('/api/link/create', () => { + it('creates new link with valid data', async () => { + const response = await fetchWithAuth('/api/link/create', { + method: 'POST', + body: JSON.stringify(testLinkPayload), + headers: { + 'Content-Type': 'application/json', + }, + }) + + expect(response.status).toBe(201) + + const data = await response.json() + expect(data.link).toBeDefined() + expect(data.link.url).toBe(testLinkPayload.url) + expect(data.link.slug).toBe(testLinkPayload.slug) + expect(data.shortLink).toContain(testLinkPayload.slug) + }) + + it('returns 409 when slug already exists', async () => { + const payload = generateMock(z.object({ + url: z.string().url(), + slug: z.string().min(1).max(50), + })) + + await fetchWithAuth('/api/link/create', { + method: 'POST', + body: JSON.stringify(payload), + headers: { + 'Content-Type': 'application/json', + }, + }) + + const duplicateResponse = await fetchWithAuth('/api/link/create', { + method: 'POST', + body: JSON.stringify(payload), + headers: { + 'Content-Type': 'application/json', + }, + }) + + expect(duplicateResponse.status).toBe(409) + }) + + it('returns 401 when accessing without auth', async () => { + const response = await fetch('/api/link/create', { + method: 'POST', + body: JSON.stringify({}), + headers: { + 'Content-Type': 'application/json', + }, + }) + + expect(response.status).toBe(401) + }) +}) + +describe.sequential('/api/link/upsert', () => { + it('creates new link with valid data', async () => { + const payload = generateMock(z.object({ + url: z.string().url(), + slug: z.string().min(1).max(50), + })) + + const response = await fetchWithAuth('/api/link/upsert', { + method: 'POST', + body: JSON.stringify(payload), + headers: { + 'Content-Type': 'application/json', + }, + }) + + expect(response.status).toBe(201) + }) + + it('updates existing link with valid data', async () => { + const response = await fetchWithAuth('/api/link/upsert', { + method: 'POST', + body: JSON.stringify(testLinkPayload), + headers: { + 'Content-Type': 'application/json', + }, + }) + + expect(response.status).toBe(200) + }) + + it('returns 401 when accessing without auth', async () => { + const response = await fetch('/api/link/upsert', { + method: 'POST', + body: JSON.stringify({}), + headers: { + 'Content-Type': 'application/json', + }, + }) + + expect(response.status).toBe(401) + }) +}) + +describe.sequential('/api/link/query', () => { + it('returns link data for valid slug', async () => { + const response = await fetchWithAuth(`/api/link/query?slug=${testLinkPayload.slug}`) + + expect(response.status).toBe(200) + }) + + it('returns 401 when accessing without auth', async () => { + const response = await fetch(`/api/link/query?slug=${testLinkPayload.slug}`) + + expect(response.status).toBe(401) + }) +}) + +describe.sequential('/api/link/list', () => { + it('returns paginated link list with valid auth', async () => { + const response = await fetchWithAuth('/api/link/list') + + expect(response.status).toBe(200) + + const data = await response.json() + expect(data).toHaveProperty('links') + expect(data.links).toBeInstanceOf(Array) + }) + + it('returns 401 when accessing without auth', async () => { + const response = await fetch('/api/link/list') + + expect(response.status).toBe(401) + }) +}) + +describe.sequential('/api/link/search', () => { + it('returns link array with valid auth', async () => { + const response = await fetchWithAuth('/api/link/search') + + expect(response.status).toBe(200) + + const data = await response.json() + expect(data).toBeInstanceOf(Array) + }) + + it('returns 401 when accessing without auth', async () => { + const response = await fetch('/api/link/search') + + expect(response.status).toBe(401) + }) +}) + +describe.sequential('/api/link/edit', () => { + it('updates existing link with valid data', async () => { + const response = await fetchWithAuth('/api/link/edit', { + method: 'PUT', + body: JSON.stringify(testLinkPayload), + headers: { + 'Content-Type': 'application/json', + }, + }) + + expect(response.status).toBe(201) + }) + + it('returns 401 when accessing without auth', async () => { + const response = await fetch('/api/link/edit', { + method: 'PUT', + body: JSON.stringify({}), + headers: { + 'Content-Type': 'application/json', + }, + }) + + expect(response.status).toBe(401) + }) +}) + +describe.sequential('/api/link/delete', () => { + it('deletes link with valid slug and auth', async () => { + const response = await fetchWithAuth('/api/link/delete', { + method: 'POST', + body: JSON.stringify({ slug: testLinkPayload.slug }), + headers: { + 'Content-Type': 'application/json', + }, + }) + + expect(response.status).toBe(204) + }) + + it('returns 401 when accessing without auth', async () => { + const response = await fetch('/api/link/delete', { + method: 'POST', + body: JSON.stringify({}), + headers: { + 'Content-Type': 'application/json', + }, + }) + + expect(response.status).toBe(401) + }) +}) diff --git a/tests/api/location.spec.ts b/tests/api/location.spec.ts new file mode 100644 index 0000000..9ed11e0 --- /dev/null +++ b/tests/api/location.spec.ts @@ -0,0 +1,23 @@ +import { describe, expect, it } from 'vitest' +import { fetch, fetchWithAuth } from '../utils' + +describe('/api/location', () => { + it('returns location data with valid auth', async () => { + const response = await fetchWithAuth('/api/location') + + expect(response.status).toBe(200) + + // TODO: request.cf mock + // const data = await response.json() + // expect(data.latitude).toBeTypeOf('string') + // expect(data.longitude).toBeTypeOf('string') + // expect(Number(data.latitude)).not.toBeNaN() + // expect(Number(data.longitude)).not.toBeNaN() + }) + + it('returns 401 when accessing without auth', async () => { + const response = await fetch('/api/location') + + expect(response.status).toBe(401) + }) +}) diff --git a/tests/api/verify.spec.ts b/tests/api/verify.spec.ts new file mode 100644 index 0000000..80ab4e7 --- /dev/null +++ b/tests/api/verify.spec.ts @@ -0,0 +1,18 @@ +import { describe, expect, it } from 'vitest' +import { fetch, fetchWithAuth } from '../utils' + +describe('/api/verify', () => { + it('returns user data with valid auth', async () => { + const response = await fetchWithAuth('/api/verify') + const data = await response.json() + + expect(data.name).toBeTypeOf('string') + expect(data.url).toBeTypeOf('string') + }) + + it('returns 401 when accessing without auth', async () => { + const response = await fetch('/api/verify') + + expect(response.status).toBe(401) + }) +}) diff --git a/tests/config.ts b/tests/config.ts deleted file mode 100644 index 2e93997..0000000 --- a/tests/config.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const config = { - host: 'http://localhost:8888', -} diff --git a/tests/setup.ts b/tests/setup.ts new file mode 100644 index 0000000..1e13aab --- /dev/null +++ b/tests/setup.ts @@ -0,0 +1,9 @@ +import fs from 'node:fs' + +export default function () { + fs.copyFileSync('.env', '.dev.vars') + + return () => { + fs.rmSync('.dev.vars') + } +} diff --git a/tests/sink.spec.ts b/tests/sink.spec.ts index a880faf..90bfc33 100644 --- a/tests/sink.spec.ts +++ b/tests/sink.spec.ts @@ -1,11 +1,8 @@ -import { fetch, setup } from '@nuxt/test-utils/e2e' import { describe, expect, it } from 'vitest' -import { config } from './config' +import { fetch } from './utils' -describe('sink test', async () => { - await setup(config) - - it('home page should return 200', async () => { +describe('/', () => { + it('returns 200 for homepage request', async () => { const response = await fetch('/') expect(response.status).toBe(200) }) diff --git a/tests/utils.ts b/tests/utils.ts new file mode 100644 index 0000000..d84cf5e --- /dev/null +++ b/tests/utils.ts @@ -0,0 +1,15 @@ +import { SELF } from 'cloudflare:test' + +export async function fetchWithAuth(path: string, options?: RequestInit) { + return SELF.fetch(`http://localhost${path}`, { + ...options, + headers: { + ...options?.headers, + Authorization: `Bearer ${import.meta.env.NUXT_SITE_TOKEN}`, + }, + }) +} + +export async function fetch(path: string, options?: RequestInit) { + return SELF.fetch(`http://localhost${path}`, options) +} diff --git a/tsconfig.json b/tsconfig.json index a746f2a..eb53c33 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,4 +1,7 @@ { // https://nuxt.com/docs/guide/concepts/typescript - "extends": "./.nuxt/tsconfig.json" + "extends": "./.nuxt/tsconfig.json", + "compilerOptions": { + "types": ["@cloudflare/vitest-pool-workers"] + } } diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..dd8c7bb --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,21 @@ +import { defineWorkersConfig } from '@cloudflare/vitest-pool-workers/config' +import { loadEnv } from 'vite' + +export default defineWorkersConfig(({ mode }) => ({ + test: { + env: loadEnv(mode, process.cwd(), ''), + globalSetup: './tests/setup.ts', + poolOptions: { + workers: { + singleWorker: true, + isolatedStorage: false, + wrangler: { + configPath: './wrangler.jsonc', + }, + miniflare: { + cf: true, + }, + }, + }, + }, +}))