This document outlines the comprehensive integration testing strategy for the Google Workspace Access CLI (gwsa).
The integration tests validate the end-to-end functionality of the gwsa CLI tool by executing real operations against the Gmail API. These tests are designed to:
- Verify all CLI commands work correctly (search, read, label)
- Confirm JSON output parsing and correctness
- Validate Gmail API interactions
- Ensure proper error handling
- Test the complete user workflow
pytest is introduced as a development-only dependency and should be added to pyproject.toml:
[project.optional-dependencies]
dev = [
"pytest>=7.0.0",
]Install with: pip install -e ".[dev]"
/gworkspace-access/
├── tests/
│ ├── __init__.py
│ ├── conftest.py # Pytest configuration and fixtures
│ └── integration/
│ ├── __init__.py
│ ├── test_mail_search.py # Search functionality tests
│ ├── test_mail_modify.py # Label and archive tests
│ └── test_mail_read.py # Message reading tests
The tests/test-config.yaml file maps gwsa profile names to test-specific settings. This allows different email accounts to use different search queries for finding test emails.
File Location: tests/test-config.yaml
Structure:
profiles:
# Profile name must match a gwsa profile name
default:
search_query: 'subject:"Your Daily Digest" from:"USPS Informed Delivery"'
test_label: "Test"
min_results: 2
days_range: 60
adc:
search_query: 'subject:"Daily Newsletter"'
test_label: "Test"
min_results: 3
days_range: 7Settings:
search_query: Gmail search query for finding routine, low-risk test emailstest_label: Label name used for add/remove tests (default: "Test")min_results: Minimum emails expected from search (default: 2)days_range: Number of days back to search (default: 60)
How It Works:
conftest.pyreads the active gwsa profile viagwsa profiles list- Looks up test settings for that profile in
test-config.yaml - If found, integration tests run with those settings
- If not found, tests requiring specific emails are skipped with helpful instructions
Adding a New Profile:
Add a section under profiles: with your gwsa profile name as the key:
profiles:
my_profile:
search_query: 'from:sender@example.com'
test_label: "Test"
min_results: 2
days_range: 30The tests/conftest.py file serves as the pytest configuration and setup/teardown for all integration tests.
-
Profile Status Validation
- Checks that gwsa has a valid, configured profile
- Verifies credentials are valid and not expired
- Exits with clear error message if profile not ready
-
Test Config Loading
- Loads
tests/test-config.yaml - Looks up settings for the active gwsa profile
- Skips tests gracefully if profile not in config
- Loads
-
CLI Installation Verification
- Validates that
gwsais installed and available - Executes
python -m gwsa.cli --helpto verify the current codebase is being tested - Exits with code 1 if the tool is not properly installed
- Validates that
-
Session-Level Fixtures
cli_runner: Fixture that invokes CLI commands via subprocesssearch_query: Search query from test config for active profiletest_label: Label name from test configtest_email_id: Message ID of a test email (found via search_query)today_minus_n_days: Date string based on profile's days_range
- The conftest uses subprocess to invoke the CLI (via
python -m gwsa.cli) rather than importing the module directly, ensuring the test environment mimics real user usage - All CLI commands output JSON, which is parsed using Python's
jsonmodule for assertions - If
gwsa setuphas not been run, tests will fail with a clear message about missing credentials - If the active profile is not in test-config.yaml, email-related tests are skipped with instructions
Purpose: Validate email search functionality and confirm test data availability.
Description: Searches for USPS Informed Delivery digest emails from the past 60 days.
Search Criteria:
subject:"Your Daily Digest" from:"USPS Informed Delivery" after:YYYY-MM-DD
Where YYYY-MM-DD is calculated as today minus 60 days (from the today_minus_60_days fixture).
Execution:
gwsa mail search 'subject:"Your Daily Digest" from:"USPS Informed Delivery" after:2025-09-29'Expected Behavior:
- Command executes successfully (exit code 0)
- Output is valid JSON
- Response is a list of message objects
Assertions:
- Result set contains between 2 and 20 messages (validates test data availability without being brittle)
- Each message object contains required fields:
id(string)subject(string, contains "Your Daily Digest")sender(string, contains "USPS Informed Delivery")date(string)
- Messages are returned in descending date order (most recent first)
Test Data Output:
- Extracts and stores the first message's ID (
test_email_id) for use by other test modules - This ID is passed to
test_mail_modify.pyandtest_mail_read.pyvia a conftest fixture
Rationale:
- Tests against real emails from a common, reliable source (USPS)
- 60-day window ensures sufficient test data across different scenarios
- 2-20 message range is realistic and avoids brittle exact counts
- Confirms search query parsing, API interaction, and JSON serialization all work
Purpose: Validate label modification and archive operations.
Setup:
- Uses
test_email_idfixture (populated bytest_mail_search.py) to identify a target USPS email - Additional search filter:
-label:Testensures the email doesn't already have the test label
Description: Applies the "Test" label to a USPS email and verifies the change.
Pre-Conditions:
- A USPS email exists matching the search criteria
- Email does NOT currently have the "Test" label
Execution Steps:
-
Search for target email:
gwsa mail search 'subject:"Your Daily Digest" from:"USPS Informed Delivery" after:2025-09-29 -label:Test'- Confirms at least one email exists without the label
-
Read email details (before):
gwsa mail read {message_id}- Parse JSON response
- Assert
labelIdsarray does NOT contain "Test" label ID
-
Apply label:
gwsa mail label {message_id} Test- Command executes successfully
- Output is valid JSON
-
Read email details (after):
gwsa mail read {message_id}- Parse JSON response
- Assert
labelIdsarray now CONTAINS "Test" label ID - Verify all other fields unchanged
Assertions:
- Label absent before operation
- Label present after operation
- No other message properties changed
- All JSON fields validated
Rationale:
- Validates complete label addition workflow
- Tests both label application and verification
- Ensures API properly updates message state
- Confirms JSON serialization of label data
Description:
Removes the "Test" label from the same email used in test_mail_label_apply.
Dependencies:
- Must run AFTER
test_mail_label_apply(same message ID) - Requires the label to exist on the message (result of previous test)
Execution Steps:
-
Search for target email:
gwsa mail search 'subject:"Your Daily Digest" from:"USPS Informed Delivery" after:2025-09-29 label:Test'- Confirms email currently has the "Test" label
-
Read email details (before):
gwsa mail read {message_id}- Parse JSON response
- Assert
labelIdsarray CONTAINS "Test" label ID
-
Remove label:
gwsa mail label {message_id} Test --remove- Command executes successfully
- Output is valid JSON
-
Read email details (after):
gwsa mail read {message_id}- Parse JSON response
- Assert
labelIdsarray does NOT contain "Test" label ID
Assertions:
- Label present before operation
- Label absent after operation
- No other message properties changed
- All JSON fields validated
Test Ordering:
- pytest is configured to run
test_mail_label_applybeforetest_mail_label_remove - Both tests use the same
message_idfrom conftest - Second test reverses the effects of the first (maintaining clean state)
Rationale:
- Validates label removal workflow
- Tests the
--removeflag - Confirms reversibility of label operations
- Leaves test email in original state (no label)
Description: Archives a USPS email by removing it from the inbox.
Execution Steps:
-
Search for unarchived target email:
gwsa mail search 'subject:"Your Daily Digest" from:"USPS Informed Delivery" after:2025-09-29 -label:Archive'- Identifies an email not yet archived
-
Read email details (before):
gwsa mail read {message_id}- Parse JSON response
- Assert
labelIdsdoes NOT contain Archive label or INBOX label
-
Apply Archive label:
gwsa mail label {message_id} Archive- (or alternatively, remove INBOX label depending on Gmail label semantics)
-
Read email details (after):
gwsa mail read {message_id}- Parse JSON response
- Assert
labelIdscontains Archive label or no longer contains INBOX
Assertions:
- Email not archived before operation
- Email archived after operation
- All other fields unchanged
Rationale:
- Tests a common user workflow (archiving emails)
- Validates label-based archival mechanism
- Ensures bulk operations can be built on this foundation
Purpose: Validate email reading and JSON response structure.
Description: Reads a USPS email and validates all expected fields are present in the JSON response.
Execution:
gwsa mail read {message_id}Where {message_id} is from the test_email_id fixture (same email used in other tests).
Expected JSON Structure:
{
"id": "string",
"subject": "string (contains 'Your Daily Digest')",
"sender": "string (contains 'USPS Informed Delivery')",
"date": "string (RFC 2822 format)",
"snippet": "string (first ~100 chars of body)",
"body": "string (full plain text body)",
"labelIds": ["array", "of", "label", "IDs"],
"raw": "string (full JSON representation of Gmail API message object)"
}Assertions:
-
All required fields present:
idis a non-empty stringsubjectis a non-empty stringsenderis a non-empty stringdateis a non-empty stringsnippetis a string (may be empty)bodyis a string (non-empty for USPS digests)labelIdsis an array (may be empty)rawis a non-empty string (valid JSON)
-
Field values are appropriate:
subjectcontains expected text ("Your Daily Digest")sendercontains expected source ("USPS Informed Delivery")dateis parseable as RFC 2822 formatlabelIdsis a valid array of Gmail label IDsrawcan be parsed as JSON
-
Body content is populated:
- USPS Informed Delivery emails should have substantial body text
- Confirms that plain text extraction works correctly
-
No extraneous fields:
- Response contains exactly the expected fields (no extra undocumented fields)
Rationale:
- Validates complete message retrieval
- Tests text extraction from complex MIME structures
- Confirms label ID population
- Ensures JSON serialization of all data types is correct
- Provides contract for downstream tools consuming the JSON output
Before running tests, ensure:
-
gwsa setuphas been completed:gwsa setup
This ensures
user_token.jsonandcredentials.jsonare in place. -
Test dependencies are installed:
pip install -e ".[dev]" -
Python 3.9+ is available:
python3 --version
Run all integration tests:
pytest tests/integration/ -vRun specific test module:
pytest tests/integration/test_mail_search.py -vRun specific test case:
pytest tests/integration/test_mail_modify.py::test_mail_label_apply -vRun with debug logging:
LOG_LEVEL=DEBUG pytest tests/integration/ -vSuccessful test run:
tests/integration/test_mail_search.py::test_mail_search_usps_digests PASSED
tests/integration/test_mail_modify.py::test_mail_label_apply PASSED
tests/integration/test_mail_modify.py::test_mail_label_remove PASSED
tests/integration/test_mail_modify.py::test_mail_archive PASSED
tests/integration/test_mail_read.py::test_mail_read_full_details PASSED
====== 5 passed in 2.45s ======
All tests invoke the CLI via subprocess using the following pattern:
result = subprocess.run(
["python", "-m", "gwsa_cli", "mail", "search", query_string],
capture_output=True,
text=True,
cwd=project_root
)Rationale:
- Uses
python -m gwsa_clirather than the installedgwsacommand - Ensures tests run against the current codebase, not a previously installed version
- Mimics real user execution environment
- Requires
gwsa_clito be importable from the current directory
All CLI output is parsed as JSON:
response = json.loads(result.stdout)Assertions:
- Output is valid JSON
- Response structure matches expected schema
- Field types are correct
- Scope: Session
- Returns: String in format
YYYY-MM-DD - Calculation:
datetime.now() - timedelta(days=60)
- Scope: Session
- Returns: Callable function
- Function signature:
cli_runner(command_args: List[str]) -> Dict[str, Any] - Return structure:
{ "returncode": int, "stdout": str, "stderr": str, "json": dict or list (if stdout is valid JSON) }
- Scope: Session
- Returns: String (Gmail message ID)
- Population: Extracted from
test_mail_search_usps_digests()results - Usage: Shared across
test_mail_modify.pyandtest_mail_read.py - Storage: Stored in session cache, not persisted to disk
@pytest.mark.integration
@pytest.mark.order(1)
def test_mail_search_usps_digests():
"""Runs first to populate test_email_id fixture."""
pass
@pytest.mark.integration
@pytest.mark.order(2)
def test_mail_label_apply():
"""Depends on test_email_id from test_mail_search."""
passThe pytest-ordering plugin (optional) ensures test execution order.
Tests should gracefully handle:
-
CLI not installed:
- Caught by conftest.py session setup
- Error message: "gwsa CLI not found. Run
pip install -e .first." - Exit code: 1
-
Credentials not configured:
- Occurs when
gwsa setuphasn't been run - CLI exits with error message about missing
user_token.json - Test assertion:
returncode != 0
- Occurs when
-
No test data available:
test_mail_search_usps_digestsreturns < 2 results- Test fails with: "Insufficient test data: expected 2-20 USPS emails, found N"
- Guidance: User should have USPS emails in their Gmail
-
Gmail API quota exceeded:
- Rare but possible with repeated test runs
- Tests will fail with API rate limit error
- Mitigation: Space out test runs or use a dedicated Gmail test account
All JSON assertions use defensive parsing:
try:
data = json.loads(output)
except json.JSONDecodeError as e:
pytest.fail(f"CLI output is not valid JSON: {e}")
assert isinstance(data, list), "Expected response to be a list"
assert len(data) >= 2, f"Expected >= 2 results, got {len(data)}"For future GitHub Actions or other CI systems:
-
Test Account Setup:
- Use a dedicated Gmail test account
- Pre-populate with USPS Informed Delivery emails
- Set
WORKSPACE_ACCESS_PROJECTvia environment variable
-
Credentials Management:
- Store
credentials.jsonanduser_token.jsonas GitHub secrets - Write files during CI job setup
- Ensure sensitive files are not committed
- Store
-
Test Isolation:
- Each test run should start with a clean label state
test_mail_label_removeensures labels are cleaned up aftertest_mail_label_apply- No permanent modifications to test emails (all changes are reversed)
-
Timeout Considerations:
- Gmail API calls may take 1-3 seconds each
- Allow 30-60 second timeout for full test suite
- Configure pytest timeout:
pytest --timeout=60
name: Integration Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: '3.11'
- name: Install dependencies
run: pip install -e ".[dev]"
- name: Setup credentials
run: |
echo "${{ secrets.CREDENTIALS_JSON }}" > credentials.json
echo "${{ secrets.USER_TOKEN_JSON }}" > user_token.json
- name: Run integration tests
run: pytest tests/integration/ -v --timeout=60
- name: Cleanup
run: rm credentials.json user_token.json
if: always()-
Batch Operations:
- Test labeling multiple emails at once
- Test searching with complex query combinations
-
Error Scenarios:
- Search with invalid query syntax
- Read non-existent message ID
- Label with invalid label name
-
Performance Tests:
- Large search result sets (1000+ emails)
- Memory usage validation
- API response time tracking
-
Label Management:
- Create custom labels via CLI
- Verify label case sensitivity
- Test special characters in label names
-
Edge Cases:
- Emails with no body content
- Emails with non-UTF8 encoding
- Extremely long subject lines
- Multiple labels on single email
Solution: Run pip install -e . in the project root directory.
Solution: Run gwsa setup to authenticate and generate credentials.
Solution: Ensure you have USPS Informed Delivery emails in your Gmail account. If not, tests will need to be adjusted or skipped.
Solution: Check that the CLI is installed from the current code (not an old version). Run python -m gwsa_cli --help to verify.
Solution: Message ID may have been deleted or is invalid. Run test_mail_search.py again to get a fresh message ID.
- When CLI command syntax changes → Update command strings in tests
- When Gmail API scope requirements change → Update test setup instructions
- When JSON response schema changes → Update assertions
- When label behavior changes → Update label tests
- Avoid hard-coded message IDs (use dynamic search)
- Don't assume specific label names exist (create them if needed)
- Use flexible assertion ranges (2-20 messages instead of exactly 5)
- Clean up test modifications (remove labels after apply/remove tests)
- Use descriptive assertion messages for debugging
The integration test suite validates the complete gwsa CLI workflow using real Gmail data and operations. Tests are organized into three modules:
- test_mail_search.py: Validates search functionality and provides test data
- test_mail_modify.py: Validates label modification and archive operations
- test_mail_read.py: Validates message reading and JSON response structure
All tests use subprocess to invoke the CLI, ensuring real-world usage patterns are tested. Tests parse JSON output and validate response structure comprehensively.
Before running tests, ensure gwsa setup has been completed and development dependencies are installed with pip install -e ".[dev]".