Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for user input and documentation improvements to smoke testing workflow #416

Merged
merged 37 commits into from
Sep 23, 2024
Merged
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
4f28b26
add os, python-version, and opa-version inputs to workflow_dispatch
mitchelbaker-cisa Sep 17, 2024
c9a9435
Remove numpy uninstall/install 1.24 from steps
mitchelbaker-cisa Sep 17, 2024
edc878b
Remove uninstall numpy/install 1.24 from macos dep setup
mitchelbaker-cisa Sep 17, 2024
98665c7
Add some initial input validation prior to smoke-test job execution
mitchelbaker-cisa Sep 17, 2024
c5f3124
add dependency to validate-inputs job for smoke-test to run
mitchelbaker-cisa Sep 17, 2024
7ff3463
set validate-inputs job to run with bash shell
mitchelbaker-cisa Sep 17, 2024
796538c
fix regex
mitchelbaker-cisa Sep 17, 2024
b937bf1
Update run_smoke_test.yml
mitchelbaker-cisa Sep 17, 2024
33d23b2
Update run_smoke_test.yml
mitchelbaker-cisa Sep 18, 2024
6687a33
Update run_smoke_test.yml
mitchelbaker-cisa Sep 18, 2024
3dd0f52
Update run_smoke_test.yml
mitchelbaker-cisa Sep 18, 2024
5217a51
Update run_smoke_test.yml
mitchelbaker-cisa Sep 18, 2024
2a6928b
Update run_smoke_test.yml
mitchelbaker-cisa Sep 18, 2024
bc655bd
Update run_smoke_test.yml
mitchelbaker-cisa Sep 18, 2024
75cdff4
Update run_smoke_test.yml
mitchelbaker-cisa Sep 18, 2024
5534060
fix configure job to include opa version
mitchelbaker-cisa Sep 18, 2024
6da7b94
update opa-version to needs.configuration.outputs.opa-version for set…
mitchelbaker-cisa Sep 18, 2024
3519800
Update input naming convention in windows dep action
mitchelbaker-cisa Sep 18, 2024
2426342
Update naming convention for os in macos dep action
mitchelbaker-cisa Sep 18, 2024
0fc2ab7
Update naming convention for os in smoke test workflow
mitchelbaker-cisa Sep 18, 2024
335b7ca
Create initial draft of README.md for smoke test documentation
mitchelbaker-cisa Sep 18, 2024
3a67352
Update README.md with prerequisite steps, directory structure, usage
mitchelbaker-cisa Sep 18, 2024
c208006
update baseline report H1 to match secure configuration baseline repo…
mitchelbaker-cisa Sep 18, 2024
228ccb5
update smoke tests to use customerdomain instead of domain; add windo…
mitchelbaker-cisa Sep 18, 2024
9c5880f
add toc, extend initial list of arguments for pytest, add additional …
mitchelbaker-cisa Sep 18, 2024
9cb6272
documentation improvements
mitchelbaker-cisa Sep 18, 2024
69a170b
update running via github actions section to reflect main branch inst…
mitchelbaker-cisa Sep 18, 2024
65b9a04
try removing environment: Development and replacing with env key/valu…
mitchelbaker-cisa Sep 18, 2024
f3de724
try updating permissions to workflow
mitchelbaker-cisa Sep 18, 2024
5360b8a
Update run_smoke_test.yml
mitchelbaker-cisa Sep 18, 2024
d690ce3
Update run_smoke_test.yml
mitchelbaker-cisa Sep 18, 2024
f9ae0c3
remove env key/value since its unneeded, call secrets vars directly; …
mitchelbaker-cisa Sep 18, 2024
362b8d7
add pull_request_review on submit trigger back into workflow
mitchelbaker-cisa Sep 18, 2024
80dd72a
add section on adding new functional tests with example
mitchelbaker-cisa Sep 18, 2024
8def1cb
cleanup documentation
mitchelbaker-cisa Sep 18, 2024
bdd0e4a
final proofreading
mitchelbaker-cisa Sep 18, 2024
8cae42e
remove correct wording and replace with valid
mitchelbaker-cisa Sep 19, 2024
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
6 changes: 2 additions & 4 deletions .github/actions/setup-dependencies-macos/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,9 @@ runs:
pip install -r requirements.txt
pip install pytest
pip install selenium
pip uninstall -y numpy
pip install numpy==1.26.4


- name: Download OPA executable
shell: bash
run: |
python download_opa.py -v ${{ inputs.opa-version }} -os ${{ inputs.operating-system }}
chmod +x opa_darwin_amd64
chmod +x opa_darwin_amd64
15 changes: 3 additions & 12 deletions .github/actions/setup-dependencies-windows/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,15 @@ runs:
pip install virtualenv
python -m venv .venv
.venv\Scripts\activate

