feat(test): migrate to cloudflare worker test suite

Modernizes testing infrastructure with dedicated Cloudflare Workers test pool:

- Replaces Nuxt Test Utils with @cloudflare/vitest-pool-workers
- Adds structured test organization for API endpoints
- Introduces comprehensive testing patterns and examples
- Implements schema-based test data generation with zod-mock
- Enhances test utilities with proper CF Workers environment

The new testing setup provides better isolation, more realistic worker runtime environment, and improved test data management through structured schemas and generators.
This commit is contained in:
ccbikai 2025-05-29 20:54:44 +08:00
parent a6ea1391f2
commit 5f9cd9675d
15 changed files with 993 additions and 359 deletions

View file

@ -3,320 +3,563 @@ description:
globs: globs:
alwaysApply: false alwaysApply: false
--- ---
# Unit Testing Guidelines # Testing Guidelines for Sink
## Overview ## 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 ## Testing Scope
### What to Test ### What We Test
- **API Endpoints**: All routes in [server/api/](mdc:server/api/) - **API Endpoints**: All routes in [server/api/](mdc:server/api/)
- Link management APIs in [server/api/link/](mdc:server/api/link/) - 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)
- Statistics APIs in [server/api/stats/](mdc:server/api/stats/) - 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)
- Authentication endpoints like [verify.ts](mdc:server/api/verify.ts) - Advanced features: [upsert.post.ts](mdc:server/api/link/upsert.post.ts), [ai.get.ts](mdc:server/api/link/ai.get.ts)
- Location services like [location.ts](mdc:server/api/location.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)
- **Input Validation**: Zod schema validation - Utility endpoints: [location.ts](mdc:server/api/location.ts), [verify.ts](mdc:server/api/verify.ts)
- **Error Handling**: Error responses and status codes - **Authentication**: Bearer token validation and authorization flows
- **Business Logic**: Core functionality and edge cases - **Input Validation**: Zod schema validation for all API inputs
- **Authentication**: Token validation and authorization - **Error Handling**: HTTP status codes and error response structures
- **Business Logic**: URL shortening, analytics, and link management features
### What NOT to Test ### What We Don't Test
- Frontend components (Vue components, pages, layouts) - Frontend components, pages, or layouts (Vue.js application layer)
- Third-party library internals - Third-party service integrations (external API dependencies)
- Cloudflare Workers runtime specifics - Cloudflare Workers runtime internals
- Database connection details - 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/ tests/
├── api/ ├── api/
│ ├── link/ │ ├── link/
│ │ ├── create.test.ts │ │ ├── create.test.ts # POST /api/link/create
│ │ ├── edit.test.ts │ │ ├── edit.test.ts # PUT /api/link/edit
│ │ ├── delete.test.ts │ │ ├── delete.test.ts # POST /api/link/delete
│ │ ├── list.test.ts │ │ ├── list.test.ts # GET /api/link/list
│ │ ├── query.test.ts │ │ ├── query.test.ts # GET /api/link/query
│ │ ├── search.test.ts │ │ ├── search.test.ts # GET /api/link/search
│ │ ├── upsert.test.ts │ │ ├── upsert.test.ts # POST /api/link/upsert
│ │ └── ai.test.ts │ │ └── ai.test.ts # GET /api/link/ai
│ ├── stats/ │ ├── stats/
│ │ ├── views.test.ts │ │ ├── views.test.ts # GET /api/stats/views
│ │ ├── counters.test.ts │ │ ├── counters.test.ts # GET /api/stats/counters
│ │ └── metrics.test.ts │ │ └── metrics.test.ts # GET /api/stats/metrics
│ ├── location.test.ts │ ├── location.test.ts # GET /api/location
│ ├── verify.test.ts │ ├── verify.test.ts # GET /api/verify
│ └── helpers/ │ └── helpers/
│ ├── fixtures.ts │ ├── fixtures.ts # Test data fixtures
│ ├── mocks.ts │ ├── mocks.ts # Mock implementations
│ └── test-utils.ts │ └── assertions.ts # Custom assertion helpers
└── sink.spec.ts (existing e2e test) ├── 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 ## Testing Patterns
### API Endpoint Testing Template ### Standard API Test Template
```typescript ```typescript
import { describe, it, expect, beforeEach, vi } from 'vitest' import { describe, it, expect } from 'vitest'
import { $fetch, setup } from '@nuxt/test-utils/e2e' 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', () => { describe('/api/link/create', () => {
beforeEach(async () => { it('creates a new link with valid data', async () => {
await setup({ const payload = generateMock(linkSchema)
// Test configuration
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('returns 409 when slug already exists', async () => {
it('should create a new link with valid data', async () => { const payload = generateMock(linkSchema)
const payload = {
url: 'https://example.com',
slug: 'test-slug'
}
const response = await $fetch('/api/link/create', { // Create the link first
method: 'POST', await fetchWithAuth('/api/link/create', {
body: payload method: 'POST',
}) body: JSON.stringify(payload),
headers: { 'Content-Type': 'application/json' },
expect(response.success).toBe(true)
expect(response.data).toHaveProperty('slug', 'test-slug')
expect(response.data).toHaveProperty('url', 'https://example.com')
}) })
it('should return validation error for invalid URL', async () => { // Attempt to create duplicate
const payload = { const duplicateResponse = await fetchWithAuth('/api/link/create', {
url: 'invalid-url', method: 'POST',
slug: 'test-slug' body: JSON.stringify(payload),
} headers: { 'Content-Type': 'application/json' },
await expect($fetch('/api/link/create', {
method: 'POST',
body: payload
})).rejects.toThrow()
}) })
it('should handle missing required fields', async () => { expect(duplicateResponse.status).toBe(409)
const payload = {
slug: 'test-slug'
// Missing URL
}
await expect($fetch('/api/link/create', {
method: 'POST',
body: payload
})).rejects.toThrow()
})
}) })
describe('Authentication', () => { it('returns 401 without authentication', async () => {
it('should require valid authentication token', async () => { const payload = generateMock(linkSchema)
const payload = {
url: 'https://example.com',
slug: 'test-slug'
}
await expect($fetch('/api/link/create', { const response = await fetch('/api/link/create', {
method: 'POST', method: 'POST',
body: payload, body: JSON.stringify(payload),
headers: { headers: { 'Content-Type': 'application/json' },
Authorization: 'Bearer invalid-token'
}
})).rejects.toThrow()
}) })
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 ```typescript
import { z } from 'zod' import { z } from 'zod'
import { describe, it, expect } from 'vitest'
describe('Link creation validation', () => { describe('Link validation', () => {
const createLinkSchema = z.object({ const createLinkSchema = z.object({
url: z.string().url(), url: z.string().url(),
slug: z.string().min(1).max(50).optional(), slug: z.string().min(1).max(50).optional(),
expiresAt: z.string().datetime().optional() expiresAt: z.string().datetime().optional(),
}) })
it('should validate correct link data', () => { it('accepts valid URL formats', () => {
const validData = { 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', url: 'https://example.com',
slug: 'test-slug' slug: 'valid-slug'
} })).not.toThrow()
expect(() => createLinkSchema.parse(validData)).not.toThrow() // Invalid slugs
}) expect(() => createLinkSchema.parse({
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 = {
url: 'https://example.com', 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 ```typescript
// tests/api/helpers/fixtures.ts describe('/api/stats/views', () => {
export const mockLinkData = { it('returns view statistics for valid slug', async () => {
valid: { // First create a link to get stats for
url: 'https://example.com', const linkPayload = generateMock(linkSchema)
slug: 'test-slug', await fetchWithAuth('/api/link/create', {
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', {
method: 'POST', method: 'POST',
body: 'invalid-json' body: JSON.stringify(linkPayload),
})).rejects.toMatchObject({ headers: { 'Content-Type': 'application/json' },
statusCode: 400
}) })
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 () => { it('handles non-existent slug gracefully', async () => {
await expect($fetch('/api/link/delete', { const response = await fetchWithAuth('/api/stats/views?slug=non-existent')
method: 'POST',
body: { slug: 'test-slug' }
})).rejects.toMatchObject({
statusCode: 401
})
})
it('should return 404 for non-existent resources', async () => { // Should either return 404 or empty stats
await expect($fetch('/api/link/query', { expect([200, 404]).toContain(response.status)
query: { slug: 'non-existent-slug' }
})).rejects.toMatchObject({ if (response.status === 200) {
statusCode: 404 const data = await response.json()
}) expect(data.total).toBe(0)
expect(data.unique).toBe(0)
}
}) })
}) })
``` ```
## Test Configuration ## Test Configuration
### Vitest Setup ### Vitest Configuration
Ensure your `vitest.config.ts` includes: The [vitest.config.ts](mdc:vitest.config.ts) uses Cloudflare Workers pool:
```typescript
import { defineVitestConfig } from '@nuxt/test-utils/config'
export default defineVitestConfig({ ```typescript
import { defineWorkersConfig } from '@cloudflare/vitest-pool-workers/config'
import { loadEnv } from 'vite'
export default defineWorkersConfig(({ mode }) => ({
test: { test: {
environment: 'nuxt', env: loadEnv(mode, process.cwd(), ''),
include: ['tests/**/*.{test,spec}.ts'], globalSetup: './tests/setup.ts',
coverage: { poolOptions: {
provider: 'v8', workers: {
include: ['server/api/**/*.ts'], singleWorker: true,
exclude: ['**/*.d.ts', '**/*.config.*'] 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 ### Rate Limiting and Performance
Set up test-specific environment variables:
```typescript ```typescript
// tests/setup.ts describe('Performance and limits', () => {
import { vi } from 'vitest' 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 const responses = await Promise.all(requests)
vi.stubEnv('SITE_TOKEN', 'test-token')
vi.stubEnv('KV_NAMESPACE', 'test-kv') // All requests should succeed
responses.forEach(response => {
expect(response.status).toBe(201)
})
})
})
``` ```
## Best Practices ## Best Practices
### Test Independence ### Test Organization
- Each test should be independent and not rely on other tests - **Sequential Testing**: Use `describe.sequential()` for tests that modify shared state
- Use `beforeEach` to set up clean state - **Isolation**: Each test should be independent and not rely on others
- Clean up after tests when necessary - **Cleanup**: Clean up created resources when necessary
- **Descriptive Names**: Use clear, behavior-focused test descriptions
### Test Data ### Data Management
- Use meaningful test data that reflects real-world scenarios - **Mock Generation**: Use `@anatine/zod-mock` for schema-consistent test data
- Create reusable fixtures for common test data - **Realistic Data**: Generate realistic URLs, slugs, and user inputs
- Avoid hardcoded values; use constants - **Edge Cases**: Test boundary conditions, empty values, and extreme inputs
- **State Management**: Be mindful of shared KV storage in sequential tests
### Assertions ### Performance Considerations
- Test both positive and negative cases - **Focused Tests**: Keep tests fast and focused on specific functionality
- Verify response structure and data types - **Parallel Execution**: Use parallel execution where state doesn't conflict
- Check error messages and status codes - **Resource Usage**: Monitor test execution time and resource consumption
- **Mock External Services**: Avoid real external API calls in tests
### Performance ### Assertion Strategies
- Keep tests fast and focused - **Comprehensive Checks**: Verify response status, structure, and data
- Use mocking to avoid external dependencies - **Type Safety**: Ensure response data matches expected TypeScript types
- Group related tests using `describe` blocks - **Error Messages**: Test error response content and structure
- **Business Logic**: Verify core URL shortening and analytics logic
### 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
## Running Tests ## Running Tests
### Commands ### Available Commands
```bash ```bash
# Run all tests # Run all tests
npm run test npm run test
# Run tests in watch mode # Run tests in watch mode (for development)
npm run test:watch npm run test -- --watch
# Run tests with coverage
npm run test:coverage
# Run specific test file # 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 # Run tests matching pattern
npm run test -- --grep "authentication" 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 ### CI/CD Integration
- Tests should run on every pull request - Tests run automatically on every pull request
- Enforce minimum coverage thresholds - Enforce test passage before merging
- Block merges if tests fail - 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

1
.gitignore vendored
View file

@ -26,3 +26,4 @@ node_modules
site site
cache cache
public/world.json public/world.json
.dev.*

View file

@ -20,7 +20,7 @@
"deploy": "npm run deploy:pages", "deploy": "npm run deploy:pages",
"deploy:pages": "wrangler pages deploy dist", "deploy:pages": "wrangler pages deploy dist",
"deploy:worker": "wrangler deploy", "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" "test": "vitest"
}, },
"dependencies": { "dependencies": {
@ -52,10 +52,12 @@
"zod": "^3.25.20" "zod": "^3.25.20"
}, },
"devDependencies": { "devDependencies": {
"@anatine/zod-mock": "^3.14.0",
"@antfu/eslint-config": "^4.13.2", "@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": "^1.4.1",
"@nuxt/eslint-config": "^1.4.1", "@nuxt/eslint-config": "^1.4.1",
"@nuxt/test-utils": "^3.19.1",
"@nuxthub/core": "^0.8.27", "@nuxthub/core": "^0.8.27",
"@nuxtjs/color-mode": "^3.5.2", "@nuxtjs/color-mode": "^3.5.2",
"@nuxtjs/i18n": "^9.5.4", "@nuxtjs/i18n": "^9.5.4",

View file

@ -87,18 +87,24 @@ importers:
specifier: ^3.25.20 specifier: ^3.25.20
version: 3.25.20 version: 3.25.20
devDependencies: 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': '@antfu/eslint-config':
specifier: ^4.13.2 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)) 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': '@nuxt/eslint':
specifier: ^1.4.1 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)) 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': '@nuxt/eslint-config':
specifier: ^1.4.1 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) 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': '@nuxthub/core':
specifier: ^0.8.27 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)) 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) version: 2.2.10(typescript@5.7.2)
wrangler: wrangler:
specifier: ^4.16.1 specifier: ^4.16.1
version: 4.16.1(@cloudflare/workers-types@4.20250520.0) version: 4.16.1
packages: packages:
@ -161,6 +167,12 @@ packages:
resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
engines: {node: '>=6.0.0'} 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': '@antfu/eslint-config@4.13.2':
resolution: {integrity: sha512-F+IVIQUCfw6eW4H06c9a9USJ3UOnoBx4I0qsTL3kO6GcyJB6mwk+nawFf95DfHKT3fJKv58YPPz0XCmsY/w0XA==} resolution: {integrity: sha512-F+IVIQUCfw6eW4H06c9a9USJ3UOnoBx4I0qsTL3kO6GcyJB6mwk+nawFf95DfHKT3fJKv58YPPz0XCmsY/w0XA==}
hasBin: true hasBin: true
@ -353,36 +365,73 @@ packages:
workerd: workerd:
optional: true 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': '@cloudflare/workerd-darwin-64@1.20250508.0':
resolution: {integrity: sha512-9x09MrA9Y5RQs3zqWvWns8xHgM2pVNXWpeJ+3hQYu4PrwPFZXtTD6b/iMmOnlYKzINlREq1RGeEybMFyWEUlUg==} resolution: {integrity: sha512-9x09MrA9Y5RQs3zqWvWns8xHgM2pVNXWpeJ+3hQYu4PrwPFZXtTD6b/iMmOnlYKzINlREq1RGeEybMFyWEUlUg==}
engines: {node: '>=16'} engines: {node: '>=16'}
cpu: [x64] cpu: [x64]
os: [darwin] 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': '@cloudflare/workerd-darwin-arm64@1.20250508.0':
resolution: {integrity: sha512-0Ili+nE2LLRzYue/yPc1pepSyNNg6LxR3/ng/rlQzVQUxPXIXldHFkJ/ynsYwQnAcf6OxasSi/kbTm6yvDoSAQ==} resolution: {integrity: sha512-0Ili+nE2LLRzYue/yPc1pepSyNNg6LxR3/ng/rlQzVQUxPXIXldHFkJ/ynsYwQnAcf6OxasSi/kbTm6yvDoSAQ==}
engines: {node: '>=16'} engines: {node: '>=16'}
cpu: [arm64] cpu: [arm64]
os: [darwin] 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': '@cloudflare/workerd-linux-64@1.20250508.0':
resolution: {integrity: sha512-5saVrZ3uVwYxvBa7BaonXjeqB6X0YF3ak05qvBaWcmZ3FNmnarMm2W8842cnbhnckDVBpB/iDo51Sy6Y7y1jcw==} resolution: {integrity: sha512-5saVrZ3uVwYxvBa7BaonXjeqB6X0YF3ak05qvBaWcmZ3FNmnarMm2W8842cnbhnckDVBpB/iDo51Sy6Y7y1jcw==}
engines: {node: '>=16'} engines: {node: '>=16'}
cpu: [x64] cpu: [x64]
os: [linux] 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': '@cloudflare/workerd-linux-arm64@1.20250508.0':
resolution: {integrity: sha512-muQe1pkxRi3eaq1Q417xvfGd2SlktbLTzNhT5Yftsx8OecWrYuB8i4ttR6Nr5ER06bfEj0FqQjqJJhcp6wLLUQ==} resolution: {integrity: sha512-muQe1pkxRi3eaq1Q417xvfGd2SlktbLTzNhT5Yftsx8OecWrYuB8i4ttR6Nr5ER06bfEj0FqQjqJJhcp6wLLUQ==}
engines: {node: '>=16'} engines: {node: '>=16'}
cpu: [arm64] cpu: [arm64]
os: [linux] 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': '@cloudflare/workerd-windows-64@1.20250508.0':
resolution: {integrity: sha512-EJj8iTWFMqjgvZUxxNvzK7frA1JMFi3y/9eDIdZPL/OaQh3cmk5Lai5DCXsKYUxfooMBZWYTp53zOLrvuJI8VQ==} resolution: {integrity: sha512-EJj8iTWFMqjgvZUxxNvzK7frA1JMFi3y/9eDIdZPL/OaQh3cmk5Lai5DCXsKYUxfooMBZWYTp53zOLrvuJI8VQ==}
engines: {node: '>=16'} engines: {node: '>=16'}
cpu: [x64] cpu: [x64]
os: [win32] 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': '@cloudflare/workers-types@4.20250520.0':
resolution: {integrity: sha512-bMPrpZREctlSaKtqIp3mdRjgyRSgioPQDUlhOeCUYRIo9DzWUACvw/u5kq3GQMmE4V1/nVCGHb7a1Ie8f5wG5g==} resolution: {integrity: sha512-bMPrpZREctlSaKtqIp3mdRjgyRSgioPQDUlhOeCUYRIo9DzWUACvw/u5kq3GQMmE4V1/nVCGHb7a1Ie8f5wG5g==}
@ -681,6 +730,10 @@ packages:
resolution: {integrity: sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==} resolution: {integrity: sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 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': '@fastify/busboy@2.1.1':
resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==}
engines: {node: '>=14'} engines: {node: '>=14'}
@ -1149,42 +1202,6 @@ packages:
engines: {node: '>=18.12.0'} engines: {node: '>=18.12.0'}
hasBin: true 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': '@nuxt/vite-builder@3.17.4':
resolution: {integrity: sha512-MRcGe02nEDpu+MnRJcmgVfHdzgt9tWvxVdJbhfd6oyX19plw/CANjgHedlpUNUxqeWXC6CQfGvoVJXn3bQlEqA==} resolution: {integrity: sha512-MRcGe02nEDpu+MnRJcmgVfHdzgt9tWvxVdJbhfd6oyX19plw/CANjgHedlpUNUxqeWXC6CQfGvoVJXn3bQlEqA==}
engines: {node: ^18.12.0 || ^20.9.0 || >=22.0.0} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0.0}
@ -2560,6 +2577,9 @@ packages:
bindings@1.5.0: bindings@1.5.0:
resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==}
birpc@0.2.14:
resolution: {integrity: sha512-37FHE8rqsYM5JEKCnXFyHpBCzvgHEExwVVTq+nUmloInU7l8ezD1TpOhKpS8oe1DTYFqEK27rFZVKG43oTqXRA==}
birpc@2.3.0: birpc@2.3.0:
resolution: {integrity: sha512-ijbtkn/F3Pvzb6jHypHRyve2QApOCZDR25D/VnkY2G/lBNcXCTsnsCxgY4k4PkVB7zfwzYbY3O9Lcqe3xufS5g==} resolution: {integrity: sha512-ijbtkn/F3Pvzb6jHypHRyve2QApOCZDR25D/VnkY2G/lBNcXCTsnsCxgY4k4PkVB7zfwzYbY3O9Lcqe3xufS5g==}
@ -2707,6 +2727,9 @@ packages:
citty@0.1.6: citty@0.1.6:
resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} 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: class-variance-authority@0.7.1:
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
@ -3306,6 +3329,9 @@ packages:
resolution: {integrity: sha512-ARFxjzizOhPqs1fYC/2NMC3N4jrQ6HvVflnXBTRqNEqJuXwyKLRr9CrJwkRcV/SnZt1sNXgsF6FPm0x57Tq0rw==} resolution: {integrity: sha512-ARFxjzizOhPqs1fYC/2NMC3N4jrQ6HvVflnXBTRqNEqJuXwyKLRr9CrJwkRcV/SnZt1sNXgsF6FPm0x57Tq0rw==}
engines: {node: ^14.14.0 || >=16.0.0} engines: {node: ^14.14.0 || >=16.0.0}
devalue@4.3.3:
resolution: {integrity: sha512-UH8EL6H2ifcY8TbD2QsxwCC/pr5xSwPvv85LrLXVihmHVC3T3YqTCIwnR5ak0yO1KYqlxrPVOA/JVZJYPy2ATg==}
devalue@5.1.1: devalue@5.1.1:
resolution: {integrity: sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==} resolution: {integrity: sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==}
@ -3347,6 +3373,10 @@ packages:
resolution: {integrity: sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==} resolution: {integrity: sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==}
engines: {node: '>=12'} engines: {node: '>=12'}
drange@1.1.1:
resolution: {integrity: sha512-pYxfDYpued//QpnLIm4Avk7rsNtAtQkUES2cwAYSvD/wd2pKD71gN2Ebj3e7klzXwjocvE8c5vx/1fxwpqmSxA==}
engines: {node: '>=4'}
dunder-proto@1.0.1: dunder-proto@1.0.1:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@ -3718,10 +3748,6 @@ packages:
engines: {node: '>= 10.17.0'} engines: {node: '>= 10.17.0'}
hasBin: true 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: fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
@ -4776,6 +4802,11 @@ packages:
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
hasBin: true hasBin: true
miniflare@4.20250523.0:
resolution: {integrity: sha512-g4F1AC5xi66rB2eQNo2Fx7EffaXhMdgUSRl/ivgb4LMALMpxghG98oC4twqVwDLWIFSVFjtL1YEuYrPO8044mg==}
engines: {node: '>=18.0.0'}
hasBin: true
minimatch@3.1.2: minimatch@3.1.2:
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
@ -5578,6 +5609,10 @@ packages:
radix3@1.1.2: radix3@1.1.2:
resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==} 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: randombytes@2.1.0:
resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
@ -5698,6 +5733,10 @@ packages:
resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==}
engines: {node: '>=18'} engines: {node: '>=18'}
ret@0.2.2:
resolution: {integrity: sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==}
engines: {node: '>=4'}
reusify@1.0.4: reusify@1.0.4:
resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==}
engines: {iojs: '>=1.0.0', node: '>=0.10.0'} engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
@ -6597,9 +6636,6 @@ packages:
yaml: yaml:
optional: true optional: true
vitest-environment-nuxt@1.0.1:
resolution: {integrity: sha512-eBCwtIQriXW5/M49FjqNKfnlJYlG2LWMSNFsRVKomc8CaMqmhQPBS5LZ9DlgYL9T8xIVsiA6RZn2lk7vxov3Ow==}
vitest@3.1.4: vitest@3.1.4:
resolution: {integrity: sha512-Ta56rT7uWxCSJXlBtKgIlApJnT6e6IGmTYxYcmxjJ4ujuZDI59GUQgVDObXXJujOmPDBYXHK1qmaGtneu6TNIQ==} resolution: {integrity: sha512-Ta56rT7uWxCSJXlBtKgIlApJnT6e6IGmTYxYcmxjJ4ujuZDI59GUQgVDObXXJujOmPDBYXHK1qmaGtneu6TNIQ==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
@ -6740,6 +6776,11 @@ packages:
engines: {node: '>=16'} engines: {node: '>=16'}
hasBin: true hasBin: true
workerd@1.20250523.0:
resolution: {integrity: sha512-OClsq9ZzZZNdkY8/JTBjf+/A6F1q/SOn3/RQWCR0kDoclxecHS6Nq80jY6NP0ubJBKnqrUggA9WOWBgwWWOGUA==}
engines: {node: '>=16'}
hasBin: true
wrangler@4.16.1: wrangler@4.16.1:
resolution: {integrity: sha512-YiLdWXcaQva2K/bqokpsZbySPmoT8TJFyJPsQPZumnkgimM9+/g/yoXArByA+pf+xU8jhw7ybQ8X1yBGXv731g==} resolution: {integrity: sha512-YiLdWXcaQva2K/bqokpsZbySPmoT8TJFyJPsQPZumnkgimM9+/g/yoXArByA+pf+xU8jhw7ybQ8X1yBGXv731g==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
@ -6750,6 +6791,16 @@ packages:
'@cloudflare/workers-types': '@cloudflare/workers-types':
optional: true 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: wrap-ansi@7.0.0:
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
engines: {node: '>=10'} engines: {node: '>=10'}
@ -6884,6 +6935,12 @@ snapshots:
'@jridgewell/gen-mapping': 0.3.5 '@jridgewell/gen-mapping': 0.3.5
'@jridgewell/trace-mapping': 0.3.25 '@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))': '@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: dependencies:
'@antfu/install-pkg': 1.1.0 '@antfu/install-pkg': 1.1.0
@ -7131,21 +7188,59 @@ snapshots:
optionalDependencies: optionalDependencies:
workerd: 1.20250508.0 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': '@cloudflare/workerd-darwin-64@1.20250508.0':
optional: true optional: true
'@cloudflare/workerd-darwin-64@1.20250523.0':
optional: true
'@cloudflare/workerd-darwin-arm64@1.20250508.0': '@cloudflare/workerd-darwin-arm64@1.20250508.0':
optional: true optional: true
'@cloudflare/workerd-darwin-arm64@1.20250523.0':
optional: true
'@cloudflare/workerd-linux-64@1.20250508.0': '@cloudflare/workerd-linux-64@1.20250508.0':
optional: true optional: true
'@cloudflare/workerd-linux-64@1.20250523.0':
optional: true
'@cloudflare/workerd-linux-arm64@1.20250508.0': '@cloudflare/workerd-linux-arm64@1.20250508.0':
optional: true optional: true
'@cloudflare/workerd-linux-arm64@1.20250523.0':
optional: true
'@cloudflare/workerd-windows-64@1.20250508.0': '@cloudflare/workerd-windows-64@1.20250508.0':
optional: true optional: true
'@cloudflare/workerd-windows-64@1.20250523.0':
optional: true
'@cloudflare/workers-types@4.20250520.0': {} '@cloudflare/workers-types@4.20250520.0': {}
'@colors/colors@1.6.0': {} '@colors/colors@1.6.0': {}
@ -7428,6 +7523,8 @@ snapshots:
'@eslint/core': 0.14.0 '@eslint/core': 0.14.0
levn: 0.4.1 levn: 0.4.1
'@faker-js/faker@9.8.0': {}
'@fastify/busboy@2.1.1': {} '@fastify/busboy@2.1.1': {}
'@fastify/busboy@3.1.1': {} '@fastify/busboy@3.1.1': {}
@ -8091,51 +8188,6 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- magicast - 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)': '@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: dependencies:
'@nuxt/kit': 3.17.4(magicast@0.3.5) '@nuxt/kit': 3.17.4(magicast@0.3.5)
@ -9679,6 +9731,8 @@ snapshots:
dependencies: dependencies:
file-uri-to-path: 1.0.0 file-uri-to-path: 1.0.0
birpc@0.2.14: {}
birpc@2.3.0: {} birpc@2.3.0: {}
bl@4.1.0: bl@4.1.0:
@ -9836,6 +9890,8 @@ snapshots:
dependencies: dependencies:
consola: 3.4.2 consola: 3.4.2
cjs-module-lexer@1.4.3: {}
class-variance-authority@0.7.1: class-variance-authority@0.7.1:
dependencies: dependencies:
clsx: 2.1.1 clsx: 2.1.1
@ -10420,6 +10476,8 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
devalue@4.3.3: {}
devalue@5.1.1: {} devalue@5.1.1: {}
devlop@1.1.0: devlop@1.1.0:
@ -10460,6 +10518,8 @@ snapshots:
dotenv@16.5.0: {} dotenv@16.5.0: {}
drange@1.1.1: {}
dunder-proto@1.0.1: dunder-proto@1.0.1:
dependencies: dependencies:
call-bind-apply-helpers: 1.0.2 call-bind-apply-helpers: 1.0.2
@ -10939,8 +10999,6 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
fake-indexeddb@6.0.1: {}
fast-deep-equal@3.1.3: {} fast-deep-equal@3.1.3: {}
fast-fifo@1.3.2: {} fast-fifo@1.3.2: {}
@ -12216,6 +12274,24 @@ snapshots:
- bufferutil - bufferutil
- utf-8-validate - 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: minimatch@3.1.2:
dependencies: dependencies:
brace-expansion: 1.1.11 brace-expansion: 1.1.11
@ -13185,6 +13261,11 @@ snapshots:
radix3@1.1.2: {} radix3@1.1.2: {}
randexp@0.5.3:
dependencies:
drange: 1.1.1
ret: 0.2.2
randombytes@2.1.0: randombytes@2.1.0:
dependencies: dependencies:
safe-buffer: 5.2.1 safe-buffer: 5.2.1
@ -13332,6 +13413,8 @@ snapshots:
onetime: 7.0.0 onetime: 7.0.0
signal-exit: 4.1.0 signal-exit: 4.1.0
ret@0.2.2: {}
reusify@1.0.4: {} reusify@1.0.4: {}
rfdc@1.4.1: {} rfdc@1.4.1: {}
@ -14303,34 +14386,6 @@ snapshots:
terser: 5.31.0 terser: 5.31.0
yaml: 2.8.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): 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: dependencies:
'@vitest/expect': 3.1.4 '@vitest/expect': 3.1.4
@ -14500,7 +14555,15 @@ snapshots:
'@cloudflare/workerd-linux-arm64': 1.20250508.0 '@cloudflare/workerd-linux-arm64': 1.20250508.0
'@cloudflare/workerd-windows-64': 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: dependencies:
'@cloudflare/kv-asset-handler': 0.4.0 '@cloudflare/kv-asset-handler': 0.4.0
'@cloudflare/unenv-preset': 2.3.2(unenv@2.0.0-rc.17)(workerd@1.20250508.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 unenv: 2.0.0-rc.17
workerd: 1.20250508.0 workerd: 1.20250508.0
optionalDependencies: optionalDependencies:
'@cloudflare/workers-types': 4.20250520.0
fsevents: 2.3.3 fsevents: 2.3.3
sharp: 0.33.5 sharp: 0.33.5
transitivePeerDependencies: transitivePeerDependencies:
- bufferutil - bufferutil
- utf-8-validate - 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: wrap-ansi@7.0.0:
dependencies: dependencies:
ansi-styles: 4.3.0 ansi-styles: 4.3.0

View file

@ -16,7 +16,7 @@ export const LinkSchema = z.object({
updatedAt: z.number().int().safe().default(() => Math.floor(Date.now() / 1000)), updatedAt: z.number().int().safe().default(() => Math.floor(Date.now() / 1000)),
expiration: z.number().int().safe().refine(expiration => expiration > 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', message: 'expiration must be greater than current time',
path: ['expiration'], // 这里指定错误消息关联到哪个字段 path: ['expiration'],
}).optional(), }).optional(),
title: z.string().trim().max(2048).optional(), title: z.string().trim().max(2048).optional(),
description: z.string().trim().max(2048).optional(), description: z.string().trim().max(2048).optional(),

