diff --git a/.cruft.json b/.cruft.json index 8454079..6b53af8 100644 --- a/.cruft.json +++ b/.cruft.json @@ -1,7 +1,7 @@ { "template": "https://github.com/kjaymiller/cookiecutter-relecloud", - "commit": "f86115e94dda2639d5678dad3a3232bacf583af2", - "checkout": "picklefix", + "commit": "a7ea908c4b190531faa473c98575da1b1ab3c7c1", + "checkout": null, "context": { "cookiecutter": { "project_name": "azure", diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 09b2a5b..e9ff795 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -32,7 +32,8 @@ "files.exclude": { ".coverage": true, ".pytest_cache": true, - "__pycache__": true + "__pycache__": true, + ".ruff_cache": true }, "[python]": { "editor.formatOnSave": true, diff --git a/.github/workflows/azure-dev.yml b/.github/workflows/azure-dev.yml index 3013f96..f825973 100644 --- a/.github/workflows/azure-dev.yml +++ b/.github/workflows/azure-dev.yml @@ -103,5 +103,5 @@ jobs: run: | python3 -m pip install --upgrade pip python3 -m pip install -r requirements-dev.txt - python3 -m playwright install --with-deps + python3 -m playwright install chromium --with-deps python3 -m pytest --exitfirst src/tests/smoke/smoketests.py --live-server-url $URI diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8abf098..ab280fc 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -37,7 +37,7 @@ jobs: run: | python3 -m pip install --upgrade pip python3 -m pip install -r requirements-dev.txt - playwright install --with-deps + playwright install chromium --with-deps python3 -m pip install -e src - name: Seed data and run Pytest tests run: | diff --git a/.vscode/launch.json b/.vscode/launch.json index acb7c6c..0a40764 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -21,6 +21,15 @@ ], "jinja": true, "justMyCode": false + }, + { + "name": "Python: Debug Tests", + "type": "python", + "request": "launch", + "program": "${file}", + "purpose": ["debug-test"], + "console": "integratedTerminal", + "env": {"PYTEST_ADDOPTS": "--no-cov"} } ] } diff --git a/README.md b/README.md index abdf2e6..d4ccc7a 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ export POSTGRES_PASSWORD= If you're running the app inside VS Code or GitHub Codespaces, you can use the "Run and Debug" button to start the app. ```sh -python3 -m flask --app src.flaskapp run --reload --port=8000 +python3 -m flask --app src.flaskapp run --debug --reload --port=8000 ``` @@ -56,7 +56,7 @@ python3 -m flask --app src.flaskapp run --reload --port=8000 ```sh python3 -m pip install -r requirements-dev.txt - python3 -m playwright install --with-deps + python3 -m playwright install chromium --with-deps ``` 3. Run the tests: diff --git a/pyproject.toml b/pyproject.toml index b346b36..9ff4061 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,3 +9,6 @@ known-first-party = ["flaskapp"] [tool.pytest.ini_options] addopts = "-ra -vv" + +[tool.coverage.report] +show_missing = true diff --git a/requirements-dev.txt b/requirements-dev.txt index c2c4369..6784c50 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -8,6 +8,9 @@ pip-tools pytest ephemeral-port-reserve pytest-playwright +coverage +pytest-cov +axe-playwright-python # Linters ruff diff --git a/src/flaskapp/pages.py b/src/flaskapp/pages.py index a5942f7..ad852ea 100644 --- a/src/flaskapp/pages.py +++ b/src/flaskapp/pages.py @@ -44,14 +44,14 @@ def cruise_detail(pk: int): ) -@bp.get("/info_request/") +@bp.get("/info_request") def info_request(): all_cruises = db.session.execute(db.select(models.Cruise)).scalars().all() return render_template("info_request_create.html", cruises=all_cruises, message=request.args.get("message")) -@bp.post("/info_request/") +@bp.post("/info_request") def create_info_request(): name = request.form["name"] db_info_request = models.InfoRequest( diff --git a/src/static/manifest.json b/src/static/manifest.json new file mode 100644 index 0000000..6abc49c --- /dev/null +++ b/src/static/manifest.json @@ -0,0 +1,30 @@ +{ + "name": "ReleCloud Space Tourism", + "short_name": "ReleCloud", + "start_url": ".", + "display": "standalone", + "background_color": "#fff", + "description": "ReleCloud Space Tourism", + "icons": [ + { + "src": "/static/res/img/favicon.ico", + "sizes": "32x32", + "type": "image/x-icon" + }, + { + "src": "/static/res/img/favicon-120-precomposed.png", + "sizes": "120x120", + "type": "image/png" + }, + { + "src": "/static/res/img/favicon-152-precomposed.png", + "sizes": "152x152", + "type": "image/png" + }, + { + "src": "/static/res/img/favicon-192.png", + "sizes": "192x192", + "type": "image/png" + } + ] +} diff --git a/src/templates/base.html b/src/templates/base.html index cbfdd2f..4efd06b 100644 --- a/src/templates/base.html +++ b/src/templates/base.html @@ -4,21 +4,26 @@ {% if prod %} {% endif %} - + - + - + - + - + - + diff --git a/src/templates/info_request_create.html b/src/templates/info_request_create.html index aae22ba..770dc6a 100644 --- a/src/templates/info_request_create.html +++ b/src/templates/info_request_create.html @@ -17,13 +17,13 @@ <h1 id="page-title">Request information</h2> <p>Fill out the form below to request information about our cruises</p> <form method="post"> - <form method="post" action="/info_request/"> + <form method="post" action="/info_request"> <label for="name">Name:</label> <input type="text" id="name" name="name" required><br><br> <label for="email">Email:</label> <input type="email" id="email" name="email" required><br><br> <label for="cruise_id">Cruise:</label> - <select name="cruise_id"> + <select name="cruise_id" id="cruise_id"> {% for cruise in cruises %} <option value="{{ cruise.id }}">{{ cruise.name }}</option> {% endfor %} diff --git a/src/tests/local/test_gunicorn.py b/src/tests/local/test_gunicorn.py new file mode 100644 index 0000000..c036c16 --- /dev/null +++ b/src/tests/local/test_gunicorn.py @@ -0,0 +1,15 @@ +import sys +from unittest import mock + +import pytest +from gunicorn.app.wsgiapp import run + + +def test_config_imports(): + argv = ["gunicorn", "--check-config", "flaskapp:create_app()", "-c", "src/gunicorn.conf.py"] + + with mock.patch.object(sys, "argv", argv): + with pytest.raises(SystemExit) as excinfo: + run() + + assert excinfo.value.args[0] == 0 diff --git a/src/tests/local/test_pages.py b/src/tests/local/test_pages.py new file mode 100644 index 0000000..6388a4c --- /dev/null +++ b/src/tests/local/test_pages.py @@ -0,0 +1,76 @@ +import pytest + +from flaskapp import db, models + + +@pytest.fixture +def client(app_with_db): + return app_with_db.test_client() + + +def test_index(client): + response = client.get("/") + + assert response.status_code == 200 + assert b"Welcome to ReleCloud" in response.data + + +def test_about(client): + response = client.get("/about") + + assert response.status_code == 200 + assert b"About ReleCloud" in response.data + + +def test_destinations(client): + response = client.get("/destinations") + + assert response.status_code == 200 + assert b"Destinations" in response.data + assert b"The Sun" in response.data + + +def test_destination_detail(client): + response = client.get("/destination/1") + + assert response.status_code == 200 + assert b"The Sun" in response.data + + +def test_cruise_detail(client): + response = client.get("/cruise/1") + + assert response.status_code == 200 + assert b"The Sun and Earth" in response.data + + +def test_info_request(client): + response = client.get("/info_request") + + assert response.status_code == 200 + assert b"Request Info" in response.data + + +def test_create_info_request(app_with_db, client): + response = client.post( + "/info_request", + data={ + "name": "Amanda Valdez", + "email": "michellewatson@gmail.com", + "notes": "Please send me more information.", + "cruise_id": "12345", + }, + ) + + assert response.status_code == 302 + assert ( + response.headers["Location"] + == "/info_request?message=Thank+you,+Amanda+Valdez!+We+will+email+you+when+we+have+more+information!" + ) + + with app_with_db.app_context(): + info_request = db.session.query(models.InfoRequest).order_by(models.InfoRequest.id.desc()).first() + assert info_request.name == "Amanda Valdez" + assert info_request.email == "michellewatson@gmail.com" + assert info_request.notes == "Please send me more information." + assert info_request.cruise_id == 12345 diff --git a/src/tests/local/test_playwright.py b/src/tests/local/test_playwright.py index 2b20c4c..3c05c4d 100644 --- a/src/tests/local/test_playwright.py +++ b/src/tests/local/test_playwright.py @@ -1,13 +1,20 @@ import re import pytest +from axe_playwright_python.sync_playwright import Axe from playwright.sync_api import Page, expect +def check_for_violations(page: Page): + results = Axe().run(page) + assert results.violations_count == 0, results.generate_report() + + def test_home(page: Page, live_server_url: str): """Test that the home page loads""" page.goto(live_server_url) expect(page).to_have_title("ReleCloud - Expand your horizons") + check_for_violations(page) @pytest.mark.parametrize( @@ -26,6 +33,7 @@ def test_header_has_request_info(page: Page, live_server_url: str, page_title, p # Request Info request_info = header.get_by_role("link", name=page_title) expect(request_info).to_have_attribute("href", re.compile(rf".*{page_url}.*")) + check_for_violations(page) def test_request_information(page: Page, live_server_url: str): @@ -33,12 +41,14 @@ def test_request_information(page: Page, live_server_url: str): page.goto(live_server_url) page.get_by_role("link", name="Request Information").click() expect(page).to_have_title("ReleCloud - Request information") + check_for_violations(page) def test_destinations(page: Page, live_server_url: str): page.goto(live_server_url) page.get_by_role("link", name="Destinations").click() expect(page).to_have_title("ReleCloud - Destinations") + check_for_violations(page) destinations = ( @@ -80,6 +90,7 @@ def test_destination_options( page.get_by_role("link", name="Destinations").click() expect(page).to_have_title("ReleCloud - Destinations") expect(page.get_by_text(destination)).to_be_visible() + check_for_violations(page) @pytest.mark.parametrize( @@ -97,6 +108,7 @@ def test_destination_options_have_cruises(page: Page, live_server_url: str, dest for page_cruise in page_cruises: assert page_cruise.text_content() in cruises + check_for_violations(page) def test_about(page: Page, live_server_url: str): @@ -104,3 +116,4 @@ def test_about(page: Page, live_server_url: str): page.goto(live_server_url) page.get_by_role("link", name="About").click() expect(page.locator("#page-title")).to_have_text(re.compile(r".*about.*", re.IGNORECASE)) + check_for_violations(page)