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")) const die = bool(core.getInput("plugins_errors_fatal"))
console.log(`Plugin errors │ ${die ? "die" : "warn"}`) console.log(`Plugin errors │ ${die ? "die" : "warn"}`)
//Verify svg
const verify = bool(core.getInput("verify"))
console.log(`Verify SVG │ ${verify}`)
//Build query //Build query
const query = JSON.parse(core.getInput("query") || "{}") const query = JSON.parse(core.getInput("query") || "{}")
console.log(`Query additional params │ ${JSON.stringify(query)}`) console.log(`Query additional params │ ${JSON.stringify(query)}`)
q = {...query, ...q, base:false, ...base, ...config, repositories, template} q = {...query, ...q, base:false, ...base, ...config, repositories, template}
//Render metrics //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`) 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 //Commit to repository
const dryrun = bool(core.getInput("dryrun")) const dryrun = bool(core.getInput("dryrun"))
if (dryrun) if (dryrun)

View File

@@ -2,4 +2,4 @@
import app from "./src/app.mjs" import app from "./src/app.mjs"
//Start app //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)", "token":"MY GITHUB API TOKEN", "//":"Your own GitHub API token (required)",
"restricted":[], "//":"List of authorized users, leave empty for unrestricted", "restricted":[], "//":"List of authorized users, leave empty for unrestricted",
"cached":3600000, "//":"Cached time for generated images, 0 to disable", "cached":3600000, "//":"Cached time for generated images, 0 to disable",
@@ -8,15 +11,13 @@
"optimize":true, "//":"Optimize SVG image", "optimize":true, "//":"Optimize SVG image",
"debug":false, "//":"Debug mode", "debug":false, "//":"Debug mode",
"repositories":100, "//":"Number of repositories to use to compute metrics", "repositories":100, "//":"Number of repositories to use to compute metrics",
"templates":{ "//":"Template configuration", "templates":{ "//":"Template configuration",
"default":"classic", "//":"Default template", "default":"classic", "//":"Default template",
"enabled":[], "//":"Enabled templates, leave empty to enable all templates" "enabled":[], "//":"Enabled templates, leave empty to enable all templates"
}, },
"plugins":{ "//":"Additional plugins (optional)", "plugins":{ "//":"Additional plugins (optional)",
"pagespeed":{ "//":"Pagespeed plugin", "pagespeed":{ "//":"Pagespeed plugin",
"enabled":true, "//":"Enable or disable PageSpeed metrics", "enabled":false, "//":"Enable or disable PageSpeed metrics",
"token":null, "//":"Pagespeed token (optional)" "token":null, "//":"Pagespeed token (optional)"
}, },
"traffic":{ "//":"Traffic plugin (GitHub API token must be RW for this to work)", "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)" "from":200, "//":"Number of activity events to base habits on (up to 1000)"
}, },
"languages":{ "//":"Languages plugin", "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", "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", "music":{ "//":"Music plugin",
"enabled":false, "//":"Enable or disable music recently played / random track from playlist", "enabled":false, "//":"Enable or disable music recently played / random track from playlist",
@@ -55,7 +56,7 @@
"enabled":false, "//":"Enable or disable personal projects display" "enabled":false, "//":"Enable or disable personal projects display"
}, },
"tweets":{ "//":"Tweets plugin", "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)" "token":null, "//":"Twitter token (required when enabled)"
} }
} }

View File

