Skip to content

Commit

Permalink
Add tests to CI. Try to improve DX with makefile.
Browse files Browse the repository at this point in the history
  • Loading branch information
eriktm committed Jan 20, 2024
1 parent e1a8cc5 commit 8d0ae66
Show file tree
Hide file tree
Showing 11 changed files with 277 additions and 21 deletions.
2 changes: 2 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
[run]
source = unicorn
omit =
unicorn/unicorn/wsgi.py
8 changes: 4 additions & 4 deletions .env.development → .env.testing
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
SECRET_KEY="n0zdo8!t*rU9_KfHAlsOEJ^VuX6WB)NP5IFT43$Seg=#(QmYRD"
SECRET_KEY="insecure-key-jN?b3Gb+h|HTRwI6~1Vi5v4ZI2r8|m::MrInqKzz][I/"
DATABASE_HOST=db
DATABASE_NAME=postgres
DATABASE_USER=postgres
DATABASE_PASSWORD=postgres
DATABASE_NAME=unicorn
DATABASE_USER=unicorn
DATABASE_PASSWORD="insecure-password-0sy3wBnJs.#%@h2TrDsy?I*p#wU+MV"
DEBUG=somethingtruethy

REDIS_CONNECTION=redis://cache:6379/1
Expand Down
61 changes: 59 additions & 2 deletions .github/workflows/build-backend.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ on:
branches: ["main"]

env:
PYTHON_VERSION: "3.10"
PYTHON_VERSION: "3.11"
POETRY_VERSION: "1.7.1" # Remember to also update in pyproject.toml
IMAGE_NAME: unicorn-backend

Expand All @@ -32,8 +32,65 @@ jobs:
- name: Execute Pre-Commit
run: pre-commit run --show-diff-on-failure --color=always --all-files

test:
runs-on: ubuntu-latest

env:
DATABASE_USER: unicorn
DATABASE_NAME: unicorn
DATABASE_PASSWORD: insecure-password-0sy3wBnJs.#%@h2TrDsy?I*p#wU+MV
POSTGRES_USER: postgres
POSTGRES_PASSWORD: insecure-password-1KUkde2hxN4d3AvxhDsOkdsQTh4LE53c

services:
db:
image: postgres:15
env:
POSTGRES_USER: ${{ env.POSTGRES_USER }}
POSTGRES_PASSWORD: ${{ env.POSTGRES_PASSWORD }}
DATABASE_USER: ${{ env.DATABASE_USER }}
DATABASE_NAME: ${{ env.DATABASE_NAME }}
DATABASE_PASSWORD: ${{ env.DATABASE_PASSWORD }}
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
--name test_db
ports:
- 5432:5432

steps:
- name: Check out repository
uses: actions/checkout@v3
- name: Run database initialization script
run: docker exec -i test_db /bin/bash < scripts/dbinit/initialize-database.sh
- name: Set up python
uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install Poetry
uses: snok/install-poetry@v1
with:
version: ${{ env.POETRY_VERSION }}
virtualenvs-create: true
virtualenvs-in-project: true
installer-parallel: true
- name: Load cached venv
id: cached-poetry-dependencies
uses: actions/cache@v3
with:
path: .venv
key: v1-venv-${{ runner.os }}-${{ env.PYTHON_VERSION }}-${{ hashFiles('**/poetry.lock') }}
- name: Install dependencies
run: poetry install --no-interaction --no-root
- name: Set pythonpath
run: echo "PYTHONPATH=$PWD" >> $GITHUB_ENV
- name: Test
run: DATABASE_HOST=localhost poetry run python unicorn/manage.py test unicorn

build:
needs: [validate]
needs: [validate, test]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand Down
27 changes: 27 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
.DEFAULT_GOAL := run

init: prepare run migrate createsuperuser

prepare:
@cp .env.testing .env

run:
@docker compose up -d

migrate:
@docker compose exec web python unicorn/manage.py migrate

createsuperuser:
@echo "Creating a Django superuser. Please fill in the details:"
@docker compose exec web python unicorn/manage.py createsuperuser

loadseed:
@docker compose exec web python unicorn/manage.py loaddata unicorn/seed.json

test:
@docker compose exec web coverage run unicorn/manage.py test unicorn

coverage:
@docker compose exec web coverage report -m
@docker compose exec web coverage html -d unicorn/htmlcov
@open unicorn/htmlcov/index.html
14 changes: 9 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,29 @@


## Running locally (with Docker 🐳)
TL;DR: run `make init` the first time.

Before running, make sure to create and populate local environment variables. You can copy the provided example file and then modifying default or adding values to blank settings.
```
cp .env.example .env
make prepare
```

Then, in order to start the development stack, run the following command:
```
docker-compose up -d
make run
```

When running for the first time, or after clearing the database, remember to run the following commands as well:
```
docker-compose exec web python unicorn/manage.py migrate
docker-compose exec web python unicorn/manage.py createsuperuser
make migrate
make createsuperuser
```

You should now be able to access the application at http://localhost:8000/

Some apps may also provide seed data. This can be loaded by running the following command with appropriate adjustments to the last argument.
```
docker-compose exec web python unicorn/manage.py loaddata unicorn/seed.json
make loadseed
```


