Merge branch 'test'
This commit is contained in:
commit
ebd69c074f
18 changed files with 1612 additions and 46 deletions
|
|
@ -156,50 +156,238 @@ export async function createLink(url: string, slug?: string): Promise<LinkData>
|
|||
}
|
||||
```
|
||||
|
||||
## Testing Requirements
|
||||
## Testing Standards
|
||||
|
||||
### Type Checking
|
||||
- Resolve all TypeScript errors
|
||||
- Use strict type configuration
|
||||
### Unit Testing Requirements
|
||||
- **Mandatory for all `server/api` endpoints** - Every API route must have comprehensive unit tests
|
||||
- Use Vitest as the primary testing framework with Nuxt Test Utils
|
||||
- Follow the Test-Driven Development (TDD) approach when possible
|
||||
- Maintain test coverage above 80% for API endpoints
|
||||
|
||||
### Code Style
|
||||
- Follow ESLint rules
|
||||
- Use Prettier for code formatting
|
||||
- Automatically run lint-staged on commit
|
||||
### Testing Scope and Strategy
|
||||
- **API Layer**: Test all endpoints in [server/api/](mdc:server/api/) directory
|
||||
- **Validation Logic**: Test Zod schemas and input validation
|
||||
- **Error Handling**: Verify error responses and status codes
|
||||
- **Authentication**: Test token validation and authorization flows
|
||||
- **Business Logic**: Cover edge cases and critical functionality
|
||||
|
||||
### Manual Testing
|
||||
- Test main user flows
|
||||
- Verify API endpoint functionality
|
||||
- Check responsive design
|
||||
### Test File Organization
|
||||
```typescript
|
||||
// File naming convention
|
||||
tests/api/link/create.test.ts // Unit tests for API endpoints
|
||||
tests/api/helpers/fixtures.ts // Shared test data
|
||||
tests/sink.spec.ts // Integration/E2E tests
|
||||
```
|
||||
|
||||
### Testing Best Practices
|
||||
- **Write descriptive test names** that explain the behavior being tested
|
||||
- **Test both happy path and error scenarios**
|
||||
- **Use meaningful assertions** that validate expected behavior
|
||||
- **Keep tests independent** - no test should depend on another test's state
|
||||
- **Mock external dependencies** to ensure test reliability
|
||||
|
||||
```typescript
|
||||
// ✅ Good test structure
|
||||
describe('POST /api/link/create', () => {
|
||||
it('should create a new link with valid URL and custom slug', async () => {
|
||||
const response = await $fetch('/api/link/create', {
|
||||
method: 'POST',
|
||||
body: { url: 'https://example.com', slug: 'my-link' }
|
||||
})
|
||||
|
||||
expect(response.success).toBe(true)
|
||||
expect(response.data.slug).toBe('my-link')
|
||||
expect(response.data.url).toBe('https://example.com')
|
||||
})
|
||||
|
||||
it('should reject requests with invalid URL format', async () => {
|
||||
await expect($fetch('/api/link/create', {
|
||||
method: 'POST',
|
||||
body: { url: 'not-a-valid-url' }
|
||||
})).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### Test Data Management
|
||||
- Use factory functions for creating test data
|
||||
- Store common test fixtures in `tests/api/helpers/fixtures.ts`
|
||||
- Avoid hardcoded values in tests; use constants and variables
|
||||
|
||||
```typescript
|
||||
// ✅ Good test data management
|
||||
const validLinkPayload = {
|
||||
url: 'https://example.com',
|
||||
slug: 'test-slug',
|
||||
expiresAt: new Date(Date.now() + 86400000).toISOString()
|
||||
}
|
||||
|
||||
const invalidUrlPayload = {
|
||||
url: 'invalid-url-format',
|
||||
slug: 'test-slug'
|
||||
}
|
||||
```
|
||||
|
||||
### Running and Maintaining Tests
|
||||
- Run tests locally before pushing changes: `npm run test`
|
||||
- Use watch mode during development: `npm run test:watch`
|
||||
- Generate coverage reports: `npm run test:coverage`
|
||||
- Tests must pass in CI/CD pipeline before merging
|
||||
|
||||
## Git Commit Conventions
|
||||
|
||||
### Commit Message Format
|
||||
Follow the conventional commit specification:
|
||||
```
|
||||
type(scope): description
|
||||
type(scope): brief description
|
||||
|
||||
body (optional)
|
||||
Optional body explaining what and why vs how
|
||||
|
||||
footer (optional)
|
||||
Optional footer with issue references
|
||||
```
|
||||
|
||||
### Commit Types
|
||||
- `feat`: New feature
|
||||
- `fix`: Bug fix
|
||||
- `docs`: Documentation update
|
||||
- `style`: Code style (no functional impact)
|
||||
- `refactor`: Refactoring
|
||||
- `perf`: Performance optimization
|
||||
- `test`: Testing related
|
||||
- `chore`: Build process or auxiliary tool changes
|
||||
- **feat**: New feature implementation
|
||||
- **fix**: Bug fix or issue resolution
|
||||
- **docs**: Documentation updates only
|
||||
- **style**: Code formatting changes (no functional impact)
|
||||
- **refactor**: Code restructuring without changing functionality
|
||||
- **perf**: Performance optimization
|
||||
- **test**: Adding or modifying tests
|
||||
- **chore**: Build process, dependency updates, or auxiliary tool changes
|
||||
- **ci**: Continuous integration configuration changes
|
||||
|
||||
### Example
|
||||
```
|
||||
feat(api): add AI-powered slug generation
|
||||
### Scope Examples
|
||||
- **api**: Changes to server/api endpoints
|
||||
- **ui**: Frontend components or styling
|
||||
- **auth**: Authentication related changes
|
||||
- **db**: Database or data storage changes
|
||||
- **config**: Configuration file updates
|
||||
|
||||
### Writing Good Commit Messages
|
||||
|
||||
#### Good Examples
|
||||
```bash
|
||||
feat(api): add AI-powered slug generation endpoint
|
||||
|
||||
Integrate Cloudflare AI Workers to automatically generate
|
||||
meaningful slugs based on URL content and page titles.
|
||||
|
||||
- Integrate Cloudflare AI for automatic slug creation
|
||||
- Add new /api/link/ai endpoint
|
||||
- Implement content analysis logic
|
||||
- Update link creation flow to support AI suggestions
|
||||
- Add comprehensive test coverage
|
||||
|
||||
Closes #123
|
||||
```
|
||||
|
||||
```bash
|
||||
fix(auth): resolve token validation edge case
|
||||
|
||||
Handle expired tokens more gracefully by returning proper
|
||||
401 status code instead of throwing unhandled exceptions.
|
||||
|
||||
Fixes #456
|
||||
```
|
||||
|
||||
```bash
|
||||
test(api): add comprehensive coverage for link endpoints
|
||||
|
||||
- Add unit tests for create, edit, delete operations
|
||||
- Include validation error scenarios
|
||||
- Test authentication requirements
|
||||
- Achieve 85% coverage for link management APIs
|
||||
```
|
||||
|
||||
#### Avoid These Patterns
|
||||
```bash
|
||||
# ❌ Too vague
|
||||
fix: stuff
|
||||
|
||||
# ❌ Not descriptive
|
||||
update code
|
||||
|
||||
# ❌ Multiple concerns in one commit
|
||||
feat: add new API endpoint and fix UI bug and update docs
|
||||
|
||||
# ❌ All caps or inconsistent formatting
|
||||
FIX: BROKEN AUTH SYSTEM
|
||||
```
|
||||
|
||||
### Commit Best Practices
|
||||
|
||||
#### Single Responsibility
|
||||
Each commit should represent one logical change:
|
||||
- One feature addition
|
||||
- One bug fix
|
||||
- One refactoring operation
|
||||
- One documentation update
|
||||
|
||||
#### Atomic Commits
|
||||
Commits should be complete and functional:
|
||||
- Code compiles without errors
|
||||
- Tests pass
|
||||
- No broken functionality introduced
|
||||
|
||||
#### Meaningful Descriptions
|
||||
- Use imperative mood ("add feature" not "added feature")
|
||||
- Capitalize the first letter
|
||||
- No period at the end of the subject line
|
||||
- Keep subject line under 50 characters
|
||||
- Use body for detailed explanations when needed
|
||||
|
||||
### Branch Naming Conventions
|
||||
```bash
|
||||
# Feature branches
|
||||
feature/ai-slug-generation
|
||||
feature/link-expiration
|
||||
|
||||
# Bug fix branches
|
||||
fix/auth-token-validation
|
||||
fix/mobile-responsive-layout
|
||||
|
||||
# Documentation branches
|
||||
docs/api-endpoint-guide
|
||||
docs/deployment-instructions
|
||||
|
||||
# Hotfix branches
|
||||
hotfix/security-vulnerability
|
||||
hotfix/production-crash
|
||||
```
|
||||
|
||||
### Pull Request Guidelines
|
||||
|
||||
#### PR Title Format
|
||||
Use the same format as commit messages:
|
||||
```
|
||||
feat(api): add AI-powered slug generation
|
||||
fix(auth): resolve token validation edge case
|
||||
docs(readme): update installation instructions
|
||||
```
|
||||
|
||||
#### PR Description Template
|
||||
```markdown
|
||||
## Summary
|
||||
Brief description of changes made
|
||||
|
||||
## Type of Change
|
||||
- [ ] Bug fix
|
||||
- [ ] New feature
|
||||
- [ ] Documentation update
|
||||
- [ ] Refactoring
|
||||
- [ ] Performance improvement
|
||||
|
||||
## Testing
|
||||
- [ ] Unit tests added/updated
|
||||
- [ ] Integration tests pass
|
||||
- [ ] Manual testing completed
|
||||
|
||||
## Checklist
|
||||
- [ ] Code follows project style guidelines
|
||||
- [ ] Self-review completed
|
||||
- [ ] Documentation updated if needed
|
||||
- [ ] Tests cover new functionality
|
||||
- [ ] No breaking changes introduced
|
||||
```
|
||||
|
||||
This standardized approach ensures clean, readable commit history and makes it easier to track changes, generate changelogs, and collaborate effectively with team members.
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ This is a full-stack application built with Nuxt.js, utilizing the following tec
|
|||
- **Database**: Cloudflare Workers KV
|
||||
- **Analytics Engine**: Cloudflare Workers Analytics Engine
|
||||
- **Deployment Platform**: Cloudflare Workers/Pages
|
||||
- **Testing Framework**: Vitest with Nuxt Test Utils
|
||||
- **Type Safety**: TypeScript with strict mode enabled
|
||||
|
||||
## Core Features
|
||||
|
||||
|
|
@ -34,13 +36,40 @@ This is a full-stack application built with Nuxt.js, utilizing the following tec
|
|||
- `layouts/`: Layout components
|
||||
- `middleware/`: Middleware
|
||||
- **server/**: Backend API code
|
||||
- `api/`: API route handlers
|
||||
- `api/`: API route handlers ([server/api/](mdc:server/api/))
|
||||
- `middleware/`: Server middleware
|
||||
- `utils/`: Server utility functions
|
||||
- **tests/**: Testing infrastructure
|
||||
- `api/`: Unit tests for API endpoints
|
||||
- `helpers/`: Test utilities and fixtures
|
||||
- [sink.spec.ts](mdc:tests/sink.spec.ts): Integration tests
|
||||
- **i18n/**: Internationalization configuration
|
||||
- **schemas/**: Data validation schemas
|
||||
- **scripts/**: Build scripts
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Testing Philosophy
|
||||
The project follows a **focused testing approach** where unit testing is exclusively applied to the `server/api` directory. This ensures:
|
||||
- **API Reliability**: All backend endpoints are thoroughly tested
|
||||
- **Data Integrity**: Input validation and business logic are verified
|
||||
- **Error Handling**: Proper error responses and status codes
|
||||
- **Security**: Authentication and authorization mechanisms
|
||||
|
||||
### Testing Architecture
|
||||
- **Unit Tests**: Comprehensive coverage of all API endpoints in [server/api/](mdc:server/api/)
|
||||
- **Integration Tests**: E2E testing for critical user flows
|
||||
- **Test Framework**: Vitest with Nuxt Test Utils for API testing
|
||||
- **Coverage Target**: Minimum 80% coverage for API endpoints
|
||||
- **CI/CD Integration**: Automated testing on every pull request
|
||||
|
||||
### Key Test Areas
|
||||
1. **Link Management APIs**: Create, edit, delete, query operations
|
||||
2. **Statistics APIs**: Analytics data retrieval and processing
|
||||
3. **Authentication**: Token validation and security checks
|
||||
4. **Input Validation**: Zod schema validation testing
|
||||
5. **Error Scenarios**: Comprehensive error handling verification
|
||||
|
||||
## Configuration Files
|
||||
|
||||
- [package.json](mdc:package.json): Project dependencies and scripts
|
||||
|
|
@ -48,3 +77,29 @@ This is a full-stack application built with Nuxt.js, utilizing the following tec
|
|||
- [wrangler.jsonc](mdc:wrangler.jsonc): Cloudflare Workers configuration
|
||||
- [tailwind.config.js](mdc:tailwind.config.js): Tailwind CSS configuration
|
||||
- [eslint.config.mjs](mdc:eslint.config.mjs): ESLint configuration
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Quality Assurance
|
||||
The project maintains high code quality through:
|
||||
- **Type Safety**: Strict TypeScript configuration
|
||||
- **Code Standards**: ESLint and Prettier enforcement
|
||||
- **Testing Requirements**: Mandatory unit tests for all API endpoints
|
||||
- **Pre-commit Hooks**: Automated linting and testing before commits
|
||||
- **Code Reviews**: Comprehensive review process for all changes
|
||||
|
||||
### Development Commands
|
||||
```bash
|
||||
# Development
|
||||
npm run dev # Start development server
|
||||
npm run test # Run all tests
|
||||
|
||||
# Quality Checks
|
||||
npm run lint # Run ESLint
|
||||
npm run lint:fix # Fix ESLint issues
|
||||
npm run build # Build for production
|
||||
|
||||
# Deployment
|
||||
npm run deploy # Deploy to Cloudflare Pages
|
||||
npm run preview # Preview with Wrangler
|
||||
```
|
||||
|
|
|
|||
565
.cursor/rules/unit-testing.mdc
Normal file
565
.cursor/rules/unit-testing.mdc
Normal file
|
|
@ -0,0 +1,565 @@
|
|||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
# Testing Guidelines for Sink
|
||||
|
||||
## Overview
|
||||
|
||||
This project implements comprehensive testing for the backend API layer using Vitest with Cloudflare Workers Pool. Testing focuses exclusively on API endpoint functionality, data validation, authentication, and business logic for the URL shortening service.
|
||||
|
||||
## Testing Stack
|
||||
|
||||
- **Test Framework**: [Vitest](https://vitest.dev/) with [`@cloudflare/vitest-pool-workers`](https://github.com/cloudflare/workers-sdk/tree/main/packages/vitest-pool-workers)
|
||||
- **Mock Generation**: [`@anatine/zod-mock`](https://github.com/anatine/zod-plugins/tree/main/packages/zod-mock) for generating realistic test data
|
||||
- **Fake Data**: [`@faker-js/faker`](https://fakerjs.dev/) for additional test data generation
|
||||
- **Environment**: Cloudflare Workers runtime with isolated storage
|
||||
|
||||
## Testing Scope
|
||||
|
||||
### What We Test
|
||||
- **API Endpoints**: All routes in [server/api/](mdc:server/api/)
|
||||
- Link management APIs: [create.post.ts](mdc:server/api/link/create.post.ts), [edit.put.ts](mdc:server/api/link/edit.put.ts), [delete.post.ts](mdc:server/api/link/delete.post.ts)
|
||||
- Link retrieval APIs: [list.get.ts](mdc:server/api/link/list.get.ts), [query.get.ts](mdc:server/api/link/query.get.ts), [search.get.ts](mdc:server/api/link/search.get.ts)
|
||||
- Advanced features: [upsert.post.ts](mdc:server/api/link/upsert.post.ts), [ai.get.ts](mdc:server/api/link/ai.get.ts)
|
||||
- Statistics endpoints: [views.get.ts](mdc:server/api/stats/views.get.ts), [counters.get.ts](mdc:server/api/stats/counters.get.ts), [metrics.get.ts](mdc:server/api/stats/metrics.get.ts)
|
||||
- Utility endpoints: [location.ts](mdc:server/api/location.ts), [verify.ts](mdc:server/api/verify.ts)
|
||||
- **Authentication**: Bearer token validation and authorization flows
|
||||
- **Input Validation**: Zod schema validation for all API inputs
|
||||
- **Error Handling**: HTTP status codes and error response structures
|
||||
- **Business Logic**: URL shortening, analytics, and link management features
|
||||
|
||||
### What We Don't Test
|
||||
- Frontend components, pages, or layouts (Vue.js application layer)
|
||||
- Third-party service integrations (external API dependencies)
|
||||
- Cloudflare Workers runtime internals
|
||||
- Database connection layer specifics
|
||||
- Build and deployment processes
|
||||
|
||||
## Project Structure
|
||||
|
||||
### Current Test Organization
|
||||
```
|
||||
tests/
|
||||
├── api/
|
||||
│ ├── link.spec.ts # All link-related endpoint tests
|
||||
│ ├── location.spec.ts # Location API tests
|
||||
│ └── verify.spec.ts # Authentication verification tests
|
||||
├── setup.ts # Global test setup and configuration
|
||||
├── utils.ts # Test utility functions and helpers
|
||||
└── sink.spec.ts # End-to-end integration tests
|
||||
```
|
||||
|
||||
### Recommended Test Structure (for expansion)
|
||||
```
|
||||
tests/
|
||||
├── api/
|
||||
│ ├── link/
|
||||
│ │ ├── create.test.ts # POST /api/link/create
|
||||
│ │ ├── edit.test.ts # PUT /api/link/edit
|
||||
│ │ ├── delete.test.ts # POST /api/link/delete
|
||||
│ │ ├── list.test.ts # GET /api/link/list
|
||||
│ │ ├── query.test.ts # GET /api/link/query
|
||||
│ │ ├── search.test.ts # GET /api/link/search
|
||||
│ │ ├── upsert.test.ts # POST /api/link/upsert
|
||||
│ │ └── ai.test.ts # GET /api/link/ai
|
||||
│ ├── stats/
|
||||
│ │ ├── views.test.ts # GET /api/stats/views
|
||||
│ │ ├── counters.test.ts # GET /api/stats/counters
|
||||
│ │ └── metrics.test.ts # GET /api/stats/metrics
|
||||
│ ├── location.test.ts # GET /api/location
|
||||
│ ├── verify.test.ts # GET /api/verify
|
||||
│ └── helpers/
|
||||
│ ├── fixtures.ts # Test data fixtures
|
||||
│ ├── mocks.ts # Mock implementations
|
||||
│ └── assertions.ts # Custom assertion helpers
|
||||
├── setup.ts
|
||||
├── utils.ts
|
||||
└── integration/
|
||||
└── sink.spec.ts # End-to-end workflow tests
|
||||
```
|
||||
|
||||
## Testing Patterns
|
||||
|
||||
### Standard API Test Template
|
||||
```typescript
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { generateMock } from '@anatine/zod-mock'
|
||||
import { z } from 'zod'
|
||||
import { fetch, fetchWithAuth } from '../utils'
|
||||
|
||||
// Define schema for test data generation
|
||||
const linkSchema = z.object({
|
||||
url: z.string().url(),
|
||||
slug: z.string().min(1).max(50),
|
||||
})
|
||||
|
||||
describe('/api/link/create', () => {
|
||||
it('creates a new link with valid data', async () => {
|
||||
const payload = generateMock(linkSchema)
|
||||
|
||||
const response = await fetchWithAuth('/api/link/create', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
|
||||
expect(response.status).toBe(201)
|
||||
|
||||
const data = await response.json()
|
||||
expect(data.link).toBeDefined()
|
||||
expect(data.link.url).toBe(payload.url)
|
||||
expect(data.link.slug).toBe(payload.slug)
|
||||
expect(data.shortLink).toContain(payload.slug)
|
||||
})
|
||||
|
||||
it('returns 409 when slug already exists', async () => {
|
||||
const payload = generateMock(linkSchema)
|
||||
|
||||
// Create the link first
|
||||
await fetchWithAuth('/api/link/create', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
|
||||
// Attempt to create duplicate
|
||||
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 without authentication', async () => {
|
||||
const payload = generateMock(linkSchema)
|
||||
|
||||
const response = await fetch('/api/link/create', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
|
||||
expect(response.status).toBe(401)
|
||||
})
|
||||
|
||||
it('validates required fields', async () => {
|
||||
const invalidPayload = { slug: 'test' } // Missing URL
|
||||
|
||||
const response = await fetchWithAuth('/api/link/create', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(invalidPayload),
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
|
||||
expect(response.status).toBe(400)
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### Authentication Testing Pattern
|
||||
```typescript
|
||||
describe('Authentication', () => {
|
||||
it('accepts valid bearer token', async () => {
|
||||
const response = await fetchWithAuth('/api/link/list')
|
||||
expect(response.status).toBe(200)
|
||||
})
|
||||
|
||||
it('rejects missing authorization header', async () => {
|
||||
const response = await fetch('/api/link/list')
|
||||
expect(response.status).toBe(401)
|
||||
})
|
||||
|
||||
it('rejects invalid bearer token', async () => {
|
||||
const response = await fetch('/api/link/list', {
|
||||
headers: { Authorization: 'Bearer invalid-token' }
|
||||
})
|
||||
expect(response.status).toBe(401)
|
||||
})
|
||||
|
||||
it('rejects malformed authorization header', async () => {
|
||||
const response = await fetch('/api/link/list', {
|
||||
headers: { Authorization: 'InvalidFormat' }
|
||||
})
|
||||
expect(response.status).toBe(401)
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### Input Validation Testing
|
||||
```typescript
|
||||
import { z } from 'zod'
|
||||
|
||||
describe('Link validation', () => {
|
||||
const createLinkSchema = z.object({
|
||||
url: z.string().url(),
|
||||
slug: z.string().min(1).max(50).optional(),
|
||||
expiresAt: z.string().datetime().optional(),
|
||||
})
|
||||
|
||||
it('accepts valid URL formats', () => {
|
||||
const testCases = [
|
||||
'https://example.com',
|
||||
'http://sub.domain.com/path',
|
||||
'https://site.com/path?query=value#anchor'
|
||||
]
|
||||
|
||||
testCases.forEach(url => {
|
||||
expect(() => createLinkSchema.parse({ url })).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
it('rejects invalid URL formats', () => {
|
||||
const testCases = [
|
||||
'not-a-url',
|
||||
'ftp://invalid-protocol.com',
|
||||
'javascript:alert("xss")',
|
||||
''
|
||||
]
|
||||
|
||||
testCases.forEach(url => {
|
||||
expect(() => createLinkSchema.parse({ url })).toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
it('validates slug constraints', () => {
|
||||
// Valid slugs
|
||||
expect(() => createLinkSchema.parse({
|
||||
url: 'https://example.com',
|
||||
slug: 'valid-slug'
|
||||
})).not.toThrow()
|
||||
|
||||
// Invalid slugs
|
||||
expect(() => createLinkSchema.parse({
|
||||
url: 'https://example.com',
|
||||
slug: ''
|
||||
})).toThrow()
|
||||
|
||||
expect(() => createLinkSchema.parse({
|
||||
url: 'https://example.com',
|
||||
slug: 'a'.repeat(51)
|
||||
})).toThrow()
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### Statistics and Analytics Testing
|
||||
```typescript
|
||||
describe('/api/stats/views', () => {
|
||||
it('returns view statistics for valid slug', async () => {
|
||||
// First create a link to get stats for
|
||||
const linkPayload = generateMock(linkSchema)
|
||||
await fetchWithAuth('/api/link/create', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(linkPayload),
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
|
||||
const response = await fetchWithAuth(
|
||||
`/api/stats/views?slug=${linkPayload.slug}&timeframe=7d`
|
||||
)
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
|
||||
const data = await response.json()
|
||||
expect(data).toHaveProperty('total')
|
||||
expect(data).toHaveProperty('unique')
|
||||
expect(data).toHaveProperty('timeframe')
|
||||
expect(typeof data.total).toBe('number')
|
||||
expect(typeof data.unique).toBe('number')
|
||||
})
|
||||
|
||||
it('handles non-existent slug gracefully', async () => {
|
||||
const response = await fetchWithAuth('/api/stats/views?slug=non-existent')
|
||||
|
||||
// Should either return 404 or empty stats
|
||||
expect([200, 404]).toContain(response.status)
|
||||
|
||||
if (response.status === 200) {
|
||||
const data = await response.json()
|
||||
expect(data.total).toBe(0)
|
||||
expect(data.unique).toBe(0)
|
||||
}
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## Test Configuration
|
||||
|
||||
### Vitest Configuration
|
||||
The [vitest.config.ts](mdc:vitest.config.ts) uses Cloudflare Workers pool:
|
||||
|
||||
```typescript
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
```
|
||||
|
||||
### 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)
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### Rate Limiting and Performance
|
||||
```typescript
|
||||
describe('Performance and limits', () => {
|
||||
it('handles multiple concurrent requests', async () => {
|
||||
const requests = Array.from({ length: 10 }, (_, i) =>
|
||||
fetchWithAuth('/api/link/create', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
url: `https://example-${i}.com`,
|
||||
slug: `test-${i}`
|
||||
}),
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
)
|
||||
|
||||
const responses = await Promise.all(requests)
|
||||
|
||||
// All requests should succeed
|
||||
responses.forEach(response => {
|
||||
expect(response.status).toBe(201)
|
||||
})
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Test Organization
|
||||
- **Sequential Testing**: Use `describe.sequential()` for tests that modify shared state
|
||||
- **Isolation**: Each test should be independent and not rely on others
|
||||
- **Cleanup**: Clean up created resources when necessary
|
||||
- **Descriptive Names**: Use clear, behavior-focused test descriptions
|
||||
|
||||
### Data Management
|
||||
- **Mock Generation**: Use `@anatine/zod-mock` for schema-consistent test data
|
||||
- **Realistic Data**: Generate realistic URLs, slugs, and user inputs
|
||||
- **Edge Cases**: Test boundary conditions, empty values, and extreme inputs
|
||||
- **State Management**: Be mindful of shared KV storage in sequential tests
|
||||
|
||||
### Performance Considerations
|
||||
- **Focused Tests**: Keep tests fast and focused on specific functionality
|
||||
- **Parallel Execution**: Use parallel execution where state doesn't conflict
|
||||
- **Resource Usage**: Monitor test execution time and resource consumption
|
||||
- **Mock External Services**: Avoid real external API calls in tests
|
||||
|
||||
### Assertion Strategies
|
||||
- **Comprehensive Checks**: Verify response status, structure, and data
|
||||
- **Type Safety**: Ensure response data matches expected TypeScript types
|
||||
- **Error Messages**: Test error response content and structure
|
||||
- **Business Logic**: Verify core URL shortening and analytics logic
|
||||
|
||||
## Running Tests
|
||||
|
||||
### Available Commands
|
||||
```bash
|
||||
# Run all tests
|
||||
npm run test
|
||||
|
||||
# Run tests in watch mode (for development)
|
||||
npm run test -- --watch
|
||||
|
||||
# Run specific test file
|
||||
npm run test tests/api/link.spec.ts
|
||||
|
||||
# Run tests with verbose output
|
||||
npm run test -- --reporter=verbose
|
||||
|
||||
# Run tests matching pattern
|
||||
npm run test -- --grep "authentication"
|
||||
|
||||
# Run tests for specific API group
|
||||
npm run test tests/api/link/ tests/api/stats/
|
||||
```
|
||||
|
||||
### Debugging Tests
|
||||
```bash
|
||||
# Run tests with debugging
|
||||
npm run test -- --inspect-brk
|
||||
|
||||
# Run single test with detailed output
|
||||
npm run test -- --run --reporter=verbose tests/api/link.spec.ts
|
||||
```
|
||||
|
||||
### CI/CD Integration
|
||||
- Tests run automatically on every pull request
|
||||
- Enforce test passage before merging
|
||||
- Consider adding coverage reporting and thresholds
|
||||
- Use test results to gate deployments
|
||||
|
||||
## Coverage Goals
|
||||
|
||||
### Target Areas
|
||||
- **API Endpoints**: 100% coverage of all public endpoints
|
||||
- **Authentication**: Complete coverage of auth flows and edge cases
|
||||
- **Validation**: All input validation paths and error scenarios
|
||||
- **Business Logic**: Core URL shortening and analytics functionality
|
||||
|
||||
### Coverage Exclusions
|
||||
- Configuration files and build scripts
|
||||
- Type definitions and interfaces
|
||||
- Third-party integrations (mock instead)
|
||||
- Development-only utilities
|
||||
|
||||
### Monitoring
|
||||
- Track coverage trends over time
|
||||
- Identify uncovered critical paths
|
||||
- Balance coverage percentage with test quality
|
||||
- Focus on meaningful coverage over arbitrary numbers
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
NUXT_PUBLIC_PREVIEW_MODE=true
|
||||
NUXT_PUBLIC_PREVIEW_MODE=false
|
||||
NUXT_PUBLIC_SLUG_DEFAULT_LENGTH=5
|
||||
NUXT_SITE_TOKEN=SinkCool
|
||||
NUXT_REDIRECT_STATUS_CODE=308
|
||||
|
|
|
|||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -26,3 +26,4 @@ node_modules
|
|||
site
|
||||
cache
|
||||
public/world.json
|
||||
.dev.*
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ export default defineNuxtConfig({
|
|||
},
|
||||
|
||||
runtimeConfig: {
|
||||
siteToken: 'SinkCool',
|
||||
siteToken: crypto.randomUUID(),
|
||||
redirectStatusCode: '301',
|
||||
linkCacheTtl: 60,
|
||||
redirectWithQuery: false,
|
||||
|
|
|
|||
15
package.json
15
package.json
|
|
@ -8,18 +8,20 @@
|
|||
"node": ">=20.11"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "nuxt dev",
|
||||
"build": "NODE_OPTIONS=--max-old-space-size=8192 nuxt build",
|
||||
"build:map": "node scripts/build-map.js",
|
||||
"build:colo": "node scripts/build-colo.js",
|
||||
"preview": "wrangler pages dev dist",
|
||||
"deploy": "wrangler pages deploy dist",
|
||||
"postinstall": "npm run build:map && nuxt prepare",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"typecheck": "nuxt typecheck",
|
||||
"lint-staged": "lint-staged",
|
||||
"wrangler": "wrangler",
|
||||
"lint-staged": "lint-staged"
|
||||
"dev": "nuxt dev",
|
||||
"deploy": "npm run deploy:pages",
|
||||
"deploy:pages": "wrangler pages deploy dist",
|
||||
"deploy:worker": "wrangler deploy",
|
||||
"preview": "wrangler dev --var $(cat .env | sed 's/=/:/g')",
|
||||
"test": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@intlify/message-compiler": "^11.1.3",
|
||||
|
|
@ -50,7 +52,9 @@
|
|||
"zod": "^3.25.20"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@anatine/zod-mock": "^3.14.0",
|
||||
"@antfu/eslint-config": "^4.13.2",
|
||||
"@cloudflare/vitest-pool-workers": "^0.8.33",
|
||||
"@nuxt/eslint": "^1.4.1",
|
||||
"@nuxt/eslint-config": "^1.4.1",
|
||||
"@nuxthub/core": "^0.8.27",
|
||||
|
|
@ -67,6 +71,7 @@
|
|||
"simple-git-hooks": "^2.13.0",
|
||||
"tailwind-merge": "^3.3.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"vitest": "^3.1.4",
|
||||
"vue-tsc": "^2.2.10",
|
||||
"wrangler": "^4.16.1"
|
||||
},
|
||||
|
|
|
|||
441
pnpm-lock.yaml
441
pnpm-lock.yaml
|
|
@ -87,9 +87,15 @@ importers:
|
|||
specifier: ^3.25.20
|
||||
version: 3.25.20
|
||||
devDependencies:
|
||||
'@anatine/zod-mock':
|
||||
specifier: ^3.14.0
|
||||
version: 3.14.0(@faker-js/faker@9.8.0)(zod@3.25.20)
|
||||
'@antfu/eslint-config':
|
||||
specifier: ^4.13.2
|
||||
version: 4.13.2(@vue/compiler-sfc@3.5.14)(eslint@9.27.0(jiti@2.4.2))(typescript@5.7.2)
|
||||
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))
|
||||
'@nuxt/eslint':
|
||||
specifier: ^1.4.1
|
||||
version: 1.4.1(@vue/compiler-sfc@3.5.14)(eslint@9.27.0(jiti@2.4.2))(magicast@0.3.5)(typescript@5.7.2)(vite@6.3.5(@types/node@20.12.11)(jiti@2.4.2)(terser@5.31.0)(yaml@2.8.0))
|
||||
|
|
@ -138,12 +144,15 @@ importers:
|
|||
tailwindcss-animate:
|
||||
specifier: ^1.0.7
|
||||
version: 1.0.7(tailwindcss@3.4.17)
|
||||
vitest:
|
||||
specifier: ^3.1.4
|
||||
version: 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)
|
||||
vue-tsc:
|
||||
specifier: ^2.2.10
|
||||
version: 2.2.10(typescript@5.7.2)
|
||||
wrangler:
|
||||
specifier: ^4.16.1
|
||||
version: 4.16.1(@cloudflare/workers-types@4.20250520.0)
|
||||
version: 4.16.1
|
||||
|
||||
packages:
|
||||
|
||||
|
|
@ -155,6 +164,12 @@ packages:
|
|||
resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
'@anatine/zod-mock@3.14.0':
|
||||
resolution: {integrity: sha512-pTYyn9dlszUl5gMGWtE7DBRvx1EcqpBJ1iJ73rlJYCZr9lgIEdZ6PCZQqoXIdlC8DYIwuT1M3/N8arkJa7A10Q==}
|
||||
peerDependencies:
|
||||
'@faker-js/faker': ^7.0.0 || ^8.0.0 || ^9.0.0
|
||||
zod: ^3.21.4
|
||||
|
||||
'@antfu/eslint-config@4.13.2':
|
||||
resolution: {integrity: sha512-F+IVIQUCfw6eW4H06c9a9USJ3UOnoBx4I0qsTL3kO6GcyJB6mwk+nawFf95DfHKT3fJKv58YPPz0XCmsY/w0XA==}
|
||||
hasBin: true
|
||||
|
|
@ -347,36 +362,73 @@ packages:
|
|||
workerd:
|
||||
optional: true
|
||||
|
||||
'@cloudflare/vitest-pool-workers@0.8.33':
|
||||
resolution: {integrity: sha512-Joh/kJhwMXP7uq0zsK2HweD+N9X2rHv51GOyaJPwwgFjfn4cRZ9flIbn9xBAMgGoNseuS38aI+cVOWFG7xlBfg==}
|
||||
peerDependencies:
|
||||
'@vitest/runner': 2.0.x - 3.1.x
|
||||
'@vitest/snapshot': 2.0.x - 3.1.x
|
||||
vitest: 2.0.x - 3.1.x
|
||||
|
||||
'@cloudflare/workerd-darwin-64@1.20250508.0':
|
||||
resolution: {integrity: sha512-9x09MrA9Y5RQs3zqWvWns8xHgM2pVNXWpeJ+3hQYu4PrwPFZXtTD6b/iMmOnlYKzINlREq1RGeEybMFyWEUlUg==}
|
||||
engines: {node: '>=16'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@cloudflare/workerd-darwin-64@1.20250523.0':
|
||||
resolution: {integrity: sha512-/K7vKkPDx9idJ7hJtqYXYsKkHX9XQ6awyDyBZ4RwbaQ/o3fyS/tgHaej2rUO6zkb7CfUxiaeAB7Z6i7KltMY5Q==}
|
||||
engines: {node: '>=16'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@cloudflare/workerd-darwin-arm64@1.20250508.0':
|
||||
resolution: {integrity: sha512-0Ili+nE2LLRzYue/yPc1pepSyNNg6LxR3/ng/rlQzVQUxPXIXldHFkJ/ynsYwQnAcf6OxasSi/kbTm6yvDoSAQ==}
|
||||
engines: {node: '>=16'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@cloudflare/workerd-darwin-arm64@1.20250523.0':
|
||||
resolution: {integrity: sha512-tVQqStt245KzkrCT6DBXoMNHaJgh/8hQy3fsG+4gHfqw/JdKEgXigkc9hWdC6BoS5DiGK+dGVJo2MnWHFC7XlQ==}
|
||||
engines: {node: '>=16'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@cloudflare/workerd-linux-64@1.20250508.0':
|
||||
resolution: {integrity: sha512-5saVrZ3uVwYxvBa7BaonXjeqB6X0YF3ak05qvBaWcmZ3FNmnarMm2W8842cnbhnckDVBpB/iDo51Sy6Y7y1jcw==}
|
||||
engines: {node: '>=16'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@cloudflare/workerd-linux-64@1.20250523.0':
|
||||
resolution: {integrity: sha512-PCPWBlwiKr9Es2TP93JVygXRPwx+AkygUMV2gFOPerVrdXUd13A4dJ68Qjpmh3O0xqmVIRV6PSogM3wNvwnw5Q==}
|
||||
engines: {node: '>=16'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@cloudflare/workerd-linux-arm64@1.20250508.0':
|
||||
resolution: {integrity: sha512-muQe1pkxRi3eaq1Q417xvfGd2SlktbLTzNhT5Yftsx8OecWrYuB8i4ttR6Nr5ER06bfEj0FqQjqJJhcp6wLLUQ==}
|
||||
engines: {node: '>=16'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@cloudflare/workerd-linux-arm64@1.20250523.0':
|
||||
resolution: {integrity: sha512-uKa/L9W1AzT+yE0wNxFZPlMXms5xmGaaOmTAK0wuLPW6qmKj1zyBidjHqQXVZ+eK/fLy3CNeyB9EBtR0/8FH7A==}
|
||||
engines: {node: '>=16'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@cloudflare/workerd-windows-64@1.20250508.0':
|
||||
resolution: {integrity: sha512-EJj8iTWFMqjgvZUxxNvzK7frA1JMFi3y/9eDIdZPL/OaQh3cmk5Lai5DCXsKYUxfooMBZWYTp53zOLrvuJI8VQ==}
|
||||
engines: {node: '>=16'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@cloudflare/workerd-windows-64@1.20250523.0':
|
||||
resolution: {integrity: sha512-H5ggClWrskRs7pj2Fd+iJpjFMrh7DZqAfhJT3IloTW85lCEY2+y/yfXEGyDsc0UTLuTS0znldcUrVCRjSiSOkw==}
|
||||
engines: {node: '>=16'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@cloudflare/workers-types@4.20250520.0':
|
||||
resolution: {integrity: sha512-bMPrpZREctlSaKtqIp3mdRjgyRSgioPQDUlhOeCUYRIo9DzWUACvw/u5kq3GQMmE4V1/nVCGHb7a1Ie8f5wG5g==}
|
||||
|
||||
|
|
@ -675,6 +727,10 @@ packages:
|
|||
resolution: {integrity: sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@faker-js/faker@9.8.0':
|
||||
resolution: {integrity: sha512-U9wpuSrJC93jZBxx/Qq2wPjCuYISBueyVUGK7qqdmj7r/nxaxwW8AQDCLeRO7wZnjj94sh3p246cAYjUKuqgfg==}
|
||||
engines: {node: '>=18.0.0', npm: '>=9.0.0'}
|
||||
|
||||
'@fastify/busboy@2.1.1':
|
||||
resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==}
|
||||
engines: {node: '>=14'}
|
||||
|
|
@ -2097,6 +2153,35 @@ packages:
|
|||
vitest:
|
||||
optional: true
|
||||
|
||||
'@vitest/expect@3.1.4':
|
||||
resolution: {integrity: sha512-xkD/ljeliyaClDYqHPNCiJ0plY5YIcM0OlRiZizLhlPmpXWpxnGMyTZXOHFhFeG7w9P5PBeL4IdtJ/HeQwTbQA==}
|
||||
|
||||
'@vitest/mocker@3.1.4':
|
||||
resolution: {integrity: sha512-8IJ3CvwtSw/EFXqWFL8aCMu+YyYXG2WUSrQbViOZkWTKTVicVwZ/YiEZDSqD00kX+v/+W+OnxhNWoeVKorHygA==}
|
||||
peerDependencies:
|
||||
msw: ^2.4.9
|
||||
vite: ^5.0.0 || ^6.0.0
|
||||
peerDependenciesMeta:
|
||||
msw:
|
||||
optional: true
|
||||
vite:
|
||||
optional: true
|
||||
|
||||
'@vitest/pretty-format@3.1.4':
|
||||
resolution: {integrity: sha512-cqv9H9GvAEoTaoq+cYqUTCGscUjKqlJZC7PRwY5FMySVj5J+xOm1KQcCiYHJOEzOKRUhLH4R2pTwvFlWCEScsg==}
|
||||
|
||||
'@vitest/runner@3.1.4':
|
||||
resolution: {integrity: sha512-djTeF1/vt985I/wpKVFBMWUlk/I7mb5hmD5oP8K9ACRmVXgKTae3TUOtXAEBfslNKPzUQvnKhNd34nnRSYgLNQ==}
|
||||
|
||||
'@vitest/snapshot@3.1.4':
|
||||
resolution: {integrity: sha512-JPHf68DvuO7vilmvwdPr9TS0SuuIzHvxeaCkxYcCD4jTk67XwL45ZhEHFKIuCm8CYstgI6LZ4XbwD6ANrwMpFg==}
|
||||
|
||||
'@vitest/spy@3.1.4':
|
||||
resolution: {integrity: sha512-Xg1bXhu+vtPXIodYN369M86K8shGLouNjoVI78g8iAq2rFoHFdajNvJJ5A/9bPMFcfQqdaCpOgWKEoMQg/s0Yg==}
|
||||
|
||||
'@vitest/utils@3.1.4':
|
||||
resolution: {integrity: sha512-yriMuO1cfFhmiGc8ataN51+9ooHRuURdfAZfwFd3usWynjzpLslZdYnRegTv32qdgtJTsj15FoeZe2g15fY1gg==}
|
||||
|
||||
'@volar/language-core@2.4.11':
|
||||
resolution: {integrity: sha512-lN2C1+ByfW9/JRPpqScuZt/4OrUUse57GLI6TbLgTIqBVemdl1wNcZ1qYGEo2+Gw8coYLgCy7SuKqn6IrQcQgg==}
|
||||
|
||||
|
|
@ -2427,6 +2512,10 @@ packages:
|
|||
as-table@1.0.55:
|
||||
resolution: {integrity: sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==}
|
||||
|
||||
assertion-error@2.0.1:
|
||||
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
ast-kit@1.4.3:
|
||||
resolution: {integrity: sha512-MdJqjpodkS5J149zN0Po+HPshkTdUyrvF7CKTafUgv69vBSPtncrj+3IiUgqdd7ElIEkbeXCsEouBUwLrw9Ilg==}
|
||||
engines: {node: '>=16.14.0'}
|
||||
|
|
@ -2485,6 +2574,9 @@ packages:
|
|||
bindings@1.5.0:
|
||||
resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==}
|
||||
|
||||
birpc@0.2.14:
|
||||
resolution: {integrity: sha512-37FHE8rqsYM5JEKCnXFyHpBCzvgHEExwVVTq+nUmloInU7l8ezD1TpOhKpS8oe1DTYFqEK27rFZVKG43oTqXRA==}
|
||||
|
||||
birpc@2.3.0:
|
||||
resolution: {integrity: sha512-ijbtkn/F3Pvzb6jHypHRyve2QApOCZDR25D/VnkY2G/lBNcXCTsnsCxgY4k4PkVB7zfwzYbY3O9Lcqe3xufS5g==}
|
||||
|
||||
|
|
@ -2590,6 +2682,10 @@ packages:
|
|||
ccount@2.0.1:
|
||||
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
|
||||
|
||||
chai@5.2.0:
|
||||
resolution: {integrity: sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
chalk@4.1.2:
|
||||
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
||||
engines: {node: '>=10'}
|
||||
|
|
@ -2601,6 +2697,10 @@ packages:
|
|||
character-entities@2.0.2:
|
||||
resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==}
|
||||
|
||||
check-error@2.1.1:
|
||||
resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==}
|
||||
engines: {node: '>= 16'}
|
||||
|
||||
chokidar@3.6.0:
|
||||
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
|
||||
engines: {node: '>= 8.10.0'}
|
||||
|
|
@ -2624,6 +2724,9 @@ packages:
|
|||
citty@0.1.6:
|
||||
resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==}
|
||||
|
||||
cjs-module-lexer@1.4.3:
|
||||
resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==}
|
||||
|
||||
class-variance-authority@0.7.1:
|
||||
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
|
||||
|
||||
|
|
@ -3112,6 +3215,10 @@ packages:
|
|||
decode-named-character-reference@1.0.2:
|
||||
resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==}
|
||||
|
||||
deep-eql@5.0.2:
|
||||
resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
deep-equal@1.0.1:
|
||||
resolution: {integrity: sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==}
|
||||
|
||||
|
|
@ -3219,6 +3326,9 @@ packages:
|
|||
resolution: {integrity: sha512-ARFxjzizOhPqs1fYC/2NMC3N4jrQ6HvVflnXBTRqNEqJuXwyKLRr9CrJwkRcV/SnZt1sNXgsF6FPm0x57Tq0rw==}
|
||||
engines: {node: ^14.14.0 || >=16.0.0}
|
||||
|
||||
devalue@4.3.3:
|
||||
resolution: {integrity: sha512-UH8EL6H2ifcY8TbD2QsxwCC/pr5xSwPvv85LrLXVihmHVC3T3YqTCIwnR5ak0yO1KYqlxrPVOA/JVZJYPy2ATg==}
|
||||
|
||||
devalue@5.1.1:
|
||||
resolution: {integrity: sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==}
|
||||
|
||||
|
|
@ -3260,6 +3370,10 @@ packages:
|
|||
resolution: {integrity: sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
drange@1.1.1:
|
||||
resolution: {integrity: sha512-pYxfDYpued//QpnLIm4Avk7rsNtAtQkUES2cwAYSvD/wd2pKD71gN2Ebj3e7klzXwjocvE8c5vx/1fxwpqmSxA==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
|
@ -3616,6 +3730,10 @@ packages:
|
|||
resolution: {integrity: sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
expect-type@1.2.1:
|
||||
resolution: {integrity: sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
exsolve@1.0.5:
|
||||
resolution: {integrity: sha512-pz5dvkYYKQ1AHVrgOzBKWeP4u4FRb3a6DNK2ucr0OoNwYIU4QWsJ+NM36LLzORT+z845MzKHHhpXiUF5nvQoJg==}
|
||||
|
||||
|
|
@ -4441,6 +4559,9 @@ packages:
|
|||
longest-streak@3.1.0:
|
||||
resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
|
||||
|
||||
loupe@3.1.3:
|
||||
resolution: {integrity: sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==}
|
||||
|
||||
lru-cache@10.4.3:
|
||||
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
|
||||
|
||||
|
|
@ -4678,6 +4799,11 @@ packages:
|
|||
engines: {node: '>=18.0.0'}
|
||||
hasBin: true
|
||||
|
||||
miniflare@4.20250523.0:
|
||||
resolution: {integrity: sha512-g4F1AC5xi66rB2eQNo2Fx7EffaXhMdgUSRl/ivgb4LMALMpxghG98oC4twqVwDLWIFSVFjtL1YEuYrPO8044mg==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
hasBin: true
|
||||
|
||||
minimatch@3.1.2:
|
||||
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
|
||||
|
||||
|
|
@ -5116,6 +5242,10 @@ packages:
|
|||
pathe@2.0.3:
|
||||
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
|
||||
|
||||
pathval@2.0.0:
|
||||
resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==}
|
||||
engines: {node: '>= 14.16'}
|
||||
|
||||
pbf@3.2.1:
|
||||
resolution: {integrity: sha512-ClrV7pNOn7rtmoQVF4TS1vyU0WhYRnP92fzbfF75jAIwpnzdJXf8iTd4CMEqO4yUenH6NDqLiwjqlh6QgZzgLQ==}
|
||||
hasBin: true
|
||||
|
|
@ -5476,6 +5606,10 @@ packages:
|
|||
radix3@1.1.2:
|
||||
resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==}
|
||||
|
||||
randexp@0.5.3:
|
||||
resolution: {integrity: sha512-U+5l2KrcMNOUPYvazA3h5ekF80FHTUG+87SEAmHZmolh1M+i/WyTCxVzmi+tidIa1tM4BSe8g2Y/D3loWDjj+w==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
randombytes@2.1.0:
|
||||
resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
|
||||
|
||||
|
|
@ -5596,6 +5730,10 @@ packages:
|
|||
resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
ret@0.2.2:
|
||||
resolution: {integrity: sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
reusify@1.0.4:
|
||||
resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==}
|
||||
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
|
||||
|
|
@ -5725,6 +5863,9 @@ packages:
|
|||
resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
siginfo@2.0.0:
|
||||
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
|
||||
|
||||
signal-exit@3.0.7:
|
||||
resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
|
||||
|
||||
|
|
@ -5818,6 +5959,9 @@ packages:
|
|||
stack-trace@0.0.10:
|
||||
resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==}
|
||||
|
||||
stackback@0.0.2:
|
||||
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
|
||||
|
||||
stacktracey@2.1.8:
|
||||
resolution: {integrity: sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==}
|
||||
|
||||
|
|
@ -6039,6 +6183,9 @@ packages:
|
|||
tiny-invariant@1.3.3:
|
||||
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
|
||||
|
||||
tinybench@2.9.0:
|
||||
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
|
||||
|
||||
tinycolor2@1.6.0:
|
||||
resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==}
|
||||
|
||||
|
|
@ -6052,9 +6199,21 @@ packages:
|
|||
resolution: {integrity: sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
tinypool@1.0.2:
|
||||
resolution: {integrity: sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
|
||||
tinyqueue@2.0.3:
|
||||
resolution: {integrity: sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==}
|
||||
|
||||
tinyrainbow@2.0.0:
|
||||
resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
||||
tinyspy@3.0.2:
|
||||
resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
||||
tmp-promise@3.0.3:
|
||||
resolution: {integrity: sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==}
|
||||
|
||||
|
|
@ -6474,6 +6633,34 @@ packages:
|
|||
yaml:
|
||||
optional: true
|
||||
|
||||
vitest@3.1.4:
|
||||
resolution: {integrity: sha512-Ta56rT7uWxCSJXlBtKgIlApJnT6e6IGmTYxYcmxjJ4ujuZDI59GUQgVDObXXJujOmPDBYXHK1qmaGtneu6TNIQ==}
|
||||
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@edge-runtime/vm': '*'
|
||||
'@types/debug': ^4.1.12
|
||||
'@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0
|
||||
'@vitest/browser': 3.1.4
|
||||
'@vitest/ui': 3.1.4
|
||||
happy-dom: '*'
|
||||
jsdom: '*'
|
||||
peerDependenciesMeta:
|
||||
'@edge-runtime/vm':
|
||||
optional: true
|
||||
'@types/debug':
|
||||
optional: true
|
||||
'@types/node':
|
||||
optional: true
|
||||
'@vitest/browser':
|
||||
optional: true
|
||||
'@vitest/ui':
|
||||
optional: true
|
||||
happy-dom:
|
||||
optional: true
|
||||
jsdom:
|
||||
optional: true
|
||||
|
||||
vscode-uri@3.1.0:
|
||||
resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==}
|
||||
|
||||
|
|
@ -6561,6 +6748,11 @@ packages:
|
|||
engines: {node: ^18.17.0 || >=20.5.0}
|
||||
hasBin: true
|
||||
|
||||
why-is-node-running@2.3.0:
|
||||
resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
|
||||
engines: {node: '>=8'}
|
||||
hasBin: true
|
||||
|
||||
wide-align@1.1.5:
|
||||
resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==}
|
||||
|
||||
|
|
@ -6581,6 +6773,11 @@ packages:
|
|||
engines: {node: '>=16'}
|
||||
hasBin: true
|
||||
|
||||
workerd@1.20250523.0:
|
||||
resolution: {integrity: sha512-OClsq9ZzZZNdkY8/JTBjf+/A6F1q/SOn3/RQWCR0kDoclxecHS6Nq80jY6NP0ubJBKnqrUggA9WOWBgwWWOGUA==}
|
||||
engines: {node: '>=16'}
|
||||
hasBin: true
|
||||
|
||||
wrangler@4.16.1:
|
||||
resolution: {integrity: sha512-YiLdWXcaQva2K/bqokpsZbySPmoT8TJFyJPsQPZumnkgimM9+/g/yoXArByA+pf+xU8jhw7ybQ8X1yBGXv731g==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
|
@ -6591,6 +6788,16 @@ packages:
|
|||
'@cloudflare/workers-types':
|
||||
optional: true
|
||||
|
||||
wrangler@4.17.0:
|
||||
resolution: {integrity: sha512-FIOriw2Z7aNALAtnt4hTojDuU44n8pGJl62id0ig0s45Mej/Clg07vpmz+QCLTT7huiaSSyA1wthYOwtp0+K6A==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@cloudflare/workers-types': ^4.20250523.0
|
||||
peerDependenciesMeta:
|
||||
'@cloudflare/workers-types':
|
||||
optional: true
|
||||
|
||||
wrap-ansi@7.0.0:
|
||||
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
|
||||
engines: {node: '>=10'}
|
||||
|
|
@ -6725,7 +6932,13 @@ snapshots:
|
|||
'@jridgewell/gen-mapping': 0.3.5
|
||||
'@jridgewell/trace-mapping': 0.3.25
|
||||
|
||||
'@antfu/eslint-config@4.13.2(@vue/compiler-sfc@3.5.14)(eslint@9.27.0(jiti@2.4.2))(typescript@5.7.2)':
|
||||
'@anatine/zod-mock@3.14.0(@faker-js/faker@9.8.0)(zod@3.25.20)':
|
||||
dependencies:
|
||||
'@faker-js/faker': 9.8.0
|
||||
randexp: 0.5.3
|
||||
zod: 3.25.20
|
||||
|
||||
'@antfu/eslint-config@4.13.2(@vue/compiler-sfc@3.5.14)(eslint@9.27.0(jiti@2.4.2))(typescript@5.7.2)(vitest@3.1.4(@types/debug@4.1.12)(@types/node@20.12.11)(jiti@2.4.2)(terser@5.31.0)(yaml@2.8.0))':
|
||||
dependencies:
|
||||
'@antfu/install-pkg': 1.1.0
|
||||
'@clack/prompts': 0.10.1
|
||||
|
|
@ -6734,7 +6947,7 @@ snapshots:
|
|||
'@stylistic/eslint-plugin': 4.2.0(eslint@9.27.0(jiti@2.4.2))(typescript@5.7.2)
|
||||
'@typescript-eslint/eslint-plugin': 8.32.1(@typescript-eslint/parser@8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.7.2))(eslint@9.27.0(jiti@2.4.2))(typescript@5.7.2)
|
||||
'@typescript-eslint/parser': 8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.7.2)
|
||||
'@vitest/eslint-plugin': 1.2.0(eslint@9.27.0(jiti@2.4.2))(typescript@5.7.2)
|
||||
'@vitest/eslint-plugin': 1.2.0(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))
|
||||
ansis: 4.0.0
|
||||
cac: 6.7.14
|
||||
eslint: 9.27.0(jiti@2.4.2)
|
||||
|
|
@ -6972,21 +7185,59 @@ snapshots:
|
|||
optionalDependencies:
|
||||
workerd: 1.20250508.0
|
||||
|
||||
'@cloudflare/unenv-preset@2.3.2(unenv@2.0.0-rc.17)(workerd@1.20250523.0)':
|
||||
dependencies:
|
||||
unenv: 2.0.0-rc.17
|
||||
optionalDependencies:
|
||||
workerd: 1.20250523.0
|
||||
|
||||
'@cloudflare/vitest-pool-workers@0.8.33(@vitest/runner@3.1.4)(@vitest/snapshot@3.1.4)(vitest@3.1.4(@types/debug@4.1.12)(@types/node@20.12.11)(jiti@2.4.2)(terser@5.31.0)(yaml@2.8.0))':
|
||||
dependencies:
|
||||
'@vitest/runner': 3.1.4
|
||||
'@vitest/snapshot': 3.1.4
|
||||
birpc: 0.2.14
|
||||
cjs-module-lexer: 1.4.3
|
||||
devalue: 4.3.3
|
||||
miniflare: 4.20250523.0
|
||||
semver: 7.7.2
|
||||
vitest: 3.1.4(@types/debug@4.1.12)(@types/node@20.12.11)(jiti@2.4.2)(terser@5.31.0)(yaml@2.8.0)
|
||||
wrangler: 4.17.0
|
||||
zod: 3.25.20
|
||||
transitivePeerDependencies:
|
||||
- '@cloudflare/workers-types'
|
||||
- bufferutil
|
||||
- utf-8-validate
|
||||
|
||||
'@cloudflare/workerd-darwin-64@1.20250508.0':
|
||||
optional: true
|
||||
|
||||
'@cloudflare/workerd-darwin-64@1.20250523.0':
|
||||
optional: true
|
||||
|
||||
'@cloudflare/workerd-darwin-arm64@1.20250508.0':
|
||||
optional: true
|
||||
|
||||
'@cloudflare/workerd-darwin-arm64@1.20250523.0':
|
||||
optional: true
|
||||
|
||||
'@cloudflare/workerd-linux-64@1.20250508.0':
|
||||
optional: true
|
||||
|
||||
'@cloudflare/workerd-linux-64@1.20250523.0':
|
||||
optional: true
|
||||
|
||||
'@cloudflare/workerd-linux-arm64@1.20250508.0':
|
||||
optional: true
|
||||
|
||||
'@cloudflare/workerd-linux-arm64@1.20250523.0':
|
||||
optional: true
|
||||
|
||||
'@cloudflare/workerd-windows-64@1.20250508.0':
|
||||
optional: true
|
||||
|
||||
'@cloudflare/workerd-windows-64@1.20250523.0':
|
||||
optional: true
|
||||
|
||||
'@cloudflare/workers-types@4.20250520.0': {}
|
||||
|
||||
'@colors/colors@1.6.0': {}
|
||||
|
|
@ -7269,6 +7520,8 @@ snapshots:
|
|||
'@eslint/core': 0.14.0
|
||||
levn: 0.4.1
|
||||
|
||||
'@faker-js/faker@9.8.0': {}
|
||||
|
||||
'@fastify/busboy@2.1.1': {}
|
||||
|
||||
'@fastify/busboy@3.1.1': {}
|
||||
|
|
@ -8962,15 +9215,56 @@ snapshots:
|
|||
vite: 6.3.5(@types/node@20.12.11)(jiti@2.4.2)(terser@5.31.0)(yaml@2.8.0)
|
||||
vue: 3.5.14(typescript@5.7.2)
|
||||
|
||||
'@vitest/eslint-plugin@1.2.0(eslint@9.27.0(jiti@2.4.2))(typescript@5.7.2)':
|
||||
'@vitest/eslint-plugin@1.2.0(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:
|
||||
'@typescript-eslint/utils': 8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.7.2)
|
||||
eslint: 9.27.0(jiti@2.4.2)
|
||||
optionalDependencies:
|
||||
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)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@vitest/expect@3.1.4':
|
||||
dependencies:
|
||||
'@vitest/spy': 3.1.4
|
||||
'@vitest/utils': 3.1.4
|
||||
chai: 5.2.0
|
||||
tinyrainbow: 2.0.0
|
||||
|
||||
'@vitest/mocker@3.1.4(vite@6.3.5(@types/node@20.12.11)(jiti@2.4.2)(terser@5.31.0)(yaml@2.8.0))':
|
||||
dependencies:
|
||||
'@vitest/spy': 3.1.4
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 0.30.17
|
||||
optionalDependencies:
|
||||
vite: 6.3.5(@types/node@20.12.11)(jiti@2.4.2)(terser@5.31.0)(yaml@2.8.0)
|
||||
|
||||
'@vitest/pretty-format@3.1.4':
|
||||
dependencies:
|
||||
tinyrainbow: 2.0.0
|
||||
|
||||
'@vitest/runner@3.1.4':
|
||||
dependencies:
|
||||
'@vitest/utils': 3.1.4
|
||||
pathe: 2.0.3
|
||||
|
||||
'@vitest/snapshot@3.1.4':
|
||||
dependencies:
|
||||
'@vitest/pretty-format': 3.1.4
|
||||
magic-string: 0.30.17
|
||||
pathe: 2.0.3
|
||||
|
||||
'@vitest/spy@3.1.4':
|
||||
dependencies:
|
||||
tinyspy: 3.0.2
|
||||
|
||||
'@vitest/utils@3.1.4':
|
||||
dependencies:
|
||||
'@vitest/pretty-format': 3.1.4
|
||||
loupe: 3.1.3
|
||||
tinyrainbow: 2.0.0
|
||||
|
||||
'@volar/language-core@2.4.11':
|
||||
dependencies:
|
||||
'@volar/source-map': 2.4.11
|
||||
|
|
@ -9377,6 +9671,8 @@ snapshots:
|
|||
dependencies:
|
||||
printable-characters: 1.0.42
|
||||
|
||||
assertion-error@2.0.1: {}
|
||||
|
||||
ast-kit@1.4.3:
|
||||
dependencies:
|
||||
'@babel/parser': 7.27.2
|
||||
|
|
@ -9432,6 +9728,8 @@ snapshots:
|
|||
dependencies:
|
||||
file-uri-to-path: 1.0.0
|
||||
|
||||
birpc@0.2.14: {}
|
||||
|
||||
birpc@2.3.0: {}
|
||||
|
||||
bl@4.1.0:
|
||||
|
|
@ -9544,6 +9842,14 @@ snapshots:
|
|||
|
||||
ccount@2.0.1: {}
|
||||
|
||||
chai@5.2.0:
|
||||
dependencies:
|
||||
assertion-error: 2.0.1
|
||||
check-error: 2.1.1
|
||||
deep-eql: 5.0.2
|
||||
loupe: 3.1.3
|
||||
pathval: 2.0.0
|
||||
|
||||
chalk@4.1.2:
|
||||
dependencies:
|
||||
ansi-styles: 4.3.0
|
||||
|
|
@ -9553,6 +9859,8 @@ snapshots:
|
|||
|
||||
character-entities@2.0.2: {}
|
||||
|
||||
check-error@2.1.1: {}
|
||||
|
||||
chokidar@3.6.0:
|
||||
dependencies:
|
||||
anymatch: 3.1.3
|
||||
|
|
@ -9579,6 +9887,8 @@ snapshots:
|
|||
dependencies:
|
||||
consola: 3.4.2
|
||||
|
||||
cjs-module-lexer@1.4.3: {}
|
||||
|
||||
class-variance-authority@0.7.1:
|
||||
dependencies:
|
||||
clsx: 2.1.1
|
||||
|
|
@ -10073,6 +10383,8 @@ snapshots:
|
|||
dependencies:
|
||||
character-entities: 2.0.2
|
||||
|
||||
deep-eql@5.0.2: {}
|
||||
|
||||
deep-equal@1.0.1: {}
|
||||
|
||||
deep-is@0.1.4: {}
|
||||
|
|
@ -10161,6 +10473,8 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
devalue@4.3.3: {}
|
||||
|
||||
devalue@5.1.1: {}
|
||||
|
||||
devlop@1.1.0:
|
||||
|
|
@ -10201,6 +10515,8 @@ snapshots:
|
|||
|
||||
dotenv@16.5.0: {}
|
||||
|
||||
drange@1.1.1: {}
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
dependencies:
|
||||
call-bind-apply-helpers: 1.0.2
|
||||
|
|
@ -10659,6 +10975,8 @@ snapshots:
|
|||
|
||||
exit-hook@2.2.1: {}
|
||||
|
||||
expect-type@1.2.1: {}
|
||||
|
||||
exsolve@1.0.5: {}
|
||||
|
||||
externality@1.0.2:
|
||||
|
|
@ -11516,6 +11834,8 @@ snapshots:
|
|||
|
||||
longest-streak@3.1.0: {}
|
||||
|
||||
loupe@3.1.3: {}
|
||||
|
||||
lru-cache@10.4.3: {}
|
||||
|
||||
lru-cache@5.1.1:
|
||||
|
|
@ -11951,6 +12271,24 @@ snapshots:
|
|||
- bufferutil
|
||||
- utf-8-validate
|
||||
|
||||
miniflare@4.20250523.0:
|
||||
dependencies:
|
||||
'@cspotcode/source-map-support': 0.8.1
|
||||
acorn: 8.14.0
|
||||
acorn-walk: 8.3.2
|
||||
exit-hook: 2.2.1
|
||||
glob-to-regexp: 0.4.1
|
||||
sharp: 0.33.5
|
||||
stoppable: 1.1.0
|
||||
undici: 5.29.0
|
||||
workerd: 1.20250523.0
|
||||
ws: 8.18.0
|
||||
youch: 3.3.4
|
||||
zod: 3.22.3
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- utf-8-validate
|
||||
|
||||
minimatch@3.1.2:
|
||||
dependencies:
|
||||
brace-expansion: 1.1.11
|
||||
|
|
@ -12565,6 +12903,8 @@ snapshots:
|
|||
|
||||
pathe@2.0.3: {}
|
||||
|
||||
pathval@2.0.0: {}
|
||||
|
||||
pbf@3.2.1:
|
||||
dependencies:
|
||||
ieee754: 1.2.1
|
||||
|
|
@ -12918,6 +13258,11 @@ snapshots:
|
|||
|
||||
radix3@1.1.2: {}
|
||||
|
||||
randexp@0.5.3:
|
||||
dependencies:
|
||||
drange: 1.1.1
|
||||
ret: 0.2.2
|
||||
|
||||
randombytes@2.1.0:
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
|
|
@ -13065,6 +13410,8 @@ snapshots:
|
|||
onetime: 7.0.0
|
||||
signal-exit: 4.1.0
|
||||
|
||||
ret@0.2.2: {}
|
||||
|
||||
reusify@1.0.4: {}
|
||||
|
||||
rfdc@1.4.1: {}
|
||||
|
|
@ -13247,6 +13594,8 @@ snapshots:
|
|||
side-channel-map: 1.0.1
|
||||
side-channel-weakmap: 1.0.2
|
||||
|
||||
siginfo@2.0.0: {}
|
||||
|
||||
signal-exit@3.0.7: {}
|
||||
|
||||
signal-exit@4.1.0: {}
|
||||
|
|
@ -13331,6 +13680,8 @@ snapshots:
|
|||
|
||||
stack-trace@0.0.10: {}
|
||||
|
||||
stackback@0.0.2: {}
|
||||
|
||||
stacktracey@2.1.8:
|
||||
dependencies:
|
||||
as-table: 1.0.55
|
||||
|
|
@ -13621,6 +13972,8 @@ snapshots:
|
|||
|
||||
tiny-invariant@1.3.3: {}
|
||||
|
||||
tinybench@2.9.0: {}
|
||||
|
||||
tinycolor2@1.6.0: {}
|
||||
|
||||
tinyexec@0.3.2: {}
|
||||
|
|
@ -13632,8 +13985,14 @@ snapshots:
|
|||
fdir: 6.4.4(picomatch@4.0.2)
|
||||
picomatch: 4.0.2
|
||||
|
||||
tinypool@1.0.2: {}
|
||||
|
||||
tinyqueue@2.0.3: {}
|
||||
|
||||
tinyrainbow@2.0.0: {}
|
||||
|
||||
tinyspy@3.0.2: {}
|
||||
|
||||
tmp-promise@3.0.3:
|
||||
dependencies:
|
||||
tmp: 0.2.3
|
||||
|
|
@ -14024,6 +14383,46 @@ snapshots:
|
|||
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:
|
||||
'@vitest/expect': 3.1.4
|
||||
'@vitest/mocker': 3.1.4(vite@6.3.5(@types/node@20.12.11)(jiti@2.4.2)(terser@5.31.0)(yaml@2.8.0))
|
||||
'@vitest/pretty-format': 3.1.4
|
||||
'@vitest/runner': 3.1.4
|
||||
'@vitest/snapshot': 3.1.4
|
||||
'@vitest/spy': 3.1.4
|
||||
'@vitest/utils': 3.1.4
|
||||
chai: 5.2.0
|
||||
debug: 4.4.1
|
||||
expect-type: 1.2.1
|
||||
magic-string: 0.30.17
|
||||
pathe: 2.0.3
|
||||
std-env: 3.9.0
|
||||
tinybench: 2.9.0
|
||||
tinyexec: 0.3.2
|
||||
tinyglobby: 0.2.13
|
||||
tinypool: 1.0.2
|
||||
tinyrainbow: 2.0.0
|
||||
vite: 6.3.5(@types/node@20.12.11)(jiti@2.4.2)(terser@5.31.0)(yaml@2.8.0)
|
||||
vite-node: 3.1.4(@types/node@20.12.11)(jiti@2.4.2)(terser@5.31.0)(yaml@2.8.0)
|
||||
why-is-node-running: 2.3.0
|
||||
optionalDependencies:
|
||||
'@types/debug': 4.1.12
|
||||
'@types/node': 20.12.11
|
||||
transitivePeerDependencies:
|
||||
- jiti
|
||||
- less
|
||||
- lightningcss
|
||||
- msw
|
||||
- sass
|
||||
- sass-embedded
|
||||
- stylus
|
||||
- sugarss
|
||||
- supports-color
|
||||
- terser
|
||||
- tsx
|
||||
- yaml
|
||||
|
||||
vscode-uri@3.1.0: {}
|
||||
|
||||
vt-pbf@3.1.3:
|
||||
|
|
@ -14114,6 +14513,11 @@ snapshots:
|
|||
dependencies:
|
||||
isexe: 3.1.1
|
||||
|
||||
why-is-node-running@2.3.0:
|
||||
dependencies:
|
||||
siginfo: 2.0.0
|
||||
stackback: 0.0.2
|
||||
|
||||
wide-align@1.1.5:
|
||||
dependencies:
|
||||
string-width: 4.2.3
|
||||
|
|
@ -14148,7 +14552,15 @@ snapshots:
|
|||
'@cloudflare/workerd-linux-arm64': 1.20250508.0
|
||||
'@cloudflare/workerd-windows-64': 1.20250508.0
|
||||
|
||||
wrangler@4.16.1(@cloudflare/workers-types@4.20250520.0):
|
||||
workerd@1.20250523.0:
|
||||
optionalDependencies:
|
||||
'@cloudflare/workerd-darwin-64': 1.20250523.0
|
||||
'@cloudflare/workerd-darwin-arm64': 1.20250523.0
|
||||
'@cloudflare/workerd-linux-64': 1.20250523.0
|
||||
'@cloudflare/workerd-linux-arm64': 1.20250523.0
|
||||
'@cloudflare/workerd-windows-64': 1.20250523.0
|
||||
|
||||
wrangler@4.16.1:
|
||||
dependencies:
|
||||
'@cloudflare/kv-asset-handler': 0.4.0
|
||||
'@cloudflare/unenv-preset': 2.3.2(unenv@2.0.0-rc.17)(workerd@1.20250508.0)
|
||||
|
|
@ -14159,13 +14571,28 @@ snapshots:
|
|||
unenv: 2.0.0-rc.17
|
||||
workerd: 1.20250508.0
|
||||
optionalDependencies:
|
||||
'@cloudflare/workers-types': 4.20250520.0
|
||||
fsevents: 2.3.3
|
||||
sharp: 0.33.5
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- utf-8-validate
|
||||
|
||||
wrangler@4.17.0:
|
||||
dependencies:
|
||||
'@cloudflare/kv-asset-handler': 0.4.0
|
||||
'@cloudflare/unenv-preset': 2.3.2(unenv@2.0.0-rc.17)(workerd@1.20250523.0)
|
||||
blake3-wasm: 2.1.5
|
||||
esbuild: 0.25.4
|
||||
miniflare: 4.20250523.0
|
||||
path-to-regexp: 6.3.0
|
||||
unenv: 2.0.0-rc.17
|
||||
workerd: 1.20250523.0
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- utf-8-validate
|
||||
|
||||
wrap-ansi@7.0.0:
|
||||
dependencies:
|
||||
ansi-styles: 4.3.0
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export const LinkSchema = z.object({
|
|||
updatedAt: z.number().int().safe().default(() => Math.floor(Date.now() / 1000)),
|
||||
expiration: z.number().int().safe().refine(expiration => expiration > Math.floor(Date.now() / 1000), {
|
||||
message: 'expiration must be greater than current time',
|
||||
path: ['expiration'], // 这里指定错误消息关联到哪个字段
|
||||
path: ['expiration'],
|
||||
}).optional(),
|
||||
title: z.string().trim().max(2048).optional(),
|
||||
description: z.string().trim().max(2048).optional(),
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ defineRouteMeta({
|
|||
export default eventHandler((event) => {
|
||||
const { request: { cf } } = event.context.cloudflare
|
||||
return {
|
||||
latitude: cf.latitude,
|
||||
longitude: cf.longitude,
|
||||
latitude: cf?.latitude,
|
||||
longitude: cf?.longitude,
|
||||
}
|
||||
})
|
||||
|
|
|
|||
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)
|
||||
})
|
||||
})
|
||||
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')
|
||||
}
|
||||
}
|
||||
9
tests/sink.spec.ts
Normal file
9
tests/sink.spec.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import { fetch } from './utils'
|
||||
|
||||
describe('/', () => {
|
||||
it('returns 200 for homepage request', async () => {
|
||||
const response = await fetch('/')
|
||||
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
|
||||
"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