View file

@ -12,7 +12,7 @@ defineRouteMeta({
export default eventHandler((event) => { export default eventHandler((event) => {
const { request: { cf } } = event.context.cloudflare const { request: { cf } } = event.context.cloudflare
return { return {
latitude: cf.latitude, latitude: cf?.latitude,
longitude: cf.longitude, longitude: cf?.longitude,
} }
}) })

227
tests/api/link.spec.ts Normal file
View file

@ -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)
})
})

View file

@ -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)
})
})

18
tests/api/verify.spec.ts Normal file
View file

@ -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)
})
})

View file

@ -1,3 +0,0 @@
export const config = {
host: 'http://localhost:8888',
}

9
tests/setup.ts Normal file
View file

@ -0,0 +1,9 @@
import fs from 'node:fs'
export default function () {
fs.copyFileSync('.env', '.dev.vars')
return () => {
fs.rmSync('.dev.vars')
}
}

View file

@ -1,11 +1,8 @@
import { fetch, setup } from '@nuxt/test-utils/e2e'
import { describe, expect, it } from 'vitest' import { describe, expect, it } from 'vitest'
import { config } from './config' import { fetch } from './utils'
describe('sink test', async () => { describe('/', () => {
await setup(config) it('returns 200 for homepage request', async () => {
it('home page should return 200', async () => {
const response = await fetch('/') const response = await fetch('/')
expect(response.status).toBe(200) expect(response.status).toBe(200)
}) })

15
tests/utils.ts Normal file
View file

@ -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)
}

View file

@ -1,4 +1,7 @@
{ {
// https://nuxt.com/docs/guide/concepts/typescript // https://nuxt.com/docs/guide/concepts/typescript
"extends": "./.nuxt/tsconfig.json" "extends": "./.nuxt/tsconfig.json",
"compilerOptions": {
"types": ["@cloudflare/vitest-pool-workers"]
}
} }

21
vitest.config.ts Normal file
View file

@ -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,
},
},
},
},
}))