Skip to content

Commit 3521f20

Browse files
puckeylehni
authored andcommitted
Add controller and validator tests
- Add controller action name routing/rejection tests - Add validator tests for range, custom keywords, and custom formats - Add Keyword, Format, and Schema type tests - Support validator option in createTestApp
1 parent 343827d commit 3521f20

4 files changed

Lines changed: 633 additions & 2 deletions

File tree

tests/server/controller.test.ts

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
import type {
2+
ModelControllerActions,
3+
ModelProperties
4+
} from '@ditojs/server'
5+
import { Model, ModelController } from '@ditojs/server'
6+
import {
7+
createTestApp,
8+
getAppUrl
9+
} from '../utils/app.js'
10+
import { createTestDatabase } from '../utils/database.js'
11+
12+
class Task extends Model {
13+
declare id: number
14+
declare name: string
15+
16+
static override properties: ModelProperties = {
17+
name: {
18+
type: 'string',
19+
required: true
20+
}
21+
}
22+
}
23+
24+
describe('Controller action names', () => {
25+
it('rejects bare action names like "test"', async () => {
26+
class Tasks extends ModelController<Task> {
27+
override modelClass = Task
28+
29+
override collection: ModelControllerActions<Tasks> = {
30+
allow: ['get'],
31+
// Bare action name — not prefixed with an HTTP method
32+
test(ctx) {
33+
return { ok: true }
34+
}
35+
}
36+
}
37+
38+
const app = createTestApp({
39+
models: { Task },
40+
controllers: { Tasks }
41+
})
42+
43+
await createTestDatabase(app)
44+
await expect(app.setup()).rejects.toThrow(/Unsupported HTTP method/)
45+
await app.knex?.destroy()
46+
})
47+
48+
it('does not route default actions when allow is omitted', async () => {
49+
class Tasks extends ModelController<Task> {
50+
override modelClass = Task
51+
52+
override collection: ModelControllerActions<Tasks> = {}
53+
}
54+
55+
const app = createTestApp({
56+
models: { Task },
57+
controllers: { Tasks }
58+
})
59+
60+
await createTestDatabase(app)
61+
await app.start()
62+
63+
try {
64+
const url = getAppUrl(app)
65+
const resp = await fetch(`${url}/tasks`)
66+
expect(resp.status).toBe(404)
67+
} finally {
68+
await app.stop()
69+
await app.knex?.destroy()
70+
}
71+
})
72+
73+
it('routes default actions listed in allow', async () => {
74+
class Tasks extends ModelController<Task> {
75+
override modelClass = Task
76+
77+
override collection: ModelControllerActions<Tasks> = {
78+
allow: ['get']
79+
}
80+
}
81+
82+
const app = createTestApp({
83+
models: { Task },
84+
controllers: { Tasks }
85+
})
86+
87+
await createTestDatabase(app)
88+
await app.start()
89+
90+
try {
91+
const url = getAppUrl(app)
92+
const resp = await fetch(`${url}/tasks`)
93+
expect(resp.status).toBe(200)
94+
} finally {
95+
await app.stop()
96+
await app.knex?.destroy()
97+
}
98+
})
99+
100+
it('returns 501 for default actions not in allow list', async () => {
101+
class Tasks extends ModelController<Task> {
102+
override modelClass = Task
103+
104+
override collection: ModelControllerActions<Tasks> = {
105+
allow: ['get']
106+
}
107+
}
108+
109+
const app = createTestApp({
110+
models: { Task },
111+
controllers: { Tasks }
112+
})
113+
114+
await createTestDatabase(app)
115+
await app.start()
116+
117+
try {
118+
const url = getAppUrl(app)
119+
// POST (create) is not in allow list
120+
const resp = await fetch(`${url}/tasks`, {
121+
method: 'POST',
122+
headers: { 'Content-Type': 'application/json' },
123+
body: JSON.stringify({ name: 'test' })
124+
})
125+
expect(resp.status).toBe(501)
126+
} finally {
127+
await app.stop()
128+
await app.knex?.destroy()
129+
}
130+
})
131+
132+
it('child allow replaces parent — parent actions need re-listing', async () => {
133+
class BaseTasks extends ModelController<Task> {
134+
override modelClass = Task
135+
136+
override collection: ModelControllerActions<BaseTasks> = {
137+
allow: ['get'],
138+
'get stats'(ctx) {
139+
ctx.body = { count: 0 }
140+
}
141+
}
142+
}
143+
144+
class Tasks extends BaseTasks {
145+
override collection: ModelControllerActions<Tasks> = {
146+
// Only lists 'get', does NOT list 'get stats'
147+
allow: ['get']
148+
}
149+
}
150+
151+
const app = createTestApp({
152+
models: { Task },
153+
controllers: { Tasks }
154+
})
155+
156+
await createTestDatabase(app)
157+
await app.start()
158+
159+
try {
160+
const url = getAppUrl(app)
161+
// Default 'get' works
162+
const get = await fetch(`${url}/tasks`)
163+
expect(get.status).toBe(200)
164+
// Parent's 'get stats' is NOT routed because child's
165+
// allow replaced the allowMap without listing it
166+
const stats = await fetch(`${url}/tasks/stats`)
167+
expect(stats.status).toBe(404)
168+
} finally {
169+
await app.stop()
170+
await app.knex?.destroy()
171+
}
172+
})
173+
174+
it('parent actions are routed when child has no allow', async () => {
175+
class BaseTasks extends ModelController<Task> {
176+
override modelClass = Task
177+
178+
override collection: ModelControllerActions<BaseTasks> = {
179+
allow: ['get'],
180+
'get stats'(ctx) {
181+
ctx.body = { count: 0 }
182+
}
183+
}
184+
}
185+
186+
class Tasks extends BaseTasks {
187+
// No override — inherits parent's collection
188+
}
189+
190+
const app = createTestApp({
191+
models: { Task },
192+
controllers: { Tasks }
193+
})
194+
195+
await createTestDatabase(app)
196+
await app.start()
197+
198+
try {
199+
const url = getAppUrl(app)
200+
const stats = await fetch(`${url}/tasks/stats`)
201+
expect(stats.status).toBe(200)
202+
expect(await stats.json()).toEqual({ count: 0 })
203+
} finally {
204+
await app.stop()
205+
await app.knex?.destroy()
206+
}
207+
})
208+
209+
it('accepts and routes "post test" action', async () => {
210+
class Tasks extends ModelController<Task> {
211+
override modelClass = Task
212+
213+
override collection: ModelControllerActions<Tasks> = {
214+
allow: ['get', 'post test'],
215+
'post test'(ctx) {
216+
ctx.body = { ok: true }
217+
}
218+
}
219+
}
220+
221+
const app = createTestApp({
222+
models: { Task },
223+
controllers: { Tasks }
224+
})
225+
226+
await createTestDatabase(app)
227+
await app.start()
228+
229+
try {
230+
const url = getAppUrl(app)
231+
const resp = await fetch(`${url}/tasks/test`, {
232+
method: 'POST',
233+
headers: { 'Content-Type': 'application/json' }
234+
})
235+
expect(resp.status).toBe(200)
236+
expect(await resp.json()).toEqual({ ok: true })
237+
} finally {
238+
await app.stop()
239+
await app.knex?.destroy()
240+
}
241+
})
242+
})

0 commit comments

Comments
 (0)