From 0574e813a6c2c7b96eb532b2d51c2a0fb713035f Mon Sep 17 00:00:00 2001 From: SilviaAmAm Date: Mon, 9 Dec 2024 11:37:42 +0100 Subject: [PATCH 1/3] :construction: [#496] Add script to measeure some endpoints --- backend/scripts/locustfile.py | 146 ++++++++++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 backend/scripts/locustfile.py diff --git a/backend/scripts/locustfile.py b/backend/scripts/locustfile.py new file mode 100644 index 000000000..528e713fb --- /dev/null +++ b/backend/scripts/locustfile.py @@ -0,0 +1,146 @@ +import os + +from locust import HttpUser, task + +LOCUST_USERNAME = os.getenv("LOCUST_USERNAME") +LOCUST_PASSWORD = os.getenv("LOCUST_PASSWORD") + + +class RecordManager(HttpUser): + csrftoken_header = None + + def on_start(self) -> None: + # Get CSRF token + headers = {"Referer": f"{self.host}/login"} + + response = self.client.get("/api/v1/whoami/", headers=headers) + headers.update({"X-CSRFToken": response.headers.get("X-CSRFToken")}) + + # Login + response = self.client.post( + "/api/v1/auth/login/", + headers=headers, + data={"username": LOCUST_USERNAME, "password": LOCUST_PASSWORD}, + ) + + self.csrftoken_header = response.headers.get("X-CSRFToken") + + @task + def get_destruction_lists(self): + self.client.get( + "/api/v1/destruction-lists/", + headers={ + "Referer": f"{self.host}/login", + "X-CSRFToken": self.csrftoken_header, + }, + ) + + @task + def get_destruction_list_items_page_1(self): + self.client.get( + "/api/v1/destruction-list-items/", + headers={ + "Referer": f"{self.host}/login", + "X-CSRFToken": self.csrftoken_header, + }, + ) + + @task + def get_zaken_page_1(self): + self.client.get( + "/api/v1/zaken/", + headers={ + "Referer": f"{self.host}/login", + "X-CSRFToken": self.csrftoken_header, + }, + ) + + @task + def get_reviewers(self): + self.client.get( + "/api/v1/reviewers/", + headers={ + "Referer": f"{self.host}/login", + "X-CSRFToken": self.csrftoken_header, + }, + ) + + @task + def get_selectielijstklasse_choices(self): + self.client.get( + "/api/v1/_selectielijstklasse-choices/", + headers={ + "Referer": f"{self.host}/login", + "X-CSRFToken": self.csrftoken_header, + }, + ) + + @task + def get_zaaktype_choices(self): + self.client.get( + "/api/v1/_zaaktypen-choices/", + headers={ + "Referer": f"{self.host}/login", + "X-CSRFToken": self.csrftoken_header, + }, + ) + + @task + def get_reviews_for_list(self): + self.client.get( + "/api/v1/destruction-list-reviews/?destructionList__uuid=3239f290-df0c-4123-8aa3-81e9e11bc5c4&ordering=-created", + headers={ + "Referer": f"{self.host}/login", + "X-CSRFToken": self.csrftoken_header, + }, + ) + + @task + def get_items_for_list(self): + self.client.get( + "/api/v1/destruction-list-items/?item-destruction_list=3239f290-df0c-4123-8aa3-81e9e11bc5c4&item-status=suggested", + headers={ + "Referer": f"{self.host}/login", + "X-CSRFToken": self.csrftoken_header, + }, + ) + + @task + def get_zaken_not_list_except(self): + self.client.get( + "/api/v1/zaken/?not_in_destruction_list_except=3239f290-df0c-4123-8aa3-81e9e11bc5c4", + headers={ + "Referer": f"{self.host}/login", + "X-CSRFToken": self.csrftoken_header, + }, + ) + + @task + def get_logs_for_list(self): + self.client.get( + "/api/v1/logs/?destruction_list=3239f290-df0c-4123-8aa3-81e9e11bc5c4", + headers={ + "Referer": f"{self.host}/login", + "X-CSRFToken": self.csrftoken_header, + }, + ) + + @task + def get_coreviewers(self): + self.client.get( + "/api/v1/co-reviewers/", + headers={ + "Referer": f"{self.host}/login", + "X-CSRFToken": self.csrftoken_header, + }, + ) + + @task + def get_coreviews(self): + self.client.get( + "/api/v1/destruction-list-co-reviews/?destructionList__uuid=3239f290-df0c-4123-8aa3-81e9e11bc5c4", + headers={ + "Referer": f"{self.host}/login", + "X-CSRFToken": self.csrftoken_header, + }, + ) From ac1ea7fbfb94a0696868478b072c1001240fad90 Mon Sep 17 00:00:00 2001 From: SilviaAmAm Date: Tue, 10 Dec 2024 17:06:29 +0100 Subject: [PATCH 2/3] :recycle: [#496] Split gherkin to mixin --- backend/src/openarchiefbeheer/utils/tests/gherkin.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/backend/src/openarchiefbeheer/utils/tests/gherkin.py b/backend/src/openarchiefbeheer/utils/tests/gherkin.py index aa6389b87..09887d3df 100644 --- a/backend/src/openarchiefbeheer/utils/tests/gherkin.py +++ b/backend/src/openarchiefbeheer/utils/tests/gherkin.py @@ -18,7 +18,7 @@ from openarchiefbeheer.zaken.tests.factories import ZaakFactory -class GherkinLikeTestCase(PlaywrightTestCase): +class GerkinMixin: """ Experimental approach to writing Gherkin-like style test scenarios. Example: @@ -532,6 +532,9 @@ async def page_should_contain_element_with_title( async def path_should_be(self, page, path): await self.url_should_be(page, self.testcase.live_server_url + path) + async def url_regex_should_be(self, page, regex_path): + await expect(page).to_have_url(re.compile(regex_path)) + async def url_should_be(self, page, url): await expect(page).to_have_url(url) @@ -571,3 +574,7 @@ async def this_number_of_zaken_should_be_visible(self, page, number): rows = await locator.locator("tbody").locator("tr").all() self.testcase.assertEqual(len(rows), number) + + +class GherkinLikeTestCase(GerkinMixin, PlaywrightTestCase): + pass From 2b88eb926083b4ac77d8c87e3dc4ebd5488937f5 Mon Sep 17 00:00:00 2001 From: SilviaAmAm Date: Tue, 10 Dec 2024 17:07:44 +0100 Subject: [PATCH 3/3] :white_check_mark: [#496] Add test for performance --- .../destruction/tests/performance/__init__.py | 0 .../tests/performance/test_performance.py | 268 ++++++++++++++++++ 2 files changed, 268 insertions(+) create mode 100644 backend/src/openarchiefbeheer/destruction/tests/performance/__init__.py create mode 100644 backend/src/openarchiefbeheer/destruction/tests/performance/test_performance.py diff --git a/backend/src/openarchiefbeheer/destruction/tests/performance/__init__.py b/backend/src/openarchiefbeheer/destruction/tests/performance/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/src/openarchiefbeheer/destruction/tests/performance/test_performance.py b/backend/src/openarchiefbeheer/destruction/tests/performance/test_performance.py new file mode 100644 index 000000000..1e983d33b --- /dev/null +++ b/backend/src/openarchiefbeheer/destruction/tests/performance/test_performance.py @@ -0,0 +1,268 @@ +import os + +from django.conf import settings +from django.test import TestCase, tag + +from playwright.async_api import async_playwright, expect +from tabulate import tabulate + +from openarchiefbeheer.utils.tests.gherkin import GerkinMixin + +TEST_ENVIRONMENT = os.getenv("TEST_ENVIRONMENT") +RECORD_MANAGER_USERNAME = os.getenv("RECORD_MANAGER_USERNAME") +RECORD_MANAGER_PASSWORD = os.getenv("RECORD_MANAGER_PASSWORD") +REVIEWER_USERNAME = os.getenv("REVIEWER_USERNAME") +REVIEWER_PASSWORD = os.getenv("REVIEWER_PASSWORD") +ARCHIVARIS_USERNAME = os.getenv("ARCHIVARIS_USERNAME") +ARCHIVARIS_PASSWORD = os.getenv("ARCHIVARIS_PASSWORD") + + +@tag("performance") +class PerformanceTest(GerkinMixin, TestCase): + live_server_url = TEST_ENVIRONMENT + + async def flush_results(self, results): + print(tabulate(results, headers=["Method", "URL", "Duration (ms)"])) + results.clear() + + async def test_performance(self): + expect.set_options(timeout=30_000) + + async with async_playwright() as p: + launch_kwargs = { + "headless": settings.PLAYWRIGHT_HEADLESS, + } + + results = [] + + async def on_response(response): + await response.finished() + + request = response.request + timings = request.timing + + results.append([request.method, request.url, timings["responseEnd"]]) + + browser = await getattr(p, settings.PLAYWRIGHT_BROWSER).launch( + **launch_kwargs + ) + page = await browser.new_page() + page.on("response", on_response) + + # + # PART 1: Record manager + # + + # Record Manager logs in + await page.goto(f"{TEST_ENVIRONMENT}/login") + await page.get_by_label("Gebruikersnaam").fill(RECORD_MANAGER_USERNAME) + await page.get_by_label("Wachtwoord").fill(RECORD_MANAGER_PASSWORD) + await page.get_by_role("button", name="Inloggen").click() + await self.then.path_should_be(page, "/destruction-lists") + + # Record Manager creates list + await self.when.user_clicks_button(page, "Vernietigingslijst opstellen") + await self.then.path_should_be(page, "/destruction-lists/create") + + await self.when.user_clicks_checkbox( + page, "(de)selecteer 10000 pagina's", index=0 + ) + await self.when.user_clicks_button( + page, "Vernietigingslijst opstellen", index=1 + ) + await self.when.user_fills_form_field(page, "Naam", "Gigantic list") + await self.when.user_fills_form_field( + page, "Reviewer", "John Doe (reviewer1)" + ) + await self.when.user_fills_form_field(page, "Opmerking", "A humongous list") + await self.when.user_clicks_button(page, "Vernietigingslijst opstellen", 2) + await self.then.path_should_be(page, "/destruction-lists") + + # Record Manager marks as ready to review + await self.when.user_clicks_button(page, "Gigantic list") + await self.when.user_clicks_button(page, "Ter beoordeling indienen") + await self.when.user_clicks_button(page, "Ter beoordeling indienen", 1) + await self.then.path_should_be(page, "/destruction-lists") + + # Record Manager logs out + await self.when.user_logs_out(page) + + # Print results + await self.flush_results(results) + + # + # PART 2: Reviewer rejects + # + + # Reviewer logs in + await page.goto(f"{TEST_ENVIRONMENT}/login") + await page.get_by_label("Gebruikersnaam").fill(REVIEWER_USERNAME) + await page.get_by_label("Wachtwoord").fill(REVIEWER_PASSWORD) + await page.get_by_role("button", name="Inloggen").click() + await self.then.path_should_be(page, "/destruction-lists") + + # Reviewer requests changes + await self.when.user_clicks_button(page, "Gigantic list") + await self.then.url_regex_should_be( + page, f"{TEST_ENVIRONMENT}/destruction-lists/.+?/review" + ) + + # Reviewer rejects list + await self.when.user_clicks_button(page, "Uitzonderen") + await self.when.user_fills_form_field( + page, "Reden", "Please reconsider this zaak" + ) + await self.when.user_clicks_button(page, "Zaak uitzonderen") + await self.when.user_clicks_button(page, "Afwijzen") + await self.when.user_fills_form_field( + page, "Reden", "Please reconsider the zaak on this list" + ) + await self.when.user_clicks_button(page, "Vernietigingslijst afwijzen") + + await self.then.path_should_be(page, "/destruction-lists") + await self.when.user_logs_out(page) + + # Print results + await self.flush_results(results) + + # + # PART 3: Record manager processes feedback + # + + # Record Manager logs in + await page.goto(f"{TEST_ENVIRONMENT}/login") + await page.get_by_label("Gebruikersnaam").fill(RECORD_MANAGER_USERNAME) + await page.get_by_label("Wachtwoord").fill(RECORD_MANAGER_PASSWORD) + await page.get_by_role("button", name="Inloggen").click() + await self.then.path_should_be(page, "/destruction-lists") + + # Record Manager processes review + await self.when.user_clicks_button(page, "Gigantic list") + await self.then.url_regex_should_be( + page, f"{TEST_ENVIRONMENT}/destruction-lists/.+?/process-review" + ) + await self.when.user_clicks_button(page, "Muteren") + await self.when.user_clicks_radio(page, "Afwijzen van het voorstel") + await self.when.user_fills_form_field( + page, "Reden", "I still want 10000 cases in my list." + ) + await self.when.user_clicks_button(page, "muteren") + await self.when.user_clicks_button(page, "Opnieuw indienen") + await self.when.user_fills_form_field( + page, "Opmerking", "Did nothing...", None, 1 + ) + await self.when.user_clicks_button(page, "Opnieuw indienen", 1) + await self.then.path_should_be(page, "/destruction-lists") + await self.when.user_logs_out(page) + + # Print results + await self.flush_results(results) + + # + # PART 4: Reviewer approves + # + + # Reviewer logs in + await page.goto(f"{TEST_ENVIRONMENT}/login") + await page.get_by_label("Gebruikersnaam").fill(REVIEWER_USERNAME) + await page.get_by_label("Wachtwoord").fill(REVIEWER_PASSWORD) + await page.get_by_role("button", name="Inloggen").click() + await self.then.path_should_be(page, "/destruction-lists") + + # Reviewer approves list + await self.when.user_clicks_button(page, "Gigantic list") + await self.then.url_regex_should_be( + page, f"{TEST_ENVIRONMENT}/destruction-lists/.+?/review" + ) + await self.when.user_clicks_button(page, "Goedkeuren") + await self.when.user_fills_form_field(page, "Opmerking", "Ship it") + await self.when.user_clicks_button(page, "Vernietigingslijst goedkeuren") + + await self.then.path_should_be(page, "/destruction-lists") + await self.when.user_logs_out(page) + + # Print results + await self.flush_results(results) + + # + # PART 5: Record manager marks as definite + # + + # Record Manager logs in + await page.goto(f"{TEST_ENVIRONMENT}/login") + await page.get_by_label("Gebruikersnaam").fill(RECORD_MANAGER_USERNAME) + await page.get_by_label("Wachtwoord").fill(RECORD_MANAGER_PASSWORD) + await page.get_by_role("button", name="Inloggen").click() + await self.then.path_should_be(page, "/destruction-lists") + + # Record Manager marks as ready to review + await self.when.user_clicks_button(page, "Gigantic list") + await self.then.url_regex_should_be( + page, f"{TEST_ENVIRONMENT}/destruction-lists/.+?/edit" + ) + await self.when.user_clicks_button(page, "Markeren als definitief") + await self.when.user_fills_form_field( + page, "Archivaris", "Kobus Lies (archivaris)" + ) + await self.when.user_fills_form_field(page, "Opmerking", "Le's goooo") + await self.when.user_clicks_button(page, "Markeer als definitief") + await self.then.path_should_be(page, "/destruction-lists") + await self.when.user_logs_out(page) + + # Print results + await self.flush_results(results) + + # + # PART 6: Archivaris approves + # + + # Archivaris logs in + await page.goto(f"{TEST_ENVIRONMENT}/login") + await page.get_by_label("Gebruikersnaam").fill(ARCHIVARIS_USERNAME) + await page.get_by_label("Wachtwoord").fill(ARCHIVARIS_PASSWORD) + await page.get_by_role("button", name="Inloggen").click() + await self.then.path_should_be(page, "/destruction-lists") + + # Archivaris approves list + await self.when.user_clicks_button(page, "Gigantic list") + await self.then.url_regex_should_be( + page, f"{TEST_ENVIRONMENT}/destruction-lists/.+?/review" + ) + await self.when.user_clicks_button(page, "Goedkeuren") + await self.when.user_fills_form_field( + page, "Opmerking", "Let's deletete 🔥" + ) + await self.when.user_clicks_button(page, "Vernietigingslijst goedkeuren") + await self.when.user_logs_out(page) + + await self.then.path_should_be(page, "/destruction-lists") + + # Print results + await self.flush_results(results) + + # + # PART 7: Record manager starts deletion process + # + + # Record Manager logs in + await page.goto(f"{TEST_ENVIRONMENT}/login") + await page.get_by_label("Gebruikersnaam").fill(RECORD_MANAGER_USERNAME) + await page.get_by_label("Wachtwoord").fill(RECORD_MANAGER_PASSWORD) + await page.get_by_role("button", name="Inloggen").click() + await self.then.path_should_be(page, "/destruction-lists") + + # Record Manager processes review + await self.when.user_clicks_button(page, "Gigantic list") + await self.then.url_regex_should_be( + page, f"{TEST_ENVIRONMENT}/destruction-lists/.+?/edit" + ) + await self.when.user_clicks_button(page, "Vernietigen starten") + await self.when.user_fills_form_field( + page, "Type naam van de lijst ter bevestiging", "Gigantic list" + ) + await self.when.user_clicks_button(page, "10000 zaken vernietigen") + await self.then.path_should_be(page, "/destruction-lists") + await self.when.user_logs_out(page) + + # Print results + await self.flush_results(results)