Add tests for web instance

This commit is contained in:
linguist
2020-12-30 18:14:20 +01:00
parent a8fe11b7b5
commit 6290ca575c
9 changed files with 294 additions and 226 deletions

View File

@@ -195,26 +195,19 @@
const die = bool(core.getInput("plugins_errors_fatal"))
console.log(`Plugin errors │ ${die ? "die" : "warn"}`)
//Verify svg
const verify = bool(core.getInput("verify"))
console.log(`Verify SVG │ ${verify}`)
//Build query
const query = JSON.parse(core.getInput("query") || "{}")
console.log(`Query additional params │ ${JSON.stringify(query)}`)
q = {...query, ...q, base:false, ...base, ...config, repositories, template}
//Render metrics
const rendered = await metrics({login:user, q, dflags}, {graphql, rest, plugins, conf, die}, {Plugins, Templates})
const rendered = await metrics({login:user, q, dflags}, {graphql, rest, plugins, conf, die, verify}, {Plugins, Templates})
console.log(`Render │ complete`)
//Verify svg
const verify = bool(core.getInput("verify"))
console.log(`Verify SVG │ ${verify}`)
if (verify) {
const [libxmljs] = [await import("libxmljs")].map(m => (m && m.default) ? m.default : m)
const parsed = libxmljs.parseXml(rendered)
if (parsed.errors.length)
throw new Error(`Malformed SVG : \n${parsed.errors.join("\n")}`)
console.log(`SVG valid │ yes`)
}
//Commit to repository
const dryrun = bool(core.getInput("dryrun"))
if (dryrun)

View File

@@ -2,4 +2,4 @@
import app from "./src/app.mjs"
//Start app
await app()
await app({mock:process.env.USE_MOCKED_DATA})

View File

