Skip to content

Commit

Permalink
Merge pull request #50 from ScilifelabDataCentre/testing
Browse files Browse the repository at this point in the history
Add environent for backend tests, along with some actual tests
  • Loading branch information
talavis authored Jan 5, 2023
2 parents f836da1 + c0727d3 commit 6196213
Show file tree
Hide file tree
Showing 13 changed files with 221 additions and 33 deletions.
27 changes: 27 additions & 0 deletions .github/workflows/backend-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: Backend Tests

on:
push:
branches:
- main
pull_request:
workflow_dispatch:

jobs:
pytest:
concurrency:
group: '${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}'
cancel-in-progress: true
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v2

- name: Run backend tests
run: docker-compose --profile testing up --exit-code-from test

- name: Publish coverage to codecov
uses: codecov/codecov-action@v3
with:
files: ./test/coverage/report.xml
6 changes: 6 additions & 0 deletions Dockerfiles/Dockerfile.backend
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ FROM base as dev
CMD ["flask", "run", "-h", "0.0.0.0", "--port", "5000"]


FROM base as test

RUN pip install pytest-cov
COPY ./test /code/test
CMD ["pytest", "test"]

FROM base as production

RUN pip install gunicorn
Expand Down
4 changes: 2 additions & 2 deletions Dockerfiles/nginx.conf.dev
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
server {
listen 80;
server_name localhost;
listen 8080;
server_name _;
location ~ ^/(api) {
proxy_pass http://backend:5000;

Expand Down
18 changes: 16 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
Form Manager
============

[![Backend Tests](https://github.com/ScilifelabDataCentre/form-manager/actions/workflows/backend-tests.yml/badge.svg)](https://github.com/ScilifelabDataCentre/form-manager/actions/workflows/backend-tests.yml)
[![codecov](https://codecov.io/github/ScilifelabDataCentre/form-manager/branch/main/graph/badge.svg?token=MQX98Q3NYU)](https://codecov.io/github/ScilifelabDataCentre/form-manager)
[![Black formatting](https://github.com/ScilifelabDataCentre/form-manager/actions/workflows/python-black.yml/badge.svg)](https://github.com/ScilifelabDataCentre/form-manager/actions/workflows/python-black.yml)
[![CodeQL](https://github.com/ScilifelabDataCentre/form-manager/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/ScilifelabDataCentre/form-manager/actions/workflows/codeql-analysis.yml)
[![Trivy Scan](https://github.com/ScilifelabDataCentre/form-manager/actions/workflows/trivy.yaml/badge.svg)](https://github.com/ScilifelabDataCentre/form-manager/actions/workflows/trivy.yaml)

Form Manager is a simple system (backend/frontend) to receive web form `POST` submissions.

Login is performed using OpenID connect. There is no internal user account management.
Expand Down Expand Up @@ -30,17 +36,25 @@ All configuration options are listed in `form_manager/conf.py`. Modify that file
A complete development environment can be activated locally by running:

```
docker-compose up
docker-compose --profile dev up
```

It will set up a database, a mail catcher, and one instance each of the backend and frontend, reachable at [http://localhost:5050](http://localhost:5050). The backend and frontend instance will use your local code, adapting to your changes.


If `FLASK_ENV` is set to `development` (done by default if you run the above command), you can log in by using the endpoint [http://localhost:5050/api/v1/development/login/[email protected]](http://localhost:5050/api/v1/development/login/[email protected]), where `[email protected]` may be exchanged to any email you want to log in as.

The easiest way to use development environment is to paste the url to the login endpoint in a web browser, and then open [http://localhost:5050](http://localhost:5050) to use the system.


## Testing

The tests can be run using the command:

```
docker-compose --profile testing up --exit-code-from test
```


## Required Run Environment

Form manager require a MongoDB instance, as well an instance of the frontend and backend. See the `docker-compose.yml` file.
Expand Down
35 changes: 32 additions & 3 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
version: "3.9"
services:
db:
profiles:
- dev
- testing
image: mongo:latest
volumes:
- mongo-data:/data/
Expand All @@ -9,13 +12,15 @@ services:
- MONGO_INITDB_ROOT_PASSWORD=mongopassword

backend:
profiles:
- dev
build:
context: ./
dockerfile: Dockerfiles/Dockerfile.backend
target: dev
environment:
FLASK_DEBUG: 1
FLASK_ENV: development
DEV_LOGIN: 1
FLASK_APP: form_manager
TZ: Europe/Stockholm
depends_on:
Expand All @@ -27,6 +32,8 @@ services:
target: /code/form_manager

frontend:
profiles:
- dev
build:
context: ./
dockerfile: Dockerfiles/Dockerfile.frontend
Expand All @@ -40,9 +47,11 @@ services:
target: /code/src

proxy:
image: nginx:alpine
profiles:
- dev
image: nginxinc/nginx-unprivileged:alpine
ports:
- 127.0.0.1:5050:80
- 127.0.0.1:5050:8080
depends_on:
- backend
- frontend
Expand All @@ -52,7 +61,27 @@ services:
source: ./Dockerfiles/nginx.conf.dev
target: /etc/nginx/conf.d/default.conf

test:
build:
context: ./
dockerfile: Dockerfiles/Dockerfile.backend
target: test
command: sh -c "pytest --color=yes --cov=./form_manager --cov-report=xml:test/coverage/report.xml"
environment:
DEV_LOGIN: 1
profiles:
- testing
depends_on:
- db
restart: "no"
volumes:
- type: bind
source: ./test
target: /code/test

mailcatcher:
profiles:
- dev
image: sj26/mailcatcher:latest
ports:
- 127.0.0.1:1080:1080
Expand Down
15 changes: 9 additions & 6 deletions form_manager/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@
from form_manager import config, data, forms, user, utils # to avoid issues with circular import


def create_app():
def create_app(testing=False):
"""Construct the core application."""
app = flask.Flask("form_manager")
if testing:
app.config["TESTING"] = True
app.config.from_object("form_manager.config.Config")
app.config.from_envvar("CONFIG_FILE", silent=True)

Expand Down Expand Up @@ -57,9 +59,9 @@ def finalize(response):
client_kwargs={"scope": "openid profile email"},
)

talisman.init_app(app)

csrf.init_app(app)
if not app.testing:
talisman.init_app(app)
csrf.init_app(app)

if app.config["REVERSE_PROXY"]:
app.wsgi_app = ProxyFix(app.wsgi_app)
Expand All @@ -72,7 +74,8 @@ def finalize(response):
def heartbeat():
return flask.Response(status=200)

if app.env == "development":
if os.environ.get("DEV_LOGIN") or app.testing:
print("asd")
activate_dev(app)

return app
Expand All @@ -89,4 +92,4 @@ def activate_dev(app):
def dev_login(email: str):
"""Force login as email."""
flask.session["email"] = email
return flask.Response(status=200)
return f"Logged in as {email}"
2 changes: 1 addition & 1 deletion form_manager/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import os


class Config(object):
class Config:
"""Base config"""

SITE_NAME = "Form Manager"
Expand Down
15 changes: 11 additions & 4 deletions form_manager/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ def add_form():
flask.abort(code=400)
entry.update(indata)
entry["owners"] = [flask.session["email"]]
flask.g.db.add_form(entry)
flask.g.data.add_form(entry)
return flask.jsonify(
{"identifier": entry["identifier"], "url": flask.url_for("forms.add_form", _external=True)}
)
Expand All @@ -137,9 +137,16 @@ def edit_form(identifier: str):
if not validate_form(indata, entry):
flask.current_app.logger.debug("Validation failed")
flask.abort(code=400)
entry.update(indata)
entry.update(indata)
flask.g.data.update_form(entry)
return ""
return flask.jsonify(
{
"status": "success",
"identifier": identifier,
"type": "PATCH",
"url": flask.url_for("forms.edit_form", identifier=identifier, _external=True),
}
)


@blueprint.route("/<identifier>", methods=["DELETE"])
Expand All @@ -164,7 +171,7 @@ def delete_form(identifier: str):
@blueprint.route("/<identifier>/incoming", methods=["POST"])
def receive_submission(identifier: str):
"""
Save a form submission to the db.
Save a form submission to the data backend.
Args:
identifier (str): The form identifier.
Expand Down
2 changes: 1 addition & 1 deletion form_manager/mongo.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def add_form(self, entry):
return self._db["forms"].insert_one(entry)

def update_form(self, entry):
return self._db["forms"].update_one({"_id": entry["_id"]}, {"$set": entry})
return self._db["forms"].update_one({"identifier": entry["identifier"]}, {"$set": entry})

def delete_form(self, identifier):
res1 = self._db["forms"].delete_one({"identifier": identifier})
Expand Down
16 changes: 16 additions & 0 deletions test/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""Helper functions for tests."""

from form_manager import config, data


def login(email, client):
return client.get(f"/api/v1/development/login/{email}")


def logout(client):
return client.get(f"/api/v1/user/logout")


USERS = ["[email protected]", "[email protected]"]

data_source = data.activate(config.Config().DB_CONF)
11 changes: 11 additions & 0 deletions test/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""Test setup."""

import pytest

from form_manager import create_app


@pytest.fixture(scope="function")
def client():
app = create_app(testing=True)
yield app.test_client()
82 changes: 82 additions & 0 deletions test/test_forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
"""Form-related tests."""

import test as helpers


def test_list_forms_anon(client):
"""
1. as anon, list forms (fail)
"""
response = client.get("/api/v1/form")
assert response.status_code == 403


def test_list_forms_user(client):
"""
1. as user, list forms (ok)
"""
helpers.login(helpers.USERS[0], client)
response = client.get("/api/v1/form")
assert response.status_code == 200
assert response.json == {"forms": [], "url": "http://localhost/api/v1/form"}


def test_add_form_anon(client):
"""
1. as anon, add form (fail)
"""
response = client.post("/api/v1/form")
assert response.status_code == 403


def test_add_list_delete_form(client):
"""
1. as user, create form (ok)
2. as user, list form (ok)
5. as user, delete form (ok)
"""
helpers.login(helpers.USERS[0], client)

response = client.post("/api/v1/form")
assert response.status_code == 200
identifier = response.json["identifier"]
assert helpers.data_source.get_form(identifier)

response = client.get(f"/api/v1/form/{identifier}")
assert response.status_code == 200
assert "form" in response.json

response = client.delete(f"/api/v1/form/{identifier}")
assert response.status_code == 200
assert not helpers.data_source.get_form(identifier)


def test_add_list_delete_form_not_allowed(client):
"""
1. as user, create form (ok)
2. as anon, list form (fail)
2. as anon, delete form (fail)
3. as other user, list form (fail)
4. as other user, delete form (fail)
5. as user, delete form (ok)
"""
helpers.login(helpers.USERS[0], client)
response = client.post("/api/v1/form")
assert response.status_code == 200
identifier = response.json["identifier"]

helpers.logout(client)
response = client.get(f"/api/v1/form/{identifier}")
assert response.status_code == 403
response = client.delete(f"/api/v1/form/{identifier}")
assert response.status_code == 403

helpers.login(helpers.USERS[1], client)
response = client.get(f"/api/v1/form/{identifier}")
assert response.status_code == 403
response = client.delete(f"/api/v1/form/{identifier}")
assert response.status_code == 403

helpers.login(helpers.USERS[0], client)
response = client.delete(f"/api/v1/form/{identifier}")
assert response.status_code == 200
Loading

0 comments on commit 6196213

Please sign in to comment.