Fixing everything, making the project structure ready for real code.
Some checks failed
Tests / Frontend Tests (TypeScript + Vue + Yarn Berry) (push) Failing after 7m49s
Tests / Backend Tests (Python 3.13 + uv) (push) Failing after 14m36s

Signed-off-by: Cliff Hill <xlorep@darkhelm.org>
This commit is contained in:
2025-10-23 12:58:32 -04:00
parent 17a8ab1a06
commit 2c8f424a81
13 changed files with 173 additions and 562 deletions

View File

@@ -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!"

View File

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

View File

@@ -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\"')",

View File

@@ -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"]

View File

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

View File

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

View File

@@ -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]

View File

@@ -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
View 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();
});

View File

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

View File

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

View File

@@ -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: {

View File

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