diff options
| -rw-r--r-- | CHANGELOG.md | 4 | ||||
| -rw-r--r-- | example_reports/cobertura-empty.xml | 1 | ||||
| -rw-r--r-- | example_reports/cobertura-invalid.xml | 1 | ||||
| -rw-r--r-- | example_reports/cobertura-report.xml | 1 | ||||
| -rw-r--r-- | package-lock.json | 64 | ||||
| -rw-r--r-- | package.json | 4 | ||||
| -rw-r--r-- | public/templates/bash.template | 14 | ||||
| -rw-r--r-- | src/formats.test.ts | 79 | ||||
| -rw-r--r-- | src/formats.ts | 28 | ||||
| -rw-r--r-- | src/routes.test.ts | 175 | ||||
| -rw-r--r-- | src/routes.ts | 109 |
11 files changed, 454 insertions, 26 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index c1b1a66..0736b0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Cobertura XML format + ### Changed - Moved stage values for gradient to environment and made accessible via metadata diff --git a/example_reports/cobertura-empty.xml b/example_reports/cobertura-empty.xml new file mode 100644 index 0000000..7459b91 --- /dev/null +++ b/example_reports/cobertura-empty.xml @@ -0,0 +1 @@ +<?xml version="1.0"?><coverage lines-covered="0" lines-valid="0" line-rate="0" branches-covered="0" branches-valid="0" branch-rate="0" complexity="0" version="1.9" timestamp="1631817281"></coverage>
\ No newline at end of file diff --git a/example_reports/cobertura-invalid.xml b/example_reports/cobertura-invalid.xml new file mode 100644 index 0000000..236f9c4 --- /dev/null +++ b/example_reports/cobertura-invalid.xml @@ -0,0 +1 @@ +<?xml version="1.0"?><b>This isn't a report. 50.64%</b>
\ No newline at end of file diff --git a/example_reports/cobertura-report.xml b/example_reports/cobertura-report.xml new file mode 100644 index 0000000..65c4036 --- /dev/null +++ b/example_reports/cobertura-report.xml @@ -0,0 +1 @@ +<?xml version="1.0"?><coverage lines-covered="194" lines-valid="202" line-rate="0.9847282864932456" branches-covered="0" branches-valid="0" branch-rate="0" complexity="0" version="1.9" timestamp="1631817281"><sources><source>/home/professor/Documents/prj/august-offensive</source></sources><packages><package name="src/messages" line-rate="1" branch-rate="0" complexity="0"><classes><class name="callback" filename="src/messages/callback.rs" line-rate="1" branch-rate="0" complexity="0"><methods/><lines><line number="13" hits="1"/><line number="19" hits="1"/><line number="20" hits="1"/><line number="29" hits="3"/><line number="31" hits="1"/><line number="33" hits="1"/><line number="34" hits="1"/><line number="39" hits="1"/><line number="42" hits="2"/><line number="46" hits="3"/><line number="49" hits="1"/><line number="50" hits="1"/><line number="51" hits="1"/><line number="53" hits="1"/><line number="56" hits="1"/><line number="59" hits="2"/><line number="60" hits="2"/></lines></class><class name="mod" filename="src/messages/mod.rs" line-rate="1" branch-rate="0" complexity="0"><methods/><lines><line number="18" hits="3"/><line number="22" hits="3"/><line number="23" hits="3"/><line number="24" hits="3"/></lines></class><class name="not_understood" filename="src/messages/not_understood.rs" line-rate="1" branch-rate="0" complexity="0"><methods/><lines><line number="10" hits="1"/><line number="16" hits="1"/><line number="17" hits="1"/><line number="26" hits="3"/><line number="28" hits="1"/><line number="31" hits="1"/><line number="34" hits="2"/><line number="38" hits="3"/><line number="40" hits="1"/><line number="41" hits="1"/><line number="44" hits="1"/><line number="47" hits="2"/><line number="48" hits="2"/></lines></class></classes></package><package name="src/routes" line-rate="0.9937888198757764" branch-rate="0" complexity="0"><classes><class name="callback" filename="src/routes/callback.rs" line-rate="1" branch-rate="0" complexity="0"><methods/><lines><line number="4" hits="1"/><line number="5" hits="1"/><line number="6" hits="2"/><line number="9" hits="1"/><line number="10" hits="1"/><line number="11" hits="1"/><line number="14" hits="2"/><line number="24" hits="3"/><line number="26" hits="1"/><line number="27" hits="1"/><line number="29" hits="1"/><line number="30" hits="3"/><line number="31" hits="1"/><line number="32" hits="1"/><line number="35" hits="1"/><line number="38" hits="1"/><line number="40" hits="2"/><line number="41" hits="1"/><line number="42" hits="2"/><line number="43" hits="1"/><line number="44" hits="2"/><line number="48" hits="3"/><line number="50" hits="1"/><line number="51" hits="1"/><line number="53" hits="1"/><line number="54" hits="3"/><line number="55" hits="1"/><line number="56" hits="1"/><line number="59" hits="1"/><line number="62" hits="1"/><line number="64" hits="2"/><line number="65" hits="1"/><line number="66" hits="2"/><line number="67" hits="1"/><line number="68" hits="2"/><line number="72" hits="3"/><line number="74" hits="1"/><line number="75" hits="1"/><line number="76" hits="1"/><line number="79" hits="1"/><line number="82" hits="1"/><line number="84" hits="2"/><line number="85" hits="1"/><line number="86" hits="2"/><line number="87" hits="2"/><line number="88" hits="2"/></lines></class><class name="format_msg" filename="src/routes/format_msg.rs" line-rate="0.9736842105263158" branch-rate="0" complexity="0"><methods/><lines><line number="11" hits="2"/><line number="12" hits="2"/><line number="15" hits="2"/><line number="27" hits="4"/><line number="28" hits="4"/><line number="29" hits="7"/><line number="30" hits="1"/><line number="33" hits="12"/><line number="34" hits="0"/><line number="35" hits="3"/><line number="47" hits="3"/><line number="49" hits="1"/><line number="50" hits="1"/><line number="52" hits="1"/><line number="57" hits="1"/><line number="60" hits="1"/><line number="61" hits="2"/><line number="65" hits="3"/><line number="67" hits="1"/><line number="68" hits="1"/><line number="71" hits="1"/><line number="74" hits="2"/><line number="75" hits="2"/><line number="79" hits="3"/><line number="81" hits="1"/><line number="82" hits="1"/><line number="87" hits="2"/><line number="90" hits="3"/><line number="93" hits="2"/><line number="94" hits="2"/><line number="96" hits="1"/><line number="97" hits="1"/><line number="103" hits="1"/><line number="104" hits="2"/><line number="109" hits="3"/><line number="116" hits="1"/><line number="119" hits="1"/><line number="122" hits="2"/></lines></class><class name="mod" filename="src/routes/mod.rs" line-rate="1" branch-rate="0" complexity="0"><methods/><lines><line number="17" hits="2"/><line number="19" hits="2"/><line number="20" hits="6"/><line number="24" hits="3"/><line number="25" hits="3"/><line number="42" hits="3"/><line number="44" hits="1"/><line number="45" hits="1"/><line number="46" hits="3"/><line number="49" hits="2"/><line number="52" hits="2"/><line number="54" hits="2"/><line number="55" hits="1"/><line number="56" hits="2"/><line number="60" hits="3"/><line number="62" hits="1"/><line number="63" hits="1"/><line number="64" hits="3"/><line number="67" hits="2"/><line number="70" hits="2"/><line number="72" hits="2"/><line number="73" hits="1"/><line number="74" hits="2"/><line number="78" hits="3"/><line number="80" hits="1"/><line number="81" hits="1"/><line number="82" hits="3"/><line number="85" hits="2"/><line number="88" hits="2"/><line number="92" hits="3"/><line number="94" hits="1"/><line number="97" hits="1"/><line number="100" hits="2"/><line number="104" hits="3"/><line number="106" hits="1"/><line number="109" hits="1"/><line number="112" hits="1"/><line number="116" hits="3"/><line number="118" hits="1"/><line number="121" hits="1"/><line number="124" hits="1"/><line number="127" hits="1"/><line number="128" hits="20"/><line number="129" hits="7"/><line number="133" hits="2"/><line number="134" hits="2"/><line number="135" hits="8"/><line number="136" hits="2"/><line number="139" hits="4"/><line number="142" hits="3"/><line number="143" hits="3"/><line number="144" hits="3"/><line number="145" hits="3"/><line number="146" hits="3"/><line number="147" hits="6"/><line number="152" hits="6"/><line number="153" hits="3"/></lines></class><class name="not_understood" filename="src/routes/not_understood.rs" line-rate="1" branch-rate="0" complexity="0"><methods/><lines><line number="4" hits="2"/><line number="6" hits="2"/><line number="9" hits="2"/><line number="10" hits="2"/><line number="21" hits="3"/><line number="23" hits="1"/><line number="24" hits="1"/><line number="27" hits="1"/><line number="30" hits="1"/><line number="32" hits="2"/><line number="33" hits="1"/><line number="34" hits="2"/><line number="38" hits="3"/><line number="40" hits="1"/><line number="41" hits="1"/><line number="44" hits="1"/><line number="47" hits="1"/><line number="49" hits="2"/><line number="50" hits="1"/><line number="51" hits="2"/></lines></class></classes></package><package name="src" line-rate="0.9603960396039604" branch-rate="0" complexity="0"><classes><class name="schema" filename="src/schema.rs" line-rate="0" branch-rate="0" complexity="0"><methods/><lines><line number="75" hits="0"/><line number="76" hits="0"/><line number="77" hits="0"/><line number="78" hits="0"/><line number="79" hits="0"/><line number="80" hits="0"/><line number="81" hits="0"/></lines></class></classes></package></packages></coverage>
\ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 13ce4c2..d9b5016 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@types/express": "^4.17.6", "@types/jsdom": "^12.2.4", "@types/mongodb": "^3.5.16", + "@types/xml2js": "^0.4.9", "badgen": "^3.0.1", "dotenv": "^8.2.0", "express": "^4.17.1", @@ -19,7 +20,8 @@ "jsdom": "^15.2.1", "mongodb": "^3.5.7", "typescript": "^3.8.3", - "winston": "^3.2.1" + "winston": "^3.2.1", + "xml2js": "^0.4.23" }, "devDependencies": { "@microsoft/tsdoc": "^0.12.19", @@ -1478,6 +1480,14 @@ "integrity": "sha512-txGIh+0eDFzKGC25zORnswy+br1Ha7hj5cMVwKIU7+s0U2AxxJru/jZSMU6OC9MJWP6+pc/hc6ZjyZShpsyY2g==", "dev": true }, + "node_modules/@types/xml2js": { + "version": "0.4.9", + "resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.9.tgz", + "integrity": "sha512-CHiCKIihl1pychwR2RNX5mAYmJDACgFVCMT5OArMaO3erzwXVcBqPcusr+Vl8yeeXukxZqtF8mZioqX+mpjjdw==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/yargs": { "version": "15.0.14", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.14.tgz", @@ -8252,6 +8262,11 @@ "node": ">=6" } }, + "node_modules/sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, "node_modules/saxes": { "version": "3.1.11", "resolved": "https://registry.npmjs.org/saxes/-/saxes-3.1.11.tgz", @@ -10098,6 +10113,26 @@ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==" }, + "node_modules/xml2js": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", + "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "engines": { + "node": ">=4.0" + } + }, "node_modules/xmlchars": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", @@ -11331,6 +11366,14 @@ "integrity": "sha512-txGIh+0eDFzKGC25zORnswy+br1Ha7hj5cMVwKIU7+s0U2AxxJru/jZSMU6OC9MJWP6+pc/hc6ZjyZShpsyY2g==", "dev": true }, + "@types/xml2js": { + "version": "0.4.9", + "resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.9.tgz", + "integrity": "sha512-CHiCKIihl1pychwR2RNX5mAYmJDACgFVCMT5OArMaO3erzwXVcBqPcusr+Vl8yeeXukxZqtF8mZioqX+mpjjdw==", + "requires": { + "@types/node": "*" + } + }, "@types/yargs": { "version": "15.0.14", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.14.tgz", @@ -16557,6 +16600,11 @@ "sparse-bitfield": "^3.0.3" } }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, "saxes": { "version": "3.1.11", "resolved": "https://registry.npmjs.org/saxes/-/saxes-3.1.11.tgz", @@ -18044,6 +18092,20 @@ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==" }, + "xml2js": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", + "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + } + }, + "xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==" + }, "xmlchars": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", diff --git a/package.json b/package.json index 58edad6..15aeb26 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "@types/express": "^4.17.6", "@types/jsdom": "^12.2.4", "@types/mongodb": "^3.5.16", + "@types/xml2js": "^0.4.9", "badgen": "^3.0.1", "dotenv": "^8.2.0", "express": "^4.17.1", @@ -33,7 +34,8 @@ "jsdom": "^15.2.1", "mongodb": "^3.5.7", "typescript": "^3.8.3", - "winston": "^3.2.1" + "winston": "^3.2.1", + "xml2js": "^0.4.23" }, "devDependencies": { "@microsoft/tsdoc": "^0.12.19", diff --git a/public/templates/bash.template b/public/templates/bash.template index 4eb3a47..375143e 100644 --- a/public/templates/bash.template +++ b/public/templates/bash.template @@ -12,6 +12,8 @@ format="tarpaulin" report="" token="$COV_TOKEN" curl_verbosity="" +content_type="text/html" +extension="html" function verbose_say() { if [ -z "$SILENT" ] && [ "$VERBOSE" == "true" ]; then @@ -51,6 +53,10 @@ verbose_say if [ "$format" == "tarpaulin" ]; then report="${REPORT_FILE:-tarpaulin-report.html}" +elif [ "$format" == "cobertura" ]; then + report="${REPORT_FILE:-cobertura.xml}" + content_type="application/xml" + extension="xml" fi if [[ ! -f "$report" ]]; then @@ -65,9 +71,9 @@ fi say "Uploading $report . . ." response=$(curl -X POST --data-binary "@$report" \ - -H 'Content-Type: text/html' \ + -H "Content-Type: $content_type" \ $curl_verbosity \ - "$url/v1/$repo/$branch/$commit.html?token=$token&format=$format") + "$url/v1/$repo/$branch/$commit.$extension?token=$token&format=$format") if [ ! -z "$response" ]; then say "Error uploading report: $response" @@ -75,10 +81,10 @@ if [ ! -z "$response" ]; then else say "Successfully uploaded report!" say - say "View uploaded report at: $url/v1/$repo/$branch/$commit.html" + say "View uploaded report at: $url/v1/$repo/$branch/$commit.$extension" say "View coverage badge at: $url/v1/$repo/$branch/$commit.svg" say say "Shorthand links are also available, as the latest commit of this branch." - say "View latest report for branch $branch: $url/v1/$repo/$branch.html" + say "View latest report for branch $branch: $url/v1/$repo/$branch.$extension" say "View latest badge for branch $branch: $url/v1/$repo/$branch.svg" fi
\ No newline at end of file diff --git a/src/formats.test.ts b/src/formats.test.ts index 1536b5d..02f9a3e 100644 --- a/src/formats.test.ts +++ b/src/formats.test.ts @@ -74,7 +74,7 @@ describe("Formats object", () => { const result = Formats.listFormats(); // Assert - expect(result).toEqual(["tarpaulin"]); + expect(result).toEqual(["tarpaulin", "cobertura"]); }); it("should return the requested format", () => { @@ -105,14 +105,14 @@ describe("Tarpaulin format", () => { expect(matcher).toEqual(defaultColorMatches); }); - it("should parse coverage from a normal tarpaulin file", () => { + it("should parse coverage from a normal tarpaulin file", async () => { // Arrange const file = fs.readFileSync(reportPath("tarpaulin-report.html"), "utf-8"); const format = Formats.getFormat("tarpaulin"); // Act - const result = format.parseCoverage(file); + const result = await format.parseCoverage(file); // Assert expect(typeof result).toEqual("number"); @@ -122,14 +122,14 @@ describe("Tarpaulin format", () => { } }); - it("should parse coverage from an empty tarpaulin file", () => { + it("should parse coverage from an empty tarpaulin file", async () => { // Arrange const file = fs.readFileSync(reportPath("tarpaulin-empty.html"), "utf-8"); const format = Formats.getFormat("tarpaulin"); // Act - const result = format.parseCoverage(file); + const result = await format.parseCoverage(file); // Assert expect(typeof result).toEqual("number"); @@ -138,14 +138,79 @@ describe("Tarpaulin format", () => { } }); - it("should return error when parsing coverage from invalid file", () => { + it("should return error when parsing coverage from invalid file", async () => { // Arrange const file = fs.readFileSync(reportPath("tarpaulin-invalid.html"), "utf-8"); const format = Formats.getFormat("tarpaulin"); // Act - const result = format.parseCoverage(file); + const result = await format.parseCoverage(file); + + // Assert + expect(typeof result).not.toEqual("number"); + if (typeof result !== "number") { + expect(result.message).toEqual("Invalid report document"); + } + }); +}); + +describe("Cobertura format", () => { + const reportPath = (file: string): string => + path.join(__dirname, "..", "example_reports", file); + + it("should use the default color matcher", () => { + // Arrange + const format = Formats.getFormat("cobertura"); + + // Act + const matcher = format.matchColor; + + // Assert + expect(matcher).toEqual(defaultColorMatches); + }); + + it("should parse coverage from a normal cobertura file", async () => { + // Arrange + const file = fs.readFileSync(reportPath("cobertura-report.xml"), "utf-8"); + + const format = Formats.getFormat("cobertura"); + + // Act + const result = await format.parseCoverage(file); + + // Assert + expect(typeof result).toEqual("number"); + if (typeof result === "number") { + // 96.17% is the result given in the document itself + expect(result.toFixed(2)).toEqual("96.04"); + } + }); + + it("should parse coverage from an empty cobertura file", async () => { + // Arrange + const file = fs.readFileSync(reportPath("cobertura-empty.xml"), "utf-8"); + + const format = Formats.getFormat("cobertura"); + + // Act + const result = await format.parseCoverage(file); + + // Assert + expect(typeof result).toEqual("number"); + if (typeof result === "number") { + expect(result.toFixed(2)).toEqual("0.00"); + } + }); + + it("should return error when parsing coverage from invalid file", async () => { + // Arrange + const file = fs.readFileSync(reportPath("cobertura-invalid.xml"), "utf-8"); + + const format = Formats.getFormat("cobertura"); + + // Act + const result = await format.parseCoverage(file); // Assert expect(typeof result).not.toEqual("number"); diff --git a/src/formats.ts b/src/formats.ts index 11113f0..db13fa6 100644 --- a/src/formats.ts +++ b/src/formats.ts @@ -1,11 +1,12 @@ import { JSDOM } from "jsdom"; +import { Parser } from "xml2js"; import { InvalidReportDocumentError } from "./errors"; type CoverageResult = number | InvalidReportDocumentError; export interface Format { // returns the coverage value as %: Number(90.0), Number(100.0), Number(89.5) - parseCoverage: (contents: string) => CoverageResult; + parseCoverage: (contents: string) => Promise<CoverageResult>; matchColor: (coverage: number, style: GradientStyle) => string; fileName: string; } @@ -46,7 +47,7 @@ export const defaultColorMatches = ( const FormatsObj: FormatObj = { formats: { tarpaulin: { - parseCoverage: (contents: string): CoverageResult => { + parseCoverage: async (contents: string): Promise<CoverageResult> => { const file = new JSDOM(contents).window.document; const scripts = file.getElementsByTagName("script"); if (scripts.length === 0) { @@ -75,6 +76,29 @@ const FormatsObj: FormatObj = { }, matchColor: defaultColorMatches, fileName: "index.html" + }, + cobertura: { + parseCoverage: async (contents: string): Promise<CoverageResult> => { + try { + const document = await new Parser().parseStringPromise(contents); + + if (document.coverage === undefined) { + return new InvalidReportDocumentError(); + } else { + const validLines = Number(document.coverage.$["lines-valid"]); + const coveredLines = Number(document.coverage.$["lines-covered"]); + // do not error if LOC is 0 + if (validLines === 0) { + return 0.0; + } + return (100 * coveredLines) / validLines; + } + } catch (err) { + return new InvalidReportDocumentError(); + } + }, + matchColor: defaultColorMatches, + fileName: "index.xml" } }, diff --git a/src/routes.test.ts b/src/routes.test.ts index e1b8f74..f8bcfcd 100644 --- a/src/routes.test.ts +++ b/src/routes.test.ts @@ -174,12 +174,18 @@ describe("Badges and reports", () => { "testbranch", "testcommit" ); - const actualReport = path.join( + const tarpaulinReport = path.join( __dirname, "..", "example_reports", "tarpaulin-report.html" ); + const coberturaReport = path.join( + __dirname, + "..", + "example_reports", + "cobertura-report.xml" + ); const fakeBadge = badgen({ label: "coverage", status: "120%", @@ -190,9 +196,13 @@ describe("Badges and reports", () => { // place test files on HOST_DIR await fs.promises.mkdir(reportPath, { recursive: true }); await fs.promises.copyFile( - actualReport, + tarpaulinReport, path.join(reportPath, "index.html") ); + await fs.promises.copyFile( + coberturaReport, + path.join(reportPath, "index.xml") + ); await fs.promises.writeFile(path.join(reportPath, "badge.svg"), fakeBadge); }); @@ -202,7 +212,7 @@ describe("Badges and reports", () => { .get("/v1/testorg/testrepo/testbranch/testcommit.html") .expect("Content-Type", /html/) .expect(200); - const buffer = await fs.promises.readFile(actualReport); + const buffer = await fs.promises.readFile(tarpaulinReport); expect(res.text).toEqual(buffer.toString("utf-8")); }); @@ -214,6 +224,24 @@ describe("Badges and reports", () => { }); }); + describe("GET /v1/:org/:repo/:branch/:commit.xml", () => { + it("should retrieve the stored report file", async () => { + const res = await (await request()) + .get("/v1/testorg/testrepo/testbranch/testcommit.xml") + .expect("Content-Type", /xml/) + .expect(200); + const buffer = await fs.promises.readFile(coberturaReport); + + expect(res.text).toEqual(buffer.toString("utf-8")); + }); + + it("should return 404 if file does not exist", async () => { + await (await request()) + .get("/v1/neorg/nerepo/nebranch/necommit.xml") + .expect(404); + }); + }); + describe("GET /v1/:org/:repo/:branch.html", () => { it("should retrieve the stored report file with the associated head commit", async () => { const mockMeta = mock(); @@ -221,7 +249,7 @@ describe("Badges and reports", () => { .get("/v1/testorg/testrepo/testbranch.html") .expect("Content-Type", /html/) .expect(200); - const buffer = await fs.promises.readFile(actualReport); + const buffer = await fs.promises.readFile(tarpaulinReport); expect(mockMeta.getHeadCommit).toHaveBeenCalledTimes(1); expect(res.text).toEqual(buffer.toString("utf-8")); @@ -248,6 +276,40 @@ describe("Badges and reports", () => { }); }); + describe("GET /v1/:org/:repo/:branch.xml", () => { + it("should retrieve the stored report file with the associated head commit", async () => { + const mockMeta = mock(); + const res = await (await request(mockMeta)) + .get("/v1/testorg/testrepo/testbranch.xml") + .expect("Content-Type", /xml/) + .expect(200); + const buffer = await fs.promises.readFile(coberturaReport); + + expect(mockMeta.getHeadCommit).toHaveBeenCalledTimes(1); + expect(res.text).toEqual(buffer.toString("utf-8")); + }); + + it("should return 404 if file does not exist", async () => { + await (await request()).get("/v1/neorg/nerepo/nebranch.xml").expect(404); + }); + + it("should return 404 if head commit not found", async () => { + const head = jest.fn( + () => new Promise(solv => solv(new BranchNotFoundError())) + ); + await (await request(mock(head))) + .get("/v1/testorg/testrepo/testbranch.xml") + .expect(404); + }); + + it("should return 500 if promise is rejected", async () => { + const head = jest.fn(() => new Promise((_, rej) => rej("fooey"))); + await (await request(mock(head))) + .get("/v1/testorg/testrepo/testbranch.xml") + .expect(500); + }); + }); + describe("GET /v1/:org/:repo/:branch/:commit.svg", () => { it("should retrieve the stored report badge", async () => { const res = await (await request()) @@ -307,9 +369,6 @@ describe("Uploads", () => { "newthis", "newthat" ); - const data = fs.promises.readFile( - path.join(__dirname, "..", "example_reports", "tarpaulin-report.html") - ); beforeEach(async () => { try { @@ -320,6 +379,10 @@ describe("Uploads", () => { }); describe("POST /v1/:org/:repo/:branch/:commit.html", () => { + const data = fs.promises.readFile( + path.join(__dirname, "..", "example_reports", "tarpaulin-report.html") + ); + it("should upload the report and generate a badge", async () => { const mockMeta = mock(); await (await request(mockMeta)) @@ -375,7 +438,10 @@ describe("Uploads", () => { it("should return 413 when request body is not the appropriate format", async () => { const file = await data; - const bigData = Buffer.concat([file, file]); + let bigData = file; + while (bigData.length <= config.uploadLimit) { + bigData = Buffer.concat([bigData, file]); + } await (await request()) .post( `/v1/testorg/testrepo/newthis/newthat.html?token=${config.token}&format=tarpaulin` @@ -404,4 +470,97 @@ describe("Uploads", () => { .expect(500); }); }); + + describe("POST /v1/:org/:repo/:branch/:commit.xml", () => { + const data = fs.promises.readFile( + path.join(__dirname, "..", "example_reports", "cobertura-report.xml") + ); + + it("should upload the report and generate a badge", async () => { + const mockMeta = mock(); + await (await request(mockMeta)) + .post( + `/v1/testorg/testrepo/newthis/newthat.xml?token=${config.token}&format=cobertura` + ) + .send(await data) + .expect(200); + + expect(mockMeta.updateBranch).toBeCalledWith({ + organization: "testorg", + repository: "testrepo", + branch: "newthis", + head: "newthat" + }); + expect(mockMeta.updateBranch).toHaveBeenCalledTimes(1); + await fs.promises.access( + path.join(reportPath, "index.xml"), + fs.constants.R_OK + ); + await fs.promises.access( + path.join(reportPath, "badge.svg"), + fs.constants.R_OK + ); + }); + + it("should return 401 when token is not correct", async () => { + await (await request()) + .post( + `/v1/testorg/testrepo/newthis/newthat.xml?token=wrong&format=cobertura` + ) + .send(await data) + .expect(401); + }); + + it("should return 406 with an invalid format", async () => { + await (await request()) + .post( + `/v1/testorg/testrepo/newthis/newthat.xml?token=${config.token}&format=pepperoni` + ) + .send(await data) + .expect(406); + }); + + it("should return 400 when request body is not the appropriate format", async () => { + await (await request()) + .post( + `/v1/testorg/testrepo/newthis/newthat.xml?token=${config.token}&format=cobertura` + ) + .send("This is not a file") + .expect(400); + }); + + it("should return 413 when request body is not the appropriate format", async () => { + const file = await data; + let bigData = file; + while (bigData.length <= config.uploadLimit) { + bigData = Buffer.concat([bigData, file]); + } + await (await request()) + .post( + `/v1/testorg/testrepo/newthis/newthat.xml?token=${config.token}&format=cobertura` + ) + .send(bigData) + .expect(413); + }); + + it("should return 500 when Metadata does not create branch", async () => { + const update = jest.fn(() => new Promise(solv => solv(false))); + await (await request(mock(jest.fn(), update))) + .post( + `/v1/testorg/testrepo/newthis/newthat.xml?token=${config.token}&format=cobertura` + ) + .send(await data) + .expect(500); + }); + + it("should return 500 when promise chain is rejected", async () => { + const update = jest.fn(() => new Promise((_, rej) => rej("fooey 2"))); + await (await request(mock(jest.fn(), update))) + .post( + `/v1/testorg/testrepo/newthis/newthat.xml?token=${config.token}&format=cobertura` + ) + .send(await data) + .expect(500); + }); + }); }); diff --git a/src/routes.ts b/src/routes.ts index 634af7d..df23a54 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -26,7 +26,7 @@ export default (metadata: Metadata): Router => { identity.branch, identity.head ); - const coverage = formatter.parseCoverage(contents); + const coverage = await formatter.parseCoverage(contents); if (typeof coverage !== "number") { return coverage; } @@ -138,6 +138,68 @@ export default (metadata: Metadata): Router => { }); }); + // Upload XML file + router.post("/v1/:org/:repo/:branch/:commit.xml", (req, res) => { + const { org, repo, branch, commit } = req.params; + + const { token, format } = req.query; + if (token != metadata.getToken()) { + return res.status(401).send(Messages.InvalidToken); + } + + if (typeof format !== "string" || !formats.listFormats().includes(format)) { + return res.status(406).send(Messages.InvalidFormat); + } + + const limit = metadata.getUploadLimit(); + if (Number(req.headers["content-length"] ?? 0) > limit) { + return res.status(413).send(Messages.FileTooLarge); + } + + let contents = ""; + req.on("data", raw => { + if (contents.length <= limit) { + contents += raw; + } + }); + req.on("end", async () => { + // Ignore large requests + if (contents.length > limit) { + return res.status(413).send(Messages.FileTooLarge); + } + + const formatter = formats.getFormat(format); + const identity = { + organization: org, + repository: repo, + branch, + head: commit + }; + + try { + const result = await commitFormatDocs(contents, identity, formatter); + + if (typeof result === "boolean") { + if (result) { + return res.status(200).send(); + } else { + logger.error( + "Unknown error while attempting to commit branch update" + ); + return res.status(500).send(Messages.UnknownError); + } + } else { + return res.status(400).send(Messages.InvalidFormat); + } + } catch (err) { + logger.error( + err ?? "Unknown error occurred while processing POST request" + ); + return res.status(500).send(Messages.UnknownError); + } + }); + }); + const retrieveFile = ( res: express.Response, identity: HeadIdentity, @@ -188,6 +250,34 @@ export default (metadata: Metadata): Router => { router.get("/v1/:org/:repo/:branch.html", (req, res) => { const { org, repo, branch } = req.params; + const format = formats.formats.tarpaulin; + + metadata.getHeadCommit(org, repo, branch).then( + result => { + if (typeof result === "string") { + const identity = { + organization: org, + repository: repo, + branch, + head: result.toString() + }; + retrieveFile(res, identity, format.fileName); + } else { + res.status(404).send(result.message); + } + }, + err => { + logger.error( + err ?? "Error occurred while fetching commit for GET request" + ); + res.status(500).send(Messages.UnknownError); + } + ); + }); + + router.get("/v1/:org/:repo/:branch.xml", (req, res) => { + const { org, repo, branch } = req.params; + const format = formats.formats.cobertura; metadata.getHeadCommit(org, repo, branch).then( result => { @@ -198,7 +288,7 @@ export default (metadata: Metadata): Router => { branch, head: result.toString() }; - retrieveFile(res, identity, "index.html"); + retrieveFile(res, identity, format.fileName); } else { res.status(404).send(result.message); } @@ -227,13 +317,26 @@ export default (metadata: Metadata): Router => { // provide hard link for commit router.get("/v1/:org/:repo/:branch/:commit.html", (req, res) => { const { org, repo, branch, commit } = req.params; + const format = formats.formats.tarpaulin; + const identity = { + organization: org, + repository: repo, + branch, + head: commit + }; + retrieveFile(res, identity, format.fileName); + }); + + router.get("/v1/:org/:repo/:branch/:commit.xml", (req, res) => { + const { org, repo, branch, commit } = req.params; + const format = formats.formats.cobertura; const identity = { organization: org, repository: repo, branch, head: commit }; - retrieveFile(res, identity, "index.html"); + retrieveFile(res, identity, format.fileName); |
