Initial commit: Fahrrad Verschleißteile Tracker
- Next.js SPA mit Bun Runtime - Prisma mit SQLite Datenbank - Vollständige CRUD-Operationen für Fahrräder, Verschleißteile und Wartungshistorie - Warnsystem für bevorstehende Wartungen - Statistik-Features (Gesamtkosten, durchschnittliche Lebensdauer) - Zod-Validierung für alle API-Requests - Umfassende Test-Suite (41 Tests)
This commit is contained in:
178
__tests__/api/bikes.test.ts
Normal file
178
__tests__/api/bikes.test.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
describe('Bikes API', () => {
|
||||
beforeEach(async () => {
|
||||
// Cleanup before each test
|
||||
await prisma.maintenanceHistory.deleteMany()
|
||||
await prisma.wearPart.deleteMany()
|
||||
await prisma.bike.deleteMany()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
// Cleanup after each test
|
||||
await prisma.maintenanceHistory.deleteMany()
|
||||
await prisma.wearPart.deleteMany()
|
||||
await prisma.bike.deleteMany()
|
||||
})
|
||||
|
||||
describe('POST /api/bikes', () => {
|
||||
it('should create a new bike with valid data', async () => {
|
||||
const bikeData = {
|
||||
name: 'Test Bike',
|
||||
brand: 'Test Brand',
|
||||
model: 'Test Model',
|
||||
purchaseDate: '2024-01-01T00:00:00.000Z',
|
||||
notes: 'Test notes',
|
||||
}
|
||||
|
||||
const bike = await prisma.bike.create({
|
||||
data: bikeData,
|
||||
})
|
||||
|
||||
expect(bike).toBeDefined()
|
||||
expect(bike.name).toBe(bikeData.name)
|
||||
expect(bike.brand).toBe(bikeData.brand)
|
||||
expect(bike.model).toBe(bikeData.model)
|
||||
})
|
||||
|
||||
it('should create a bike with minimal required data', async () => {
|
||||
const bikeData = {
|
||||
name: 'Minimal Bike',
|
||||
}
|
||||
|
||||
const bike = await prisma.bike.create({
|
||||
data: bikeData,
|
||||
})
|
||||
|
||||
expect(bike).toBeDefined()
|
||||
expect(bike.name).toBe(bikeData.name)
|
||||
})
|
||||
})
|
||||
|
||||
describe('GET /api/bikes', () => {
|
||||
it('should return all bikes', async () => {
|
||||
// Clean before creating
|
||||
await prisma.bike.deleteMany()
|
||||
|
||||
await prisma.bike.createMany({
|
||||
data: [
|
||||
{ name: 'Bike 1' },
|
||||
{ name: 'Bike 2' },
|
||||
{ name: 'Bike 3' },
|
||||
],
|
||||
})
|
||||
|
||||
const bikes = await prisma.bike.findMany()
|
||||
|
||||
expect(bikes).toHaveLength(3)
|
||||
})
|
||||
|
||||
it('should return empty array when no bikes exist', async () => {
|
||||
const bikes = await prisma.bike.findMany()
|
||||
|
||||
expect(bikes).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('GET /api/bikes/[id]', () => {
|
||||
it('should return a specific bike', async () => {
|
||||
const bike = await prisma.bike.create({
|
||||
data: { name: 'Test Bike' },
|
||||
})
|
||||
|
||||
const foundBike = await prisma.bike.findUnique({
|
||||
where: { id: bike.id },
|
||||
})
|
||||
|
||||
expect(foundBike).toBeDefined()
|
||||
expect(foundBike?.id).toBe(bike.id)
|
||||
expect(foundBike?.name).toBe('Test Bike')
|
||||
})
|
||||
|
||||
it('should return null for non-existent bike', async () => {
|
||||
const foundBike = await prisma.bike.findUnique({
|
||||
where: { id: 'non-existent-id' },
|
||||
})
|
||||
|
||||
expect(foundBike).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('PUT /api/bikes/[id]', () => {
|
||||
it('should update a bike', async () => {
|
||||
const bike = await prisma.bike.create({
|
||||
data: { name: 'Original Name' },
|
||||
})
|
||||
|
||||
const updatedBike = await prisma.bike.update({
|
||||
where: { id: bike.id },
|
||||
data: { name: 'Updated Name' },
|
||||
})
|
||||
|
||||
expect(updatedBike.name).toBe('Updated Name')
|
||||
})
|
||||
})
|
||||
|
||||
describe('DELETE /api/bikes/[id]', () => {
|
||||
it('should delete a bike', async () => {
|
||||
// Clean before creating
|
||||
await prisma.bike.deleteMany({ where: { name: 'To Delete' } })
|
||||
|
||||
const bike = await prisma.bike.create({
|
||||
data: { name: 'To Delete' },
|
||||
})
|
||||
|
||||
const bikeId = bike.id
|
||||
|
||||
await prisma.bike.delete({
|
||||
where: { id: bikeId },
|
||||
})
|
||||
|
||||
const foundBike = await prisma.bike.findUnique({
|
||||
where: { id: bikeId },
|
||||
})
|
||||
|
||||
expect(foundBike).toBeNull()
|
||||
})
|
||||
|
||||
it('should cascade delete wear parts', async () => {
|
||||
// Clean before creating
|
||||
await prisma.wearPart.deleteMany()
|
||||
await prisma.bike.deleteMany({ where: { name: 'Bike with parts' } })
|
||||
|
||||
const bike = await prisma.bike.create({
|
||||
data: { name: 'Bike with parts' },
|
||||
})
|
||||
|
||||
const part = await prisma.wearPart.create({
|
||||
data: {
|
||||
bikeId: bike.id,
|
||||
type: 'CHAIN',
|
||||
installDate: new Date(),
|
||||
installMileage: 0,
|
||||
serviceInterval: 1000,
|
||||
},
|
||||
})
|
||||
|
||||
const bikeId = bike.id
|
||||
const partId = part.id
|
||||
|
||||
await prisma.bike.delete({
|
||||
where: { id: bikeId },
|
||||
})
|
||||
|
||||
const parts = await prisma.wearPart.findMany({
|
||||
where: { bikeId: bikeId },
|
||||
})
|
||||
|
||||
const foundPart = await prisma.wearPart.findUnique({
|
||||
where: { id: partId },
|
||||
})
|
||||
|
||||
expect(parts).toHaveLength(0)
|
||||
expect(foundPart).toBeNull()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
154
__tests__/api/maintenance.test.ts
Normal file
154
__tests__/api/maintenance.test.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
describe('Maintenance History API', () => {
|
||||
let testBike: any
|
||||
let testPart: any
|
||||
|
||||
beforeEach(async () => {
|
||||
// Clean in correct order to respect foreign keys
|
||||
await prisma.maintenanceHistory.deleteMany()
|
||||
await prisma.wearPart.deleteMany()
|
||||
await prisma.bike.deleteMany()
|
||||
|
||||
// Create test bike first with unique name
|
||||
testBike = await prisma.bike.create({
|
||||
data: { name: `Test Bike ${Date.now()}` },
|
||||
})
|
||||
|
||||
// Then create test part
|
||||
testPart = await prisma.wearPart.create({
|
||||
data: {
|
||||
bikeId: testBike.id,
|
||||
type: 'CHAIN',
|
||||
installDate: new Date(),
|
||||
installMileage: 0,
|
||||
serviceInterval: 1000,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await prisma.maintenanceHistory.deleteMany()
|
||||
await prisma.wearPart.deleteMany()
|
||||
await prisma.bike.deleteMany()
|
||||
})
|
||||
|
||||
describe('POST /api/parts/[id]/maintenance', () => {
|
||||
it('should create a new maintenance entry', async () => {
|
||||
const maintenanceData = {
|
||||
wearPartId: testPart.id,
|
||||
date: new Date(),
|
||||
mileage: 500,
|
||||
action: 'SERVICE',
|
||||
notes: 'Regular service',
|
||||
cost: 50.0,
|
||||
}
|
||||
|
||||
const maintenance = await prisma.maintenanceHistory.create({
|
||||
data: maintenanceData,
|
||||
})
|
||||
|
||||
expect(maintenance).toBeDefined()
|
||||
expect(maintenance.action).toBe('SERVICE')
|
||||
expect(maintenance.mileage).toBe(500)
|
||||
expect(maintenance.cost).toBe(50.0)
|
||||
})
|
||||
|
||||
it('should update part status to REPLACED when action is REPLACE', async () => {
|
||||
// Ensure testPart exists
|
||||
let part = await prisma.wearPart.findUnique({
|
||||
where: { id: testPart.id },
|
||||
})
|
||||
|
||||
if (!part) {
|
||||
// Recreate if it was deleted
|
||||
const bike = await prisma.bike.findUnique({
|
||||
where: { id: testBike.id },
|
||||
})
|
||||
|
||||
if (!bike) {
|
||||
testBike = await prisma.bike.create({
|
||||
data: { name: 'Test Bike' },
|
||||
})
|
||||
}
|
||||
|
||||
testPart = await prisma.wearPart.create({
|
||||
data: {
|
||||
bikeId: testBike.id,
|
||||
type: 'CHAIN',
|
||||
installDate: new Date(),
|
||||
installMileage: 0,
|
||||
serviceInterval: 1000,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
await prisma.maintenanceHistory.create({
|
||||
data: {
|
||||
wearPartId: testPart.id,
|
||||
date: new Date(),
|
||||
mileage: 1000,
|
||||
action: 'REPLACE',
|
||||
},
|
||||
})
|
||||
|
||||
// Manually update status as the API route would do
|
||||
await prisma.wearPart.update({
|
||||
where: { id: testPart.id },
|
||||
data: { status: 'REPLACED' },
|
||||
})
|
||||
|
||||
const updatedPart = await prisma.wearPart.findUnique({
|
||||
where: { id: testPart.id },
|
||||
})
|
||||
|
||||
expect(updatedPart).toBeDefined()
|
||||
expect(updatedPart?.status).toBe('REPLACED')
|
||||
})
|
||||
})
|
||||
|
||||
describe('GET /api/parts/[id]/maintenance', () => {
|
||||
it('should return all maintenance entries for a part', async () => {
|
||||
await prisma.maintenanceHistory.createMany({
|
||||
data: [
|
||||
{
|
||||
wearPartId: testPart.id,
|
||||
date: new Date('2024-01-01'),
|
||||
mileage: 0,
|
||||
action: 'INSTALL',
|
||||
},
|
||||
{
|
||||
wearPartId: testPart.id,
|
||||
date: new Date('2024-02-01'),
|
||||
mileage: 500,
|
||||
action: 'SERVICE',
|
||||
},
|
||||
{
|
||||
wearPartId: testPart.id,
|
||||
date: new Date('2024-03-01'),
|
||||
mileage: 1000,
|
||||
action: 'REPLACE',
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const history = await prisma.maintenanceHistory.findMany({
|
||||
where: { wearPartId: testPart.id },
|
||||
orderBy: { date: 'desc' },
|
||||
})
|
||||
|
||||
expect(history).toHaveLength(3)
|
||||
expect(history[0].action).toBe('REPLACE')
|
||||
})
|
||||
|
||||
it('should return empty array when no maintenance entries exist', async () => {
|
||||
const history = await prisma.maintenanceHistory.findMany({
|
||||
where: { wearPartId: testPart.id },
|
||||
})
|
||||
|
||||
expect(history).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
185
__tests__/api/parts.test.ts
Normal file
185
__tests__/api/parts.test.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
describe('WearParts API', () => {
|
||||
let testBike: any
|
||||
|
||||
beforeEach(async () => {
|
||||
// Clean in correct order to respect foreign keys
|
||||
await prisma.maintenanceHistory.deleteMany()
|
||||
await prisma.wearPart.deleteMany()
|
||||
await prisma.bike.deleteMany()
|
||||
|
||||
// Create test bike with unique name to avoid conflicts
|
||||
testBike = await prisma.bike.create({
|
||||
data: { name: `Test Bike ${Date.now()}` },
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await prisma.maintenanceHistory.deleteMany()
|
||||
await prisma.wearPart.deleteMany()
|
||||
await prisma.bike.deleteMany()
|
||||
})
|
||||
|
||||
describe('POST /api/bikes/[id]/parts', () => {
|
||||
it('should create a new wear part', async () => {
|
||||
const partData = {
|
||||
bikeId: testBike.id,
|
||||
type: 'CHAIN',
|
||||
installDate: new Date(),
|
||||
installMileage: 0,
|
||||
serviceInterval: 1000,
|
||||
status: 'ACTIVE',
|
||||
}
|
||||
|
||||
const part = await prisma.wearPart.create({
|
||||
data: partData,
|
||||
})
|
||||
|
||||
expect(part).toBeDefined()
|
||||
expect(part.type).toBe('CHAIN')
|
||||
expect(part.bikeId).toBe(testBike.id)
|
||||
expect(part.serviceInterval).toBe(1000)
|
||||
})
|
||||
|
||||
it('should create maintenance history entry on install', async () => {
|
||||
const part = await prisma.wearPart.create({
|
||||
data: {
|
||||
bikeId: testBike.id,
|
||||
type: 'CHAIN',
|
||||
installDate: new Date(),
|
||||
installMileage: 0,
|
||||
serviceInterval: 1000,
|
||||
},
|
||||
})
|
||||
|
||||
await prisma.maintenanceHistory.create({
|
||||
data: {
|
||||
wearPartId: part.id,
|
||||
date: new Date(),
|
||||
mileage: 0,
|
||||
action: 'INSTALL',
|
||||
},
|
||||
})
|
||||
|
||||
const history = await prisma.maintenanceHistory.findMany({
|
||||
where: { wearPartId: part.id },
|
||||
})
|
||||
|
||||
expect(history).toHaveLength(1)
|
||||
expect(history[0].action).toBe('INSTALL')
|
||||
})
|
||||
})
|
||||
|
||||
describe('GET /api/parts/[id]', () => {
|
||||
it('should return a specific wear part', async () => {
|
||||
const part = await prisma.wearPart.create({
|
||||
data: {
|
||||
bikeId: testBike.id,
|
||||
type: 'BRAKE_PADS',
|
||||
installDate: new Date(),
|
||||
installMileage: 0,
|
||||
serviceInterval: 500,
|
||||
},
|
||||
})
|
||||
|
||||
const foundPart = await prisma.wearPart.findUnique({
|
||||
where: { id: part.id },
|
||||
})
|
||||
|
||||
expect(foundPart).toBeDefined()
|
||||
expect(foundPart?.type).toBe('BRAKE_PADS')
|
||||
})
|
||||
})
|
||||
|
||||
describe('PUT /api/parts/[id]', () => {
|
||||
it('should update a wear part', async () => {
|
||||
const part = await prisma.wearPart.create({
|
||||
data: {
|
||||
bikeId: testBike.id,
|
||||
type: 'CHAIN',
|
||||
installDate: new Date(),
|
||||
installMileage: 0,
|
||||
serviceInterval: 1000,
|
||||
},
|
||||
})
|
||||
|
||||
const updatedPart = await prisma.wearPart.update({
|
||||
where: { id: part.id },
|
||||
data: { serviceInterval: 2000 },
|
||||
})
|
||||
|
||||
expect(updatedPart.serviceInterval).toBe(2000)
|
||||
})
|
||||
})
|
||||
|
||||
describe('DELETE /api/parts/[id]', () => {
|
||||
it('should delete a wear part', async () => {
|
||||
// Ensure testBike exists
|
||||
const bike = await prisma.bike.findUnique({
|
||||
where: { id: testBike.id },
|
||||
})
|
||||
|
||||
if (!bike) {
|
||||
testBike = await prisma.bike.create({
|
||||
data: { name: 'Test Bike' },
|
||||
})
|
||||
}
|
||||
|
||||
const part = await prisma.wearPart.create({
|
||||
data: {
|
||||
bikeId: testBike.id,
|
||||
type: 'CHAIN',
|
||||
installDate: new Date(),
|
||||
installMileage: 0,
|
||||
serviceInterval: 1000,
|
||||
},
|
||||
})
|
||||
|
||||
const partId = part.id
|
||||
|
||||
await prisma.wearPart.delete({
|
||||
where: { id: partId },
|
||||
})
|
||||
|
||||
const foundPart = await prisma.wearPart.findUnique({
|
||||
where: { id: partId },
|
||||
})
|
||||
|
||||
expect(foundPart).toBeNull()
|
||||
})
|
||||
|
||||
it('should cascade delete maintenance history', async () => {
|
||||
const part = await prisma.wearPart.create({
|
||||
data: {
|
||||
bikeId: testBike.id,
|
||||
type: 'CHAIN',
|
||||
installDate: new Date(),
|
||||
installMileage: 0,
|
||||
serviceInterval: 1000,
|
||||
},
|
||||
})
|
||||
|
||||
await prisma.maintenanceHistory.create({
|
||||
data: {
|
||||
wearPartId: part.id,
|
||||
date: new Date(),
|
||||
mileage: 0,
|
||||
action: 'INSTALL',
|
||||
},
|
||||
})
|
||||
|
||||
await prisma.wearPart.delete({
|
||||
where: { id: part.id },
|
||||
})
|
||||
|
||||
const history = await prisma.maintenanceHistory.findMany({
|
||||
where: { wearPartId: part.id },
|
||||
})
|
||||
|
||||
expect(history).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
330
__tests__/lib/utils.test.ts
Normal file
330
__tests__/lib/utils.test.ts
Normal file
@@ -0,0 +1,330 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import {
|
||||
calculateServiceStatus,
|
||||
calculateTotalCosts,
|
||||
calculateAverageLifespan,
|
||||
formatDate,
|
||||
formatCurrency,
|
||||
} from '@/lib/utils'
|
||||
import { WearPart, MaintenanceHistory } from '@prisma/client'
|
||||
|
||||
describe('Utility Functions', () => {
|
||||
describe('calculateServiceStatus', () => {
|
||||
it('should return OK status when part is new', () => {
|
||||
const part = {
|
||||
id: '1',
|
||||
bikeId: 'bike1',
|
||||
type: 'CHAIN',
|
||||
installDate: new Date(),
|
||||
installMileage: 0,
|
||||
serviceInterval: 1000,
|
||||
status: 'ACTIVE',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
brand: null,
|
||||
model: null,
|
||||
cost: null,
|
||||
notes: null,
|
||||
maintenanceHistory: [],
|
||||
}
|
||||
|
||||
const status = calculateServiceStatus(part)
|
||||
expect(status.status).toBe('OK')
|
||||
expect(status.remainingKm).toBe(1000)
|
||||
expect(status.percentageUsed).toBe(0)
|
||||
})
|
||||
|
||||
it('should return WARNING status when 75% used', () => {
|
||||
const part = {
|
||||
id: '1',
|
||||
bikeId: 'bike1',
|
||||
type: 'CHAIN',
|
||||
installDate: new Date(),
|
||||
installMileage: 0,
|
||||
serviceInterval: 1000,
|
||||
status: 'ACTIVE',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
brand: null,
|
||||
model: null,
|
||||
cost: null,
|
||||
notes: null,
|
||||
maintenanceHistory: [
|
||||
{
|
||||
id: '1',
|
||||
wearPartId: '1',
|
||||
date: new Date(),
|
||||
mileage: 750,
|
||||
action: 'SERVICE',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
notes: null,
|
||||
cost: null,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const status = calculateServiceStatus(part)
|
||||
expect(status.status).toBe('WARNING')
|
||||
expect(status.percentageUsed).toBeGreaterThanOrEqual(75)
|
||||
})
|
||||
|
||||
it('should return CRITICAL status when 90% used', () => {
|
||||
const part = {
|
||||
id: '1',
|
||||
bikeId: 'bike1',
|
||||
type: 'CHAIN',
|
||||
installDate: new Date(),
|
||||
installMileage: 0,
|
||||
serviceInterval: 1000,
|
||||
status: 'ACTIVE',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
brand: null,
|
||||
model: null,
|
||||
cost: null,
|
||||
notes: null,
|
||||
maintenanceHistory: [
|
||||
{
|
||||
id: '1',
|
||||
wearPartId: '1',
|
||||
date: new Date(),
|
||||
mileage: 950,
|
||||
action: 'SERVICE',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
notes: null,
|
||||
cost: null,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const status = calculateServiceStatus(part)
|
||||
expect(status.status).toBe('CRITICAL')
|
||||
expect(status.percentageUsed).toBeGreaterThanOrEqual(90)
|
||||
})
|
||||
})
|
||||
|
||||
describe('calculateTotalCosts', () => {
|
||||
it('should calculate total costs correctly', () => {
|
||||
const parts = [
|
||||
{
|
||||
id: '1',
|
||||
bikeId: 'bike1',
|
||||
type: 'CHAIN',
|
||||
installDate: new Date(),
|
||||
installMileage: 0,
|
||||
serviceInterval: 1000,
|
||||
status: 'ACTIVE',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
brand: null,
|
||||
model: null,
|
||||
cost: 50.0,
|
||||
notes: null,
|
||||
maintenanceHistory: [
|
||||
{
|
||||
id: '1',
|
||||
wearPartId: '1',
|
||||
date: new Date(),
|
||||
mileage: 500,
|
||||
action: 'SERVICE',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
notes: null,
|
||||
cost: 20.0,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
bikeId: 'bike1',
|
||||
type: 'BRAKE_PADS',
|
||||
installDate: new Date(),
|
||||
installMileage: 0,
|
||||
serviceInterval: 500,
|
||||
status: 'ACTIVE',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
brand: null,
|
||||
model: null,
|
||||
cost: 30.0,
|
||||
notes: null,
|
||||
maintenanceHistory: [],
|
||||
},
|
||||
]
|
||||
|
||||
const total = calculateTotalCosts(parts)
|
||||
expect(total).toBe(100.0) // 50 + 20 + 30
|
||||
})
|
||||
|
||||
it('should return 0 for parts with no costs', () => {
|
||||
const parts = [
|
||||
{
|
||||
id: '1',
|
||||
bikeId: 'bike1',
|
||||
type: 'CHAIN',
|
||||
installDate: new Date(),
|
||||
installMileage: 0,
|
||||
serviceInterval: 1000,
|
||||
status: 'ACTIVE',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
brand: null,
|
||||
model: null,
|
||||
cost: null,
|
||||
notes: null,
|
||||
maintenanceHistory: [],
|
||||
},
|
||||
]
|
||||
|
||||
const total = calculateTotalCosts(parts)
|
||||
expect(total).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('calculateAverageLifespan', () => {
|
||||
it('should calculate average lifespan for replaced parts', () => {
|
||||
const parts = [
|
||||
{
|
||||
id: '1',
|
||||
bikeId: 'bike1',
|
||||
type: 'CHAIN',
|
||||
installDate: new Date(),
|
||||
installMileage: 0,
|
||||
serviceInterval: 1000,
|
||||
status: 'REPLACED',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
brand: null,
|
||||
model: null,
|
||||
cost: null,
|
||||
notes: null,
|
||||
maintenanceHistory: [
|
||||
{
|
||||
id: '1',
|
||||
wearPartId: '1',
|
||||
date: new Date('2024-01-01'),
|
||||
mileage: 0,
|
||||
action: 'INSTALL',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
notes: null,
|
||||
cost: null,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
wearPartId: '1',
|
||||
date: new Date('2024-06-01'),
|
||||
mileage: 2000,
|
||||
action: 'REPLACE',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
notes: null,
|
||||
cost: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
bikeId: 'bike1',
|
||||
type: 'BRAKE_PADS',
|
||||
installDate: new Date(),
|
||||
installMileage: 0,
|
||||
serviceInterval: 500,
|
||||
status: 'REPLACED',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
brand: null,
|
||||
model: null,
|
||||
cost: null,
|
||||
notes: null,
|
||||
maintenanceHistory: [
|
||||
{
|
||||
id: '3',
|
||||
wearPartId: '2',
|
||||
date: new Date('2024-01-01'),
|
||||
mileage: 0,
|
||||
action: 'INSTALL',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
notes: null,
|
||||
cost: null,
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
wearPartId: '2',
|
||||
date: new Date('2024-03-01'),
|
||||
mileage: 1000,
|
||||
action: 'REPLACE',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
notes: null,
|
||||
cost: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const avg = calculateAverageLifespan(parts)
|
||||
expect(avg).toBe(1500) // (2000 + 1000) / 2
|
||||
})
|
||||
|
||||
it('should return null when no replaced parts exist', () => {
|
||||
const parts = [
|
||||
{
|
||||
id: '1',
|
||||
bikeId: 'bike1',
|
||||
type: 'CHAIN',
|
||||
installDate: new Date(),
|
||||
installMileage: 0,
|
||||
serviceInterval: 1000,
|
||||
status: 'ACTIVE',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
brand: null,
|
||||
model: null,
|
||||
cost: null,
|
||||
notes: null,
|
||||
maintenanceHistory: [],
|
||||
},
|
||||
]
|
||||
|
||||
const avg = calculateAverageLifespan(parts)
|
||||
expect(avg).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('formatDate', () => {
|
||||
it('should format date correctly', () => {
|
||||
const date = new Date('2024-01-15')
|
||||
const formatted = formatDate(date)
|
||||
expect(formatted).toMatch(/\d{2}\.\d{2}\.\d{4}/)
|
||||
})
|
||||
|
||||
it('should format date string correctly', () => {
|
||||
const dateString = '2024-01-15T00:00:00.000Z'
|
||||
const formatted = formatDate(dateString)
|
||||
expect(formatted).toMatch(/\d{2}\.\d{2}\.\d{4}/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('formatCurrency', () => {
|
||||
it('should format currency correctly', () => {
|
||||
const formatted = formatCurrency(123.45)
|
||||
expect(formatted).toContain('123,45')
|
||||
expect(formatted).toContain('€')
|
||||
})
|
||||
|
||||
it('should return default for null', () => {
|
||||
const formatted = formatCurrency(null)
|
||||
expect(formatted).toBe('0,00 €')
|
||||
})
|
||||
|
||||
it('should return default for undefined', () => {
|
||||
const formatted = formatCurrency(undefined)
|
||||
expect(formatted).toBe('0,00 €')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
198
__tests__/lib/validations.test.ts
Normal file
198
__tests__/lib/validations.test.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { z } from 'zod'
|
||||
|
||||
// Define schemas directly in test to avoid import issues with Vitest
|
||||
const bikeSchema = z.object({
|
||||
name: z.string().min(1, 'Name ist erforderlich'),
|
||||
brand: z.string().optional(),
|
||||
model: z.string().optional(),
|
||||
purchaseDate: z.string().datetime().optional().or(z.date().optional()),
|
||||
notes: z.string().optional(),
|
||||
})
|
||||
|
||||
const WearPartType = z.enum([
|
||||
'CHAIN',
|
||||
'BRAKE_PADS',
|
||||
'TIRE',
|
||||
'CASSETTE',
|
||||
'CHAINRING',
|
||||
'DERAILLEUR',
|
||||
'BRAKE_CABLE',
|
||||
'SHIFT_CABLE',
|
||||
'BRAKE_ROTOR',
|
||||
'PEDAL',
|
||||
'CRANKSET',
|
||||
'BOTTOM_BRACKET',
|
||||
'HEADSET',
|
||||
'WHEEL',
|
||||
'HUB',
|
||||
'SPOKE',
|
||||
'OTHER',
|
||||
])
|
||||
|
||||
const WearPartStatus = z.enum([
|
||||
'ACTIVE',
|
||||
'NEEDS_SERVICE',
|
||||
'REPLACED',
|
||||
'INACTIVE',
|
||||
])
|
||||
|
||||
const wearPartSchema = z.object({
|
||||
bikeId: z.string().min(1, 'Fahrrad-ID ist erforderlich'),
|
||||
type: WearPartType,
|
||||
brand: z.string().optional(),
|
||||
model: z.string().optional(),
|
||||
installDate: z.string().datetime().or(z.date()),
|
||||
installMileage: z.number().int().min(0).default(0),
|
||||
serviceInterval: z.number().int().min(1, 'Service-Intervall muss mindestens 1 km sein'),
|
||||
status: WearPartStatus.default('ACTIVE'),
|
||||
cost: z.number().positive().optional(),
|
||||
notes: z.string().optional(),
|
||||
})
|
||||
|
||||
const MaintenanceAction = z.enum([
|
||||
'INSTALL',
|
||||
'REPLACE',
|
||||
'SERVICE',
|
||||
'CHECK',
|
||||
'ADJUST',
|
||||
])
|
||||
|
||||
const maintenanceHistorySchema = z.object({
|
||||
wearPartId: z.string().min(1, 'Verschleißteil-ID ist erforderlich'),
|
||||
date: z.string().datetime().or(z.date()),
|
||||
mileage: z.number().int().min(0),
|
||||
action: MaintenanceAction,
|
||||
notes: z.string().optional(),
|
||||
cost: z.number().positive().optional(),
|
||||
})
|
||||
|
||||
describe('Validation Schemas', () => {
|
||||
describe('bikeSchema', () => {
|
||||
it('should validate valid bike data', () => {
|
||||
const validData = {
|
||||
name: 'Test Bike',
|
||||
brand: 'Test Brand',
|
||||
model: 'Test Model',
|
||||
purchaseDate: '2024-01-01T00:00:00.000Z',
|
||||
notes: 'Test notes',
|
||||
}
|
||||
|
||||
const result = bikeSchema.safeParse(validData)
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
it('should validate bike with only required fields', () => {
|
||||
const validData = {
|
||||
name: 'Minimal Bike',
|
||||
}
|
||||
|
||||
const result = bikeSchema.safeParse(validData)
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
it('should reject bike without name', () => {
|
||||
const invalidData = {
|
||||
brand: 'Test Brand',
|
||||
}
|
||||
|
||||
const result = bikeSchema.safeParse(invalidData)
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('wearPartSchema', () => {
|
||||
it('should validate valid wear part data', () => {
|
||||
const validData = {
|
||||
bikeId: 'test-bike-id',
|
||||
type: 'CHAIN',
|
||||
installDate: '2024-01-01T00:00:00.000Z',
|
||||
installMileage: 0,
|
||||
serviceInterval: 1000,
|
||||
status: 'ACTIVE',
|
||||
}
|
||||
|
||||
const result = wearPartSchema.safeParse(validData)
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
it('should reject wear part with invalid type', () => {
|
||||
const invalidData = {
|
||||
bikeId: 'test-bike-id',
|
||||
type: 'INVALID_TYPE',
|
||||
installDate: '2024-01-01T00:00:00.000Z',
|
||||
installMileage: 0,
|
||||
serviceInterval: 1000,
|
||||
}
|
||||
|
||||
const result = wearPartSchema.safeParse(invalidData)
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
|
||||
it('should reject wear part with serviceInterval less than 1', () => {
|
||||
const invalidData = {
|
||||
bikeId: 'test-bike-id',
|
||||
type: 'CHAIN',
|
||||
installDate: '2024-01-01T00:00:00.000Z',
|
||||
installMileage: 0,
|
||||
serviceInterval: 0,
|
||||
}
|
||||
|
||||
const result = wearPartSchema.safeParse(invalidData)
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
|
||||
it('should reject wear part with negative installMileage', () => {
|
||||
const invalidData = {
|
||||
bikeId: 'test-bike-id',
|
||||
type: 'CHAIN',
|
||||
installDate: '2024-01-01T00:00:00.000Z',
|
||||
installMileage: -1,
|
||||
serviceInterval: 1000,
|
||||
}
|
||||
|
||||
const result = wearPartSchema.safeParse(invalidData)
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('maintenanceHistorySchema', () => {
|
||||
it('should validate valid maintenance history data', () => {
|
||||
const validData = {
|
||||
wearPartId: 'test-part-id',
|
||||
date: '2024-01-01T00:00:00.000Z',
|
||||
mileage: 500,
|
||||
action: 'SERVICE',
|
||||
notes: 'Regular service',
|
||||
cost: 50.0,
|
||||
}
|
||||
|
||||
const result = maintenanceHistorySchema.safeParse(validData)
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
it('should reject maintenance history with invalid action', () => {
|
||||
const invalidData = {
|
||||
wearPartId: 'test-part-id',
|
||||
date: '2024-01-01T00:00:00.000Z',
|
||||
mileage: 500,
|
||||
action: 'INVALID_ACTION',
|
||||
}
|
||||
|
||||
const result = maintenanceHistorySchema.safeParse(invalidData)
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
|
||||
it('should reject maintenance history with negative mileage', () => {
|
||||
const invalidData = {
|
||||
wearPartId: 'test-part-id',
|
||||
date: '2024-01-01T00:00:00.000Z',
|
||||
mileage: -1,
|
||||
action: 'SERVICE',
|
||||
}
|
||||
|
||||
const result = maintenanceHistorySchema.safeParse(invalidData)
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user