@@ -5,20 +5,44 @@
import cache from "memory-cache" import cache from "memory-cache"
import ratelimit from "express-rate-limit" import ratelimit from "express-rate-limit"
import compression from "compression" import compression from "compression"
import util from "util"
import setup from "./setup.mjs" import setup from "./setup.mjs"
import metrics from "./metrics.mjs" import metrics from "./metrics.mjs"
import util from "util" import mocks from "./mocks.mjs"
/** App */ /** App */
export default async function () { export default async function ({mock = false} = {}) {
//Load configuration settings //Load configuration settings
const {conf, Plugins, Templates} = await setup() 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 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 //Load octokits
const graphql = octokit.graphql.defaults({headers:{authorization: `token ${token}`}}) const api = {graphql:octokit.graphql.defaults({headers:{authorization: `token ${token}`}}), rest:new OctokitRest.Octokit({auth:token})}
const rest = new OctokitRest.Octokit({auth:token}) //Apply mocking if needed
if (mock)
Object.assign(api, await mocks(api))
const {graphql, rest} = api
//Setup server //Setup server
const app = express() const app = express()
@@ -108,7 +132,8 @@
try { try {
//Render //Render
console.debug(`metrics/app/${login} > ${util.inspect(req.query, {depth:Infinity, maxStringLength:256})}`) 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 //Cache
if ((!debug)&&(cached)&&(login !== "placeholder")) if ((!debug)&&(cached)&&(login !== "placeholder"))
cache.put(login, rendered, cached) cache.put(login, rendered, cached)
@@ -136,13 +161,14 @@
//Listen //Listen
app.listen(port, () => console.log([ app.listen(port, () => console.log([
`Listening on port | ${port}`, `Listening on port ${port}`,
`Debug mode | ${debug}`, `Debug mode ${debug}`,
`Restricted to users | ${restricted.size ? [...restricted].join(", ") : "(unrestricted)"}`, `Restricted to users ${restricted.size ? [...restricted].join(", ") : "(unrestricted)"}`,
`Cached time | ${cached} seconds`, `Cached time ${cached} seconds`,
`Rate limiter | ${ratelimiter ? util.inspect(ratelimiter, {depth:Infinity, maxStringLength:256}) : "(enabled)"}`, `Rate limiter ${ratelimiter ? util.inspect(ratelimiter, {depth:Infinity, maxStringLength:256}) : "(enabled)"}`,
`Max simultaneous users | ${maxusers ? `${maxusers} users` : "(unrestricted)"}`, `Max simultaneous users ${maxusers ? `${maxusers} users` : "(unrestricted)"}`,
`Plugins enabled | ${enabled.join(", ")}` `Plugins enabled ${enabled.join(", ")}`,
`Server ready !`
].join("\n"))) ].join("\n")))
} }

View File

@@ -12,7 +12,7 @@
import util from "util" import util from "util"
//Setup //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 //Compute rendering
try { try {
@@ -92,6 +92,15 @@
rendered = optimized 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 //Result
console.debug(`metrics/compute/${login} > success`) console.debug(`metrics/compute/${login} > success`)
return rendered return rendered

View File

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

View File

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

View File

@@ -67,8 +67,8 @@
//Compute registration date //Compute registration date
const diff = (Date.now()-(new Date(data.user.createdAt)).getTime())/(365*24*60*60*1000) const diff = (Date.now()-(new Date(data.user.createdAt)).getTime())/(365*24*60*60*1000)
const years = Math.floor(diff) const years = Math.floor(diff)
const months = Math.ceil((diff-years)*12) const months = Math.floor((diff-years)*12)
computed.registration = years ? `${years} year${s(years)} ago` : `${months} month${s(months)} ago` 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]) 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 //Compute calendar

View File

@@ -1,8 +1,14 @@
/**
* @jest-environment node
*/
//Imports //Imports
const processes = require("child_process") const processes = require("child_process")
const yaml = require("js-yaml") const yaml = require("js-yaml")
const fs = require("fs") const fs = require("fs")
const path = require("path") const path = require("path")
const url = require("url")
const axios = require("axios")
//Github action //Github action
const action = yaml.safeLoad(fs.readFileSync(path.join(__dirname, "../action.yml"), "utf8")) const action = yaml.safeLoad(fs.readFileSync(path.join(__dirname, "../action.yml"), "utf8"))
@@ -22,13 +28,19 @@
}) })
}) })
//Tests run //Web instance
describe.each([ const web = {}
["classic", {}], 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
["terminal", {}], beforeAll(async () => await new Promise((solve, reject) => {
["repository", {repo:"metrics"}], let stdout = ""
])("Template : %s", (template, query) => { web.instance = processes.spawn("node", ["index.mjs"], {env:{...process.env, USE_MOCKED_DATA:true}})
for (const [name, input, {skip = []} = {}] of [ 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)", {
base:"header" base:"header"
}], }],
@@ -195,7 +207,16 @@
["Gists plugin (default)", { ["Gists plugin (default)", {
plugin_gists:true, plugin_gists:true,
}, {skip:["terminal"]}], }, {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)) if (skip.includes(template))
test.skip(name, () => null) test.skip(name, () => null)
else else
@@ -210,3 +231,23 @@
...input ...input
})).toBe(true), 60*1e3) })).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)
})
)