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 ( / m a x / 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 ( / a t l e a s t o n e / 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