Skip to content

Commit f2f7917

Browse files
committed
feat: add POST /api/v1/project/{projectId}/gh-org endpoint
1 parent 494d838 commit f2f7917

File tree

2 files changed

+123
-3
lines changed

2 files changed

+123
-3
lines changed

__tests__/httpServer/apiV1.test.js

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ let serverStop
3434
let app
3535
let knex
3636
let getAllProjects
37+
let addProject
38+
let getAllGithubOrganizationsByProjectsId
3739

3840
beforeAll(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,85 @@ 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)
250+
expect(response.body).toHaveProperty('project_id', projectId)
251+
252+
// Verify organization was added to the database
253+
const orgs = await getAllGithubOrganizationsByProjectsId([projectId])
254+
expect(orgs.length).toBe(1)
255+
expect(orgs[0].html_url).toBe(githubOrgUrl)
256+
})
257+
258+
test('should return 400 for invalid project ID', async () => {
259+
const response = await app
260+
.post('/api/v1/project/invalid/gh-org')
261+
.send({ githubOrgUrl: 'https://github.com/expressjs' })
262+
263+
expect(response.status).toBe(400)
264+
expect(response.body).toHaveProperty('errors')
265+
expect(response.body.errors[0]).toHaveProperty('message', 'Invalid project ID')
266+
})
267+
268+
test('should return 400 for invalid GitHub organization URL', async () => {
269+
const response = await app
270+
.post(`/api/v1/project/${projectId}/gh-org`)
271+
.send({ githubOrgUrl: 'https://invalid-url.com/org' })
272+
273+
expect(response.status).toBe(400)
274+
expect(response.body).toHaveProperty('errors')
275+
expect(response.body.errors[0]).toHaveProperty('message', 'must match pattern "https://github.com/[^/]+"')
276+
})
277+
278+
test('should return 404 for project not found', async () => {
279+
const nonExistentProjectId = 9999999
280+
281+
const response = await app
282+
.post(`/api/v1/project/${nonExistentProjectId}/gh-org`)
283+
.send({ githubOrgUrl: 'https://github.com/expressjs' })
284+
285+
expect(response.status).toBe(404)
286+
expect(response.body).toHaveProperty('errors')
287+
expect(response.body.errors[0]).toHaveProperty('message', 'Project not found')
288+
})
289+
290+
test('should return 409 for duplicate GitHub organization', async () => {
291+
const githubOrgUrl = 'https://github.com/expressjs'
292+
293+
// First add the organization
294+
await app
295+
.post(`/api/v1/project/${projectId}/gh-org`)
296+
.send({ githubOrgUrl })
297+
298+
// Try to add it again
299+
const response = await app
300+
.post(`/api/v1/project/${projectId}/gh-org`)
301+
.send({ githubOrgUrl })
302+
303+
expect(response.status).toBe(409)
304+
expect(response.body).toHaveProperty('errors')
305+
expect(response.body.errors[0]).toHaveProperty('message', 'GitHub organization already exists for this project')
306+
})
307+
308+
test.todo('should return 500 for internal server error')
309+
})
225310
})

src/httpServer/routers/apiV1.js

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
const pkg = require('../../../package.json')
2-
const { logger } = require('../../utils')
2+
const { logger, validateGithubUrl } = require('../../utils')
33
const { initializeStore } = require('../../store')
44
const _ = require('lodash')
55
const { isSlug } = require('validator')
@@ -27,14 +27,49 @@ const runWorkflow = ({ workflowName, knex, data } = {}) => new Promise((resolve,
2727
})
2828

2929
function createApiRouter (knex, express) {
30-
const { addProject, getProjectByName } = initializeStore(knex)
30+
const { addProject, getProjectByName, addGithubOrganization, getProjectById, getAllGithubOrganizationsByProjectsId } = initializeStore(knex)
3131

3232
const router = express.Router()
3333

3434
router.get('/__health', (req, res) => {
3535
res.json({ status: 'ok', timestamp: new Date().toISOString(), version: pkg.version, name: pkg.name })
3636
})
3737

38+
router.post('/project/:projectId/gh-org', async (req, res) => {
39+
const projectId = parseInt(req.params.projectId)
40+
const { githubOrgUrl } = req.body
41+
42+
if (!projectId || !Number.isInteger(projectId)) {
43+
return res.status(400).json({ errors: [{ message: 'Invalid project ID' }] })
44+
}
45+
if (!githubOrgUrl || !validateGithubUrl(githubOrgUrl)) {
46+
return res.status(400).json({ errors: [{ message: 'Invalid GitHub organization name' }] })
47+
}
48+
49+
const project = await getProjectById(projectId)
50+
if (!project) {
51+
return res.status(404).json({ errors: [{ message: 'Project not found' }] })
52+
}
53+
54+
const existingGhOrg = await getAllGithubOrganizationsByProjectsId([projectId])
55+
if (existingGhOrg.some(org => org.html_url === githubOrgUrl)) {
56+
return res.status(409).json({ errors: [{ message: 'GitHub organization already exists for this project' }] })
57+
}
58+
59+
try {
60+
const org = await addGithubOrganization({
61+
html_url: githubOrgUrl,
62+
login: githubOrgUrl.split('https://github.com/')[1],
63+
project_id: project.id
64+
})
65+
66+
return res.status(201).json(org)
67+
} catch (err) {
68+
logger.error(err)
69+
return res.status(500).json({ errors: [{ message: 'Internal server error' }] })
70+
}
71+
})
72+
3873
router.post('/project', async (req, res) => {
3974
try {
4075
// Validate request body

0 commit comments

Comments
 (0)