- name: Install dependencies
shell: powershell
run: |
python -m pip install .
pip install -r requirements.txt
pip install pytest
pip install selenium
pip uninstall -y numpy
pip install numpy==1.26.4

# Below python v3.9, a lower numpy v1.24.4 is used
#$pythonVersion = [version]${{ inputs.python-version }}
#if ($pythonVersion -ge [version]"3.8.18") {
# pip uninstall -y numpy
# pip install numpy==1.26.4
#}


- name: Download OPA executable
shell: powershell
run: python download_opa.py -v ${{ inputs.opa-version }} -os ${{ inputs.operating-system }}
run: python download_opa.py -v ${{ inputs.opa-version }} -os ${{ inputs.operating-system }}
77 changes: 61 additions & 16 deletions .github/workflows/run_smoke_test.yml
Original file line number Diff line number Diff line change
@@ -1,31 +1,76 @@
name: Run Smoke Test
on:
workflow_call:
workflow_dispatch:
pull_request:
types: [opened, reopened]
branches:
- "main"
pull_request_review:
types: [submitted]
push:
# Uncomment when testing locally
#paths:
# - ".github/workflows/run_smoke_test.yml"
# - ".github/actions/setup-dependencies-windows/action.yml"
# - ".github/actions/setup-dependencies-macos/action.yml"
# These paths are primarily for workflow testing purposes
paths:
- ".github/workflows/run_smoke_test.yml"
- ".github/actions/setup-dependencies-windows/action.yml"
- ".github/actions/setup-dependencies-macos/action.yml"
branches:
- "main"
- "*smoketest*"
workflow_call:
workflow_dispatch:
inputs:
operating-system:
description: "Choose operating system(s), format must be an array of strings:"
required: true
type: string
default: "['windows-latest', 'macos-latest']"
python-version:
description: "Choose python version(s), format must be an array of strings:"
required: true
type: string
default: "['3.10']"
opa-version:
description: "Choose OPA version"
required: true
type: string
default: "0.60.0"

jobs:
configuration:
runs-on: ubuntu-latest
outputs:
operating-system: ${{ steps.variables.outputs.operating-system }}
python-version: ${{ steps.variables.outputs.python-version }}
opa-version: ${{ steps.variables.outputs.opa-version }}
steps:
- name: Configure variable outputs
id: variables
run: |
# For manual runs
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
operatingsystem_val="${{ inputs.operating-system }}"
pythonversion_val="${{ inputs.python-version }}"
opaversion_val="${{ inputs.opa-version }}"

# Default values for other events
else
operatingsystem_val="['windows-latest', 'macos-latest']"
pythonversion_val="['3.10']"
opaversion_val="0.60.0"
fi
echo "operating-system=$operatingsystem_val" >> "$GITHUB_OUTPUT"
echo "python-version=$pythonversion_val" >> "$GITHUB_OUTPUT"
echo "opa-version=$opaversion_val" >> "$GITHUB_OUTPUT"

smoke-test:
needs: configuration
strategy:
fail-fast: false
matrix:
os: [windows-latest, macos-latest]
operating-system: ${{ fromJSON(needs.configuration.outputs.operating-system) }}
# See https://raw.githubusercontent.com/actions/python-versions/main/versions-manifest.json,
# ctrl + f and search "python-3.<minor>.<patch>-<darwin-arm64/win32/linux>" for supported versions
python-version: ["3.9", "3.12"] # "3.8 fails with numpy uninstall"
runs-on: ${{ matrix.os }}
environment: Development
python-version: ${{ fromJSON(needs.configuration.outputs.python-version) }}
runs-on: ${{ matrix.operating-system }}
steps:
- name: Checkout Repository
uses: actions/checkout@v4
Expand All @@ -38,19 +83,19 @@ jobs:
cache-dependency-path: "requirements.txt"

- name: Setup Dependencies (Windows)
if: ${{ matrix.os == 'windows-latest' }}
if: ${{ matrix.operating-system == 'windows-latest' }}
uses: ./.github/actions/setup-dependencies-windows
with:
operating-system: "windows"
opa-version: "0.60.0"
opa-version: ${{ needs.configuration.outputs.opa-version }}
python-version: ${{ matrix.python-version }}

- name: Setup Dependencies (macOS)
if: ${{ matrix.os == 'macos-latest' }}
if: ${{ matrix.operating-system == 'macos-latest' }}
uses: ./.github/actions/setup-dependencies-macos
with:
operating-system: "macos"
opa-version: "0.60.0"
opa-version: ${{ needs.configuration.outputs.opa-version }}
python-version: ${{ matrix.python-version }}