@@ -1,4 +1,7 @@
{
"//":"This is an example of configuration file for web instance",
"//":"It is not needed when using metrics as GitHub action",
"token":"MY GITHUB API TOKEN", "//":"Your own GitHub API token (required)",
"restricted":[], "//":"List of authorized users, leave empty for unrestricted",
"cached":3600000, "//":"Cached time for generated images, 0 to disable",
@@ -8,15 +11,13 @@
"optimize":true, "//":"Optimize SVG image",
"debug":false, "//":"Debug mode",
"repositories":100, "//":"Number of repositories to use to compute metrics",
"templates":{ "//":"Template configuration",
"default":"classic", "//":"Default template",
"enabled":[], "//":"Enabled templates, leave empty to enable all templates"
},
"plugins":{ "//":"Additional plugins (optional)",
"pagespeed":{ "//":"Pagespeed plugin",
"enabled":true, "//":"Enable or disable PageSpeed metrics",
"enabled":false, "//":"Enable or disable PageSpeed metrics",
"token":null, "//":"Pagespeed token (optional)"
},
"traffic":{ "//":"Traffic plugin (GitHub API token must be RW for this to work)",
@@ -30,10 +31,10 @@
"from":200, "//":"Number of activity events to base habits on (up to 1000)"
},
"languages":{ "//":"Languages plugin",
"enabled":true, "//":"Enable or disable most used languages metrics"
"enabled":false, "//":"Enable or disable most used languages metrics"
},
"followup":{ "//":"Follow-up plugin",
"enabled":true, "//":"Enable or disable owned repositories issues and pull requests metrics"
"enabled":false, "//":"Enable or disable owned repositories issues and pull requests metrics"
},
"music":{ "//":"Music plugin",
"enabled":false, "//":"Enable or disable music recently played / random track from playlist",
@@ -55,7 +56,7 @@
"enabled":false, "//":"Enable or disable personal projects display"
},
"tweets":{ "//":"Tweets plugin",
"enabled":true, "//":"Enable or disable recent tweets display",
"enabled":false, "//":"Enable or disable recent tweets display",
"token":null, "//":"Twitter token (required when enabled)"
}
}

View File

@@ -5,20 +5,44 @@
import cache from "memory-cache"
import ratelimit from "express-rate-limit"
import compression from "compression"
import util from "util"
import setup from "./setup.mjs"
import metrics from "./metrics.mjs"
import util from "util"
import mocks from "./mocks.mjs"
/** App */
export default async function () {
export default async function ({mock = false} = {}) {
//Load configuration settings
const {conf, Plugins, Templates} = await setup()
const {token, maxusers = 0, restricted = [], debug = false, cached = 30*60*1000, port = 3000, ratelimiter = null, plugins = null} = conf.settings
//Apply configuration mocking if needed
if (mock) {
console.debug(`metrics/app > using mocked settings`)
const {settings} = conf
//Mock token if it's undefined
if (!settings.token)
settings.token = (console.debug(`metrics/app > using mocked token`), "MOCKED_TOKEN")
//Mock plugins state and tokens if they're undefined
for (const plugin of Object.keys(Plugins)) {
if (!settings.plugins[plugin])
settings.plugins[plugin] = {}
settings.plugins[plugin].enabled = settings.plugins[plugin].enabled ?? (console.debug(`metrics/app > using mocked token enable state for ${plugin}`), true)
if (["tweets", "pagespeed"].includes(plugin))
settings.plugins[plugin].token = settings.plugins[plugin].token ?? (console.debug(`metrics/app > using mocked token for ${plugin}`), "MOCKED_TOKEN")
if (["music"].includes(plugin))
settings.plugins[plugin].token = settings.plugins[plugin].token ?? (console.debug(`metrics/app > using mocked token for ${plugin}`), "MOCKED_CLIENT_ID, MOCKED_CLIENT_SECRET, MOCKED_REFRESH_TOKEN")
}
console.debug(util.inspect(settings, {depth:Infinity, maxStringLength:256}))
}
//Load octokits
const graphql = octokit.graphql.defaults({headers:{authorization: `token ${token}`}})
const rest = new OctokitRest.Octokit({auth:token})
const api = {graphql:octokit.graphql.defaults({headers:{authorization: `token ${token}`}}), rest:new OctokitRest.Octokit({auth:token})}
//Apply mocking if needed
if (mock)
Object.assign(api, await mocks(api))
const {graphql, rest} = api
//Setup server
const app = express()
@@ -108,7 +132,8 @@
try {
//Render
console.debug(`metrics/app/${login} > ${util.inspect(req.query, {depth:Infinity, maxStringLength:256})}`)
const rendered = await metrics({login, q:parse(req.query)}, {graphql, rest, plugins, conf}, {Plugins, Templates})
const q = parse(req.query)
const rendered = await metrics({login, q}, {graphql, rest, plugins, conf, die:q["plugins.errors.fatal"] ?? false, verify:q["verify"] ?? false}, {Plugins, Templates})
//Cache
if ((!debug)&&(cached)&&(login !== "placeholder"))
cache.put(login, rendered, cached)
@@ -136,13 +161,14 @@
//Listen
app.listen(port, () => console.log([
`Listening on port | ${port}`,
`Debug mode | ${debug}`,
`Restricted to users | ${restricted.size ? [...restricted].join(", ") : "(unrestricted)"}`,
`Cached time | ${cached} seconds`,
`Rate limiter | ${ratelimiter ? util.inspect(ratelimiter, {depth:Infinity, maxStringLength:256}) : "(enabled)"}`,
`Max simultaneous users | ${maxusers ? `${maxusers} users` : "(unrestricted)"}`,
`Plugins enabled | ${enabled.join(", ")}`
`Listening on port ${port}`,
`Debug mode ${debug}`,
`Restricted to users ${restricted.size ? [...restricted].join(", ") : "(unrestricted)"}`,
`Cached time ${cached} seconds`,
`Rate limiter ${ratelimiter ? util.inspect(ratelimiter, {depth:Infinity, maxStringLength:256}) : "(enabled)"}`,
`Max simultaneous users ${maxusers ? `${maxusers} users` : "(unrestricted)"}`,
`Plugins enabled ${enabled.join(", ")}`,
`Server ready !`
].join("\n")))
}

View File

@@ -12,7 +12,7 @@
import util from "util"
//Setup
export default async function metrics({login, q, dflags = []}, {graphql, rest, plugins, conf, die = false}, {Plugins, Templates}) {
export default async function metrics({login, q, dflags = []}, {graphql, rest, plugins, conf, die = false, verify = false}, {Plugins, Templates}) {
//Compute rendering
try {
@@ -92,6 +92,15 @@
rendered = optimized
}
//Verify svg
if (verify) {
console.debug(`metrics/compute/${login} > verify SVG`)
const libxmljs = (await import("libxmljs")).default
const parsed = libxmljs.parseXml(rendered)
if (parsed.errors.length)
throw new Error(`Malformed SVG : \n${parsed.errors.join("\n")}`)
}
//Result
console.debug(`metrics/compute/${login} > success`)
return rendered

View File

@@ -507,7 +507,7 @@
axios.post = new Proxy(unmocked.post, {
apply:function(target, that, args) {
//Arguments
const [url, body, options] = args
const [url, body] = args
//Spotify api
if (/accounts.spotify.com.api.token/.test(url)) {
//Access token generator

View File

@@ -33,13 +33,11 @@
for (const project of projects.nodes) {
//Format date
const time = (Date.now()-new Date(project.updatedAt).getTime())/(24*60*60*1000)
let updated
let updated = new Date(project.updatedAt).toDateString().substring(4)
if (time < 1)
updated = "less than 1 day ago"
else if (time < 30)
updated = `${Math.floor(time)} day${time >= 2 ? "s" : ""} ago`
else
updated = new Date(project.updatedAt).toDateString().substring(4)
//Format progress
const {enabled, todoCount:todo, inProgressCount:doing, doneCount:done} = project.progress
//Append

View File

@@ -67,8 +67,8 @@
//Compute registration date
const diff = (Date.now()-(new Date(data.user.createdAt)).getTime())/(365*24*60*60*1000)
const years = Math.floor(diff)
const months = Math.ceil((diff-years)*12)
computed.registration = years ? `${years} year${s(years)} ago` : `${months} month${s(months)} ago`
const months = Math.floor((diff-years)*12)
computed.registration = years ? `${years} year${s(years)} ago` : months ? `${months} month${s(months)} ago` : `${Math.ceil(diff*365)} day${s(Math.ceil(diff*365))} ago`
computed.cakeday = [new Date(), new Date(data.user.createdAt)].map(date => date.toISOString().match(/(?<mmdd>\d{2}-\d{2})(?=T)/)?.groups?.mmdd).every((v, _, a) => v === a[0])
//Compute calendar

View File

@@ -1,8 +1,14 @@
/**
* @jest-environment node
*/
//Imports
const processes = require("child_process")
const yaml = require("js-yaml")
const fs = require("fs")
const path = require("path")
const url = require("url")
const axios = require("axios")
//Github action
const action = yaml.safeLoad(fs.readFileSync(path.join(__dirname, "../action.yml"), "utf8"))
@@ -22,13 +28,19 @@
})
})
//Tests run
describe.each([
["classic", {}],
["terminal", {}],
["repository", {repo:"metrics"}],
])("Template : %s", (template, query) => {
for (const [name, input, {skip = []} = {}] of [
//Web instance
const web = {}
web.run = async (vars) => (await axios(`http://localhost:3000/lowlighter?${new url.URLSearchParams(Object.fromEntries(Object.entries(vars).map(([key, value]) => [key.replace(/^plugin_/, "").replace(/_/g, "."), value])))}`)).status === 200
beforeAll(async () => await new Promise((solve, reject) => {
let stdout = ""
web.instance = processes.spawn("node", ["index.mjs"], {env:{...process.env, USE_MOCKED_DATA:true}})
web.instance.stdout.on("data", data => (stdout += data, /Server ready !/.test(stdout) ? solve() : null))
web.instance.stderr.on("data", data => console.error(`${data}`))
}))
afterAll(async () => await web.instance.kill())
//Test cases
const tests = [
["Base (header)", {
base:"header"
}],
@@ -195,7 +207,16 @@
["Gists plugin (default)", {
plugin_gists:true,
}, {skip:["terminal"]}],
])
]
//Tests run
describe("GitHub Action", () =>
describe.skip.each([
["classic", {}],
["terminal", {}],
["repository", {repo:"metrics"}],
])("Template : %s", (template, query) => {
for (const [name, input, {skip = []} = {}] of tests)
if (skip.includes(template))
test.skip(name, () => null)
else
@@ -210,3 +231,23 @@
...input
})).toBe(true), 60*1e3)
})
)
describe("Web instance", () =>
describe.each([
// ["classic", {}],
["terminal", {}],
// ["repository", {repo:"metrics"}],
])("Template : %s", (template, query) => {
for (const [name, input, {skip = []} = {}] of tests)
if (skip.includes(template))
test.skip(name, () => null)
else
test(name, async () => expect(await web.run({
template, base:0, ...query,
config_timezone:"Europe/Paris",
plugins_errors_fatal:true, verify:true,
...input
})).toBe(true), 60*1e3)
})
)