Expand All @@ -33,6 +35,8 @@ Ensure you have `pre-commit` installed - `brew install pre-commit` (or replace b

Run `pre-commit install` to have it check your staged changes before allowing you to commit. To skip the pre-commit checks (usually not recommended, but helpful when you'd want to save WIP or similar), use `git commit --no-verify`.

Also, make sure to check that tests are passing with `make test`. Coverage can optionally be checked with `make coverage`.


# Authentication providers
## Keycloak
Expand Down
17 changes: 11 additions & 6 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
version: '3.7'
version: "3.7"

services:
db:
image: postgres:14-alpine
image: postgres:15-alpine
ports:
- "127.0.0.1:5432:5432"
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data/
- ./scripts/dbinit/:/docker-entrypoint-initdb.d/
environment:
- POSTGRES_PASSWORD=postgres
env_file: .env
restart: unless-stopped

cache:
image: redis:alpine
ports:
- "127.0.0.1:6379:6379"
- "6379:6379"
restart: unless-stopped

web:
build:
Expand All @@ -23,10 +27,11 @@ services:
volumes:
- ./unicorn:/app/unicorn
ports:
- "127.0.0.1:8000:8000"
- "8000:8000"
depends_on:
- db
- cache
env_file: .env

volumes:
postgres_data:
postgres_data:
66 changes: 65 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ django-redis = "~5.4.0"
pillow = "^10.2.0"
django-auditlog = "~2.3.0"
psycopg = {extras = ["binary"], version = "^3.1.16"}
coverage = "^7.4.0"


[tool.poetry.group.dev.dependencies]
Expand Down
17 changes: 17 additions & 0 deletions scripts/dbinit/initialize-database.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/bin/bash
set -e

psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
CREATE USER $DATABASE_USER WITH PASSWORD '$DATABASE_PASSWORD';
CREATE DATABASE $DATABASE_NAME;
GRANT ALL PRIVILEGES ON DATABASE $DATABASE_NAME TO $DATABASE_USER;
ALTER DATABASE $DATABASE_NAME OWNER TO $DATABASE_USER;
ALTER USER $DATABASE_USER CREATEDB;
CREATE SCHEMA $DATABASE_USER AUTHORIZATION $DATABASE_USER;
\connect $DATABASE_NAME;
ALTER ROLE $DATABASE_USER SET client_encoding TO 'utf8';
ALTER ROLE $DATABASE_USER SET default_transaction_isolation TO 'read committed';
ALTER ROLE $DATABASE_USER SET timezone TO 'UTC';
EOSQL
77 changes: 77 additions & 0 deletions unicorn/competitions/tests/test_signals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
from unittest import mock

from competitions.constants import (
COMPETITION_VISIBILITY_CREW,
COMPETITION_VISIBILITY_HIDDEN,
COMPETITION_VISIBILITY_PUBLIC,
GENRE_CATEGORY_OTHER,
)
from competitions.models import Competition, Genre
from competitions.signals import add_competition_view_published
from django.contrib.auth.models import Group
from django.db.models.signals import post_save
from django.test import TestCase
from django.utils import timezone


class AddCompetitionViewPublishedTestCase(TestCase):
def setUp(self):
post_save.connect(add_competition_view_published, sender=Competition)

self.anon, _ = Group.objects.get_or_create(name="p-anonymous")
self.crew, _ = Group.objects.get_or_create(name="p-crew")

now = timezone.now()
later = now + timezone.timedelta(days=1)

self.genre = Genre.objects.create(category=GENRE_CATEGORY_OTHER, name="Genre")
self.competition = Competition.objects.create(
genre=self.genre,
name="Competition",
published=False,
run_time_start=now,
run_time_end=later,
)

def tearDown(self):
post_save.disconnect(add_competition_view_published, sender=Competition)

@mock.patch("competitions.signals.remove_perm")
def test_visibility_hidden(self, mock_remove):
self.competition.visibility = COMPETITION_VISIBILITY_HIDDEN
self.competition.save()

mock_remove.assert_any_call("view_competition", self.anon, self.competition)
mock_remove.assert_any_call("view_competition", self.crew, self.competition)

@mock.patch("competitions.signals.assign_perm")
@mock.patch("competitions.signals.remove_perm")
def test_visibility_crew(self, mock_remove, mock_assign):
self.competition.visibility = COMPETITION_VISIBILITY_CREW
self.competition.published = False
self.competition.save()

mock_remove.assert_any_call("view_competition", self.anon, self.competition)
mock_remove.assert_any_call("view_competition", self.crew, self.competition)

self.competition.published = True
self.competition.save()

mock_remove.assert_called_with("view_competition", self.anon, self.competition)
mock_assign.assert_called_with("view_competition", self.crew, self.competition)

@mock.patch("competitions.signals.assign_perm")
@mock.patch("competitions.signals.remove_perm")
def test_visibility_public(self, mock_remove, mock_assign):
self.competition.visibility = COMPETITION_VISIBILITY_PUBLIC
self.competition.published = False
self.competition.save()

mock_remove.assert_any_call("view_competition", self.crew, self.competition)
mock_remove.assert_any_call("view_competition", self.anon, self.competition)

self.competition.published = True
self.competition.save()

mock_remove.assert_called_with("view_competition", self.crew, self.competition)
mock_assign.assert_called_with("view_competition", self.anon, self.competition)
Loading

0 comments on commit 8d0ae66

Please sign in to comment.