@@ -34,6 +34,8 @@ let serverStop
3434let app
3535let knex
3636let getAllProjects
37+ let addProject
38+ let getAllGithubOrganizationsByProjectsId
3739
3840beforeAll ( async ( ) => {
3941 // Initialize server asynchronously
@@ -43,7 +45,9 @@ beforeAll(async () => {
4345 app = request ( server )
4446 knex = knexInit ( dbSettings ) ;
4547 ( {
46- getAllProjects
48+ getAllProjects,
49+ addProject,
50+ getAllGithubOrganizationsByProjectsId
4751 } = initializeStore ( knex ) )
4852} )
4953
@@ -222,4 +226,165 @@ describe('HTTP Server API V1', () => {
222226
223227 test . todo ( 'should return 500 when workflow execution times out' )
224228 } )
229+
230+ describe ( 'POST /api/v1/project/:projectId/gh-org' , ( ) => {
231+ let projectId
232+
233+ beforeEach ( async ( ) => {
234+ // Create a test project for each test
235+ const project = await addProject ( { name : 'test-project' } )
236+ projectId = project . id
237+ } )
238+
239+ test ( 'should return 201 and add a new GitHub organization' , async ( ) => {
240+ const githubOrgUrl = 'https://github.com/expressjs'
241+
242+ const response = await app
243+ . post ( `/api/v1/project/${ projectId } /gh-org` )
244+ . send ( { githubOrgUrl } )
245+
246+ expect ( response . status ) . toBe ( 201 )
247+ expect ( response . body ) . toHaveProperty ( 'id' )
248+ expect ( response . body ) . toHaveProperty ( 'login' , 'expressjs' )
249+ expect ( response . body ) . toHaveProperty ( 'html_url' , githubOrgUrl . toLowerCase ( ) )
250+ expect ( response . body ) . toHaveProperty ( 'project_id' , projectId )
251+
252+ // Verify the Location header is set correctly
253+ expect ( response . headers ) . toHaveProperty ( 'location' , `/api/v1/project/${ projectId } /gh-org/${ response . body . id } ` )
254+
255+ // Verify organization was added to the database
256+ const orgs = await getAllGithubOrganizationsByProjectsId ( [ projectId ] )
257+ expect ( orgs . length ) . toBe ( 1 )
258+ expect ( orgs [ 0 ] . html_url ) . toBe ( githubOrgUrl . toLowerCase ( ) )
259+ } )
260+
261+ test ( 'should correctly extract organization login from URL with trailing slash' , async ( ) => {
262+ const githubOrgUrl = 'https://github.com/expressjs/'
263+
264+ const response = await app
265+ . post ( `/api/v1/project/${ projectId } /gh-org` )
266+ . send ( { githubOrgUrl } )
267+
268+ expect ( response . status ) . toBe ( 201 )
269+ expect ( response . body ) . toHaveProperty ( 'login' , 'expressjs' )
270+ expect ( response . body ) . toHaveProperty ( 'html_url' , githubOrgUrl . toLowerCase ( ) . replace ( / \/ $ / , '' ) )
271+ } )
272+
273+ test ( 'should correctly extract organization login from URL with query parameters' , async ( ) => {
274+ const githubOrgUrl = 'https://github.com/expressjs?tab=repositories'
275+
276+ const response = await app
277+ . post ( `/api/v1/project/${ projectId } /gh-org` )
278+ . send ( { githubOrgUrl } )
279+
280+ expect ( response . status ) . toBe ( 201 )
281+ expect ( response . body ) . toHaveProperty ( 'login' , 'expressjs' )
282+ // The stored URL should be normalized without query parameters
283+ expect ( response . body . html_url ) . not . toContain ( '?' )
284+ } )
285+
286+ test ( 'should return 400 for invalid project ID' , async ( ) => {
287+ const response = await app
288+ . post ( '/api/v1/project/invalid/gh-org' )
289+ . send ( { githubOrgUrl : 'https://github.com/expressjs' } )
290+
291+ expect ( response . status ) . toBe ( 400 )
292+ expect ( response . body ) . toHaveProperty ( 'errors' )
293+ expect ( response . body . errors [ 0 ] ) . toHaveProperty ( 'message' , 'must be integer' )
294+ } )
295+
296+ test ( 'should return 400 for zero project ID' , async ( ) => {
297+ const response = await app
298+ . post ( '/api/v1/project/0/gh-org' )
299+ . send ( { githubOrgUrl : 'https://github.com/expressjs' } )
300+
301+ expect ( response . status ) . toBe ( 400 )
302+ expect ( response . body ) . toHaveProperty ( 'errors' )
303+ expect ( response . body . errors [ 0 ] ) . toHaveProperty ( 'message' , 'Invalid project ID. Must be a positive integer.' )
304+ } )
305+
306+ test ( 'should return 400 for negative project ID' , async ( ) => {
307+ const response = await app
308+ . post ( '/api/v1/project/-1/gh-org' )
309+ . send ( { githubOrgUrl : 'https://github.com/expressjs' } )
310+
311+ expect ( response . status ) . toBe ( 400 )
312+ expect ( response . body ) . toHaveProperty ( 'errors' )
313+ expect ( response . body . errors [ 0 ] ) . toHaveProperty ( 'message' , 'Invalid project ID. Must be a positive integer.' )
314+ } )
315+
316+ test ( 'should return 400 for invalid GitHub organization URL' , async ( ) => {
317+ const response = await app
318+ . post ( `/api/v1/project/${ projectId } /gh-org` )
319+ . send ( { githubOrgUrl : 'https://invalid-url.com/org' } )
320+
321+ expect ( response . status ) . toBe ( 400 )
322+ expect ( response . body ) . toHaveProperty ( 'errors' )
323+ expect ( response . body . errors [ 0 ] ) . toHaveProperty ( 'message' , 'must match pattern "^https://github.com/[^/]+"' )
324+ } )
325+
326+ test ( 'should return 404 for project not found' , async ( ) => {
327+ const nonExistentProjectId = 9999999
328+
329+ const response = await app
330+ . post ( `/api/v1/project/${ nonExistentProjectId } /gh-org` )
331+ . send ( { githubOrgUrl : 'https://github.com/expressjs' } )
332+
333+ expect ( response . status ) . toBe ( 404 )
334+ expect ( response . body ) . toHaveProperty ( 'errors' )
335+ expect ( response . body . errors [ 0 ] ) . toHaveProperty ( 'message' , 'Project not found' )
336+ } )
337+
338+ test ( 'should return 409 for duplicate GitHub organization' , async ( ) => {
339+ const githubOrgUrl = 'https://github.com/expressjs'
340+
341+ // First add the organization
342+ await app
343+ . post ( `/api/v1/project/${ projectId } /gh-org` )
344+ . send ( { githubOrgUrl } )
345+
346+ // Try to add it again
347+ const response = await app
348+ . post ( `/api/v1/project/${ projectId } /gh-org` )
349+ . send ( { githubOrgUrl } )
350+
351+ expect ( response . status ) . toBe ( 409 )
352+ expect ( response . body ) . toHaveProperty ( 'errors' )
353+ expect ( response . body . errors [ 0 ] ) . toHaveProperty ( 'message' , 'GitHub organization already exists for this project' )
354+ } )
355+
356+ test ( 'should return 409 for duplicate GitHub organization with different case' , async ( ) => {
357+ // First add the organization with lowercase
358+ await app
359+ . post ( `/api/v1/project/${ projectId } /gh-org` )
360+ . send ( { githubOrgUrl : 'https://github.com/expressjs' } )
361+
362+ // Try to add it again with uppercase
363+ const response = await app
364+ . post ( `/api/v1/project/${ projectId } /gh-org` )
365+ . send ( { githubOrgUrl : 'https://github.com/ExpressJS' } )
366+
367+ expect ( response . status ) . toBe ( 409 )
368+ expect ( response . body ) . toHaveProperty ( 'errors' )
369+ expect ( response . body . errors [ 0 ] ) . toHaveProperty ( 'message' , 'GitHub organization already exists for this project' )
370+ } )
371+
372+ test ( 'should return 409 for duplicate GitHub organization with trailing slash' , async ( ) => {
373+ // First add the organization without trailing slash
374+ await app
375+ . post ( `/api/v1/project/${ projectId } /gh-org` )
376+ . send ( { githubOrgUrl : 'https://github.com/expressjs' } )
377+
378+ // Try to add it again with trailing slash
379+ const response = await app
380+ . post ( `/api/v1/project/${ projectId } /gh-org` )
381+ . send ( { githubOrgUrl : 'https://github.com/expressjs/' } )
382+
383+ expect ( response . status ) . toBe ( 409 )
384+ expect ( response . body ) . toHaveProperty ( 'errors' )
385+ expect ( response . body . errors [ 0 ] ) . toHaveProperty ( 'message' , 'GitHub organization already exists for this project' )
386+ } )
387+
388+ test . todo ( 'should return 500 for internal server error' )
389+ } )
225390} )
0 commit comments