Skip to content

Commit 5fde320

Browse files
committed
test: add channels integration tests
1 parent 4ffb120 commit 5fde320

File tree

1 file changed

+308
-0
lines changed

1 file changed

+308
-0
lines changed

tests/integration/channels.test.js

Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,308 @@
1+
/**
2+
* tests/integration/channels.test.js
3+
*
4+
* Tests all channel-related HTTP routes:
5+
* GET /channels
6+
* POST /channels
7+
* DELETE /channels/:name
8+
* PATCH /channels/:name (rename)
9+
* POST /channels/:name/pin
10+
*/
11+
12+
import { describe, it, expect, beforeEach } from 'vitest'
13+
import request from 'supertest'
14+
import { setup, resetDb, getApp, insertItem, insertChannel } from '../helpers/setup.js'
15+
16+
setup()
17+
18+
beforeEach(async () => {
19+
await resetDb()
20+
})
21+
22+
// ─────────────────────────────────────────────────────────────────────────────
23+
// GET /channels
24+
// ─────────────────────────────────────────────────────────────────────────────
25+
26+
describe('GET /channels', () => {
27+
it('returns the four default channels after reset', async () => {
28+
const res = await request(getApp()).get('/channels')
29+
30+
expect(res.status).toBe(200)
31+
expect(res.body).toHaveLength(4)
32+
const names = res.body.map(c => c.name)
33+
expect(names).toContain('general')
34+
expect(names).toContain('projects')
35+
expect(names).toContain('assets')
36+
expect(names).toContain('temp')
37+
})
38+
39+
it('returns pinned channels first', async () => {
40+
await request(getApp()).post('/channels/general/pin')
41+
42+
const res = await request(getApp()).get('/channels')
43+
expect(res.body[0].name).toBe('general')
44+
expect(res.body[0].pinned).toBe(1)
45+
})
46+
47+
it('includes id, name, and pinned fields on each channel', async () => {
48+
const res = await request(getApp()).get('/channels')
49+
const ch = res.body[0]
50+
51+
expect(ch).toHaveProperty('id')
52+
expect(ch).toHaveProperty('name')
53+
expect(ch).toHaveProperty('pinned')
54+
})
55+
})
56+
57+
// ─────────────────────────────────────────────────────────────────────────────
58+
// POST /channels
59+
// ─────────────────────────────────────────────────────────────────────────────
60+
61+
describe('POST /channels', () => {
62+
it('creates a new channel and returns it', async () => {
63+
const res = await request(getApp())
64+
.post('/channels')
65+
.send({ name: 'design' })
66+
67+
expect(res.status).toBe(200)
68+
expect(res.body.name).toBe('design')
69+
expect(res.body.id).toBeDefined()
70+
})
71+
72+
it('new channel appears in GET /channels', async () => {
73+
await request(getApp()).post('/channels').send({ name: 'design' })
74+
75+
const res = await request(getApp()).get('/channels')
76+
expect(res.body.some(c => c.name === 'design')).toBe(true)
77+
})
78+
79+
it('trims whitespace from channel name', async () => {
80+
const res = await request(getApp())
81+
.post('/channels')
82+
.send({ name: ' design ' })
83+
84+
expect(res.status).toBe(200)
85+
expect(res.body.name).toBe('design')
86+
})
87+
88+
it('returns 400 when name is missing', async () => {
89+
const res = await request(getApp())
90+
.post('/channels')
91+
.send({})
92+
93+
expect(res.status).toBe(400)
94+
})
95+
96+
it('returns 400 for a duplicate channel name', async () => {
97+
await request(getApp()).post('/channels').send({ name: 'design' })
98+
99+
const res = await request(getApp())
100+
.post('/channels')
101+
.send({ name: 'design' })
102+
103+
expect(res.status).toBe(400)
104+
})
105+
106+
it('returns 400 for name longer than 32 characters', async () => {
107+
const res = await request(getApp())
108+
.post('/channels')
109+
.send({ name: 'a'.repeat(33) })
110+
111+
expect(res.status).toBe(400)
112+
})
113+
114+
it('returns 400 for name with invalid characters', async () => {
115+
const res = await request(getApp())
116+
.post('/channels')
117+
.send({ name: 'bad@name!' })
118+
119+
expect(res.status).toBe(400)
120+
})
121+
122+
it('accepts names with letters, numbers, hyphens, underscores, spaces', async () => {
123+
const valid = ['my-channel', 'my_channel', 'channel 1', 'Channel2']
124+
for (const name of valid) {
125+
const res = await request(getApp()).post('/channels').send({ name })
126+
expect(res.status).toBe(200)
127+
// clean up between iterations
128+
await request(getApp()).delete(`/channels/${name}`)
129+
}
130+
})
131+
132+
it('returns 400 when channel limit of 10 is reached', async () => {
133+
// already have 4 default channels — add 6 more to hit the limit
134+
for (let i = 5; i <= 10; i++) {
135+
await request(getApp()).post('/channels').send({ name: `ch${i}` })
136+
}
137+
138+
const res = await request(getApp())
139+
.post('/channels')
140+
.send({ name: 'overflow' })
141+
142+
expect(res.status).toBe(400)
143+
expect(res.body.error).toMatch(/max/i)
144+
})
145+
})
146+
147+
// ─────────────────────────────────────────────────────────────────────────────
148+
// DELETE /channels/:name
149+
// ─────────────────────────────────────────────────────────────────────────────
150+
151+
describe('DELETE /channels/:name', () => {
152+
it('deletes a channel and returns 200', async () => {
153+
await request(getApp()).post('/channels').send({ name: 'to-delete' })
154+
155+
const res = await request(getApp()).delete('/channels/to-delete')
156+
expect(res.status).toBe(200)
157+
})
158+
159+
it('deleted channel no longer appears in GET /channels', async () => {
160+
await request(getApp()).post('/channels').send({ name: 'to-delete' })
161+
await request(getApp()).delete('/channels/to-delete')
162+
163+
const res = await request(getApp()).get('/channels')
164+
expect(res.body.some(c => c.name === 'to-delete')).toBe(false)
165+
})
166+
167+
it('also deletes all items inside the channel', async () => {
168+
await request(getApp()).post('/channels').send({ name: 'doomed' })
169+
await insertItem({ content: 'will be gone', channel: 'doomed' })
170+
171+
await request(getApp()).delete('/channels/doomed')
172+
173+
const res = await request(getApp()).get('/items/doomed')
174+
expect(res.body.items).toHaveLength(0)
175+
})
176+
177+
it('returns 404 for a non-existent channel', async () => {
178+
const res = await request(getApp()).delete('/channels/doesnotexist')
179+
expect(res.status).toBe(404)
180+
})
181+
182+
it('returns 403 when trying to delete a pinned channel', async () => {
183+
await request(getApp()).post('/channels/general/pin')
184+
185+
const res = await request(getApp()).delete('/channels/general')
186+
expect(res.status).toBe(403)
187+
})
188+
189+
it('returns 400 when trying to delete the last remaining channel', async () => {
190+
// delete 3 of the 4 default channels
191+
await request(getApp()).delete('/channels/projects')
192+
await request(getApp()).delete('/channels/assets')
193+
await request(getApp()).delete('/channels/temp')
194+
195+
// now only general remains — should be protected
196+
const res = await request(getApp()).delete('/channels/general')
197+
expect(res.status).toBe(400)
198+
expect(res.body.error).toMatch(/at least one/i)
199+
})
200+
})
201+
202+
// ─────────────────────────────────────────────────────────────────────────────
203+
// PATCH /channels/:name (rename)
204+
// ─────────────────────────────────────────────────────────────────────────────
205+
206+
describe('PATCH /channels/:name (rename)', () => {
207+
it('renames a channel and returns old and new name', async () => {
208+
const res = await request(getApp())
209+
.patch('/channels/general')
210+
.send({ name: 'main' })
211+
212+
expect(res.status).toBe(200)
213+
expect(res.body.oldName).toBe('general')
214+
expect(res.body.newName).toBe('main')
215+
})
216+
217+
it('renamed channel appears under new name in GET /channels', async () => {
218+
await request(getApp()).patch('/channels/general').send({ name: 'main' })
219+
220+
const res = await request(getApp()).get('/channels')
221+
const names = res.body.map(c => c.name)
222+
expect(names).toContain('main')
223+
expect(names).not.toContain('general')
224+
})
225+
226+
it('items in the channel move to the new name', async () => {
227+
await insertItem({ content: 'test item', channel: 'general' })
228+
229+
await request(getApp()).patch('/channels/general').send({ name: 'main' })
230+
231+
const res = await request(getApp()).get('/items/main')
232+
expect(res.body.items.some(i => i.content === 'test item')).toBe(true)
233+
})
234+
235+
it('returns 404 for a non-existent channel', async () => {
236+
const res = await request(getApp())
237+
.patch('/channels/doesnotexist')
238+
.send({ name: 'new-name' })
239+
240+
expect(res.status).toBe(404)
241+
})
242+
243+
it('returns 400 when new name is missing', async () => {
244+
const res = await request(getApp())
245+
.patch('/channels/general')
246+
.send({})
247+
248+
expect(res.status).toBe(400)
249+
})
250+
251+
it('returns 400 when new name is longer than 32 characters', async () => {
252+
const res = await request(getApp())
253+
.patch('/channels/general')
254+
.send({ name: 'a'.repeat(33) })
255+
256+
expect(res.status).toBe(400)
257+
})
258+
259+
it('returns 400 when new name has invalid characters', async () => {
260+
const res = await request(getApp())
261+
.patch('/channels/general')
262+
.send({ name: 'bad@name!' })
263+
264+
expect(res.status).toBe(400)
265+
})
266+
267+
it('returns 400 when renaming to an already existing channel name', async () => {
268+
const res = await request(getApp())
269+
.patch('/channels/general')
270+
.send({ name: 'projects' })
271+
272+
expect(res.status).toBe(400)
273+
})
274+
})
275+
276+
// ─────────────────────────────────────────────────────────────────────────────
277+
// POST /channels/:name/pin
278+
// ─────────────────────────────────────────────────────────────────────────────
279+
280+
describe('POST /channels/:name/pin', () => {
281+
it('pins an unpinned channel', async () => {
282+
const res = await request(getApp()).post('/channels/general/pin')
283+
284+
expect(res.status).toBe(200)
285+
expect(res.body.pinned).toBe(1)
286+
})
287+
288+
it('unpins a pinned channel (toggles)', async () => {
289+
await request(getApp()).post('/channels/general/pin')
290+
291+
const res = await request(getApp()).post('/channels/general/pin')
292+
expect(res.body.pinned).toBe(0)
293+
})
294+
295+
it('pinned channel appears first in GET /channels', async () => {
296+
await request(getApp()).post('/channels/temp/pin')
297+
298+
const res = await request(getApp()).get('/channels')
299+
expect(res.body[0].name).toBe('temp')
300+
})
301+
302+
it('pinned channel is protected from deletion', async () => {
303+
await request(getApp()).post('/channels/general/pin')
304+
305+
const res = await request(getApp()).delete('/channels/general')
306+
expect(res.status).toBe(403)
307+
})
308+
})

0 commit comments

Comments
 (0)