Fixing everything, making the project structure ready for real code.
Signed-off-by: Cliff Hill <xlorep@darkhelm.org>
This commit is contained in:
@@ -148,14 +148,14 @@ jobs:
|
||||
|
||||
echo "=== Running Backend Tests ==="
|
||||
|
||||
# Run pytest with typeguard and coverage
|
||||
# Run pytest with automatic typeguard hooks and coverage
|
||||
python -m pytest tests/ -v \
|
||||
--cov=backend \
|
||||
--cov-report=term-missing \
|
||||
--cov-report=xml \
|
||||
--typeguard-packages=backend
|
||||
--cov-fail-under=95
|
||||
|
||||
echo "✓ Backend tests completed with typeguard and coverage!"
|
||||
echo "✓ Backend tests completed with automatic typeguard hooks and 95% coverage!"
|
||||
|
||||
frontend-tests:
|
||||
name: Frontend Tests (TypeScript + Vue + Yarn Berry)
|
||||
@@ -205,9 +205,9 @@ jobs:
|
||||
echo "Repository setup complete"
|
||||
ls -la
|
||||
|
||||
- name: Install Node.js 20
|
||||
- name: Install Node.js 24
|
||||
run: |
|
||||
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
|
||||
curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash -
|
||||
sudo apt-get install -y nodejs
|
||||
|
||||
- name: Setup Yarn Berry
|
||||
@@ -278,4 +278,4 @@ jobs:
|
||||
# Run Vitest tests with coverage
|
||||
yarn test:coverage
|
||||
|
||||
echo "✓ Frontend tests completed with coverage!"
|
||||
echo "✓ Frontend tests completed with automatic Zod validation hooks and 85% coverage!"
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
"""Pytest configuration for automatic typeguard integration."""
|
||||
|
||||
from typeguard import install_import_hook
|
||||
|
||||
def pytest_configure(config):
|
||||
|
||||
def pytest_configure(config) -> None: # noqa: ARG001
|
||||
"""Configure pytest to use typeguard automatically."""
|
||||
# This enables typeguard for all functions with type hints
|
||||
# without requiring @typechecked decorators
|
||||
pass
|
||||
# Install typeguard import hook to automatically check all functions
|
||||
# with type hints in the backend package during test runs
|
||||
install_import_hook("backend")
|
||||
|
||||
# Also check any other packages in src/
|
||||
install_import_hook("src")
|
||||
|
||||
print("🛡️ Automatic typeguard hooks installed for test run")
|
||||
|
||||
|
||||
def pytest_runtest_setup(item):
|
||||
"""Set up typeguard for each test run."""
|
||||
# Import hook is automatically enabled via --typeguard-packages
|
||||
# The import hook is automatically enabled via pytest_configure
|
||||
# All functions with type hints will be automatically checked
|
||||
pass
|
||||
|
||||
@@ -15,31 +15,6 @@ dev = [
|
||||
"pytest-mock>=3.12.0"
|
||||
]
|
||||
|
||||
[project]
|
||||
dependencies = [
|
||||
"fastapi>=0.104.0",
|
||||
"uvicorn[standard]>=0.24.0",
|
||||
"sqlalchemy>=2.0.0",
|
||||
"pydantic>=2.5.0"
|
||||
]
|
||||
description = "Plex Playlist Management API"
|
||||
name = "plex-playlist-backend"
|
||||
requires-python = ">=3.12"
|
||||
version = "0.1.0"
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"ruff>=0.6.0",
|
||||
"pyright>=1.1.380",
|
||||
"darglint>=1.8.1",
|
||||
"pytest>=7.4.0",
|
||||
"pytest-asyncio>=0.21.0",
|
||||
"pytest-cov>=4.1.0",
|
||||
"typeguard>=4.1.0",
|
||||
"httpx>=0.25.0", # For testing async HTTP calls
|
||||
"pytest-mock>=3.12.0"
|
||||
]
|
||||
|
||||
[tool.coverage]
|
||||
|
||||
[tool.coverage.report]
|
||||
@@ -78,8 +53,7 @@ addopts = [
|
||||
"--cov=backend",
|
||||
"--cov-report=term-missing:skip-covered",
|
||||
"--cov-report=html",
|
||||
"--cov-report=xml",
|
||||
"--typeguard-packages=backend"
|
||||
"--cov-report=xml"
|
||||
]
|
||||
markers = [
|
||||
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
"""
|
||||
Plex Playlist Backend Package.
|
||||
|
||||
This package provides the FastAPI backend for playlist management
|
||||
with automatic typeguard validation.
|
||||
"""
|
||||
|
||||
from .main import PlaylistManager, app, calculate_playlist_duration, process_user_data
|
||||
from .main import app
|
||||
|
||||
__all__ = ["app", "process_user_data", "calculate_playlist_duration", "PlaylistManager"]
|
||||
__all__ = ["app"]
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
"""
|
||||
Hypermodern Python setup for automatic typeguard validation.
|
||||
|
||||
This approach enables runtime type checking without requiring decorators
|
||||
or explicit validation calls in your application code.
|
||||
Plex Playlist Backend API.
|
||||
"""
|
||||
|
||||
from fastapi import FastAPI
|
||||
@@ -10,13 +7,10 @@ from fastapi import FastAPI
|
||||
# Create FastAPI application instance
|
||||
app = FastAPI(
|
||||
title="Plex Playlist Backend",
|
||||
description="API for managing Plex playlists with automatic type validation",
|
||||
description="API for managing Plex playlists",
|
||||
version="0.1.0",
|
||||
)
|
||||
|
||||
# No imports needed in application code - typeguard works automatically
|
||||
# when enabled via pytest configuration
|
||||
|
||||
|
||||
@app.get("/")
|
||||
def read_root() -> dict[str, str]:
|
||||
@@ -28,93 +22,3 @@ def read_root() -> dict[str, str]:
|
||||
def health_check() -> dict[str, str]:
|
||||
"""Health check endpoint."""
|
||||
return {"status": "healthy"}
|
||||
|
||||
|
||||
def process_user_data(user_id: int, name: str, email: str) -> dict[str, str | int]:
|
||||
"""Process user data with automatic type validation.
|
||||
|
||||
This function has type hints, and when running under pytest with
|
||||
--typeguard-packages, all calls will be automatically validated
|
||||
without any decorators or explicit checks.
|
||||
|
||||
Args:
|
||||
user_id: Unique identifier for the user
|
||||
name: User's full name
|
||||
email: User's email address
|
||||
|
||||
Returns:
|
||||
Dictionary containing processed user information
|
||||
"""
|
||||
return {
|
||||
"id": user_id,
|
||||
"name": name.title(),
|
||||
"email": email.lower(),
|
||||
"display_name": f"{name} ({email})",
|
||||
}
|
||||
|
||||
|
||||
def calculate_playlist_duration(track_durations: list[float]) -> float:
|
||||
"""Calculate total playlist duration.
|
||||
|
||||
Automatic type validation ensures track_durations is actually a list
|
||||
of floats, without any explicit validation code.
|
||||
|
||||
Args:
|
||||
track_durations: List of track durations in seconds
|
||||
|
||||
Returns:
|
||||
Total duration in seconds
|
||||
"""
|
||||
return sum(track_durations)
|
||||
|
||||
|
||||
class PlaylistManager:
|
||||
"""Playlist management with automatic type validation."""
|
||||
|
||||
def __init__(self, name: str, max_size: int = 1000) -> None:
|
||||
"""Initialize playlist manager.
|
||||
|
||||
Args:
|
||||
name: Name of the playlist manager
|
||||
max_size: Maximum number of tracks allowed
|
||||
"""
|
||||
self.name = name
|
||||
self.max_size = max_size
|
||||
self.tracks: list[str] = []
|
||||
|
||||
def add_track(self, track_id: str) -> bool:
|
||||
"""Add a track to the playlist.
|
||||
|
||||
Args:
|
||||
track_id: Unique identifier for the track
|
||||
|
||||
Returns:
|
||||
True if track was added successfully
|
||||
"""
|
||||
if len(self.tracks) >= self.max_size:
|
||||
return False
|
||||
|
||||
self.tracks.append(track_id)
|
||||
return True
|
||||
|
||||
def get_track_count(self) -> int:
|
||||
"""Get the number of tracks in the playlist.
|
||||
|
||||
Returns:
|
||||
Number of tracks currently in the playlist
|
||||
"""
|
||||
return len(self.tracks)
|
||||
|
||||
|
||||
# API endpoints using the classes and functions with automatic validation
|
||||
@app.post("/api/users")
|
||||
def create_user(user_id: int, name: str, email: str) -> dict[str, str | int]:
|
||||
"""Create user endpoint with automatic type validation."""
|
||||
return process_user_data(user_id, name, email)
|
||||
|
||||
|
||||
@app.post("/api/playlists/{playlist_name}/duration")
|
||||
def calculate_duration(track_durations: list[float]) -> dict[str, float]:
|
||||
"""Calculate playlist duration endpoint."""
|
||||
total_duration = calculate_playlist_duration(track_durations)
|
||||
return {"total_duration": total_duration}
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
"""Example tests for the Plex Playlist backend with automatic typeguard."""
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def add_numbers(a: int, b: int) -> int:
|
||||
"""Add two numbers together.
|
||||
|
||||
Args:
|
||||
a: First number to add
|
||||
b: Second number to add
|
||||
|
||||
Returns:
|
||||
The sum of a and b
|
||||
"""
|
||||
return a + b
|
||||
|
||||
|
||||
def create_playlist_data(
|
||||
name: str, description: str | None = None
|
||||
) -> dict[str, str | None]:
|
||||
"""Create playlist data structure.
|
||||
|
||||
Args:
|
||||
name: Name of the playlist
|
||||
description: Optional description
|
||||
|
||||
Returns:
|
||||
Dictionary containing playlist data
|
||||
"""
|
||||
return {
|
||||
"name": name,
|
||||
"description": description,
|
||||
"id": f"playlist_{name.lower().replace(' ', '_')}",
|
||||
}
|
||||
|
||||
|
||||
class TestMathFunctions:
|
||||
"""Test mathematical functions with automatic type checking."""
|
||||
|
||||
def test_add_numbers_valid_types(self) -> None:
|
||||
"""Test adding numbers with correct types."""
|
||||
result = add_numbers(5, 3)
|
||||
assert result == 8
|
||||
assert isinstance(result, int)
|
||||
|
||||
def test_add_numbers_invalid_types(self) -> None:
|
||||
"""Test that typeguard catches invalid types automatically."""
|
||||
with pytest.raises(TypeError):
|
||||
# Typeguard will automatically catch this type violation
|
||||
add_numbers("5", 3) # type: ignore[arg-type]
|
||||
|
||||
def test_add_numbers_coverage_example(self) -> None:
|
||||
"""Test different number combinations for coverage."""
|
||||
assert add_numbers(0, 0) == 0
|
||||
assert add_numbers(-1, 1) == 0
|
||||
assert add_numbers(100, 200) == 300
|
||||
|
||||
|
||||
class TestPlaylistFunctions:
|
||||
"""Test playlist-related functions with automatic type checking."""
|
||||
|
||||
def test_create_playlist_data_with_description(self) -> None:
|
||||
"""Test creating playlist data with description."""
|
||||
result = create_playlist_data("My Playlist", "A great playlist")
|
||||
|
||||
expected = {
|
||||
"name": "My Playlist",
|
||||
"description": "A great playlist",
|
||||
"id": "playlist_my_playlist",
|
||||
}
|
||||
assert result == expected
|
||||
|
||||
def test_create_playlist_data_without_description(self) -> None:
|
||||
"""Test creating playlist data without description."""
|
||||
result = create_playlist_data("Simple Playlist")
|
||||
|
||||
expected = {
|
||||
"name": "Simple Playlist",
|
||||
"description": None,
|
||||
"id": "playlist_simple_playlist",
|
||||
}
|
||||
assert result == expected
|
||||
|
||||
def test_create_playlist_invalid_name_type(self) -> None:
|
||||
"""Test that typeguard automatically catches invalid name type."""
|
||||
with pytest.raises(TypeError):
|
||||
# No decorator needed - typeguard import hook handles this
|
||||
create_playlist_data(123, "description") # type: ignore[arg-type]
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
class TestIntegrationExample:
|
||||
"""Example integration tests with automatic type validation."""
|
||||
|
||||
def test_integration_placeholder(self) -> None:
|
||||
"""Placeholder for real integration tests."""
|
||||
# All functions called here will have automatic type checking
|
||||
assert True
|
||||
@@ -1,86 +0,0 @@
|
||||
"""
|
||||
Tests demonstrating hypermodern typeguard usage.
|
||||
No decorators needed - validation happens automatically.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from backend import PlaylistManager, calculate_playlist_duration, process_user_data
|
||||
|
||||
|
||||
class TestHypermodernTypeguard:
|
||||
"""Tests showing automatic type validation without decorators."""
|
||||
|
||||
def test_process_user_data_valid_types(self) -> None:
|
||||
"""Test with correct types - should work normally."""
|
||||
result = process_user_data(123, "John Doe", "JOHN@EXAMPLE.COM")
|
||||
|
||||
expected = {
|
||||
"id": 123,
|
||||
"name": "John Doe",
|
||||
"email": "john@example.com",
|
||||
"display_name": "John Doe (john@example.com)",
|
||||
}
|
||||
assert result == expected
|
||||
|
||||
def test_process_user_data_invalid_user_id(self) -> None:
|
||||
"""Test with wrong user_id type - typeguard catches automatically."""
|
||||
with pytest.raises(TypeError):
|
||||
# This will fail because "123" is str, not int
|
||||
process_user_data("123", "John", "john@example.com") # type: ignore[arg-type]
|
||||
|
||||
def test_process_user_data_invalid_name(self) -> None:
|
||||
"""Test with wrong name type - typeguard catches automatically."""
|
||||
with pytest.raises(TypeError):
|
||||
# This will fail because 123 is int, not str
|
||||
process_user_data(123, 123, "john@example.com") # type: ignore[arg-type]
|
||||
|
||||
def test_calculate_duration_valid(self) -> None:
|
||||
"""Test duration calculation with valid types."""
|
||||
durations = [3.5, 4.2, 2.8, 5.1]
|
||||
total = calculate_playlist_duration(durations)
|
||||
|
||||
assert abs(total - 15.6) < 0.001 # Float comparison
|
||||
|
||||
def test_calculate_duration_invalid_list_type(self) -> None:
|
||||
"""Test with wrong parameter type - not a list."""
|
||||
with pytest.raises(TypeError):
|
||||
# This will fail because "not a list" is str, not list[float]
|
||||
calculate_playlist_duration("not a list") # type: ignore[arg-type]
|
||||
|
||||
def test_calculate_duration_invalid_element_types(self) -> None:
|
||||
"""Test with wrong element types in list."""
|
||||
with pytest.raises(TypeError):
|
||||
# This will fail because list contains strings, not floats
|
||||
calculate_playlist_duration(["3.5", "4.2"]) # type: ignore[list-item]
|
||||
|
||||
def test_playlist_manager_creation(self) -> None:
|
||||
"""Test playlist manager creation with valid types."""
|
||||
manager = PlaylistManager("My Playlist", 500)
|
||||
|
||||
assert manager.name == "My Playlist"
|
||||
assert manager.max_size == 500
|
||||
assert manager.get_track_count() == 0
|
||||
|
||||
def test_playlist_manager_invalid_name_type(self) -> None:
|
||||
"""Test playlist manager creation with invalid name type."""
|
||||
with pytest.raises(TypeError):
|
||||
# This will fail because 123 is int, not str
|
||||
PlaylistManager(123) # type: ignore[arg-type]
|
||||
|
||||
def test_playlist_manager_add_track_valid(self) -> None:
|
||||
"""Test adding tracks with valid types."""
|
||||
manager = PlaylistManager("Test Playlist", 2)
|
||||
|
||||
assert manager.add_track("track_1") is True
|
||||
assert manager.add_track("track_2") is True
|
||||
assert manager.add_track("track_3") is False # Exceeds max_size
|
||||
|
||||
assert manager.get_track_count() == 2
|
||||
|
||||
def test_playlist_manager_add_track_invalid_type(self) -> None:
|
||||
"""Test adding track with invalid type."""
|
||||
manager = PlaylistManager("Test Playlist")
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
# This will fail because 123 is int, not str
|
||||
manager.add_track(123) # type: ignore[arg-type]
|
||||
@@ -3,6 +3,9 @@
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=24.6.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc && vite build",
|
||||
|
||||
133
frontend/src/test-setup.ts
Normal file
133
frontend/src/test-setup.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* Automatic Zod validation setup for test runs
|
||||
* This file sets up global hooks that automatically validate all Zod schemas during testing
|
||||
*/
|
||||
|
||||
import { beforeAll, afterAll } from 'vitest';
|
||||
import { z } from 'zod';
|
||||
|
||||
// Store original Zod parse methods
|
||||
const originalParse = z.ZodType.prototype.parse;
|
||||
const originalSafeParse = z.ZodType.prototype.safeParse;
|
||||
|
||||
let validationCount = 0;
|
||||
let validationErrors: string[] = [];
|
||||
|
||||
/**
|
||||
* Enhanced parse method that logs validation attempts during tests
|
||||
* @param data - The data to validate against the schema
|
||||
* @returns The parsed and validated data
|
||||
*/
|
||||
function enhancedParse<T>(this: z.ZodType<T>, data: unknown): T {
|
||||
try {
|
||||
const result = originalParse.call(this, data);
|
||||
validationCount++;
|
||||
|
||||
// Log successful validations in test mode
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
console.log(`✅ Zod validation passed (${validationCount} total)`);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
validationErrors.push(`Zod validation failed: ${error}`);
|
||||
|
||||
// Log failed validations in test mode
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
console.error(`❌ Zod validation failed:`, error);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhanced safeParse method that logs validation attempts during tests
|
||||
* @param data - The data to validate against the schema
|
||||
* @returns Safe parse result with success flag and data or error
|
||||
*/
|
||||
function enhancedSafeParse<T>(
|
||||
this: z.ZodType<T>,
|
||||
data: unknown
|
||||
): z.SafeParseReturnType<unknown, T> {
|
||||
const result = originalSafeParse.call(this, data);
|
||||
validationCount++;
|
||||
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
if (result.success) {
|
||||
console.log(`✅ Zod safe validation passed (${validationCount} total)`);
|
||||
} else {
|
||||
console.warn(`⚠️ Zod safe validation failed but was handled gracefully`);
|
||||
validationErrors.push(`Zod safe validation failed: ${result.error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Install automatic Zod validation hooks for testing
|
||||
*/
|
||||
export function installZodTestHooks(): void {
|
||||
// Replace Zod's parse methods with enhanced versions
|
||||
z.ZodType.prototype.parse = enhancedParse;
|
||||
z.ZodType.prototype.safeParse = enhancedSafeParse;
|
||||
|
||||
// Enable global auto-validation flag
|
||||
(globalThis as any).__AUTO_VALIDATE__ = true;
|
||||
|
||||
console.log('🛡️ Automatic Zod validation hooks installed for test run');
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove automatic Zod validation hooks
|
||||
*/
|
||||
export function uninstallZodTestHooks(): void {
|
||||
// Restore original methods
|
||||
z.ZodType.prototype.parse = originalParse;
|
||||
z.ZodType.prototype.safeParse = originalSafeParse;
|
||||
|
||||
// Disable global auto-validation flag
|
||||
(globalThis as any).__AUTO_VALIDATE__ = false;
|
||||
|
||||
console.log('🔧 Zod validation hooks removed');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get validation statistics for test reporting
|
||||
* @returns Object containing validation count and errors
|
||||
*/
|
||||
export function getValidationStats(): {
|
||||
count: number;
|
||||
errors: string[];
|
||||
} {
|
||||
return {
|
||||
count: validationCount,
|
||||
errors: [...validationErrors],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset validation statistics
|
||||
*/
|
||||
export function resetValidationStats(): void {
|
||||
validationCount = 0;
|
||||
validationErrors = [];
|
||||
}
|
||||
|
||||
// Automatic setup for Vitest
|
||||
beforeAll(() => {
|
||||
installZodTestHooks();
|
||||
resetValidationStats();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
const stats = getValidationStats();
|
||||
console.log(`📊 Test run completed with ${stats.count} Zod validations`);
|
||||
|
||||
if (stats.errors.length > 0) {
|
||||
console.warn(`⚠️ ${stats.errors.length} validation errors encountered during tests`);
|
||||
}
|
||||
|
||||
uninstallZodTestHooks();
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Hypermodern Zod validation setup - automatic runtime validation without explicit .parse() calls
|
||||
* Follows the same philosophy as typeguard for Python
|
||||
* Zod validation setup with automatic test hooks
|
||||
* Schemas are automatically validated during test runs via test-setup.ts
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
@@ -28,99 +28,3 @@ export const UserSchema = z.object({
|
||||
// Inferred types (no manual type definitions needed)
|
||||
export type Playlist = z.infer<typeof PlaylistSchema>;
|
||||
export type User = z.infer<typeof UserSchema>;
|
||||
|
||||
/**
|
||||
* Global schema registry for automatic validation
|
||||
*/
|
||||
class SchemaRegistry {
|
||||
private static schemas = new Map<string, z.ZodTypeAny>([
|
||||
['Playlist', PlaylistSchema as z.ZodTypeAny],
|
||||
['User', UserSchema as z.ZodTypeAny],
|
||||
]);
|
||||
|
||||
static register<T extends z.ZodTypeAny>(name: string, schema: T): void {
|
||||
this.schemas.set(name, schema as z.ZodTypeAny);
|
||||
}
|
||||
|
||||
static get(name: string): z.ZodTypeAny | undefined {
|
||||
return this.schemas.get(name);
|
||||
}
|
||||
|
||||
static validate<T>(typeName: string, data: unknown): T {
|
||||
const schema = this.schemas.get(typeName);
|
||||
if (!schema) {
|
||||
throw new Error(`No schema registered for type: ${typeName}`);
|
||||
}
|
||||
return schema.parse(data) as T;
|
||||
}
|
||||
|
||||
static safeValidate<T>(
|
||||
typeName: string,
|
||||
data: unknown
|
||||
): { success: true; data: T } | { success: false; error: z.ZodError } {
|
||||
const schema = this.schemas.get(typeName);
|
||||
if (!schema) {
|
||||
return {
|
||||
success: false,
|
||||
error: new z.ZodError([
|
||||
{
|
||||
code: 'custom',
|
||||
message: `No schema registered for type: ${typeName}`,
|
||||
path: [],
|
||||
},
|
||||
]),
|
||||
};
|
||||
}
|
||||
|
||||
const result = schema.safeParse(data);
|
||||
return result.success
|
||||
? { success: true, data: result.data as T }
|
||||
: { success: false, error: result.error };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatic validation wrapper - works like typeguard for Python
|
||||
* In development/test: validates automatically
|
||||
* In production: can be stripped by build process
|
||||
*/
|
||||
export class AutoValidator {
|
||||
static validate<T>(typeName: string, data: unknown): T {
|
||||
// Check if auto-validation is enabled (development/test mode)
|
||||
if (typeof globalThis !== 'undefined' && (globalThis as any).__AUTO_VALIDATE__) {
|
||||
return SchemaRegistry.validate<T>(typeName, data);
|
||||
}
|
||||
|
||||
// In production, validation is skipped for performance
|
||||
return data as T;
|
||||
}
|
||||
|
||||
static safeValidate<T>(
|
||||
typeName: string,
|
||||
data: unknown
|
||||
): { success: true; data: T } | { success: false; error: z.ZodError } {
|
||||
if (typeof globalThis !== 'undefined' && (globalThis as any).__AUTO_VALIDATE__) {
|
||||
return SchemaRegistry.safeValidate<T>(typeName, data);
|
||||
}
|
||||
|
||||
// In production, assume data is valid
|
||||
return { success: true, data: data as T };
|
||||
}
|
||||
|
||||
static register<T extends z.ZodTypeAny>(name: string, schema: T): void {
|
||||
SchemaRegistry.register(name, schema);
|
||||
}
|
||||
}
|
||||
|
||||
// Enable automatic validation in development/test environments
|
||||
declare global {
|
||||
var __AUTO_VALIDATE__: boolean;
|
||||
}
|
||||
|
||||
// Check environment and log status
|
||||
const isDev = typeof globalThis !== 'undefined' && (globalThis as any).__AUTO_VALIDATE__;
|
||||
if (isDev) {
|
||||
console.log('🛡️ Hypermodern Zod validation enabled (development/test mode)');
|
||||
} else {
|
||||
console.log('🏃♂️ Production mode - Zod validation optimized away');
|
||||
}
|
||||
|
||||
@@ -1,41 +1,6 @@
|
||||
/**
|
||||
* Example unit tests with automatic Zod validation
|
||||
* Uses hypermodern approach where validation happens automatically
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import App from '@/App.vue'
|
||||
import { AutoValidator, type Playlist, type User } from '@/validation'
|
||||
|
||||
/**
|
||||
* Creates a playlist - validation happens automatically in development/test mode
|
||||
* @param name - Playlist name
|
||||
* @param description - Optional description
|
||||
* @returns Playlist object (automatically validated)
|
||||
*/
|
||||
function createPlaylist(name: string, description?: string): Playlist {
|
||||
const rawData = {
|
||||
id: crypto.randomUUID(),
|
||||
name,
|
||||
description,
|
||||
tracks: []
|
||||
}
|
||||
|
||||
// In hypermodern setup, this would be handled by build-time transform
|
||||
// For now, we use our auto-validator
|
||||
return AutoValidator.validate<Playlist>('Playlist', rawData)
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulates API response handling with automatic validation
|
||||
* @param apiResponse - Raw API data
|
||||
* @returns Validated user object
|
||||
*/
|
||||
function handleUserApiResponse(apiResponse: unknown): User {
|
||||
// Validation happens automatically - no explicit .parse() needed
|
||||
return AutoValidator.validate<User>('User', apiResponse)
|
||||
}
|
||||
|
||||
describe('App.vue', () => {
|
||||
it('renders properly', () => {
|
||||
@@ -43,81 +8,3 @@ describe('App.vue', () => {
|
||||
expect(wrapper.text()).toContain('Plex Playlist')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Automatic Zod validation (hypermodern approach)', () => {
|
||||
it('validates playlist creation automatically', () => {
|
||||
const playlist = createPlaylist('Test Playlist', 'Test description')
|
||||
|
||||
expect(playlist.name).toBe('Test Playlist')
|
||||
expect(playlist.description).toBe('Test description')
|
||||
expect(playlist.tracks).toEqual([])
|
||||
expect(playlist.id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i)
|
||||
})
|
||||
|
||||
it('creates playlist without description automatically', () => {
|
||||
const playlist = createPlaylist('Simple Playlist')
|
||||
|
||||
expect(playlist.name).toBe('Simple Playlist')
|
||||
expect(playlist.description).toBeUndefined()
|
||||
expect(playlist.tracks).toEqual([])
|
||||
})
|
||||
|
||||
it('automatically validates API responses', () => {
|
||||
const apiResponse = {
|
||||
id: 123,
|
||||
username: 'testuser',
|
||||
email: 'test@example.com',
|
||||
preferences: {
|
||||
theme: 'dark',
|
||||
notifications: false
|
||||
}
|
||||
}
|
||||
|
||||
const user = handleUserApiResponse(apiResponse)
|
||||
expect(user.username).toBe('testuser')
|
||||
expect(user.preferences.theme).toBe('dark')
|
||||
})
|
||||
|
||||
it('automatically catches validation errors', () => {
|
||||
const invalidPlaylistData = {
|
||||
id: 'not-a-uuid', // Invalid UUID
|
||||
name: '', // Empty name
|
||||
tracks: 'invalid' // Should be array
|
||||
}
|
||||
|
||||
expect(() => {
|
||||
AutoValidator.validate<Playlist>('Playlist', invalidPlaylistData)
|
||||
}).toThrow()
|
||||
})
|
||||
|
||||
it('provides safe validation with error handling', () => {
|
||||
const malformedData = {
|
||||
id: 'not-a-uuid',
|
||||
description: 'Missing name field'
|
||||
}
|
||||
|
||||
const result = AutoValidator.safeValidate<Playlist>('Playlist', malformedData)
|
||||
|
||||
expect(result.success).toBe(false)
|
||||
if (!result.success) {
|
||||
expect(result.error.issues).toBeDefined()
|
||||
expect(result.error.issues.length).toBeGreaterThan(0)
|
||||
}
|
||||
})
|
||||
|
||||
it('handles successful safe validation', () => {
|
||||
const validData = {
|
||||
id: crypto.randomUUID(),
|
||||
name: 'Valid Playlist',
|
||||
tracks: ['track1', 'track2']
|
||||
}
|
||||
|
||||
const result = AutoValidator.safeValidate<Playlist>('Playlist', validData)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
expect(result.data.name).toBe('Valid Playlist')
|
||||
expect(result.data.tracks).toEqual(['track1', 'track2'])
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,33 +2,10 @@ import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
// Custom plugin for automatic Zod validation in development
|
||||
function zodAutoValidation() {
|
||||
return {
|
||||
name: 'zod-auto-validation',
|
||||
transform(code: string, id: string) {
|
||||
// Only apply in development/test environments
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
return null
|
||||
}
|
||||
|
||||
// Add automatic validation for functions with Zod schemas
|
||||
if (id.includes('src/') && id.endsWith('.ts')) {
|
||||
// This would normally be a more sophisticated transform
|
||||
// For now, we rely on our AutoValidator pattern
|
||||
return null
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
zodAutoValidation()
|
||||
vue()
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
|
||||
@@ -12,6 +12,7 @@ export default defineConfig({
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
globals: true,
|
||||
setupFiles: ['./src/test-setup.ts'], // Automatically install Zod validation hooks
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'json', 'html'],
|
||||
@@ -23,7 +24,15 @@ export default defineConfig({
|
||||
'coverage/',
|
||||
'tests/',
|
||||
'playwright.config.ts'
|
||||
]
|
||||
],
|
||||
thresholds: {
|
||||
global: {
|
||||
lines: 85,
|
||||
functions: 85,
|
||||
branches: 85,
|
||||
statements: 85
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user