Skip to content
This repository has been archived by the owner on Jul 25, 2024. It is now read-only.

frontend changes for async_download #123

Merged
merged 1 commit into from
Jul 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -130,3 +130,4 @@ app/static/dist

# ide
.idea/
.vscode/
14 changes: 14 additions & 0 deletions app/main/download_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from enum import StrEnum
from typing import Any

import requests
from flask import abort, current_app

from app.const import MIMETYPE
Expand Down Expand Up @@ -314,3 +315,16 @@ def get_human_readable_file_size(file_size_bytes: int) -> str:
return f"{round(file_size_kb / 1024, 1)} MB"
else:
return f"{round(file_size_kb / (1024 * 1024), 1)} GB"


def process_async_download(query_params: dict) -> int:
"""Calls data-store for a file download request.

:param query_params: Query parameters for the API request.
:return: The status code of the API response.
"""

request_url = Config.DATA_STORE_API_HOST + "/trigger_async_download"
response = requests.post(request_url, data=query_params)

return response.status_code
26 changes: 12 additions & 14 deletions app/main/routes.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
# isort: off
from datetime import datetime

from flask import (
flash,
Expand All @@ -10,7 +9,6 @@
abort,
current_app,
g,
send_file,
)

# isort: on
Expand All @@ -31,7 +29,7 @@
get_presigned_url,
get_region_checkboxes,
get_returns,
process_api_response,
process_async_download,
)
from app.main.forms import DownloadForm, RetrieveForm

Expand Down Expand Up @@ -88,8 +86,6 @@ def download():
to_quarter = request.form.get("to-quarter")
to_year = request.form.get("to-year")

current_datetime = datetime.now().strftime("%Y-%m-%d-%H%M%S")

reporting_period_start = (
financial_quarter_from_mapping(quarter=from_quarter, year=from_year) if to_quarter and to_year else None
)
Expand All @@ -98,7 +94,7 @@ def download():
financial_quarter_to_mapping(quarter=to_quarter, year=to_year) if to_quarter and to_year else None
)

query_params = {"file_format": file_format}
query_params = {"email_address": g.user.email, "file_format": file_format}
if orgs:
query_params["organisations"] = orgs
if regions:
Expand All @@ -112,7 +108,7 @@ def download():
if reporting_period_end:
query_params["rp_end"] = reporting_period_end

content_type, file_content = process_api_response(query_params)
status_code = process_async_download(query_params)

current_app.logger.info(
"Request for download by {user_id} with {query_params}",
Expand All @@ -124,15 +120,17 @@ def download():
},
)

return send_file(
file_content,
download_name=f"download-{current_datetime}.{file_format}",
as_attachment=True,
mimetype=content_type,
)
if status_code == 204:
return redirect(url_for("main.request_received"))
else:
current_app.logger.error(
"Response status code from data-store/trigger_async_download: {content_type}",
extra=dict(content_type=status_code),
)
return render_template("500.html")


@bp.route("/request-received", methods=["GET", "POST"])
@bp.route("/request-received", methods=["GET"])
@login_required(return_app=SupportedApp.POST_AWARD_FRONTEND)
def request_received():
return render_template("request-received.html", user_email=g.user.email)
Expand Down
22 changes: 21 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import io
from typing import Generator
from unittest.mock import Mock

import pytest
from flask.testing import FlaskClient
Expand All @@ -12,7 +13,7 @@
@pytest.fixture()
def mocked_auth(monkeypatch):
def access_token(return_app=SupportedApp.POST_AWARD_FRONTEND, auto_redirect=True):
return {"accountId": "test-user", "roles": []}
return {"accountId": "test-user", "roles": [], "email": "[email protected]"}

monkeypatch.setattr(
"fsd_utils.authentication.decorators._check_access_token",
Expand Down Expand Up @@ -71,3 +72,22 @@ def mock_get_response_json(flask_test_client, mocker):
"app.main.routes.process_api_response",
return_value=("application/json", io.BytesIO(b'{"data": "test"}')),
)


@pytest.fixture
def mocked_routes_process_async_download(flask_test_client, mocker):
return mocker.patch("app.main.routes.process_async_download", return_value=204)


@pytest.fixture
def mocked_download_data_process_async_download(flask_test_client, mocker):
mock_response = Mock()
mock_response.status_code = 204
return mocker.patch("app.main.download_data.requests.post", return_value=mock_response)


@pytest.fixture
def mocked_failing_download_data_process_async_download(flask_test_client, mocker):
mock_response = Mock()
mock_response.status_code = 500
return mocker.patch("app.main.download_data.requests.post", return_value=mock_response)
8 changes: 2 additions & 6 deletions tests/test_logging.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
import pytest