- name: Setup credentials for service account
Expand All @@ -61,4 +106,4 @@ jobs:
json: ${{ secrets.GWS_GITHUB_AUTOMATION_CREDS }}

- name: Run ScubaGoggles and check for correct output
run: pytest -s -vvv ./Testing/Functional/SmokeTests/ --subjectemail="${{ secrets.GWS_SUBJECT_EMAIL }}" --domain="${{ secrets.GWS_DOMAIN }}"
run: pytest ./Testing/Functional/SmokeTests/ -vvv --subjectemail="${{ secrets.GWS_SUBJECT_EMAIL }}" --customerdomain="${{ secrets.GWS_DOMAIN }}"
111 changes: 111 additions & 0 deletions Testing/Functional/SmokeTests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# ScubaGoggles Functional Smoke Testing Automation
The ScubaGoggles repository consists of an automation suite to help test the functionality of the ScubaGoggles tool itself. The test automation is geared towards contributors who want to execute the functional smoke testing orchestrator as part of their development/testing activity.

This README outlines the ScubaGoggles software test automation and its usage. The document also contains instructions for adding new functional tests to existing automation suite.

## Table of Contents
- [Smoke Testing Prerequisites](#smoke-testing-prerequisites)
- [Pytest and Selenium](#pytest-and-selenium)
- [Google Service Account](#google-service-account)
- [Functional Smoke Testing Structure](#functional-smoke-testing-structure)
- [Smoke Testing Classes and Methods](#smoke-testing-classes-and-methods)
- [Automated workflow via GitHub Actions](#automated-workflow-via-github-actions)
- [Functional Smoke Testing Usage](#functional-smoke-testing-usage)
- [Running in a Local Development Environment](#running-in-a-local-development-environment)
- [Running Remotely via GitHub Actions](#running-remotely-via-github-actions)

## Smoke Testing Prerequisites ##
Running the ScubaGoggles functional smoke tests requires a Windows, MacOS, or Linux computer or VM. The development environment should have Python v3.10.x installed at a minimum ([refer to our installing Python dependencies documentation if its not already installed](https://github.com/cisagov/ScubaGoggles/blob/main/docs/installation/DownloadAndInstall.md#installing-python-dependencies)), Pytest, and Selenium installed locally.

### Pytest and Selenium ###
Pytest is a Python testing framework which is commonly used for unit, integration, and functional testing. ([Pytest Get Started](https://docs.pytest.org/en/stable/getting-started.html))

Selenium supports automation of all the major browsers in the market through the use of WebDriver. ([Selenium Get Started](https://www.selenium.dev/documentation/webdriver/getting_started/))

To install Pytest and Selenium on your development environment, open a new terminal session and run the following command:

```
pip install pytest selenium
```

> [!NOTE]
> The functional smoke tests use Chrome as its WebDriver when running Selenium tests. If you don't already have the Google Chrome web browser installed, [setup ChromeDriver here](https://developer.chrome.com/docs/chromedriver/get-started).

### Google Service Account ###
The ScubaGoggles functional smoke tests must be executed with a service account. [Refer to our documentation here on how to get setup.](https://github.com/cisagov/ScubaGoggles/blob/main/docs/authentication/ServiceAccount.md#using-a-service-account)

A `credentials.json` file is required at the root directory of the ScubaGoggles project if running the functional smoke tests in a local development environment.

Take note of the `subjectemail`, the email used to authenticate with GWS that has necessary administrator permissions, and the GWS `customerdomain` that ScubaGoggles is run against. Both credentials are required in a later step.

## Functional Smoke Testing Structure ##
ScubaGoggles functional smoke testing has two main components: the smoke testing orchestrator and the automated workflow which is run via GitHub Actions.

### Smoke Testing Classes and Methods ###
The smoke testing orchestrator ([/Testing/Functional/SmokeTests/smoke_test.py](https://github.com/cisagov/ScubaGoggles/blob/main/Testing/Functional/SmokeTests/smoke_test.py)) executes each test declared inside the `SmokeTest` class. The tests currently cover:
- if the `scubagoggles gws` command generates correct output for all baselines
adhilto marked this conversation as resolved.
Show resolved Hide resolved
- if ScubaResults.json contains API errors or exceptions
- if the generated baseline reports are correct, i.e. BaselineReports.html, CalendarReport.html, ChatReport.html

The smoke testing utils ([/Testing/Functional/SmokeTests/smoke_test_utils.py](https://github.com/cisagov/ScubaGoggles/blob/main/Testing/Functional/SmokeTests/smoke_test_utils.py)) stores helper methods which perform various operations.

The Selenium Browser class ([/Testing/Functional/SmokeTests/selenium_browser.py](https://github.com/cisagov/ScubaGoggles/blob/main/Testing/Functional/SmokeTests/selenium_browser.py)) encapsulates the setup, usage, and teardown of Selenium WebDriver instances.

The Pytest configuration methods ([/Testing/Functional/SmokeTests/conftest.py](https://github.com/cisagov/ScubaGoggles/blob/main/Testing/Functional/conftest.py)) declare various Pytest fixtures, allowing for the use of CLI arguments when invoking the Pytest command.

### Automated Workflow via GitHub Actions ###
The automated workflow for running the functional smoke tests ([/.github/workflows/run_smoke_test.yml](https://github.com/cisagov/ScubaGoggles/blob/main/.github/workflows/run_smoke_test.yml)) is triggered on `push` events to the main branch, `pull_request` events when a pull request is opened/reopened, and manually with customer user input via workflow_dispatch.

## Functional Smoke Testing Usage ##
After completing all of the prerequisite steps, the functional smoke tests can be run on a local development environment or remotely via GitHub Actions.

### Running in a Local Development Environment ###
> [!IMPORTANT]
> Ensure that you have correctly setup a Google service account and that the `credentials.json` stored at the root directory of the ScubaGoggles project is up to date. If you haven't already, please refer back to the [prerequisite step on Google Service Accounts](#google-service-account) for how to get setup before proceeding.

The following arguments are required when running the functional smoke tests:
- `--subjectemail="user@domain.com"` (the email used to authenticate with GWS, must have necessary administrator permissions)
- `--customerdomain="domain.com"` (the domain that ScubaGoggles is run against)

Replace `user@domain.com` with your email and `domain.com` with your domain, then run the following command to execute the functional smoke tests:
```
pytest ./Testing/Functional/SmokeTests/ -vvv --subjectemail="user@domain.com" --customerdomain="domain.com"
```

Common Pytest parameters and their use cases:
- `-v` or `--verbose` (shows individual test names and results)
- `-vv` (increases verbosity further, shows detailed output about each test)
- `-vvv` (shows even more detailed output and debug-level information)
- `-s` (disables output capturing allowing print() statements and logs to be shown in the console)
- `-k` (run tests that match a keyword)

Example (only runs test_scubagoggles_output, deselects the rest):
```
pytest ./Testing/Functional/SmokeTests/ -vvv -k test_scubagoggles_output --subjectemail="user@domain.com" --customerdomain="domain.com"
```

- `--tb=short`, `tb=long`, or `tb=no` (provide either brief, full, or suppress the traceback output for failed tests)
- `-q` (reduces output to show only minimal information)

Run `pytest -h` for a full list of CLI options, or [learn more about Pytest usage here.](https://docs.pytest.org/en/7.1.x/how-to/usage.html)

### Running Remotely via GitHub Actions ###
Go to the [run_smoke_test.yml workflow](https://github.com/cisagov/ScubaGoggles/actions/workflows/run_smoke_test.yml) in the GitHub Actions tab, then click the "Run workflow" dropdown button.

The default values are the following:
- ref branch: `main` but can be set to any branch
- operating system: `['windows-latest', 'macos-latest']` ([list of supported GitHub-hosted runners](https://docs.github.com/en/actions/using-github-hosted-runners/using-github-hosted-runners/about-github-hosted-runners#standard-github-hosted-runners-for-public-repositories))
- python version: `['3.10']`
- opa version: "0.60.0"

![Screenshot (226)](https://github.com/user-attachments/assets/6f25b7a9-3981-4866-a413-93df4bae1130)

Feel free to play around with the inputs then click the "Run workflow" button when ready. The workflow will create a matrix strategy for each combination. For example, passing `['windows-latest', 'macos-latest']`, `['3.10', '3.11', 3.12']`, and OPA version `0.60.0` will create the following:

![Screenshot (218)](https://github.com/user-attachments/assets/212b4e4b-d552-4dc9-a3f6-7f0e29accc4b)

Some factors to consider:
- Each input is required so an empty string will fail validation. `[]`, `['']`, `['', ]` may also cause the workflow to error out, although this is expected behavior.
- `ubuntu-latest` has not been tested as a value for operating system. Support can be added for this, although its dependent on if this is something we want to test for ScubaGoggles as a whole.
- Python versions <3.10.x are not supported and will cause the smoke test workflow to fail.
- [Due to the lack of an array input type from GitHub](https://github.com/orgs/community/discussions/11692), the required format is an array of strings for the operating system and python version inputs. This is something to capture as a future todo once arrays are available.
1 change: 1 addition & 0 deletions Testing/Functional/SmokeTests/selenium_browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class Browser:
def __init__(self):
chrome_options = Options()
chrome_options.add_argument("--headless")
chrome_options.add_argument("--window-size=1200,800")
chrome_options.add_argument("--no-sandbox")
chrome_options.add_argument("--disable-dev-shm-usage")

Expand Down
4 changes: 2 additions & 2 deletions Testing/Functional/SmokeTests/smoke_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def test_scubaresults(self):
except (ValueError, Exception) as e:
pytest.fail(f"An error occurred, {e}")

def test_scubagoggles_report(self, browser, domain):
def test_scubagoggles_report(self, browser, customerdomain):
"""
Test if the generated baseline reports are correct,
i.e. BaselineReports.html, CalendarReport.html, ChatReport.html
Expand All @@ -66,7 +66,7 @@ def test_scubagoggles_report(self, browser, domain):
output_path: str = get_output_path()
report_path: str = prepend_file_protocol(os.path.join(output_path, BASELINE_REPORTS))
browser.get(report_path)
run_selenium(browser, domain)
run_selenium(browser, customerdomain)
except (ValueError, AssertionError, Exception) as e:
browser.quit()
pytest.fail(f"An error occurred, {e}")
24 changes: 12 additions & 12 deletions Testing/Functional/SmokeTests/smoke_test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from scubagoggles.utils import get_package_version

OUTPUT_DIRECTORY = "GWSBaselineConformance"
BASELINE_REPORT_H1 = "SCuBA GWS Security Baseline Conformance Reports"
BASELINE_REPORT_H1 = "SCuBA GWS Secure Configuration Baseline Reports"
CISA_GOV_URL = "https://www.cisa.gov/scuba"
SCUBAGOGGLES_BASELINES_URL = "https://github.com/cisagov/ScubaGoggles/tree/main/baselines"

Expand Down Expand Up @@ -125,13 +125,13 @@ def verify_scubaresults(jsonfile):
if summary["Errors"] != 0:
raise ValueError(f"{product} contains errors in the report")

def run_selenium(browser, domain):
def run_selenium(browser, customerdomain):
"""
Run Selenium tests against the generated reports.

Args:
browser: A Selenium WebDriver instance
domain: The user's domain
customerdomain: The customer domain
"""
verify_navigation_links(browser)
h1 = browser.find_element(By.TAG_NAME, "h1").text
Expand All @@ -149,9 +149,9 @@ def run_selenium(browser, domain):
if len(reports_table) == 10:
for i in range(len(reports_table)):

# Check if domain is present in agency table
# Check if customerdomain is present in agency table
# Skip tool version if assessing the parent report
verify_tenant_table(browser, domain, True)
verify_tenant_table(browser, customerdomain, True)

reports_table = get_reports_table(browser)[i]
baseline_report = reports_table.find_elements(By.TAG_NAME, "td")[0]
Expand All @@ -169,8 +169,8 @@ def run_selenium(browser, domain):
h1 = browser.find_element(By.TAG_NAME, "h1").text
assert h1 == products[product]["title"]

# Check if domain and tool version are present in individual report
verify_tenant_table(browser, domain, False)
# Check if customerdomain and tool version are present in individual report
verify_tenant_table(browser, customerdomain, False)

policy_tables = browser.find_elements(By.TAG_NAME, "table")
for table in policy_tables[1:]:
Expand Down Expand Up @@ -239,14 +239,14 @@ def get_reports_table(browser):
.find_elements(By.TAG_NAME, "tr")
)

def verify_tenant_table(browser, domain, parent):
def verify_tenant_table(browser, customerdomain, parent):
"""
Get the tenant table rows elements from the DOM.
(Table at the top of each report with user domain, baseline/tool version)
(Table at the top of each report with customer domain, baseline/tool version)

Args:
browser: A Selenium WebDriver instance
domain: The user's domain
customerdomain: The customer domain
parent: boolean to determine parent/individual reports
"""
tenant_table_rows = (
Expand All @@ -255,8 +255,8 @@ def verify_tenant_table(browser, domain, parent):
.find_elements(By.TAG_NAME, "tr")
)
assert len(tenant_table_rows) == 2
customer_domain = tenant_table_rows[1].find_elements(By.TAG_NAME, "td")[0].text
assert customer_domain == domain
domain = tenant_table_rows[1].find_elements(By.TAG_NAME, "td")[0].text
assert domain == customerdomain

if not parent:
# Check for correct tool version, e.g. 0.2.0
Expand Down
Loading