Skip to content

Commit 7e0a8fc

Browse files
User Tools Utility (#36)
<!-- markdownlint-disable-next-line first-line-heading --> ## Description <!-- Describe your changes in detail. --> This adds in user tool support, so users can be managed from a JSON file rather than the code directly. ## Context <!-- Why is this change required? What problem does it solve? --> Allows for tidy user management, in an easy to maintain format rather than in code. ## Type of changes <!-- What types of changes does your code introduce? Put an `x` in all the boxes that apply. --> - [ ] Refactoring (non-breaking change) - [x] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would change existing functionality) - [ ] Bug fix (non-breaking change which fixes an issue) ## Checklist <!-- Go over all the following points, and put an `x` in all the boxes that apply. --> - [x] I am familiar with the [contributing guidelines](https://github.com/nhs-england-tools/playwright-python-blueprint/blob/main/CONTRIBUTING.md) - [x] I have followed the code style of the project - [x] I have added tests to cover my changes (where appropriate) - [x] I have updated the documentation accordingly - [ ] This PR is a result of pair or mob programming --- ## Sensitive Information Declaration To ensure the utmost confidentiality and protect your and others privacy, we kindly ask you to NOT including [PII (Personal Identifiable Information) / PID (Personal Identifiable Data)](https://digital.nhs.uk/data-and-information/keeping-data-safe-and-benefitting-the-public) or any other sensitive data in this PR (Pull Request) and the codebase changes. We will remove any PR that do contain any sensitive information. We really appreciate your cooperation in this matter. - [x] I confirm that neither PII/PID nor sensitive data are included in this PR and the codebase changes.
1 parent 4692285 commit 7e0a8fc

File tree

13 files changed

+290
-30
lines changed

13 files changed

+290
-30
lines changed

.vscode/settings.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,15 @@
77
"cSpell.words": [
88
"addopts",
99
"codegen",
10+
"customisable",
11+
"customised",
12+
"initialise",
1013
"Licence",
1114
"organisation",
1215
"pytest",
16+
"pytestmark",
17+
"retcode",
18+
"ruleset",
1319
"utilise",
1420
"utilised",
1521
"Utilising"

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,8 @@ This blueprint also provides the following utility classes, that can be used to
7878
|-------|-----------|
7979
|[Axe](./docs/utility-guides/Axe.md)|Accessibility scanning using axe-core.|
8080
|[Date Time Utility](./docs/utility-guides/DateTimeUtility.md)|Basic functionality for managing date/times.|
81-
|NHSNumberTools|Basic tools for working with NHS numbers.|
81+
|[NHSNumberTools](./docs/utility-guides/NHSNumberTools.md)|Basic tools for working with NHS numbers.|
82+
|[User Tools](./docs/utility-guides/UserTools.md)|Basic user management tool.|
8283

8384
## Contributing
8485

docs/getting-started/2_Blueprint_File_Breakdown.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ This guide outlines the breakdown of this blueprint, and specifically the files
99
- [Directories \& Files Directly Impacting Tests](#directories--files-directly-impacting-tests)
1010
- [`requirements.txt`](#requirementstxt)
1111
- [`pytest.ini`](#pytestini)
12+
- [`users.json`](#usersjson)
1213
- [`tests/`](#tests)
1314
- [`pages/`](#pages)
1415
- [`utils/`](#utils)
@@ -36,6 +37,11 @@ This file outlines the configuration of pytest, and ultimately how Playwright al
3637

3738
Any configuration you want to apply to all of your test executions should be placed in this file where possible, to ensure easy maintenance.
3839

40+
### `users.json`
41+
42+
This file outlines the users you may want to use as part of your testing, and is utilised by the User Tools utility. Further information on how this file is used
43+
can be found in the [User Tools Utility Guide](../utility-guides/UserTools.md).
44+
3945
### `tests/`
4046

4147
This directory is designed to house all of your tests intended for execution.
@@ -66,3 +72,4 @@ The following directories and files are specific for this repository, and may re
6672
- `scripts/`: This directory houses the scripts used by this repository, primarily as part of the CI/CD checks.
6773
- `tests_utils/`: This directory houses the unit tests for the utilities provided by this repository. You may want to copy these over if you want to ensure utilities are behaving as expected.
6874
- `.editorconfig`, `.gitattributes`, `.gitignore`, `.gitleaks.toml`, `.gitleaksignore`: These files are configuration for git, and quality and security checks provided via the CI/CD checks.
75+
- `Makefile`: This file is used to import some of the scripts for CI/CD checks, but can be customised per project if needed. The template this project is based from provides a more comprehensive example [here](https://github.com/nhs-england-tools/repository-template/blob/main/Makefile).
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Utility Guide: NHS Number Tools
2+
3+
The NHS Number Tools utility provided by this blueprint allows for the easy management of NHS numbers, and provides
4+
common functionality that may apply to many services in relation to NHS Number management.
5+
6+
## Table of Contents
7+
8+
- [Utility Guide: NHS Number Tools](#utility-guide-nhs-number-tools)
9+
- [Table of Contents](#table-of-contents)
10+
- [Using the NHS Number Tools class](#using-the-nhs-number-tools-class)
11+
- [`spaced_nhs_number()`: Return Spaced NHS Number](#spaced_nhs_number-return-spaced-nhs-number)
12+
- [Required Arguments](#required-arguments)
13+
- [Returns](#returns)
14+
15+
## Using the NHS Number Tools class
16+
17+
You can initialise the NHS Number Tools class by using the following code in your test file:
18+
19+
from utils.nhs_number_tools import NHSNumberTools
20+
21+
## `spaced_nhs_number()`: Return Spaced NHS Number
22+
23+
The `spaced_nhs_number()` method is designed to take the provided NHS number and return it in a formatted
24+
string of the format `nnn nnn nnnn`. It's a static method so can be used in the following way:
25+
26+
# Return formatted NHS number
27+
spaced_nhs_number = NHSNumberTools.spaced_nhs_number("1234567890")
28+
29+
### Required Arguments
30+
31+
The following are required for `NHSNumberTools.spaced_nhs_number()`:
32+
33+
| Argument | Format | Description |
34+
| ---------- | -------------- | ------------------------- |
35+
| nhs_number | `str` or `int` | The NHS number to format. |
36+
37+
### Returns
38+
39+
A `str` with the provided NHS number in `nnn nnn nnnn` format. For example, `NHSNumberTools.spaced_nhs_number(1234567890)` would return `123 456 7890`.

docs/utility-guides/UserTools.md

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# Utility Guide: User Tools
2+
3+
The User Tools utility provided by this blueprint allows for the easy management of test users via a json file included
4+
at the base of the repository.
5+
6+
## Table of Contents
7+
8+
- [Utility Guide: User Tools](#utility-guide-user-tools)
9+
- [Table of Contents](#table-of-contents)
10+
- [Using the User Tools class](#using-the-user-tools-class)
11+
- [Managing Users](#managing-users)
12+
- [Considering Security](#considering-security)
13+
- [`retrieve_user()`: Retrieve User Details](#retrieve_user-retrieve-user-details)
14+
- [Required Arguments](#required-arguments)
15+
- [Returns](#returns)
16+
- [Example Usage](#example-usage)
17+
18+
## Using the User Tools class
19+
20+
You can initialise the User Tools class by using the following code in your test file:
21+
22+
from utils.user_tools import UserTools
23+
24+
This module has been designed as a static class, so you do not need to instantiate it when you want to retrieve any user information.
25+
26+
## Managing Users
27+
28+
For this class, users are managed via the [users.json](../../users.json) file provided with this repository. For any new users you need to
29+
add, the idea is to just add a new record, with any appropriate metadata you need for the user whilst they interact with your application.
30+
31+
For example, adding a record like so (this example shows the entire `users.json` file):
32+
33+
{
34+
"Documentation User": {
35+
"username": "DOC_USER",
36+
"roles": ["Example Role A"],
37+
"unique_id": 42
38+
}
39+
}
40+
41+
The data you require for these users can be completely customised for what information you need, so whilst the example shows `username`, `roles`
42+
and `unique_id` as possible values we may want to use, this is not an exhaustive list. The key that is used (so in this example, `"Documentation User"`)
43+
is also customisable and should be how you want to easily reference retrieving this user in your tests.
44+
45+
### Considering Security
46+
47+
An important note on managing users in this way is that passwords or security credentials should **never** be stored in the `users.json` file. These
48+
are considered secrets, and whilst it may be convenient to store them in this file, it goes against the
49+
[security principles outlined in the Software Engineering Quality Framework](https://github.com/NHSDigital/software-engineering-quality-framework/blob/main/practices/security.md#application-level-security).
50+
51+
With this in mind, it's recommended to do the following when it comes to managing these types of credentials:
52+
53+
- When running locally, store any secret values in a local configuration file and set this file in `.gitignore` so it is not committed to the codebase.
54+
- When running via a CI/CD process, store any secret values in an appropriate secret store and pass the values into pytest at runtime.
55+
56+
## `retrieve_user()`: Retrieve User Details
57+
58+
The `retrieve_user()` method is designed to easily retrieve the data for a specific user entry from the `users.json` file. This is a static method,
59+
so can be called using the following logic:
60+
61+
# Retrieving documentation user details from example
62+
user_details = UserTools.retrieve_user("Documentation User")
63+
64+
### Required Arguments
65+
66+
The following are required for `UserTools.retrieve_user()`:
67+
68+
| Argument | Format | Description |
69+
| -------- | ------ | ------------------------------------------------------- |
70+
| user | `str` | The key from `users.json` for the user details required |
71+
72+
### Returns
73+
74+
A Python `dict` object that contains the values associated with the provided user argument.
75+
76+
### Example Usage
77+
78+
When using a `users.json` file as set up in the example above:
79+
80+
from utils.user_tools import UserTools
81+
from playwright.sync_api import Page
82+
83+
def test_login(page: Page) -> None:
84+
# Retrieving documentation user details from example
85+
user_details = UserTools.retrieve_user("Documentation User")
86+
87+
# Use values to populate a form
88+
page.get_by_role("textbox", name="Username").fill(user_details["username"])
89+
page.get_by_role("textbox", name="ID").fill(user_details["unique_id"])
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"Test User": {
3+
"username": "TEST_USER1",
4+
"test_key": "TEST A"
5+
},
6+
"Test User 2": {
7+
"username": "TEST_USER2",
8+
"test_key": "TEST B"
9+
}
10+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import pytest
2+
from utils.nhs_number_tools import NHSNumberTools, NHSNumberToolsException
3+
4+
5+
pytestmark = [pytest.mark.utils]
6+
7+
def test_nhs_number_checks() -> None:
8+
assert NHSNumberTools._nhs_number_checks("1234567890") == None
9+
10+
with pytest.raises(Exception, match=r'The NHS number provided \(A234567890\) is not numeric.'):
11+
NHSNumberTools._nhs_number_checks("A234567890")
12+
13+
with pytest.raises(NHSNumberToolsException, match=r'The NHS number provided \(123\) is not 10 digits'):
14+
NHSNumberTools._nhs_number_checks("123")
15+
16+
def test_spaced_nhs_number() -> None:
17+
assert NHSNumberTools.spaced_nhs_number("1234567890") == "123 456 7890"
18+
assert NHSNumberTools.spaced_nhs_number(3216549870) == "321 654 9870"

tests_utils/test_user_tools.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import pytest
2+
import utils.user_tools
3+
from utils.user_tools import UserTools, UserToolsException
4+
from pathlib import Path
5+
6+
7+
pytestmark = [pytest.mark.utils]
8+
9+
def test_retrieve_user(monkeypatch: object) -> None:
10+
monkeypatch.setattr(utils.user_tools, "USERS_FILE", Path(__file__).parent / "resources" / "test_users.json")
11+
12+
test_user = UserTools.retrieve_user("Test User")
13+
assert test_user["username"] == "TEST_USER1"
14+
assert test_user["test_key"] == "TEST A"
15+
16+
test_user2 = UserTools.retrieve_user("Test User 2")
17+
assert test_user2["username"] == "TEST_USER2"
18+
assert test_user2["test_key"] == "TEST B"
19+
20+
with pytest.raises(UserToolsException, match=r'User \[Invalid User\] is not present in users.json'):
21+
UserTools.retrieve_user("Invalid User")

users.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"_comment": "This file can be used to store data on users for your application, and then pulled through using the utils.user_tools UserTools utility. The documentation for this utility explains how this file is read.",
3+
"Example User 1": {
4+
"username": "EXAMPLE_USER1",
5+
"roles": ["Example Role A"]
6+
},
7+
"Example User 2": {
8+
"username": "EXAMPLE_USER2",
9+
"roles": ["Example Role B", "Example Role C"]
10+
}
11+
}

utils/axe.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from pathlib import Path
77

88

9+
logger = logging.getLogger(__name__)
910
AXE_PATH = Path(__file__).parent / "resources" / "axe.js"
1011
PATH_FOR_REPORT = Path(__file__).parent.parent / "axe-reports"
1112
DEFAULT_WCAG_RULESET = ['wcag2a', 'wcag21a', 'wcag2aa', 'wcag21aa', 'wcag22a', 'wcag22aa', 'best-practice']
@@ -36,13 +37,16 @@ def run(page: Page,
3637
strict_mode (bool): [Optional] If true, raise an exception if a violation is detected. If false (default), proceed with test execution.
3738
html_report_generated (bool): [Optional] If true (default), generates a html report for the page scanned. If false, no html report is generated.
3839
json_report_generated (bool): [Optional] If true (default), generates a json report for the page scanned. If false, no json report is generated.
40+
41+
Returns:
42+
dict: A Python dictionary with the axe-core output of the page scanned.
3943
"""
4044

4145
page.evaluate(AXE_PATH.read_text(encoding="UTF-8"))
4246

4347
response = page.evaluate("axe." + Axe._build_run_command(ruleset) + ".then(results => {return results;})")
4448

45-
logging.info(f"""Axe scan summary of [{response["url"]}]: Passes = {len(response["passes"])},
49+
logger.info(f"""Axe scan summary of [{response["url"]}]: Passes = {len(response["passes"])},
4650
Violations = {len(response["violations"])}, Inapplicable = {len(response["inapplicable"])},
4751
Incomplete = {len(response["incomplete"])}""")
4852

@@ -81,6 +85,9 @@ def run_list(page: Page,
8185
strict_mode (bool): [Optional] If true, raise an exception if a violation is detected. If false (default), proceed with test execution.
8286
html_report_generated (bool): [Optional] If true (default), generates a html report for the page scanned. If false, no html report is generated.
8387
json_report_generated (bool): [Optional] If true (default), generates a json report for the page scanned. If false, no json report is generated.
88+
89+
Returns:
90+
dict: A Python dictionary with the axe-core output of all the pages scanned, with the page list used as the key for each report.
8491
"""
8592
results = {}
8693
for selected_page in page_list:
@@ -120,24 +127,24 @@ def _create_path_for_report(filename: str) -> Path:
120127
return PATH_FOR_REPORT / filename
121128

122129
@staticmethod
123-
def _create_json_report(data: dict, filename_overide: str = "") -> None:
124-
filename = f"{Axe._modify_filename_for_report(data["url"])}.json" if filename_overide == "" else f"{filename_overide}.json"
130+
def _create_json_report(data: dict, filename_override: str = "") -> None:
131+
filename = f"{Axe._modify_filename_for_report(data["url"])}.json" if filename_override == "" else f"{filename_override}.json"
125132
full_path = Axe._create_path_for_report(filename)
126133

127134
with open(full_path, 'w') as file:
128135
file.writelines(json.dumps(data))
129136

130-
logging.info(f"JSON report generated: {full_path}")
137+
logger.info(f"JSON report generated: {full_path}")
131138

132139
@staticmethod
133-
def _create_html_report(data: dict, filename_overide: str = "") -> None:
134-
filename = f"{Axe._modify_filename_for_report(data["url"])}.html" if filename_overide == "" else f"{filename_overide}.html"
140+
def _create_html_report(data: dict, filename_override: str = "") -> None:
141+
filename = f"{Axe._modify_filename_for_report(data["url"])}.html" if filename_override == "" else f"{filename_override}.html"
135142
full_path = Axe._create_path_for_report(filename)
136143

137144
with open(full_path, 'w') as file:
138145
file.writelines(Axe._generate_html(data))
139146

140-
logging.info(f"HTML report generated: {full_path}")
147+
logger.info(f"HTML report generated: {full_path}")
141148

142149
@staticmethod
143150
def _generate_html(data: dict) -> str:

0 commit comments

Comments
 (0)