@pytest.mark.usefixtures("mock_get_response_xlsx")
def test_download_logging(flask_test_client, caplog):
def test_download_logging(flask_test_client, caplog, mocked_routes_process_async_download):
flask_test_client.post("/download", data={"file_format": "xlsx"})
log_line = [record for record in caplog.records if hasattr(record, "request_type")]
assert len(log_line) == 1
assert log_line[0].request_type == "download"
assert log_line[0].user_id == "test-user"
assert log_line[0].query_params == {"file_format": "xlsx"}
assert log_line[0].query_params == {"file_format": "xlsx", "email_address": "[email protected]"}
72 changes: 38 additions & 34 deletions tests/test_routes.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import re
from datetime import datetime
from unittest.mock import patch

import pytest
from bs4 import BeautifulSoup
from flask import url_for

from app.main.download_data import FileMetadata
from app.main.download_data import FileMetadata, process_async_download


def test_index_page_redirect(flask_test_client):
Expand Down Expand Up @@ -36,20 +35,45 @@ def test_download_get(requests_mock, flask_test_client):
assert page.select_one(".govuk-back-link") is None


@pytest.mark.usefixtures("mock_get_response_json")
def test_download_post_json(flask_test_client):
response = flask_test_client.post("/download", data={"file_format": "json"})
assert response.status_code == 200
assert response.mimetype == "application/json"
assert response.data == b'{"data": "test"}'
def test_process_async_download_call(flask_test_client, mocked_routes_process_async_download):
"""Test that the download route calls the process_async_download function with the correct parameters,
including the user email address."""

flask_test_client.post("/download", data={"file_format": "xlsx"})
assert mocked_routes_process_async_download.called
assert mocked_routes_process_async_download.call_args.args[0] == {
"file_format": "xlsx",
"email_address": "[email protected]",
}


def test_process_async_download_function(flask_test_client, mocked_download_data_process_async_download):
"""Test that app/main/process_async_download() function returns status code 204 when
the download request is successful."""

status_code = process_async_download({"file_format": "xlsx", "email_address": "[email protected]"})
assert status_code == 204


def test_async_download_redirect_OK(flask_test_client, mocked_download_data_process_async_download):
"""Test that the download route redirects to the request-received page after a successful download request."""

@pytest.mark.usefixtures("mock_get_response_xlsx")
def test_download_post_xlsx(flask_test_client):
response = flask_test_client.post("/download", data={"file_format": "xlsx"})
assert response.status_code == 200
assert response.mimetype == "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
assert response.data == b"xlsx data"
assert response.status_code == 302 # redirect to request-received page
assert response.headers["Content-Type"] == "text/html; charset=utf-8"
expected_location = url_for("main.request_received", _external=False)
assert response.headers["Location"] == expected_location


def test_async_download_redirect_error(flask_test_client, mocked_failing_download_data_process_async_download):
"""Test that the download route does not redirect, but renders the 500 error page when
the download request fails."""

response = flask_test_client.post("/download", data={"file_format": "xlsx"})
assert response.status_code != 302
assert response.headers["Content-Type"] == "text/html; charset=utf-8"
assert b"Sorry, there is a problem with the service" in response.data
assert b"Try again later." in response.data


def test_download_post_unknown_format(flask_test_client):
Expand All @@ -71,26 +95,6 @@ def test_download_post_unknown_format_from_api(mock_get_response, flask_test_cli
assert response.status_code == 500


@pytest.mark.usefixtures("mock_get_response_xlsx")
def test_download_fails_csrf(flask_test_client):
flask_test_client.application.config["WTF_CSRF_ENABLED"] = True
response = flask_test_client.post("/download", data={"file_format": "json"})
assert response.status_code == 302


@pytest.mark.usefixtures("mock_get_response_xlsx")
def test_download_filename_date(flask_test_client):
response = flask_test_client.post("/download", data={"file_format": "xlsx"})

# Regex pattern for datetime format %Y-%m-%d-%H%M%S
datetime_pattern = r"^\d{4}-\d{2}-\d{2}-\d{6}$"
extracted_datetime = re.search(r"\d{4}-\d{2}-\d{2}-\d{6}", response.headers["Content-Disposition"]).group()

# Assert datetime stamp on file is in correct format
assert re.match(datetime_pattern, extracted_datetime)
assert datetime.strptime(extracted_datetime, "%Y-%m-%d-%H%M%S")


def test_known_http_error_redirect(flask_test_client):
# induce a known error
response = flask_test_client.get("/unknown-page")
Expand Down
Loading