Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 68 additions & 15 deletions __tests__/httpServer.test.js
Original file line number Diff line number Diff line change
@@ -1,28 +1,81 @@
const request = require('supertest')
const { generateStaticReports } = require('../src/reports')
const serverModule = require('../src/httpServer')
const server = serverModule()
const app = request(server)
let server
let serverStop
let app

// Cleanup after all tests
afterAll(() => {
server?.close()
// Mocks
jest.mock('../src/reports', () => ({
generateStaticReports: jest.fn()
}))

beforeAll(async () => {
// Initialize server asynchronously
const serverInstance = serverModule()
server = await serverInstance.start()
serverStop = serverInstance.stop
app = request(server)
})

afterAll(async () => {
// Cleanup after all tests
await serverStop?.()
})

beforeEach(() => {
jest.clearAllMocks()
})

describe('HTTP Server API', () => {
test('health check endpoint should return status ok', async () => {
const response = await app.get('/api/v1/__health')
describe('GET /api/v1/__health', () => {
test('should return status ok', async () => {
const response = await app.get('/api/v1/__health')

expect(response.status).toBe(200)
expect(response.body).toHaveProperty('status', 'ok')
expect(response.body).toHaveProperty('timestamp')
expect(response.status).toBe(200)
expect(response.body).toHaveProperty('status', 'ok')
expect(response.body).toHaveProperty('timestamp')

const timestamp = new Date(response.body.timestamp)
expect(timestamp.toISOString()).toBe(response.body.timestamp)
const timestamp = new Date(response.body.timestamp)
expect(timestamp.toISOString()).toBe(response.body.timestamp)
})
})

test('non-existent API endpoint should return 404', async () => {
const response = await app.get('/api/v1/non-existent-endpoint')
describe('POST /api/v1/generate-reports', () => {
test('should return status completed when report generation succeeds', async () => {
generateStaticReports.mockResolvedValueOnce()

const response = await app.post('/api/v1/generate-reports')

expect(generateStaticReports).toHaveBeenCalledWith(expect.anything(), { clearPreviousReports: true })
expect(response.status).toBe(202)
expect(response.body).toHaveProperty('status', 'completed')
expect(response.body).toHaveProperty('startedAt')
expect(response.body).toHaveProperty('finishedAt')

const startedAt = new Date(response.body.startedAt)
const finishedAt = new Date(response.body.finishedAt)
expect(startedAt.toISOString()).toBe(response.body.startedAt)
expect(finishedAt.toISOString()).toBe(response.body.finishedAt)
expect(finishedAt >= startedAt).toBe(true)
})

test('should return status failed when report generation fails', async () => {
generateStaticReports.mockRejectedValueOnce(new Error('Report generation failed'))

const response = await app.post('/api/v1/generate-reports')

expect(generateStaticReports).toHaveBeenCalledWith(expect.anything(), { clearPreviousReports: true })
expect(response.status).toBe(500)
expect(response.body).toHaveProperty('status', 'failed')
expect(response.body).toHaveProperty('startedAt')
expect(response.body).toHaveProperty('finishedAt')

expect(response.status).toBe(404)
const startedAt = new Date(response.body.startedAt)
const finishedAt = new Date(response.body.finishedAt)
expect(startedAt.toISOString()).toBe(response.body.startedAt)
expect(finishedAt.toISOString()).toBe(response.body.finishedAt)
expect(finishedAt >= startedAt).toBe(true)
})
})
})
7 changes: 5 additions & 2 deletions server.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
const server = require('./src/httpServer')
const server = require('./src/httpServer');

server()
(async () => {
const serverInstance = server()
await serverInstance.start()
})()
4 changes: 2 additions & 2 deletions src/cli/workflows.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const inquirer = require('inquirer').default
const debug = require('debug')('cli:workflows')
const { updateGithubOrgs, upsertGithubRepositories, runAllTheComplianceChecks, upsertOSSFScorecardAnalysis } = require('../workflows')
const { generateReports } = require('../reports')
const { generateStaticReports } = require('../reports')
const { bulkImport } = require('../importers')
const { logger } = require('../utils')

Expand All @@ -28,7 +28,7 @@ const commandList = [{
}, {
name: 'generate-reports',
description: 'Generate the reports for the stored data.',
workflow: generateReports
workflow: generateStaticReports
}, {
name: 'bulk-import',
description: 'Bulk import data from a CSV file.',
Expand Down
32 changes: 32 additions & 0 deletions src/httpServer/apiV1.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
const { generateStaticReports } = require('../reports')
const { logger } = require('../utils')

function createApiRouter (knex, express) {
const router = express.Router()

router.get('/__health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() })
})

router.post('/generate-reports', async (req, res) => {
const startTs = new Date().toISOString()
try {
await generateStaticReports(knex, { clearPreviousReports: true })
res.status(202).json({
status: 'completed',
startedAt: startTs,
finishedAt: new Date().toISOString()
})
} catch (error) {
logger.error(error)
res.status(500).json({
status: 'failed',
startedAt: startTs,
finishedAt: new Date().toISOString()
})
}
})
return router
}

module.exports = { createApiRouter }
42 changes: 25 additions & 17 deletions src/httpServer/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,18 @@ const serveStatic = require('serve-static')
const serveIndex = require('serve-index')
const { join } = require('path')
const { getConfig } = require('../config')
const { logger } = require('../utils')
const { logger, checkDatabaseConnection } = require('../utils')
const { createApiRouter } = require('./apiV1')

const publicPath = join(process.cwd(), 'output')
const { staticServer } = getConfig()
const { staticServer, dbSettings } = getConfig()
const knex = require('knex')(dbSettings)

// Create Express app
const app = express()

// API Routes
app.use('/api/v1', createApiRouter())
app.use('/api/v1', createApiRouter(knex, express))

// Static file serving
app.use(serveStatic(publicPath, {
Expand All @@ -33,21 +35,27 @@ app.use((err, req, res, next) => {
})
})

// Create API router
function createApiRouter () {
const router = express.Router()

// Health check endpoint
router.get('/__health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() })
})
return router
}

// Create HTTP server
const server = http.createServer(app)

module.exports = () => server.listen(staticServer.port, () => {
logger.info(`Server running at http://${staticServer.ip}:${staticServer.port}/`)
logger.info(`API available at http://${staticServer.ip}:${staticServer.port}/api/v1/`)
module.exports = () => ({
start: async () => {
const isDbConnected = await checkDatabaseConnection(knex)
if (!isDbConnected) {
const err = new Error('Failed to connect to database')
logger.error(err)
throw err
}
return server.listen(staticServer.port, () => {
logger.info(`Server running at http://${staticServer.ip}:${staticServer.port}/`)
logger.info(`API available at http://${staticServer.ip}:${staticServer.port}/api/v1/`)
})
},
stop: async () => {
await knex.destroy()
await new Promise((resolve, reject) => {
server.close(err => (err ? reject(err) : resolve()))
})
logger.info('Server stopped')
}
})
42 changes: 30 additions & 12 deletions src/reports/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const { logger } = require('../utils')
const ejs = require('ejs')
const { mkdir, readdir, copyFile, readFile, writeFile } = require('node:fs').promises
const { mkdir, readdir, copyFile, readFile, writeFile, rm } = require('node:fs').promises
const { join } = require('path')
const { initializeStore } = require('../store')

Expand Down Expand Up @@ -37,18 +37,35 @@ const copyFolder = async (from, to) => {
}
}

const generateReports = async (knex) => {
const generateStaticReports = async (knex, options = { clearPreviousReports: false }) => {
const { clearPreviousReports } = options
if (clearPreviousReports) {
logger.info('Clearing previous reports')
await rm(destinationFolder, { recursive: true, force: true })
}

logger.info('Generating reports')
const { getAllProjects, getAllChecklists, getAllComplianceChecks, getAllAlerts, getAllResults, getAllTasks, getAllGithubOrganizationsByProjectsId, getAllGithubRepositories, getAllOSSFResults } = initializeStore(knex)
// @TODO: Run the queries in parallel
const projects = await getAllProjects()
const checklists = await getAllChecklists()
const checks = await getAllComplianceChecks()
const alerts = await getAllAlerts()
const results = await getAllResults()
const tasks = await getAllTasks()
const ossfScorecardResults = await getAllOSSFResults()
const githubRepos = await getAllGithubRepositories()
// Run the queries in parallel
const [
projects,
checklists,
checks,
alerts,
results,
tasks,
ossfScorecardResults,
githubRepos
] = await Promise.all([
getAllProjects(),
getAllChecklists(),
getAllComplianceChecks(),
getAllAlerts(),
getAllResults(),
getAllTasks(),
getAllOSSFResults(),
getAllGithubRepositories()
])

// @TODO: Read the files in parallel
const indexTemplate = await readFile(indexTemplatePath, 'utf8')
Expand Down Expand Up @@ -101,8 +118,9 @@ const generateReports = async (knex) => {

// Save the index HTML file
await writeFile('output/index.html', indexHtml)
logger.info('Reports generated successfully')
}

module.exports = {
generateReports
generateStaticReports
}
13 changes: 12 additions & 1 deletion src/utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,16 @@ const isDateWithinPolicy = (targetDate, policy) => {
return isBefore(currentDate, expirationDate) // Check if current date is before expiration
}

const checkDatabaseConnection = async (knex) => {
try {
await knex.raw('SELECT 1')
return true
} catch (error) {
logger.error('Database connection failed', { error })
return false
}
}

module.exports = {
isDateWithinPolicy,
validateGithubUrl,
Expand All @@ -131,5 +141,6 @@ module.exports = {
groupArrayItemsByCriteria,
redactSensitiveData,
logger,
generatePercentage
generatePercentage,
checkDatabaseConnection
}