Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@
"start": "npm run build && NODE_ENV=development webpack-dev-server --host 0.0.0.0 --open --hot --config build/webpack.config.js",
"build": "npm --prefix admin run build && NODE_ENV=production webpack --config build/webpack.config.js",
"serve": "cd src/server && NODE_ENV=development node app.js",
"lint": "eslint ."
"lint": "eslint .",
"test": "node --test 'test/**/*.test.js'"
},
"husky": {
"hooks": {
Expand Down
54 changes: 38 additions & 16 deletions src/server/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,23 @@ const websocketProxyOption = {
changeOrigin: true,
}

function sendJson(res, statusCode, payload) {
res.statusCode = statusCode
res.setHeader('Content-Type', 'application/json')
res.end(JSON.stringify(payload))
}

function readJsonFileOr500(res, filePath, callback) {
jsonfile.readFile(filePath, (err, obj) => {
if (err) {
console.log('[ERROR]' + err)
sendJson(res, 500, { message: 'Failed to read server data' })
return
}
callback(obj)
})
}

Object.defineProperty(Array.prototype, 'flat', {
value: function (depth = 1) {
return this.reduce(function (flat, toFlatten) {
Expand Down Expand Up @@ -59,6 +76,12 @@ const refresh = spawn('node', ['refresh.js'], {
shell: true,
stdio: 'inherit',
})
refresh.on('exit', (code) => {
console.log(`[ERROR] refresh.js exited with code ${code}. Leaderboard data may become stale.`)
})
refresh.on('error', (err) => {
console.log(`[ERROR] Failed to start refresh.js: ${err.message}`)
})
process.on('exit', () => {
refresh.kill() // kill it when exit
})
Expand All @@ -71,15 +94,13 @@ const server = http
switch (route) {
case '/data':
res.setHeader('Cache-Control', 'no-store')
jsonfile.readFile(dataPath, (err, obj) => {
if (err) console.log('[ERROR]' + err)
readJsonFileOr500(res, dataPath, (obj) => {
res.end(JSON.stringify(obj))
})
break
case '/log':
res.setHeader('Cache-Control', 'no-store')
jsonfile.readFile(logPath, (err, obj) => {
if (err) console.log('[ERROR]' + err)
readJsonFileOr500(res, logPath, (obj) => {
res.end(JSON.stringify(obj))
})
break
Expand All @@ -104,7 +125,7 @@ const server = http
)
var contributorsList = []

Util.post(req, async (params) => {
Util.post(req, res, async (params) => {
const { token } = params
if (token === adminPassword) {
await Promise.all(
Expand Down Expand Up @@ -176,7 +197,7 @@ const server = http
return
}

Util.post(req, (params) => {
Util.post(req, res, (params) => {
const { token, includedRepositories } = params

if (token !== adminPassword) {
Expand All @@ -197,7 +218,7 @@ const server = http
res.end('Permission denied\n')
return
}
Util.post(req, (params) => {
Util.post(req, res, (params) => {
const { token, startDate } = params

if (token !== adminPassword) {
Expand All @@ -219,7 +240,7 @@ const server = http
return
}

Util.post(req, (params) => {
Util.post(req, res, (params) => {
const { token, interval } = params

if (token !== adminPassword) {
Expand All @@ -241,7 +262,7 @@ const server = http
return
}

Util.post(req, (params) => {
Util.post(req, res, (params) => {
const { token, username } = params

if (token !== adminPassword) {
Expand Down Expand Up @@ -272,7 +293,7 @@ const server = http
return
}

Util.post(req, (params) => {
Util.post(req, res, (params) => {
const { token, username } = params

if (token !== adminPassword) {
Expand All @@ -288,6 +309,10 @@ const server = http
API.getContributorAvatar(username).then((result) => {
if (result === '') {
res.end(JSON.stringify({ message: 'Not found' }))
} else if (!result) {
res.end(JSON.stringify({
message: 'Unable to fetch contributor profile. Check GitHub token and network connectivity.',
}))
} else {
// Add this contributor in config.json
Config.contributors.unshift(username)
Expand Down Expand Up @@ -332,8 +357,7 @@ const server = http
return
}

jsonfile.readFile(dataPath, async (err, obj) => {
if (err) console.log('[ERROR]' + err)
readJsonFileOr500(res, dataPath, async (obj) => {
res.end(JSON.stringify(await API.getStats(obj)))
})
break
Expand All @@ -343,8 +367,7 @@ const server = http
return
}

jsonfile.readFile(dataPath, async (err, obj) => {
if (err) console.log('[ERROR]' + err)
readJsonFileOr500(res, dataPath, async (obj) => {
const query = url.parse(req.url, true).query

// Gets list of contributors sorted by parameter if provided
Expand Down Expand Up @@ -381,8 +404,7 @@ const server = http
return
}

jsonfile.readFile(dataPath, async (err, obj) => {
if (err) console.log('[ERROR]' + err)
readJsonFileOr500(res, dataPath, async (obj) => {
const query = url.parse(req.url, true).query

if (query.username) {
Expand Down
17 changes: 11 additions & 6 deletions src/server/refresh.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ if (fs.existsSync(logPath)) {
async function getAllContributorsInfo() {
let Config = jsonfile.readFileSync(configPath)
let organization = Config.organization
let contributors = Config.contributors
let includedRepositories = Config.includedRepositories
let contributors = Array.isArray(Config.contributors) ? Config.contributors : []
let includedRepositories = Array.isArray(Config.includedRepositories) ? Config.includedRepositories : []

interval = contributors.length < 150 ? 150 : (contributors.length + 10) // update interval

Expand All @@ -40,11 +40,12 @@ async function getAllContributorsInfo() {

await Promise.delay(delay * 1000)

API.getContributorInfo(organization, contributor, includedRepositories).then( res => {
try {
const res = await API.getContributorInfo(organization, contributor, includedRepositories)
Config = jsonfile.readFileSync(configPath) // update Config
delay = Config.delay // update delay

if (res.avatarUrl !== '' && res.issuesNumber !== -1 && res.mergedPRsNumber !== -1 && res.openPRsNumber != -1) {
if (res && res.avatarUrl !== '' && res.issuesNumber !== -1 && res.mergedPRsNumber !== -1 && res.openPRsNumber !== -1) {

dataBuffer = jsonfile.readFileSync(dataPath)

Expand All @@ -57,8 +58,12 @@ async function getAllContributorsInfo() {
if (err) console.error(err)
})
}
} else {
console.log(`[WARNING] Skipped update for ${contributor}. Check GitHub token, rate limit, or network status.`)
}
})
} catch (err) {
console.log(`[ERROR] Failed to update ${contributor}: ${err && err.message ? err.message : err}`)
}

// Record time
logBuffer.endtime = Date.now()
Expand All @@ -70,4 +75,4 @@ async function getAllContributorsInfo() {
}

getAllContributorsInfo()
setInterval(getAllContributorsInfo, interval * delay * 1000)
setInterval(getAllContributorsInfo, interval * delay * 1000)
51 changes: 41 additions & 10 deletions src/server/util/API.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,19 @@ const chalk = require('chalk')

const BASEURL = 'https://github.com'
const APIHOST = 'https://api.github.com'
let hasLoggedBadCredentials = false

function getRateLimitResetMessage(headers) {
const resetHeader = headers && headers['x-ratelimit-reset']
if (!resetHeader) {
return ''
}
const resetTime = Number(resetHeader) * 1000
if (!Number.isFinite(resetTime)) {
return ''
}
return ` Retry after ${new Date(resetTime).toISOString()}.`
}

async function get(url, _authToken) {
try {
Expand All @@ -23,25 +36,40 @@ async function get(url, _authToken) {
})
} catch (err) {
if (err.code === 'ECONNABORTED') {
console.log(chalk.yellow('[WARNING] Time Out.'))
console.log(chalk.yellow('[WARNING] GitHub API request timed out.'))
return
}
if (err.response !== undefined) {
const message = err.response.data.message
switch (message) {
case 'Bad credentials':
if (message === 'Bad credentials') {
if (!hasLoggedBadCredentials) {
console.log(
chalk.red(
'[ERROR] GitHub token is invalid or expired. Please update src/server/config.json (authToken) and restart the server.'
)
)
hasLoggedBadCredentials = true
}
return
}
if (message && message.indexOf('API rate limit exceeded') === 0) {
console.log(
chalk.red(
'[ERROR] Your GitHub Token is not correct! Please check it in the config.json.'
chalk.yellow(
'[WARNING] GitHub API rate limit exceeded.' +
getRateLimitResetMessage(err.response.headers)
)
)
process.exit()
break
default:
console.log(chalk.yellow('[WARNING] ' + message))
return
}
console.log(chalk.yellow('[WARNING] ' + message))
} else {
console.log(err)
console.log(
chalk.yellow(
`[WARNING] GitHub API request failed: ${
err.message || 'Unknown network error'
}`
)
)
}
}
}
Expand Down Expand Up @@ -130,6 +158,9 @@ async function getContributorInfo(
contributor,
includedRepositories
) {
if (!Array.isArray(includedRepositories)) {
includedRepositories = []
}
const home = BASEURL + '/' + contributor
const avatarUrl = await getContributorAvatar(contributor)
let OpenPRsURL = `/search/issues?q=is:pr+author:${contributor}+is:Open+created:>=${Config.startDate}`
Expand Down
5 changes: 4 additions & 1 deletion src/server/util/Util.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
function post(req, callback) {
function post(req, res, callback) {
Comment thread
srijnabhargav marked this conversation as resolved.
if(req.method === 'POST') {
let body = ''

Expand All @@ -10,6 +10,9 @@ function post(req, callback) {
try {
callback(JSON.parse(body))
} catch (ex) {
res.statusCode = 400
res.setHeader('Content-Type', 'application/json')
res.end(JSON.stringify({ message: 'Invalid JSON body' }))
return
}
})
Expand Down
8 changes: 8 additions & 0 deletions test/.eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"env": {
"node": true
},
"globals": {
"test": "readonly"
}
}
15 changes: 15 additions & 0 deletions test/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Regression tests

This directory holds **automated regression tests** for server behavior (starting with invalid JSON handling for admin `POST` bodies and related error-handling work).

Tests are run with Node’s built-in test runner (`node --test`). **Node.js 18+** is required.

`npm test` runs `test/**/*.test.js` so new files can be added without changing the script.

Some tests were initially drafted with AI assistance; they are **kept in the repository on purpose** so the project builds a lasting regression suite instead of generating tests only to discard them after a green run.

From the repo root:

```bash
npm test
```
39 changes: 39 additions & 0 deletions test/util-post.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
const { test } = require('node:test')
const assert = require('assert')
const { EventEmitter } = require('events')
const Util = require('../src/server/util/Util')

test('Util.post sends 400 JSON when body is not valid JSON', async () => {
const req = new EventEmitter()
req.method = 'POST'

let settle
const done = new Promise((resolve) => {
settle = resolve
})

const res = {
statusCode: 200,
headers: {},
setHeader(name, value) {
this.headers[name] = value
},
end(payload) {
this.body = payload
settle()
}
}

Util.post(req, res, () => {
assert.fail('callback must not run when JSON.parse throws')
})

req.emit('data', Buffer.from('{not-json'))
req.emit('end')

await done

assert.strictEqual(res.statusCode, 400)
assert.strictEqual(res.headers['Content-Type'], 'application/json')
assert.deepStrictEqual(JSON.parse(res.body), { message: 'Invalid JSON body' })
})