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:
parent
a6ea1391f2
commit
5f9cd9675d
15 changed files with 993 additions and 359 deletions
|
|
@ -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
1
.gitignore
vendored
|
|
@ -26,3 +26,4 @@ node_modules
|
||||||
site
|
site
|
||||||
cache
|
cache
|
||||||
public/world.json
|
public/world.json
|
||||||
|
.dev.*
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
326
pnpm-lock.yaml
326
pnpm-lock.yaml
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
|
|
||||||
|
|
@ -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
227
tests/api/link.spec.ts
Normal 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)
|
||||||
|
})
|
||||||
|
})
|
||||||
23
tests/api/location.spec.ts
Normal file
23
tests/api/location.spec.ts
Normal 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
18
tests/api/verify.spec.ts
Normal 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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
export const config = {
|
|
||||||
host: 'http://localhost:8888',
|
|
||||||
}
|
|
||||||
9
tests/setup.ts
Normal file
9
tests/setup.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import fs from 'node:fs'
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
fs.copyFileSync('.env', '.dev.vars')
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
fs.rmSync('.dev.vars')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
15
tests/utils.ts
Normal 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)
|
||||||
|
}
|
||||||
|
|
@ -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
21
vitest.config.ts
Normal 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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}))
|
||||||
Loading…
Reference in a new issue