Skip to content

Commit 35ee87b

Browse files
authored
feat: add POST /api/v1/project endpoint (#239)
1 parent 68ec9d0 commit 35ee87b

File tree

6 files changed

+332
-3
lines changed

6 files changed

+332
-3
lines changed

__tests__/httpServer/apiV1.test.js

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
const request = require('supertest')
22
const { generateStaticReports } = require('../../src/reports')
3+
const knexInit = require('knex')
4+
const { getConfig } = require('../../src/config')
5+
const { resetDatabase, initializeStore } = require('../../__utils__')
36
const pkg = require('../../package.json')
47
const serverModule = require('../../src/httpServer')
8+
const { dbSettings } = getConfig('test')
59
let server
610
let serverStop
711
let app
12+
let knex
13+
let getAllProjects
814

915
// Mocks
1016
jest.mock('../../src/reports', () => ({
@@ -17,17 +23,26 @@ beforeAll(async () => {
1723
server = await serverInstance.start()
1824
serverStop = serverInstance.stop
1925
app = request(server)
26+
knex = knexInit(dbSettings);
27+
({
28+
getAllProjects
29+
} = initializeStore(knex))
2030
})
2131

2232
afterAll(async () => {
2333
// Cleanup after all tests
2434
await serverStop?.()
35+
await resetDatabase(knex)
36+
await knex.destroy()
2537
})
2638

27-
beforeEach(() => {
39+
beforeEach(async () => {
40+
await resetDatabase(knex)
2841
jest.clearAllMocks()
2942
})
3043

44+
afterEach(jest.clearAllMocks)
45+
3146
describe('HTTP Server API V1', () => {
3247
describe('GET /api/v1/__health', () => {
3348
test('should return status ok', async () => {
@@ -44,6 +59,62 @@ describe('HTTP Server API V1', () => {
4459
})
4560
})
4661

62+
describe('POST /api/v1/project', () => {
63+
test('should return 200 and create a new project', async () => {
64+
// Initial state
65+
let projects = await getAllProjects()
66+
expect(projects.length).toBe(0)
67+
// Request
68+
const newProject = { name: 'eslint' }
69+
const response = await app.post('/api/v1/project').send(newProject)
70+
// Database changes
71+
projects = await getAllProjects()
72+
expect(projects.length).toBe(1)
73+
expect(projects[0].name).toBe('eslint')
74+
// Response details
75+
expect(response.status).toBe(201)
76+
expect(response.headers).toHaveProperty('location', `/api/v1/project/${projects[0].id}`)
77+
expect(response.body).toHaveProperty('id')
78+
expect(response.body).toHaveProperty('name', newProject.name)
79+
expect(response.body).toHaveProperty('created_at')
80+
expect(response.body).toHaveProperty('updated_at')
81+
})
82+
test('should return 400 for invalid project name', async () => {
83+
// Initial state
84+
let projects = await getAllProjects()
85+
expect(projects.length).toBe(0)
86+
// Request
87+
const invalidProject = { name: 'Invalid Name!' }
88+
const response = await app.post('/api/v1/project').send(invalidProject)
89+
// Database changes
90+
projects = await getAllProjects()
91+
expect(projects.length).toBe(0)
92+
// Response details
93+
expect(response.status).toBe(400)
94+
expect(response.body).toStrictEqual({ errors: [{ errorCode: 'pattern.openapi.validation', message: 'must match pattern "^[a-zA-Z0-9_-]+$"', path: '/body/name' }], name: 'Bad Request', path: '/api/v1/project', status: 400 })
95+
})
96+
test('should return 409 if the project already exists', async () => {
97+
// Initial state
98+
let projects = await getAllProjects()
99+
expect(projects.length).toBe(0)
100+
// Create the project first
101+
const existingProject = { name: 'eslint' }
102+
await app.post('/api/v1/project').send(existingProject)
103+
projects = await getAllProjects()
104+
expect(projects.length).toBe(1)
105+
// Request to create the same project again
106+
const response = await app.post('/api/v1/project').send(existingProject)
107+
// Database changes
108+
projects = await getAllProjects()
109+
expect(projects.length).toBe(1) // Still only one project
110+
// Response details
111+
expect(response.status).toBe(409)
112+
expect(response.body).toStrictEqual({ errors: [{ message: 'Project already exists.' }] })
113+
})
114+
115+
test.todo('should return 500 for internal server error')
116+
})
117+
47118
describe('POST /api/v1/generate-reports', () => {
48119
test('should return status completed when report generation succeeds', async () => {
49120
generateStaticReports.mockResolvedValueOnce()

package-lock.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
"express": "^5.1.0",
5757
"inquirer": "12.1.0",
5858
"knex": "3.1.0",
59+
"lodash": "^4.17.21",
5960
"nock": "14.0.1",
6061
"octokit": "3.2.1",
6162
"pg": "8.13.1",

src/httpServer/routers/apiV1.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,47 @@
11
const { generateStaticReports } = require('../../reports')
22
const pkg = require('../../../package.json')
33
const { logger } = require('../../utils')
4+
const { initializeStore } = require('../../store')
5+
const _ = require('lodash')
6+
const { isSlug } = require('validator')
47

58
function createApiRouter (knex, express) {
9+
const { addProject, getProjectByName } = initializeStore(knex)
10+
611
const router = express.Router()
712

813
router.get('/__health', (req, res) => {
914
res.json({ status: 'ok', timestamp: new Date().toISOString(), version: pkg.version, name: pkg.name })
1015
})
1116

17+
router.post('/project', async (req, res) => {
18+
try {
19+
// Validate request body
20+
const { name } = req.body
21+
const projectName = _.kebabCase(name)
22+
23+
// Check data and database
24+
if (!projectName || !isSlug(projectName)) {
25+
return res.status(400).json({ errors: [{ message: 'Invalid project name. Must be a slug.' }] })
26+
}
27+
const existingProject = await getProjectByName(projectName)
28+
if (existingProject) {
29+
return res.status(409).json({ errors: [{ message: 'Project already exists.' }] })
30+
}
31+
32+
// Modify database
33+
const project = await addProject({ name: projectName })
34+
35+
// Return response
36+
return res.status(201)
37+
.header('Location', `/api/v1/project/${project.id}`)
38+
.json(project)
39+
} catch (err) {
40+
logger.error(err)
41+
return res.status(500).json({ errors: [{ message: 'Internal server error' }] })
42+
}
43+
})
44+
1245
router.post('/generate-reports', async (req, res) => {
1346
const startTs = new Date().toISOString()
1447
try {

0 commit comments

Comments
 (0)