@@ -1,414 +0,0 @@
import { describe , it , expect , vi , beforeEach } from "vitest" ;
import { Hono } from "hono" ;
import type { AppEnv , StaffRow } from "../middleware/rbac.js" ;
import { petsRouter } from "../routes/pets.js" ;
// ─── Mock staff fixtures ──────────────────────────────────────────────────────
const MANAGER : StaffRow = {
id : "staff-manager-id" ,
oidcSub : "oidc-manager-sub" ,
userId : null ,
role : "manager" ,
isSuperUser : true ,
name : "Manager McManager" ,
email : "manager@example.com" ,
active : true ,
icalToken : null ,
createdAt : new Date ( ) ,
updatedAt : new Date ( ) ,
} ;
// ─── Mutable mock state ───────────────────────────────────────────────────────
const CLIENT_ID = "a0000000-0000-4000-8000-000000000001" ;
const PET_ID = "b0000000-0000-4000-8000-000000000002" ;
let petRows : Record < string , unknown > [ ] = [ ] ;
let appointmentRows : Record < string , unknown > [ ] = [ ] ;
let insertedValues : Record < string , unknown > [ ] = [ ] ;
let updatedValues : Record < string , unknown > [ ] = [ ] ;
let deletedId : string | null = null ;
function resetMock() {
petRows = [ {
id : PET_ID ,
clientId : CLIENT_ID ,
name : "Biscuit" ,
species : "dog" ,
breed : "Golden Retriever" ,
weightKg : "30.00" ,
dateOfBirth : null ,
healthAlerts : null ,
groomingNotes : null ,
cutStyle : null ,
shampooPreference : null ,
specialCareNotes : null ,
customFields : { } ,
photoKey : null ,
photoUploadedAt : null ,
image : null ,
coatType : null ,
temperamentScore : null ,
temperamentFlags : [ ] ,
medicalAlerts : [ ] ,
preferredCuts : [ ] ,
createdAt : new Date ( ) ,
updatedAt : new Date ( ) ,
} ] ;
appointmentRows = [ ] ;
insertedValues = [ ] ;
updatedValues = [ ] ;
deletedId = null ;
}
function makeSelectChainable ( rows : unknown [ ] ) : unknown {
const chain = new Proxy ( [ . . . rows ] , {
get ( target , prop ) {
if ( prop === "where" || prop === "orderBy" || prop === "limit" ) {
return ( ) = > chain ;
}
// @ts-expect-error proxy
return target [ prop ] ;
} ,
} ) ;
return chain ;
}
function makeInsertChainable ( ) : unknown {
let vals : Record < string , unknown > = { } ;
const chain = new Proxy ( { } , {
get ( target , prop ) {
if ( prop === "values" ) {
return ( v : Record < string , unknown > ) = > { vals = v ; return chain ; } ;
}
if ( prop === "returning" ) {
return ( ) = > {
insertedValues . push ( vals ) ;
return [ vals . id ? { . . . vals , id : vals.id ? ? PET_ID } : { . . . vals , id : PET_ID } ] ;
} ;
}
return chain ;
} ,
} ) ;
return chain ;
}
function makeUpdateChainable ( ) : unknown {
let vals : Record < string , unknown > = { } ;
let whereId : string | null = null ;
const chain = new Proxy ( { } , {
get ( target , prop ) {
if ( prop === "set" ) {
return ( v : Record < string , unknown > ) = > { vals = v ; return chain ; } ;
}
if ( prop === "where" ) {
return ( cond : unknown ) = > {
// Extract id from condition if it's an eq call
if ( whereId ) vals = { . . . vals } ;
return chain ;
} ;
}
if ( prop === "returning" ) {
return ( ) = > {
const merged = { . . . petRows [ 0 ] , . . . vals } ;
updatedValues . push ( vals ) ;
return [ merged ] ;
} ;
}
return chain ;
} ,
} ) ;
return chain ;
}
function makeDeleteChainable ( ) : unknown {
let whereId : string | null = null ;
const chain = new Proxy ( { } , {
get ( target , prop ) {
if ( prop === "where" ) {
return ( cond : unknown ) = > {
whereId = PET_ID ;
return chain ;
} ;
}
if ( prop === "returning" ) {
return ( ) = > {
const row = petRows [ 0 ] ;
deletedId = row . id as string ;
return [ row ] ;
} ;
}
return chain ;
} ,
} ) ;
return chain ;
}
vi . mock ( "../db" , ( ) = > {
const pets = new Proxy ( { _name : "pets" } , { get : ( t , p ) = > p === "_name" ? "pets" : { } } ) ;
const appointments = new Proxy ( { _name : "appointments" } , { get : ( t , p ) = > p === "_name" ? "appointments" : { } } ) ;
return {
getDb : ( ) = > ( {
select : ( ) = > ( {
from : ( table : unknown ) = > {
const name = ( table as { _name? : string } ) . _name ;
if ( name === "appointments" ) return makeSelectChainable ( appointmentRows ) ;
return makeSelectChainable ( petRows ) ;
} ,
} ) ,
insert : ( ) = > makeInsertChainable ( ) ,
update : ( ) = > makeUpdateChainable ( ) ,
delete : ( ) = > makeDeleteChainable ( ) ,
} ) ,
pets ,
appointments ,
and : ( . . . conds : unknown [ ] ) = > conds ,
eq : ( col : unknown , val : unknown ) = > ( { col , val } ) ,
exists : ( q : unknown ) = > q ,
or : ( . . . conds : unknown [ ] ) = > conds ,
} ;
} ) ;
// ─── Helpers ──────────────────────────────────────────────────────────────────
function makeApp ( staff : StaffRow = MANAGER ) {
const app = new Hono < AppEnv > ( ) ;
app . use ( "*" , async ( c , next ) = > {
c . set ( "staff" , staff ) ;
await next ( ) ;
} ) ;
return app . route ( "/pets" , petsRouter ) ;
}
function createApp() {
const app = makeApp ( MANAGER ) ;
return app ;
}
// ─── Tests ────────────────────────────────────────────────────────────────────
describe ( "Extended pet profile fields — validation" , ( ) = > {
beforeEach ( resetMock ) ;
it ( "rejects temperamentScore of 0 (below min)" , async ( ) = > {
const app = createApp ( ) ;
const res = await app . request ( "/pets" , {
method : "POST" ,
headers : { "Content-Type" : "application/json" } ,
body : JSON.stringify ( { clientId : CLIENT_ID , name : "Test" , species : "dog" , temperamentScore : 0 } ) ,
} ) ;
expect ( res . status ) . toBe ( 400 ) ;
const body = await res . json ( ) ;
expect ( body . success ) . toBe ( false ) ;
} ) ;
it ( "rejects temperamentScore of 6 (above max)" , async ( ) = > {
const app = createApp ( ) ;
const res = await app . request ( "/pets" , {
method : "POST" ,
headers : { "Content-Type" : "application/json" } ,
body : JSON.stringify ( { clientId : CLIENT_ID , name : "Test" , species : "dog" , temperamentScore : 6 } ) ,
} ) ;
expect ( res . status ) . toBe ( 400 ) ;
const body = await res . json ( ) ;
expect ( body . success ) . toBe ( false ) ;
} ) ;
it ( "rejects non-integer temperamentScore" , async ( ) = > {
const app = createApp ( ) ;
const res = await app . request ( "/pets" , {
method : "POST" ,
headers : { "Content-Type" : "application/json" } ,
body : JSON.stringify ( { clientId : CLIENT_ID , name : "Test" , species : "dog" , temperamentScore : 3.5 } ) ,
} ) ;
expect ( res . status ) . toBe ( 400 ) ;
} ) ;
it ( "rejects invalid medicalAlert severity" , async ( ) = > {
const app = createApp ( ) ;
const res = await app . request ( "/pets" , {
method : "POST" ,
headers : { "Content-Type" : "application/json" } ,
body : JSON.stringify ( {
clientId : CLIENT_ID ,
name : "Test" ,
species : "dog" ,
medicalAlerts : [ { type : "seizure" , description : "xyz" , severity : "critical" } ] ,
} ) ,
} ) ;
expect ( res . status ) . toBe ( 400 ) ;
} ) ;
it ( "accepts valid temperamentScore 1– 5" , async ( ) = > {
const app = createApp ( ) ;
for ( const score of [ 1 , 2 , 3 , 4 , 5 ] ) {
resetMock ( ) ;
const res = await app . request ( "/pets" , {
method : "POST" ,
headers : { "Content-Type" : "application/json" } ,
body : JSON.stringify ( { clientId : CLIENT_ID , name : "Test" , species : "dog" , temperamentScore : score } ) ,
} ) ;
expect ( res . status ) . toBe ( 201 ) ;
}
} ) ;
it ( "accepts all valid medicalAlert severity values" , async ( ) = > {
const app = createApp ( ) ;
for ( const severity of [ "low" , "medium" , "high" ] as const ) {
resetMock ( ) ;
const res = await app . request ( "/pets" , {
method : "POST" ,
headers : { "Content-Type" : "application/json" } ,
body : JSON.stringify ( {
clientId : CLIENT_ID ,
name : "Test" ,
species : "dog" ,
medicalAlerts : [ { type : "allergy" , description : "Sensitive to chicken" , severity } ] ,
} ) ,
} ) ;
expect ( res . status ) . toBe ( 201 ) ;
}
} ) ;
} ) ;
describe ( "Extended pet profile fields — create" , ( ) = > {
beforeEach ( resetMock ) ;
it ( "accepts all extended fields on create" , async ( ) = > {
const app = createApp ( ) ;
const res = await app . request ( "/pets" , {
method : "POST" ,
headers : { "Content-Type" : "application/json" } ,
body : JSON.stringify ( {
clientId : CLIENT_ID ,
name : "Biscuit" ,
species : "dog" ,
breed : "Golden Retriever" ,
coatType : "double" ,
temperamentScore : 4 ,
temperamentFlags : [ "anxious_with_dryers" , "gentle" ] ,
medicalAlerts : [
{ type : "seizure" , description : "Occasional episodes" , severity : "medium" } ,
] ,
preferredCuts : [ "puppy cut" , "teddy bear" ] ,
} ) ,
} ) ;
expect ( res . status ) . toBe ( 201 ) ;
const body = await res . json ( ) ;
expect ( body . coatType ) . toBe ( "double" ) ;
expect ( body . temperamentScore ) . toBe ( 4 ) ;
expect ( body . temperamentFlags ) . toEqual ( [ "anxious_with_dryers" , "gentle" ] ) ;
expect ( body . medicalAlerts ) . toEqual ( [ { type : "seizure" , description : "Occasional episodes" , severity : "medium" } ] ) ;
expect ( body . preferredCuts ) . toEqual ( [ "puppy cut" , "teddy bear" ] ) ;
} ) ;
it ( "create without extended fields works (all optional)" , async ( ) = > {
const app = createApp ( ) ;
const res = await app . request ( "/pets" , {
method : "POST" ,
headers : { "Content-Type" : "application/json" } ,
body : JSON.stringify ( { clientId : CLIENT_ID , name : "Basil" , species : "cat" } ) ,
} ) ;
expect ( res . status ) . toBe ( 201 ) ;
} ) ;
} ) ;
describe ( "Extended pet profile fields — update" , ( ) = > {
beforeEach ( resetMock ) ;
it ( "updates coatType" , async ( ) = > {
const app = createApp ( ) ;
const res = await app . request ( ` /pets/ ${ PET_ID } ` , {
method : "PATCH" ,
headers : { "Content-Type" : "application/json" } ,
body : JSON.stringify ( { coatType : "smooth" } ) ,
} ) ;
expect ( res . status ) . toBe ( 200 ) ;
const body = await res . json ( ) ;
expect ( body . coatType ) . toBe ( "smooth" ) ;
} ) ;
it ( "updates temperamentScore" , async ( ) = > {
const app = createApp ( ) ;
const res = await app . request ( ` /pets/ ${ PET_ID } ` , {
method : "PATCH" ,
headers : { "Content-Type" : "application/json" } ,
body : JSON.stringify ( { temperamentScore : 2 } ) ,
} ) ;
expect ( res . status ) . toBe ( 200 ) ;
const body = await res . json ( ) ;
expect ( body . temperamentScore ) . toBe ( 2 ) ;
} ) ;
it ( "rejects temperamentScore 0 on update" , async ( ) = > {
const app = createApp ( ) ;
const res = await app . request ( ` /pets/ ${ PET_ID } ` , {
method : "PATCH" ,
headers : { "Content-Type" : "application/json" } ,
body : JSON.stringify ( { temperamentScore : 0 } ) ,
} ) ;
expect ( res . status ) . toBe ( 400 ) ;
} ) ;
it ( "rejects invalid severity on update" , async ( ) = > {
const app = createApp ( ) ;
const res = await app . request ( ` /pets/ ${ PET_ID } ` , {
method : "PATCH" ,
headers : { "Content-Type" : "application/json" } ,
body : JSON.stringify ( {
medicalAlerts : [ { type : "x" , description : "y" , severity : "urgent" } ] ,
} ) ,
} ) ;
expect ( res . status ) . toBe ( 400 ) ;
} ) ;
it ( "rejects too many temperamentFlags (>20)" , async ( ) = > {
const app = createApp ( ) ;
const flags = Array . from ( { length : 21 } , ( _ , i ) = > ` flag_ ${ i } ` ) ;
const res = await app . request ( "/pets" , {
method : "POST" ,
headers : { "Content-Type" : "application/json" } ,
body : JSON.stringify ( { clientId : CLIENT_ID , name : "Test" , species : "dog" , temperamentFlags : flags } ) ,
} ) ;
expect ( res . status ) . toBe ( 400 ) ;
} ) ;
it ( "rejects too many preferredCuts (>20)" , async ( ) = > {
const app = createApp ( ) ;
const cuts = Array . from ( { length : 21 } , ( _ , i ) = > ` cut_ ${ i } ` ) ;
const res = await app . request ( "/pets" , {
method : "POST" ,
headers : { "Content-Type" : "application/json" } ,
body : JSON.stringify ( { clientId : CLIENT_ID , name : "Test" , species : "dog" , preferredCuts : cuts } ) ,
} ) ;
expect ( res . status ) . toBe ( 400 ) ;
} ) ;
it ( "rejects too many medicalAlerts (>50)" , async ( ) = > {
const app = createApp ( ) ;
const alerts = Array . from ( { length : 51 } , ( _ , i ) = > ( {
type : ` type_ ${ i } ` ,
description : ` desc_ ${ i } ` ,
severity : "low" as const ,
} ) ) ;
const res = await app . request ( "/pets" , {
method : "POST" ,
headers : { "Content-Type" : "application/json" } ,
body : JSON.stringify ( { clientId : CLIENT_ID , name : "Test" , species : "dog" , medicalAlerts : alerts } ) ,
} ) ;
expect ( res . status ) . toBe ( 400 ) ;
} ) ;
it ( "returns extended fields in GET response" , async ( ) = > {
petRows = [ { . . . petRows [ 0 ] , coatType : "wire" , temperamentScore : 3 , temperamentFlags : [ "gentle" ] , medicalAlerts : [ ] , preferredCuts : [ "scissor cut" ] } ] ;
const app = createApp ( ) ;
const res = await app . request ( ` /pets/ ${ PET_ID } ` ) ;
expect ( res . status ) . toBe ( 200 ) ;
const body = await res . json ( ) ;
expect ( body . coatType ) . toBe ( "wire" ) ;
expect ( body . temperamentScore ) . toBe ( 3 ) ;
expect ( body . temperamentFlags ) . toEqual ( [ "gentle" ] ) ;
expect ( body . preferredCuts ) . toEqual ( [ "scissor cut" ] ) ;
} ) ;
} ) ;