From faef64e04f38bfdbc5c5e0ba725aee1d677385c0 Mon Sep 17 00:00:00 2001 From: Johann Rudloff Date: Sat, 27 Oct 2018 15:31:02 +0200 Subject: [PATCH] Add server. --- .gitignore | 2 + LICENSE | 25 +++ Pipfile | 15 ++ Pipfile.lock | 269 +++++++++++++++++++++++++++++++++ README.md | 30 ++++ dronecov.py | 178 ++++++++++++++++++++++ runtests.sh | 12 ++ templates/badge-template.svg | 21 +++ tests/common.yaml | 3 + tests/test_auth.tavern.yaml | 33 ++++ tests/test_minimal.tavern.yaml | 67 ++++++++ tests/utils.py | 10 ++ 12 files changed, 665 insertions(+) create mode 100644 LICENSE create mode 100644 Pipfile create mode 100644 Pipfile.lock create mode 100644 README.md create mode 100755 dronecov.py create mode 100755 runtests.sh create mode 100644 templates/badge-template.svg create mode 100644 tests/common.yaml create mode 100644 tests/test_auth.tavern.yaml create mode 100644 tests/test_minimal.tavern.yaml create mode 100644 tests/utils.py diff --git a/.gitignore b/.gitignore index f7c3006..87aa392 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ +/dronecov.db /reporter/dist/ /reporter/node_modules/ +/tests/tmp.db diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5121a28 --- /dev/null +++ b/LICENSE @@ -0,0 +1,25 @@ +BSD 2-Clause License + +Copyright (c) 2018, Johann Rudloff +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..69cf57d --- /dev/null +++ b/Pipfile @@ -0,0 +1,15 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +flask = ">=1.0.2" +Flask-SQLAlchemy = ">=2.3.2" +gunicorn = "*" + +[dev-packages] +tavern = ">=0.19" + +[requires] +python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..922115a --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,269 @@ +{ + "_meta": { + "hash": { + "sha256": "8f5482fe1915694a24c884eff2548d07eae97528b2a7277a07a4c6494663d716" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "click": { + "hashes": [ + "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", + "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" + ], + "version": "==7.0" + }, + "flask": { + "hashes": [ + "sha256:2271c0070dbcb5275fad4a82e29f23ab92682dc45f9dfbc22c02ba9b9322ce48", + "sha256:a080b744b7e345ccfcbc77954861cb05b3c63786e93f2b3875e0913d44b43f05" + ], + "index": "pypi", + "version": "==1.0.2" + }, + "flask-sqlalchemy": { + "hashes": [ + "sha256:3bc0fac969dd8c0ace01b32060f0c729565293302f0c4269beed154b46bec50b", + "sha256:5971b9852b5888655f11db634e87725a9031e170f37c0ce7851cf83497f56e53" + ], + "index": "pypi", + "version": "==2.3.2" + }, + "gunicorn": { + "hashes": [ + "sha256:aa8e0b40b4157b36a5df5e599f45c9c76d6af43845ba3b3b0efe2c70473c2471", + "sha256:fa2662097c66f920f53f70621c6c58ca4a3c4d3434205e608e121b5b3b71f4f3" + ], + "index": "pypi", + "version": "==19.9.0" + }, + "itsdangerous": { + "hashes": [ + "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19", + "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749" + ], + "version": "==1.1.0" + }, + "jinja2": { + "hashes": [ + "sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd", + "sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4" + ], + "version": "==2.10" + }, + "markupsafe": { + "hashes": [ + "sha256:a6be69091dac236ea9c6bc7d012beab42010fa914c459791d627dad4910eb665" + ], + "version": "==1.0" + }, + "sqlalchemy": { + "hashes": [ + "sha256:c5951d9ef1d5404ed04bae5a16b60a0779087378928f997a294d1229c6ca4d3e" + ], + "version": "==1.2.12" + }, + "werkzeug": { + "hashes": [ + "sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c", + "sha256:d5da73735293558eb1651ee2fddc4d0dedcfa06538b8813a2e20011583c9e49b" + ], + "version": "==0.14.1" + } + }, + "develop": { + "atomicwrites": { + "hashes": [ + "sha256:0312ad34fcad8fac3704d441f7b317e50af620823353ec657a53e981f92920c0", + "sha256:ec9ae8adaae229e4f8446952d204a3e4b5fdd2d099f9be3aaf556120135fb3ee" + ], + "version": "==1.2.1" + }, + "attrs": { + "hashes": [ + "sha256:10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69", + "sha256:ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb" + ], + "version": "==18.2.0" + }, + "certifi": { + "hashes": [ + "sha256:339dc09518b07e2fa7eda5450740925974815557727d6bd35d319c1524a04a4c", + "sha256:6d58c986d22b038c8c0df30d639f23a3e6d172a05c3583e766f4c0b785c0986a" + ], + "version": "==2018.10.15" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "contextlib2": { + "hashes": [ + "sha256:509f9419ee91cdd00ba34443217d5ca51f5a364a404e1dce9e8979cea969ca48", + "sha256:f5260a6e679d2ff42ec91ec5252f4eeffdcf21053db9113bd0a8e4d953769c00" + ], + "version": "==0.5.5" + }, + "docopt": { + "hashes": [ + "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491" + ], + "version": "==0.6.2" + }, + "future": { + "hashes": [ + "sha256:eb6d4df04f1fb538c99f69c9a28b255d1ee4e825d479b9c62fc38c0cf38065a4" + ], + "version": "==0.17.0" + }, + "idna": { + "hashes": [ + "sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e", + "sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16" + ], + "version": "==2.7" + }, + "jmespath": { + "hashes": [ + "sha256:6a81d4c9aa62caf061cb517b4d9ad1dd300374cd4706997aff9cd6aedd61fc64", + "sha256:f11b4461f425740a1d908e9a3f7365c3d2e569f6ca68a2ff8bc5bcd9676edd63" + ], + "version": "==0.9.3" + }, + "more-itertools": { + "hashes": [ + "sha256:c187a73da93e7a8acc0001572aebc7e3c69daf7bf6881a2cea10650bd4420092", + "sha256:c476b5d3a34e12d40130bc2f935028b5f636df8f372dc2c1c01dc19681b2039e", + "sha256:fcbfeaea0be121980e15bc97b3817b5202ca73d0eae185b4550cbfce2a3ebb3d" + ], + "version": "==4.3.0" + }, + "paho-mqtt": { + "hashes": [ + "sha256:31911f6031de306c27ed79dc77b690d7c55b0dcb0f0434ca34ec6361d0371122" + ], + "version": "==1.3.1" + }, + "pbr": { + "hashes": [ + "sha256:8fc938b1123902f5610b06756a31b1e6febf0d105ae393695b0c9d4244ed2910", + "sha256:f20ec0abbf132471b68963bb34d9c78e603a5cf9e24473f14358e66551d47475" + ], + "version": "==5.1.0" + }, + "pluggy": { + "hashes": [ + "sha256:447ba94990e8014ee25ec853339faf7b0fc8050cdc3289d4d71f7f410fb90095", + "sha256:bde19360a8ec4dfd8a20dcb811780a30998101f078fc7ded6162f0076f50508f" + ], + "version": "==0.8.0" + }, + "py": { + "hashes": [ + "sha256:bf92637198836372b520efcba9e020c330123be8ce527e535d185ed4b6f45694", + "sha256:e76826342cefe3c3d5f7e8ee4316b80d1dd8a300781612ddbc765c17ba25a6c6" + ], + "version": "==1.7.0" + }, + "pyjwt": { + "hashes": [ + "sha256:30b1380ff43b55441283cc2b2676b755cca45693ae3097325dea01f3d110628c", + "sha256:4ee413b357d53fd3fb44704577afac88e72e878716116270d722723d65b42176" + ], + "version": "==1.6.4" + }, + "pykwalify": { + "hashes": [ + "sha256:428733907fe5c458fbea5de63a755f938edccd622c7a1d0b597806141976f00e", + "sha256:7e8b39c5a3a10bc176682b3bd9a7422c39ca247482df198b402e8015defcceb2" + ], + "version": "==1.7.0" + }, + "pytest": { + "hashes": [ + "sha256:212be78a6fa5352c392738a49b18f74ae9aeec1040f47c81cadbfd8d1233c310", + "sha256:6f6c1efc8d0ccc21f8f6c34d8330baca883cf109b66b3df954b0a117e5528fb4" + ], + "version": "==3.9.2" + }, + "python-box": { + "hashes": [ + "sha256:16ba64b0efabee84f08b1d6721c627ee8e53e6259be7211e84a4f3d3f9212c3c", + "sha256:b79b37b46d2b7067a956c97eb1d6176536f3c6a307cdb12136ade67ea4308b4a", + "sha256:c5499c733fd4447270b82aa7a8e369387fefe9a53990682229b6d667c4e5d633" + ], + "version": "==3.2.1" + }, + "python-dateutil": { + "hashes": [ + "sha256:2f13d3ea236aeb237e7258d5729c46eafe1506fd7f8507f34730734ed8b37454", + "sha256:f7cde3aecf8a797553d6ec49b65f0fbcffe7ffb971ccac452d181c28fd279936" + ], + "version": "==2.7.4" + }, + "pyyaml": { + "hashes": [ + "sha256:3d7da3009c0f3e783b2c873687652d83b1bbfd5c88e9813fb7e5b03c0dd3108b", + "sha256:3ef3092145e9b70e3ddd2c7ad59bdd0252a94dfe3949721633e41344de00a6bf", + "sha256:40c71b8e076d0550b2e6380bada1f1cd1017b882f7e16f09a65be98e017f211a", + "sha256:558dd60b890ba8fd982e05941927a3911dc409a63dcb8b634feaa0cda69330d3", + "sha256:a7c28b45d9f99102fa092bb213aa12e0aaf9a6a1f5e395d36166639c1f96c3a1", + "sha256:aa7dd4a6a427aed7df6fb7f08a580d68d9b118d90310374716ae90b710280af1", + "sha256:bc558586e6045763782014934bfaf39d48b8ae85a2713117d16c39864085c613", + "sha256:d46d7982b62e0729ad0175a9bc7e10a566fc07b224d2c79fafb5e032727eaa04", + "sha256:d5eef459e30b09f5a098b9cea68bebfeb268697f78d647bd255a085371ac7f3f", + "sha256:e01d3203230e1786cd91ccfdc8f8454c8069c91bee3962ad93b87a4b2860f537", + "sha256:e170a9e6fcfd19021dd29845af83bb79236068bf5fd4df3327c1be18182b2531" + ], + "version": "==3.13" + }, + "requests": { + "hashes": [ + "sha256:99dcfdaaeb17caf6e526f32b6a7b780461512ab3f1d992187801694cba42770c", + "sha256:a84b8c9ab6239b578f22d1c21d51b696dcfe004032bb80ea832398d6909d7279" + ], + "version": "==2.20.0" + }, + "six": { + "hashes": [ + "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", + "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb" + ], + "version": "==1.11.0" + }, + "stevedore": { + "hashes": [ + "sha256:b92bc7add1a53fb76c634a178978d113330aaf2006f9498d9e2414b31fbfc104", + "sha256:c58b7c231a9c4890cd3c2b5d2b23bd63fa807ff934d68579e3f6c3a1735e8a7c" + ], + "version": "==1.30.0" + }, + "tavern": { + "hashes": [ + "sha256:3564d0f059321e3699fd32d3d5c14970582f8502a6a2bc103fc81ff86f23d580" + ], + "index": "pypi", + "version": "==0.19.1" + }, + "urllib3": { + "hashes": [ + "sha256:41c3db2fc01e5b907288010dec72f9d0a74e37d6994e6eb56849f59fea2265ae", + "sha256:8819bba37a02d143296a4d032373c4dd4aca11f6d4c9973335ca75f9c8475f59" + ], + "version": "==1.24" + } + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..3ca9daa --- /dev/null +++ b/README.md @@ -0,0 +1,30 @@ +# Lighweight Coverage Tracking Server for Drone CI + +This is the target server, where the coverage reporter can post test results. + + +# Running + + pipenv install + + # Optional, set database URI (default is ./dronecov.db) + export DRONECOV_DB_URI=sqlite:///./var/dronecov_data.db + + pipenv run ./dronecov.py init + pipenv run gunicorn -b 127.0.0.1:5000 dronecov:app + + # Generate access token + pipenv run ./dronecov.py token username "Token Name / Description" + + +# Develpment + +Run development server: + + pipenv install --dev + + DRONECOV_DB_URI=sqlite:///./tests/tmp.db FLASK_DEBUG=1 FLASK_APP=dronecov.py pipenv run flask run + +Run tests: + + ./runtests.sh diff --git a/dronecov.py b/dronecov.py new file mode 100755 index 0000000..4463437 --- /dev/null +++ b/dronecov.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python3 + +from flask import Flask, abort, json, render_template, request +import flask +from flask_sqlalchemy import SQLAlchemy + +import datetime +import os + +MIME_TYPE_SVG = 'image/svg+xml;charset=utf-8' + +colormap = { + 'green': '#97ca00', + 'orange': '#fe7d37', + 'red': '#e05d44', +} + +app = Flask(__name__) +app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('DRONECOV_DB_URI', 'sqlite:///./dronecov.db') +app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False +db = SQLAlchemy(app) + +class CoverageInfo(db.Model): + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(255), nullable=False) + reponame = db.Column(db.String(255), nullable=False) + branch = db.Column(db.String(255), nullable=False) + build_id = db.Column(db.String(8), nullable=False) + coverage = db.Column(db.Float(), nullable=False) + created_at = db.Column(db.DateTime(), nullable=False, default=datetime.datetime.utcnow) + + def __repr__(self): + return '' % (self.username, self.reponame, self.branch, self.build_id,self.coverage) + +class AccessToken(db.Model): + id = db.Column(db.Integer, primary_key=True) + token = db.Column(db.String(32), unique=True, nullable=False) + name = db.Column(db.String(255), nullable=False) + username = db.Column(db.String(255), nullable=False) + created_at = db.Column(db.DateTime(), nullable=False, default=datetime.datetime.utcnow) + +# Test support: +# For the test DB, create all tables without asking +if app.config['SQLALCHEMY_DATABASE_URI'] == 'sqlite:///./tests/tmp.db': + db.create_all() + +class UnauthorizedException(Exception): + pass + +class TokenUnauthorizedException(Exception): + pass + +def coverage_precision(cov): + if cov >= 99.95: + return "100" + if cov >= 9.995: + # return "%d.%01d" % (cov, ((cov-int(cov))*10)) + return "%.1f" % cov + return "%.2f" % cov + +def format_coverage(cov): + return coverage_precision(cov) + " %" + +def render_color(cov: float, threshold_warn: float, threshold_error: float) -> str: + if cov <= threshold_error: + return "red" + if cov <= threshold_warn: + return "orange" + return "green" + +@app.errorhandler(UnauthorizedException) +def handle_unauthorized(error): + return ("Unauthorized", 401, {}) + +@app.errorhandler(TokenUnauthorizedException) +def handle_unauthorized(error): + return ("Forbidden", 403, {}) + +@app.route('////coverage.svg') +def get_coverage_svg(user: str, repo: str, branch: str): + try: + threshold_error = float(request.args.get('error', 5)) + threshold_warn = float(request.args.get('warn', 80)) + except ValueError as e: + return (str(e), 400, {}) + + cov = db.session.query(CoverageInfo).filter_by( + username=user, + reponame=repo, + branch=branch).order_by(CoverageInfo.created_at.desc()).first() + + if cov is not None: + if app.debug and 'cov' in request.args: + cov.coverage = float(request.args.get('cov')) + coverage_string = format_coverage(cov.coverage) + color = colormap[render_color(cov.coverage, threshold_warn, threshold_error)] + else: + coverage_string = 'N/A' + color = colormap['red'] + + return (render_template('badge-template.svg', + w1=60, w2=54, pad=4, + coverage=coverage_string, + color=color + ), 200, { + 'Content-Type': MIME_TYPE_SVG, + }) + +AUTH_PREFIX = 'Bearer ' + +def validate_coverage_report(user: str, repo: str, branch: str, cov_json) -> CoverageInfo: + cov_total = float(cov_json.get('coverage_total')) + build_number = int(cov_json.get('build_number')) + + return CoverageInfo(coverage=cov_total, + build_id=build_number, + username=user, + reponame=repo, + branch=branch) + +def token_can_access(token: str, user: str, repo: str): + """Check that token can access "user/repo", otherwise throw an exception.""" + tk = db.session.query(AccessToken).filter_by(username=user, token=token).first() + if tk is None: + raise TokenUnauthorizedException() + +def check_authorization(user: str, repo: str): + auth = request.headers.get('Authorization', '') + token = auth[len(AUTH_PREFIX):] + if not (auth.startswith(AUTH_PREFIX) and len(token) == 32): + raise UnauthorizedException() + return token_can_access(token, user, repo) + +@app.route('////coverage', methods=['POST']) +def update_coverage(user: str, repo: str, branch: str): + check_authorization(user, repo) + + try: + cov = validate_coverage_report(user, repo, branch, request.json) + except (TypeError, ValueError) as e: + return (str(e), 400, {}) + + db.session.add(cov) + db.session.commit() + + return ('OK', 201, None) + +def generate_token() -> str: + import random + alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' + return ''.join(random.choice(alphabet) for _ in range(32)) + +if __name__ == '__main__': + import sys + if sys.argv[1] == 'init': + db.create_all() + print("DB created.") + elif sys.argv[1] in ['token', 'token-batch']: + user_repo = sys.argv[2] + if '/' not in user_repo: + user_repo += '/*' + user, repo = user_repo.split('/') + if repo not in ['', '*']: + print("warning: repo name is ignored, token is valid for all repos belonging to " + user) + t = AccessToken(username = user, + name = sys.argv[3]) + t.token = generate_token() + db.session.add(t) + db.session.commit() + + if sys.argv[1] == 'token': + print('Name: %s' % (t.name)) + print('Access Token: %s' % (t.token)) + print('Valid repos: %s/*' % (t.username)) + else: + # Batch mode, print token and nothing else + print(t.token) + diff --git a/runtests.sh b/runtests.sh new file mode 100755 index 0000000..b77c4bd --- /dev/null +++ b/runtests.sh @@ -0,0 +1,12 @@ +#!/bin/sh + +set -e +set -u + +ACCESS_TOKEN=$(DRONECOV_DB_URI=sqlite:///./tests/tmp.db pipenv run ./dronecov.py token-batch testuser token-name) +export ACCESS_TOKEN + +PYTHONPATH="$PWD:${PYTHONPATH:-}" \ + pipenv run pytest \ + --tavern-global-cfg tests/common.yaml \ + tests/*.tavern.yaml -v diff --git a/templates/badge-template.svg b/templates/badge-template.svg new file mode 100644 index 0000000..235c9a4 --- /dev/null +++ b/templates/badge-template.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + coverage + coverage + {{coverage}} + {{coverage}} + + diff --git a/tests/common.yaml b/tests/common.yaml new file mode 100644 index 0000000..c262d0d --- /dev/null +++ b/tests/common.yaml @@ -0,0 +1,3 @@ +--- +variables: + host: http://localhost:5000 diff --git a/tests/test_auth.tavern.yaml b/tests/test_auth.tavern.yaml new file mode 100644 index 0000000..b64daf1 --- /dev/null +++ b/tests/test_auth.tavern.yaml @@ -0,0 +1,33 @@ +--- +test_name: Access without token and with invalid token is rejectd + +stages: + - name: POST without token + + request: + url: '{host}/testuser/testrepo/master/coverage' + method: POST + headers: + content-type: application/json + json: + coverage_total: 50 + build_number: 50 + + response: + status_code: 401 + + + - name: POST with invlaid token + + request: + url: '{host}/testuser/testrepo/master/coverage' + method: POST + headers: + content-type: application/json + authorization: 'Bearer zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz' + json: + coverage_total: 50 + build_number: 50 + + response: + status_code: 403 diff --git a/tests/test_minimal.tavern.yaml b/tests/test_minimal.tavern.yaml new file mode 100644 index 0000000..6411f28 --- /dev/null +++ b/tests/test_minimal.tavern.yaml @@ -0,0 +1,67 @@ +--- +test_name: POST coverage report and verify returned SVG + +stages: + - name: post initial coverage + + request: + url: '{host}/testuser/testrepo/master/coverage' + method: POST + headers: + content-type: application/json + authorization: 'Bearer {tavern.env_vars.ACCESS_TOKEN}' + json: + coverage_total: 42.3 + build_number: 18 + + response: + status_code: 201 + + - name: verify returned coverage is correct + + request: + url: '{host}/testuser/testrepo/master/coverage.svg' + method: GET + + response: + status_code: 200 + headers: + content-type: image/svg+xml;charset=utf-8 + body: + $ext: + function: tests.utils:validate_svg + extra_kwargs: + coverage: "42.3" + + + - name: update coverage + + request: + url: '{host}/testuser/testrepo/master/coverage' + method: POST + headers: + content-type: application/json + authorization: 'Bearer {tavern.env_vars.ACCESS_TOKEN}' + json: + coverage_total: 2.311 + build_number: 19 + + response: + status_code: 201 + + - name: verify updated coverage is returned correctly + + request: + url: '{host}/testuser/testrepo/master/coverage.svg' + method: GET + + response: + status_code: 200 + headers: + content-type: image/svg+xml;charset=utf-8 + body: + $ext: + function: tests.utils:validate_svg + extra_kwargs: + coverage: "2.31" + diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 0000000..c9183a8 --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,10 @@ +import xml.etree.ElementTree as ET + +def validate_svg(response, coverage): + xml = ET.fromstring(response.text) + ns = {'svg': 'http://www.w3.org/2000/svg'} + cov = xml.find('./svg:g[2]/svg:text[3]', ns).text + actual = cov + expected = coverage + '\u2009%' + if actual != expected: + raise AssertionError(actual + " != " + expected) -- 2.43.0