diff --git a/.gitignore b/.gitignore index b863bfb62..a8519f0cb 100644 --- a/.gitignore +++ b/.gitignore @@ -46,5 +46,7 @@ sandbox/specification/* /sandbox/specification/* /integration-test-results.xml /specification/tmp/* -/tests/e2e/data/out* +/tests/e2e/data/out/* /tests/e2e/reports/* +.vscode/launch.json +.vscode/mcp.json diff --git a/tests/e2e/HANDOVER.md b/tests/e2e/HANDOVER.md new file mode 100644 index 000000000..91a3ead57 --- /dev/null +++ b/tests/e2e/HANDOVER.md @@ -0,0 +1,443 @@ +# E2E Test Automation Framework - Technical Handover Documentation + +**Document Version**: 1.0 +**Last Updated**: January 2025 +**Framework Version**: 1.0 +**Audience**: QA Engineers, DevOps Engineers, Development Team Leads + +--- + +## ๐Ÿ“‹ Executive Summary + +This document provides technical handover information for the NHS Eligibility Signposting API E2E Test Automation Framework. The framework is production-ready and has been successfully validating API functionality with comprehensive AWS integration. + +### Framework Capabilities + +- โœ… **Automated BDD Testing** with Behave framework +- โœ… **AWS Cloud Integration** (DynamoDB, S3, SSM) +- โœ… **mTLS Authentication** with automatic certificate management +- โœ… **Dynamic Test Data** generation and cleanup +- โœ… **CI/CD Ready** with Jenkins and GitHub Actions support + +### Current Test Coverage + +- **14 comprehensive scenarios** covering RSV vaccination eligibility +- **API validation** with schema verification +- **End-to-end workflow** from data setup to cleanup +- **Multi-environment support** (test, staging configurations) + +--- + +## ๐Ÿ—๏ธ Technical Architecture + +### Framework Components + +```mermaid +graph TB + subgraph "E2E Framework" + A[Behave BDD Runner] --> B[Feature Files] + A --> C[Step Definitions] + A --> D[Environment Hooks] + end + + subgraph "AWS Integration" + E[DynamoDB] --> F[Test Data Storage] + G[S3] --> H[Config Management] + I[SSM] --> J[Certificate Storage] + end + + subgraph "API Testing" + K[mTLS Client] --> L[NHS API Gateway] + L --> M[Eligibility Service] + end + + C --> E + C --> G + D --> I + C --> K +``` + +### Key Technologies + +| Component | Technology | Version | Purpose | +| --------------- | ----------- | ------- | ----------------------- | +| BDD Framework | Behave | Latest | Test scenario execution | +| HTTP Client | Requests | Latest | API communication | +| AWS SDK | Boto3 | Latest | AWS service integration | +| Data Processing | Python JSON | 3.13+ | Test data manipulation | +| Environment | Poetry | Latest | Dependency management | + +### Data Flow Architecture + +```mermaid +sequenceDiagram + participant TR as Test Runner + participant AWS as AWS Services + participant API as NHS API Gateway + participant DB as DynamoDB + + Note over TR,DB: Test Setup Phase + TR->>AWS: Download mTLS certificates (SSM) + TR->>AWS: Generate & upload test data (DynamoDB) + TR->>AWS: Upload configurations (S3) + + Note over TR,DB: Test Execution Phase + TR->>API: Execute eligibility check (mTLS) + API->>DB: Query eligibility data + DB-->>API: Return eligibility results + API-->>TR: Return API response + TR->>TR: Validate response against expected + + Note over TR,DB: Test Cleanup Phase + TR->>AWS: Cleanup test data (DynamoDB) + TR->>AWS: Remove temporary files (S3) +``` + +--- + +## ๐Ÿ”ง Implementation Details + +### Directory Structure and Responsibilities + +``` +tests/e2e/ +โ”œโ”€โ”€ features/ +โ”‚ โ”œโ”€โ”€ eligibility_check/ +โ”‚ โ”‚ โ””โ”€โ”€ eligibility_check.feature # ๐Ÿ“ Gherkin scenarios (14 test cases) +โ”‚ โ”œโ”€โ”€ steps/ +โ”‚ โ”‚ โ”œโ”€โ”€ eligibility_check_steps.py # ๐Ÿ Step implementations +โ”‚ โ”‚ โ””โ”€โ”€ helpers/ +โ”‚ โ”‚ โ”œโ”€โ”€ dynamodb_data_generator.py # ๐Ÿ”„ Data generation with date variables +โ”‚ โ”‚ โ””โ”€โ”€ dynamodb_data_uploader.py # โฌ†๏ธ AWS DynamoDB operations +โ”‚ โ””โ”€โ”€ environment.py # ๐ŸŒ Behave hooks and AWS setup +โ”œโ”€โ”€ data/ +โ”‚ โ”œโ”€โ”€ in/dynamoDB/ # ๐Ÿ“„ Test input data (14 JSON files) +โ”‚ โ”œโ”€โ”€ responses/ # โœ… Expected API responses (14 files) +โ”‚ โ”œโ”€โ”€ s3/ # โš™๏ธ Campaign configurations +โ”‚ โ””โ”€โ”€ configs/ # ๐ŸŒ Global settings +โ”œโ”€โ”€ utils/ +โ”‚ โ””โ”€โ”€ config.py # ๐Ÿ“‹ Environment config & JSON schemas +โ””โ”€โ”€ tests/ # ๐Ÿงช Legacy pytest tests (maintained) +``` + +### Core Implementation Files + +#### 1. Test Scenario Definition + +**File**: [`features/eligibility_check/eligibility_check.feature`](features/eligibility_check/eligibility_check.feature) + +- **Purpose**: Gherkin BDD scenarios for API testing +- **Coverage**: 14 NHS numbers with different eligibility scenarios +- **Format**: Scenario Outline with Examples table +- **Maintainer**: QA Team + +#### 2. Step Implementation + +**File**: [`features/steps/eligibility_check_steps.py`](features/steps/eligibility_check_steps.py) + +- **Purpose**: Python implementation of Gherkin steps +- **Key Functions**: + - mTLS certificate download from SSM + - DynamoDB test data management + - API request execution with authentication + - JSON response validation with diff reporting +- **Dependencies**: boto3, requests, jsonschema +- **Maintainer**: QA/DevOps Team + +#### 3. Data Management + +**Files**: + +- [`helpers/dynamodb_data_generator.py`](features/steps/helpers/dynamodb_data_generator.py) +- [`helpers/dynamodb_data_uploader.py`](features/steps/helpers/dynamodb_data_uploader.py) + +**Capabilities**: + +- **Dynamic Date Variables**: `<>`, `<>` +- **GUID Generation**: `<>` +- **Batch Upload/Delete**: Efficient DynamoDB operations +- **Error Handling**: Graceful failure with detailed logging + +#### 4. Environment Configuration + +**File**: [`features/environment.py`](features/environment.py) + +- **Purpose**: Behave lifecycle hooks +- **Key Functions**: + - AWS credential validation + - S3 file uploads (optional) + - Feature-level data cleanup + - Error handling and graceful failures + +--- + +## ๐Ÿš€ Deployment and Maintenance + +### Infrastructure Requirements + +#### AWS Resources + +| Service | Resource | Purpose | Configuration | +| ----------------------- | ----------------------- | ------------------- | ----------------- | +| **DynamoDB** | `eligibilty_data_store` | Test data storage | On-demand billing | +| **S3** | User-defined bucket | Configuration files | Standard storage | +| **SSM Parameter Store** | `/test/mtls/*` | mTLS certificates | SecureString type | + +#### Required IAM Permissions + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "dynamodb:PutItem", + "dynamodb:DeleteItem", + "dynamodb:Scan", + "dynamodb:BatchWriteItem" + ], + "Resource": "arn:aws:dynamodb:eu-west-2:*:table/eligibilty_data_store" + }, + { + "Effect": "Allow", + "Action": ["ssm:GetParameter"], + "Resource": "arn:aws:ssm:eu-west-2:*:parameter/test/mtls/*" + }, + { + "Effect": "Allow", + "Action": ["s3:PutObject", "s3:DeleteObject"], + "Resource": "arn:aws:s3:::your-bucket/*" + } + ] +} +``` + +### Environment Management + +#### Development Environment + +```bash +# Local development setup +cd tests/e2e +poetry install +cp .env.example .env +# Configure .env with development AWS credentials +poetry run behave +``` + +#### CI/CD Environment + +- **GitHub Actions**: [Workflow example in README](README.md#github-actions-example) +- **Jenkins**: [Pipeline example in README](README.md#jenkins-pipeline-example) +- **Secrets Management**: AWS credentials via CI/CD secrets + +### Monitoring and Logging + +#### Application Logs + +- **Framework logs**: Console output with timestamps +- **AWS operation logs**: Detailed boto3 operation results +- **Test execution logs**: Behave standard output +- **Debug mode**: `--verbose --no-capture` for detailed output + +#### Key Metrics to Monitor + +- **Test execution time**: Average ~2-3 minutes for full suite +- **AWS API call success rate**: Should be >99% +- **Data cleanup success rate**: Should be 100% +- **Certificate refresh frequency**: Daily recommended + +--- + +## ๐Ÿ”„ Maintenance Procedures + +### Regular Maintenance Tasks + +#### Weekly Tasks + +- [ ] **Validate test data**: Ensure all 14 test scenarios pass +- [ ] **Check AWS credentials**: Verify access keys haven't expired +- [ ] **Review logs**: Check for any recurring warnings or errors + +#### Monthly Tasks + +- [ ] **Update dependencies**: `poetry update` and test +- [ ] **Review test coverage**: Add scenarios for new API features +- [ ] **Validate environments**: Test against staging and production configs +- [ ] **Certificate rotation**: Verify mTLS certificates are current + +#### Quarterly Tasks + +- [ ] **Performance review**: Analyze execution times and optimize +- [ ] **Documentation updates**: Keep README and handover docs current +- [ ] **Framework upgrades**: Update Behave, boto3, and other dependencies +- [ ] **Security audit**: Review AWS permissions and access patterns + +### Adding New Test Scenarios + +#### Process for New NHS Numbers + +1. **Create test data file**: Copy and modify existing DynamoDB JSON +2. **Create expected response**: Define expected API response JSON +3. **Update feature file**: Add new row to Examples table +4. **Test locally**: Validate new scenario works +5. **Update documentation**: Record new test case in handover docs + +#### Process for New API Endpoints + +1. **Create new feature file**: Follow existing Gherkin patterns +2. **Implement step definitions**: Reuse existing helpers where possible +3. **Add configuration**: Update [`utils/config.py`](utils/config.py) with new schemas +4. **Create test data**: Generate appropriate DynamoDB and response files +5. **Test end-to-end**: Validate full workflow including cleanup + +### Troubleshooting Common Issues + +#### AWS Authentication Problems + +**Symptoms**: `NoCredentialsError`, `AccessDenied` +**Solutions**: + +1. Verify AWS credentials in `.env` file +2. Check IAM permissions match requirements above +3. Validate session token hasn't expired +4. Test AWS access: `aws sts get-caller-identity` + +#### DynamoDB Operation Failures + +**Symptoms**: `ResourceNotFoundException`, upload/delete errors +**Solutions**: + +1. Confirm table name matches `DYNAMODB_TABLE_NAME` setting +2. Verify table exists in correct AWS region +3. Check table has sufficient capacity (on-demand recommended) +4. Validate JSON test data format + +#### mTLS Certificate Issues + +**Symptoms**: SSL errors, authentication failures +**Solutions**: + +1. Verify SSM parameters exist: `aws ssm get-parameter --name "/test/mtls/api_private_key_cert"` +2. Check certificate expiration dates +3. Validate certificate format (PEM) +4. Ensure proper AWS permissions for SSM access + +#### Test Data Cleanup Problems + +**Symptoms**: Leftover data in DynamoDB, failing subsequent tests +**Solutions**: + +1. Check `KEEP_SEED` setting (should be `false` for CI/CD) +2. Manual cleanup: Review and delete items from DynamoDB console +3. Verify cleanup permissions in IAM policy +4. Run tests with `--verbose` to see cleanup operations + +--- + +## ๐Ÿ“ˆ Performance and Scalability + +### Current Performance Metrics + +- **Full test suite execution**: ~2-3 minutes +- **Single scenario execution**: ~10-15 seconds +- **AWS data upload time**: ~5-10 seconds for 14 records +- **mTLS handshake time**: ~1-2 seconds per request + +### Scaling Considerations + +- **Parallel execution**: Behave supports parallel test runs +- **Data isolation**: Each test scenario uses unique NHS numbers +- **AWS limits**: DynamoDB has 25 request units per second default +- **Certificate caching**: mTLS certificates cached per test session + +### Optimization Opportunities + +1. **Implement data pooling**: Reuse DynamoDB records across tests +2. **Parallel test execution**: Split scenarios across multiple runners +3. **Certificate persistence**: Cache certificates between test runs +4. **Response caching**: Cache API responses for repeated validation + +--- + +## ๐Ÿ” Security Considerations + +### Security Best Practices + +- **Credential Management**: Use AWS IAM roles where possible +- **Certificate Storage**: mTLS certificates stored securely in SSM +- **Test Data**: Contains synthetic NHS numbers only +- **Network Security**: API calls use mTLS encryption +- **Access Control**: Least privilege IAM permissions + +### Compliance Notes + +- **Data Protection**: Test data does not contain real patient information +- **Audit Trail**: All AWS operations logged via CloudTrail +- **Access Logging**: Test execution logged for compliance review +- **Certificate Rotation**: mTLS certificates should be rotated regularly + +--- + +## ๐Ÿ“ž Support and Contacts + +### Team Responsibilities + +| Role | Team/Person | Responsibility | +| ------------------------- | ---------------- | ----------------------------- | +| **Framework Maintenance** | QA Team | Test scenarios, documentation | +| **AWS Infrastructure** | DevOps Team | AWS resources, certificates | +| **API Development** | Development Team | API changes, new endpoints | +| **CI/CD Pipeline** | DevOps Team | Build pipelines, deployment | + +### Escalation Process + +1. **Level 1**: Check README troubleshooting section +2. **Level 2**: Review framework logs and AWS console +3. **Level 3**: Contact QA team for framework issues +4. **Level 4**: Contact DevOps team for infrastructure issues +5. **Level 5**: Contact development team for API-related issues + +## ๐Ÿ“š Reference Documentation + +### Internal Documentation + +- **[Main README](README.md)**: User guide and quick start +- **[API Specification](../../../specification/)**: Eligibility API details +- **[Project README](../../../README.md)**: Overall project documentation + +### External Documentation + +- **[Behave Documentation](https://behave.readthedocs.io/)**: BDD framework guide +- **[Boto3 Documentation](https://boto3.amazonaws.com/v1/documentation/api/latest/index.html)**: AWS SDK reference +- **[NHS API Platform](https://digital.nhs.uk/developer/api-catalogue)**: NHS API guidelines +- **[AWS Testing Best Practices](https://aws.amazon.com/testing/)** + +### Training Resources + +- **BDD Testing**: [Cucumber School](https://school.cucumber.io/) +- **AWS Testing**: [AWS Testing Best Practices](https://aws.amazon.com/testing/) +- **Python Testing**: [Real Python Testing Guide](https://realpython.com/python-testing/) + +### Dependencies to Monitor + +- **Behave**: Framework updates and new features +- **Boto3**: AWS SDK improvements and new services +- **NHS API Platform**: Changes to authentication or standards +- **Python**: Language version updates and compatibility + +--- + +**Document Control** + +- **Created**: July 2025 +- **Last Review**: July 2025 +- **Next Review**: TBD +- **Version**: 1.0 +- **Classification**: Internal Use +- **Distribution**: NHS QA Team + +--- + +_This document contains technical handover information for the NHS Eligibility Signposting API E2E Test Automation Framework. For user guides and quick start information, see the [main README](README.md)._ diff --git a/tests/e2e/README.md b/tests/e2e/README.md index 3804c2863..74da684c2 100644 --- a/tests/e2e/README.md +++ b/tests/e2e/README.md @@ -1,292 +1,505 @@ -# Eligibility Signposting API Test Automation Framework +# Eligibility Signposting API - E2E Test Automation Framework -This repository contains a Python-based test automation framework for the Eligibility Signposting API. The framework uses pytest and requests to implement API tests that were previously executed manually using Postman. It also includes BDD-style tests using Behave (not pytest-bdd). +A comprehensive BDD-based test automation framework for the NHS Eligibility Signposting API using Behave, AWS integration, and mTLS authentication. -## Framework Structure +## ๐Ÿš€ Quick Start Guide (5 Minutes) + +### Prerequisites Check + +Before starting, ensure you have: + +- Python 3.13+ installed +- Poetry installed (`curl -sSL https://install.python-poetry.org | python3 -`) +- AWS CLI installed and configured +- Access to NHS test environment AWS account + +### 1. Environment Setup ```bash -qa-automation/ -โ”œโ”€โ”€ tests/ -โ”‚ โ””โ”€โ”€ eligibility_signposting/ -โ”‚ โ”œโ”€โ”€ test_eligibility_check.py # Tests for eligibility check endpoint -โ”‚ โ”œโ”€โ”€ test_eligibility_check_bdd.py # BDD tests for eligibility check -โ”‚ โ””โ”€โ”€ conftest.py # Pytest fixtures -โ”œโ”€โ”€ features/ -โ”‚ โ”œโ”€โ”€ eligibility_check/ -โ”‚ โ”‚ โ””โ”€โ”€ eligibility_check.feature # Behave feature file -โ”‚ โ”œโ”€โ”€ steps/ -โ”‚ โ”‚ โ””โ”€โ”€ eligibility_check_steps.py # Behave step definitions -โ”‚ โ””โ”€โ”€ conftest.py # Behave fixtures (if needed) -โ”œโ”€โ”€ utils/ -โ”‚ โ”œโ”€โ”€ api_client.py # Reusable HTTP client -โ”‚ โ””โ”€โ”€ config.py # Environment config and schemas -โ”œโ”€โ”€ .env # Environment variables (not in version control) -โ”œโ”€โ”€ pytest.ini # Pytest configuration -โ””โ”€โ”€ pyproject.toml # Poetry project file +# Clone and navigate to the project +cd tests/e2e + +# Install dependencies +poetry install + +# Copy environment template +cp .env.example .env + +# Edit .env with your credentials (see Environment Configuration section) ``` -## Setup and Installation +### 2. First Test Run -1. Clone the repository: +```bash +# Run all tests (includes automatic setup and cleanup) +cd tests/e2e +poetry run behave - ```bash - git clone https://github.com/ivma1-nhs/qa-automation.git - cd qa-automation - ``` +# Run a single scenario for quick validation +poetry run behave features/eligibility_check/eligibility_check.feature --name="Eligibility check returns 2xx response for NHS number queries" +``` -2. Create a virtual environment (optional but recommended): +### 3. Success Indicators - ```bash - python -m venv venv - source venv/bin/activate # On Windows: venv\Scripts\activate - ``` +โœ… **Tests pass**: You should see scenarios marked as "passed" +โœ… **AWS connectivity**: mTLS certificates downloaded from SSM +โœ… **Data setup**: DynamoDB test data uploaded successfully +โœ… **API responses**: 200 status codes with valid JSON responses -3. Install Poetry (if not already installed): +**If tests fail**, check the [Troubleshooting](#troubleshooting) section below. - ```bash - curl -sSL https://install.python-poetry.org | python3 - - # Or see https://python-poetry.org/docs/#installation for details - ``` +--- -4. Install dependencies: +## ๐Ÿ“‹ Framework Overview - ```bash - poetry install - ``` +This framework provides automated end-to-end testing for the NHS Eligibility Signposting API with the following capabilities: + +### Key Features + +- **BDD Testing**: Gherkin scenarios using Behave framework +- **AWS Integration**: Automated DynamoDB test data management and S3 configuration +- **mTLS Authentication**: Automatic certificate retrieval from AWS SSM Parameter Store +- **Dynamic Test Data**: Date-based variable resolution for realistic test scenarios +- **Automated Cleanup**: Configurable test data cleanup after execution + +### Framework Architecture + +``` +tests/e2e/ +โ”œโ”€โ”€ features/ # BDD test scenarios +โ”‚ โ”œโ”€โ”€ eligibility_check/ # Feature files +โ”‚ โ”‚ โ””โ”€โ”€ eligibility_check.feature +โ”‚ โ”œโ”€โ”€ steps/ # Step definitions +โ”‚ โ”‚ โ”œโ”€โ”€ eligibility_check_steps.py +โ”‚ โ”‚ โ””โ”€โ”€ helpers/ # Utility classes +โ”‚ โ”‚ โ”œโ”€โ”€ dynamodb_data_generator.py +โ”‚ โ”‚ โ””โ”€โ”€ dynamodb_data_uploader.py +โ”‚ โ””โ”€โ”€ environment.py # Behave environment hooks +โ”œโ”€โ”€ data/ # Test data files +โ”‚ โ”œโ”€โ”€ in/dynamoDB/ # DynamoDB test records +โ”‚ โ”œโ”€โ”€ responses/ # Expected API responses +โ”‚ โ”œโ”€โ”€ s3/ # S3 configuration files +โ”‚ โ””โ”€โ”€ configs/ # Global configurations +โ”œโ”€โ”€ utils/ # Framework utilities +โ”‚ โ””โ”€โ”€ config.py # Configuration and schemas +โ”œโ”€โ”€ tests/ # pytest-based tests (legacy) +โ”œโ”€โ”€ .env.example # Environment template +โ””โ”€โ”€ README.md # This file +``` + +### Test Data Flow + +```mermaid +graph LR + A[Test Start] --> B[Download mTLS Certs] + B --> C[Generate Test Data] + C --> D[Upload to DynamoDB] + D --> E[Upload Config to S3] + E --> F[Execute API Tests] + F --> G[Validate Responses] + G --> H[Cleanup Data] + H --> I[Test Complete] +``` + +--- + +## โš™๏ธ Environment Configuration + +### Required Environment Variables + +Copy [`tests/e2e/.env.example`](.env.example) to [`tests/e2e/.env`](.env) and configure: + +```bash +# AWS Configuration (REQUIRED) +AWS_REGION=eu-west-2 +AWS_ACCESS_KEY_ID=your_aws_access_key +AWS_SECRET_ACCESS_KEY=your_aws_secret_key +AWS_SESSION_TOKEN=your_aws_session_token + +# DynamoDB Configuration (REQUIRED) +DYNAMODB_TABLE_NAME=eligibilty_data_store + +# S3 Configuration (OPTIONAL) +S3_BUCKET_NAME=your-bucket-name +S3_UPLOAD_DIR=qa/json +S3_JSON_SOURCE_DIR=./data/s3 + +# API Configuration (AUTOMATIC) +BASE_URL=https://test.eligibility-signposting-api.nhs.uk +API_GATEWAY_URL=https://test.eligibility-signposting-api.nhs.uk + +# Test Configuration (OPTIONAL) +ABORT_ON_AWS_FAILURE=false +KEEP_SEED=false # Set to true to keep test data after tests +VALID_NHS_NUMBER=50000000004 +INVALID_NHS_NUMBER=9876543210 +``` + +### AWS Permissions Required + +Your AWS credentials need the following permissions: + +- **SSM**: `GetParameter` (for mTLS certificates) +- **DynamoDB**: `PutItem`, `DeleteItem`, `Scan` (for test data management) +- **S3**: `PutObject`, `DeleteObject` (optional, for configuration files) + +--- + +## ๐Ÿงช Test Execution + +### Running Tests -5. Configure environment variables: - - Copy the `.env.example` file to `.env` (if not already present) - - Update the values in `.env` with your sandbox credentials +**Run all tests:** -## Running Tests +```bash +poetry run behave +``` + +**Run specific feature:** -### Running API (pytest) tests +```bash +poetry run behave features/eligibility_check/eligibility_check.feature +``` -Run all pytest-based tests: +**Run with specific tags (if configured):** ```bash -poetry run pytest +poetry run behave --tags=@smoke ``` -Run a specific pytest test file: +**Debug mode with verbose output:** ```bash -poetry run pytest tests/eligibility_signposting/test_eligibility_check.py +poetry run behave --verbose --no-capture ``` -### Running BDD tests with Behave +### Test Data Management + +The framework automatically manages test data: + +1. **Data Generation**: [`dynamodb_data_generator.py`](features/steps/helpers/dynamodb_data_generator.py) processes JSON templates with date variables +2. **Data Upload**: [`dynamodb_data_uploader.py`](features/steps/helpers/dynamodb_data_uploader.py) handles DynamoDB operations +3. **Data Cleanup**: Automatic cleanup unless `KEEP_SEED=true` -Run all Behave feature tests: +**Manual data operations:** ```bash +# Keep test data after execution (useful for debugging) +export KEEP_SEED=true +poetry run behave + +# Force cleanup (if previous tests left data) +export KEEP_SEED=false poetry run behave ``` -This will discover and run all feature files in the `features/` directory using Behave. +### Available Test Scenarios -## Extending the Framework +Current test scenarios in [`eligibility_check.feature`](features/eligibility_check/eligibility_check.feature): -### Adding New Test Files +| NHS Number | Scenario | Expected Response | +| ---------- | ---------------------------- | ------------------------------------------------------------- | +| 5000000001 | Standard eligibility check | [`AUTO_RSV_SB_001.json`](data/responses/AUTO_RSV_SB_001.json) | +| 5000000002 | Alternative eligibility path | [`AUTO_RSV_SB_002.json`](data/responses/AUTO_RSV_SB_002.json) | +| ... | ... | ... | +| 5000000014 | Complex eligibility scenario | [`AUTO_RSV_SB_014.json`](data/responses/AUTO_RSV_SB_014.json) | -1. Create a new test file in the appropriate directory: +--- - ```python - # tests/eligibility_signposting/test_new_feature.py - import pytest - - @pytest.mark.new_feature - class TestNewFeature: - def test_something(self, api_client): - # Test implementation - pass - ``` +## ๐Ÿ”ง Extending the Framework -2. Add the new marker to pytest.ini if needed: +### Adding New Test Scenarios - ```ini - markers = - new_feature: marks tests related to the new feature - ``` +1. **Create test data files:** -### Adding New BDD Tests (Behave) + ```bash + # Add DynamoDB record + cp data/in/dynamoDB/AUTO_RSV_SB_001.json data/in/dynamoDB/AUTO_RSV_SB_015.json -1. Create a new feature file: + # Add expected response + cp data/responses/AUTO_RSV_SB_001.json data/responses/AUTO_RSV_SB_015.json - ```gherkin - # features/new_feature/new_feature.feature - Feature: New Feature - As a user of the Eligibility Signposting API - I want to use the new feature - So that I can achieve my goal - - Scenario: Successful use of new feature - Given the API is available - When I make a request to the new feature endpoint - Then the response should be successful + # Edit both files with new test data ``` -2. Create step definitions: +2. **Update feature file:** - ```python - # features/steps/new_feature_steps.py - from behave import given, when, then - - @when('I make a request to the new feature endpoint') - def step_impl_make_request(context): - # Implementation - pass - - @then('the response should be successful') - def step_impl_check_success(context): - # Implementation - pass + ```gherkin + # Add new row to Examples table in eligibility_check.feature + | 5000000015 | AUTO_RSV_SB_015.json | ``` -3. Run the BDD tests with: - +3. **Test your changes:** ```bash - poetry run behave + poetry run behave --name="5000000015" ``` -### Adding New API Endpoints +### Adding New Features -1. Update the config.py file with the new endpoint: +1. **Create new feature file:** - ```python - # utils/config.py - NEW_ENDPOINT = '/new-endpoint' + ```bash + mkdir -p features/new_feature + touch features/new_feature/new_feature.feature ``` -2. Add a new method to the ApiClient class: +2. **Create step definitions:** + + ```bash + touch features/steps/new_feature_steps.py + ``` +3. **Use existing helpers or create new ones:** ```python - # utils/api_client.py - def get_new_endpoint(self, param1, param2): - url = f"{self.base_url}/new-endpoint" - params = {"param1": param1, "param2": param2} - response = requests.get(url, headers=self.headers, params=params) - return response + from helpers.dynamodb_data_uploader import DynamoDBDataUploader + from helpers.dynamodb_data_generator import JsonTestDataProcessor ``` -### Adding New Response Schemas +### Dynamic Test Data Variables -1. Add the new schema to config.py: +The framework supports date-based variables in test data: - ```python - # utils/config.py - NEW_ENDPOINT_SCHEMA = { - "type": "object", - "properties": { - # Schema definition - } - } - ``` +```json +{ + "LAST_SUCCESSFUL_DATE": "<>", // 30 days ago + "NEXT_DUE_DATE": "<>", // 90 days from now + "BIRTH_DATE": "<>" // 75 years ago +} +``` -2. Use the schema in your tests: +Supported formats: - ```python - from utils.config import NEW_ENDPOINT_SCHEMA - import jsonschema +- `<>` - Add/subtract N days +- `<>` - Add/subtract N months +- `<>` - Add/subtract N years +- `<>` - Generate random GUID - def test_new_endpoint_schema(self, api_client): - response = api_client.get_new_endpoint("value1", "value2") - response_json = response.json() - jsonschema.validate(instance=response_json, schema=NEW_ENDPOINT_SCHEMA) - ``` +--- -### Adding DynamoDB Integration +## ๐Ÿ” mTLS Configuration -When the DynamoDB-backed API is ready, you can extend the framework by: +The framework automatically handles mTLS authentication: -1. Adding DynamoDB client configuration: +### Certificate Management - ```python - # utils/dynamo_client.py - import boto3 - from utils.config import AWS_REGION, AWS_ACCESS_KEY, AWS_SECRET_KEY - - class DynamoClient: - def __init__(self): - self.client = boto3.client( - 'dynamodb', - region_name=AWS_REGION, - aws_access_key_id=AWS_ACCESS_KEY, - aws_secret_access_key=AWS_SECRET_KEY - ) - - def get_item(self, table_name, key): - response = self.client.get_item( - TableName=table_name, - Key=key - ) - return response - ``` +- **Storage**: Certificates stored in AWS SSM Parameter Store +- **Retrieval**: Automatic download at test startup +- **Paths**: + - Private Key: `/test/mtls/api_private_key_cert` + - Client Cert: `/test/mtls/api_client_cert` + - CA Cert: `/test/mtls/api_ca_cert` -2. Adding fixtures in conftest.py: +### Certificate Setup (For DevOps) - ```python - # tests/eligibility_signposting/conftest.py - from utils.dynamo_client import DynamoClient +```bash +# Store certificates in SSM (DevOps task) +aws ssm put-parameter \ + --name "/test/mtls/api_private_key_cert" \ + --value file://private_key.pem \ + --type SecureString + +aws ssm put-parameter \ + --name "/test/mtls/api_client_cert" \ + --value file://client_cert.pem \ + --type SecureString + +aws ssm put-parameter \ + --name "/test/mtls/api_ca_cert" \ + --value file://ca_cert.pem \ + --type SecureString +``` - @pytest.fixture - def dynamo_client(): - return DynamoClient() - ``` +--- -3. Using the DynamoDB client in tests: +## ๐Ÿ› Troubleshooting - ```python - def test_with_dynamodb(self, api_client, dynamo_client): - # Test implementation using both API and DynamoDB - pass - ``` +### Common Issues and Solutions -## Best Practices - -1. **Test Independence**: Each test should be independent and not rely on the state from other tests. -2. **Fixtures**: Use fixtures for common setup and clean-up operations. -3. **Multiple Test Cases**: Use pytest's parameterize feature for testing multiple scenarios. -4. **Assertions**: Use descriptive assertions to make test failures clear. -5. **Documentation**: Document your tests with docstrings and comments. -6. **Environment Variables**: Use environment variables for sensitive information and configuration. - -## Continuous Integration - -This framework can be integrated with CI/CD pipelines: - -1. Add a GitHub Actions workflow: - - ```yaml - # .github/workflows/test.yml - name: API Tests - - on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - - jobs: - test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: '3.9' - - name: Install dependencies - run: | - poetry install - - name: Run tests - run: | - poetry run pytest --html=report.html - env: - BASE_URL: ${{ secrets.BASE_URL }} - API_KEY: ${{ secrets.API_KEY }} - - name: Upload test report - uses: actions/upload-artifact@v2 - with: - name: test-report - path: report.html - ``` +**โŒ AWS Authentication Failed** + +```bash +# Check AWS credentials +aws sts get-caller-identity + +# Ensure you have the correct session token +export AWS_SESSION_TOKEN=your_session_token +``` + +**โŒ DynamoDB Access Denied** + +```bash +# Verify table name and permissions +aws dynamodb describe-table --table-name eligibilty_data_store +``` + +**โŒ mTLS Certificate Download Failed** + +```bash +# Check SSM parameters exist +aws ssm get-parameter --name "/test/mtls/api_private_key_cert" +``` + +**โŒ API Connection Timeout** + +```bash +# Check network connectivity +curl -I https://test.eligibility-signposting-api.nhs.uk/health +``` + +**โŒ Test Data Upload Failed** + +- Verify [`DYNAMODB_TABLE_NAME`](.env) matches actual table +- Check your AWS permissions include `dynamodb:PutItem` +- Ensure JSON files in [`data/in/dynamoDB/`](data/in/dynamoDB/) are valid + +**โŒ JSON Response Validation Failed** + +- Check expected response files in [`data/responses/`](data/responses/) +- Verify NHS numbers match between test data and feature file +- Use `--no-capture` flag to see detailed diff output + +### Debug Mode + +```bash +# Run with maximum verbosity +poetry run behave --verbose --no-capture --no-capture-stderr + +# Run single scenario with debug +poetry run behave features/eligibility_check/eligibility_check.feature:19 --verbose +``` + +### Logging + +Framework logs are written to console. Key log messages: + +- `โœ… AWS credentials loaded` +- `โœ… mTLS certificates downloaded` +- `โœ… DynamoDB data uploaded: X items` +- `โœ… Test scenario passed` + +--- + +## ๐Ÿ”„ CI/CD Integration + +### GitHub Actions Example + +```yaml +name: E2E Tests +on: [push, pull_request] + +jobs: + e2e-tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.13" + + - name: Install Poetry + uses: snok/install-poetry@v1 + + - name: Install dependencies + run: | + cd tests/e2e + poetry install + + - name: Run E2E tests + env: + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_SESSION_TOKEN: ${{ secrets.AWS_SESSION_TOKEN }} + AWS_REGION: eu-west-2 + DYNAMODB_TABLE_NAME: eligibilty_data_store + run: | + cd tests/e2e + poetry run behave --junit --junit-directory reports/ + + - name: Upload test results + uses: actions/upload-artifact@v3 + if: always() + with: + name: test-results + path: tests/e2e/reports/ +``` + +--- + +## ๐Ÿ“š Additional Resources + +### Framework Dependencies + +- **[Behave](https://behave.readthedocs.io/)**: BDD framework for Python +- **[Boto3](https://boto3.amazonaws.com/v1/documentation/api/latest/index.html)**: AWS SDK for Python +- **[Requests](https://docs.python-requests.org/)**: HTTP library for API calls +- **[Python-dotenv](https://pypi.org/project/python-dotenv/)**: Environment variable management + +### Related Documentation + +- [NHS API Platform Documentation](https://digital.nhs.uk/developer/api-catalogue) +- [Eligibility Signposting API Specification](../../../specification/) +- [Main Project README](../../../README.md) + +### File Structure Reference + +``` +tests/e2e/ +โ”œโ”€โ”€ ๐Ÿ“ features/ +โ”‚ โ”œโ”€โ”€ ๐Ÿ“ eligibility_check/ +โ”‚ โ”‚ โ””โ”€โ”€ ๐Ÿ“„ eligibility_check.feature # BDD scenarios +โ”‚ โ”œโ”€โ”€ ๐Ÿ“ steps/ +โ”‚ โ”‚ โ”œโ”€โ”€ ๐Ÿ“„ eligibility_check_steps.py # Step implementations +โ”‚ โ”‚ โ””โ”€โ”€ ๐Ÿ“ helpers/ +โ”‚ โ”‚ โ”œโ”€โ”€ ๐Ÿ“„ dynamodb_data_generator.py # Test data processing +โ”‚ โ”‚ โ””โ”€โ”€ ๐Ÿ“„ dynamodb_data_uploader.py # AWS data operations +โ”‚ โ”œโ”€โ”€ ๐Ÿ“„ environment.py # Behave hooks and setup +โ”‚ โ””โ”€โ”€ ๐Ÿ“„ __init__.py +โ”œโ”€โ”€ ๐Ÿ“ data/ +โ”‚ โ”œโ”€โ”€ ๐Ÿ“ in/dynamoDB/ # DynamoDB test records +โ”‚ โ”‚ โ”œโ”€โ”€ ๐Ÿ“„ AUTO_RSV_SB_001.json +โ”‚ โ”‚ โ””โ”€โ”€ ๐Ÿ“„ ... (014 test files) +โ”‚ โ”œโ”€โ”€ ๐Ÿ“ responses/ # Expected API responses +โ”‚ โ”‚ โ”œโ”€โ”€ ๐Ÿ“„ AUTO_RSV_SB_001.json +โ”‚ โ”‚ โ””โ”€โ”€ ๐Ÿ“„ ... (014 response files) +โ”‚ โ”œโ”€โ”€ ๐Ÿ“ s3/ # S3 configuration files +โ”‚ โ”‚ โ””โ”€โ”€ ๐Ÿ“„ AUTO_RSV_SB_001.json +โ”‚ โ””โ”€โ”€ ๐Ÿ“ configs/ # Global configurations +โ”œโ”€โ”€ ๐Ÿ“ utils/ +โ”‚ โ”œโ”€โ”€ ๐Ÿ“„ config.py # Framework configuration +โ”‚ โ””โ”€โ”€ ๐Ÿ“„ __init__.py +โ”œโ”€โ”€ ๐Ÿ“ tests/ # Legacy pytest tests +โ”œโ”€โ”€ ๐Ÿ“„ .env.example # Environment template +โ”œโ”€โ”€ ๐Ÿ“„ .gitignore # Git ignore rules +โ””โ”€โ”€ ๐Ÿ“„ README.md # This documentation +``` + +--- + +## โ“ Support and Contribution + +### Getting Help + +1. **Check this README** for common issues and solutions +2. **Review logs** using `--verbose --no-capture` flags +3. **Validate environment** using the troubleshooting section +4. **Contact the team** if issues persist + +### Contributing + +1. **Follow existing patterns** when adding new tests +2. **Update documentation** when adding new features +3. **Test your changes** before submitting +4. **Keep test data minimal** and realistic + +### Version History + +- **v1.0**: Initial BDD framework with AWS integration +- **Current**: Comprehensive mTLS authentication and data management + +--- + +_Last Updated: July 2025_ +_Framework Version: 1.0_ +_Maintained by: NHS Digital QA Team_ diff --git a/tests/e2e/data/configs/AUTO_RSV_SB_001.json b/tests/e2e/data/configs/AUTO_RSV_SB_001.json new file mode 100644 index 000000000..f313f5691 --- /dev/null +++ b/tests/e2e/data/configs/AUTO_RSV_SB_001.json @@ -0,0 +1,275 @@ +{ + "CampaignConfig": { + "ID": "<>", + "Version": "1", + "Name": "Automation RSV - Smoke Test Config", + "Type": "V", + "Target": "RSV", + "Manager": "person1@nhs.net", + "Approver": "person1@nhs.net", + "Reviewer": "person1@nhs.net", + "IterationFrequency": "X", + "IterationType": "O", + "IterationTime": "07:00:00", + "DefaultCommsRouting": "BOOK_LOCAL", + "StartDate": "20250601", + "EndDate": "20260601", + "ApprovalMinimum": 0, + "ApprovalMaximum": 0, + "Iterations": [ + { + "ID": ",<>", + "DefaultCommsRouting": "BOOK_LOCAL", + "Version": "1", + "Name": "Automation RSV - Smoke Test Config Iteration", + "IterationDate": "20250601", + "IterationNumber": 1, + "CommsType": "I", + "ApprovalMinimum": 0, + "ApprovalMaximum": 0, + "Type": "O", + "IterationCohorts": [ + { + "CohortLabel": "rsv_75_rolling", + "CohortGroup": "rsv_age_rolling", + "PositiveDescription": "are aged 75 to 79 years old.", + "NegativeDescription": "are not aged 75 to 79 years old.", + "Priority": 0 + }, + { + "CohortLabel": "rsv_75to79_2024", + "CohortGroup": "rsv_age_catchup", + "PositiveDescription": "turned 80 between 2nd September 2024 and 31st August 2025", + "NegativeDescription": "did not turn 80 between 2nd September 2024 and 31st August 2025", + "Priority": 10 + }, + { + "CohortLabel": "elid_all_people", + "CohortGroup": "elid_all_people", + "PositiveDescription": "", + "NegativeDescription": "", + "Priority": 20 + }, + { + "CohortLabel": "no_group_description", + "CohortGroup": "", + "PositiveDescription": "", + "NegativeDescription": "", + "Priority": 30 + } + ], + "IterationRules": [ + { + "Type": "F", + "Name": "Assure only already vaccinated taken from magic cohort", + "Description": "Exclude anyone who has NOT been given a dose of RSV Vaccination from the magic cohort", + "Operator": "is_empty", + "Comparator": "", + "AttributeTarget": "RSV", + "AttributeLevel": "TARGET", + "AttributeName": "LAST_SUCCESSFUL_DATE", + "CohortLabel": "elid_all_people", + "Priority": 100 + }, + { + "Type": "F", + "Name": "Under Age - Under 75 Years on day of execution", + "Description": "Ensure anyone who has a PDS date of birth which determines their age to be less than 75 years is filtered out.", + "Priority": 120, + "AttributeLevel": "PERSON", + "AttributeName": "DATE_OF_BIRTH", + "Operator": "Y>", + "Comparator": "-75", + "CohortLabel": "rsv_75to79_2024" + }, + { + "Type": "F", + "Name": "Under Age - Under 75 Years on day of execution", + "Description": "Ensure anyone who has a PDS date of birth which determines their age to be less than 75 years is filtered out.", + "Priority": 125, + "AttributeLevel": "PERSON", + "AttributeName": "DATE_OF_BIRTH", + "Operator": "Y>", + "Comparator": "-75", + "CohortLabel": "rsv_75_rolling" + }, + { + "Type": "F", + "Name": "Exclude Too OLD", + "Description": "Exclude anyone over 80", + "Priority": 130, + "AttributeLevel": "PERSON", + "AttributeName": "DATE_OF_BIRTH", + "Operator": "<", + "Comparator": "-80" + }, + { + "Type": "S", + "Name": "AlreadyVaccinated", + "Description": "##You've had your RSV vaccination\nWe believe you had your vaccination on <>.", + "Priority": 550, + "AttributeLevel": "TARGET", + "AttributeTarget": "RSV", + "AttributeName": "LAST_SUCCESSFUL_DATE", + "Operator": "Y>=", + "Comparator": "-25", + "RuleStop": "Y" + }, + { + "Type": "S", + "Name": "NotAvailable", + "Description": "NotAvailable|Vaccinations are not currently available.", + "Priority": 510, + "AttributeLevel": "PERSON", + "AttributeName": "ICB", + "Operator": "=", + "Comparator": "SUPPRESSED_ICB" + }, + { + "Type": "S", + "Name": "NotYetDue", + "Description": "NotYetDue|Your next dose is not yet due.", + "Priority": 520, + "AttributeTarget": "RSV", + "AttributeLevel": "TARGET", + "AttributeName": "LAST_SUCCESSFUL_DATE", + "Operator": "=", + "Comparator": "20250326", + "RuleStop": "Y" + }, + { + "Type": "S", + "Name": "TooClose", + "Description": "TooClose|Your previous vaccination was less than 91 days ago.", + "Priority": 530, + "AttributeTarget": "RSV", + "AttributeLevel": "TARGET", + "AttributeName": "LAST_SUCCESSFUL_DATE", + "Operator": "=", + "Comparator": "20250327", + "RuleStop": "Y" + }, + { + "Type": "S", + "Name": "OtherSetting", + "Description": "OtherSetting|## Getting the vaccine\n\nOur record show you're living in a setting where care is provided.\n\nIf you think you should have the RSV vaccine, speak to a member of staff where you live.", + "Priority": 540, + "AttributeLevel": "PERSON", + "AttributeName": "CARE_HOME_FLAG", + "Operator": "=", + "Comparator": "Y" + }, + { + "Type": "R", + "Name": "Actionable Not Vaccinated", + "Description": "Book An Appointment", + "Priority": 1010, + "Operator": "is_empty", + "Comparator": "", + "AttributeTarget": "RSV", + "AttributeLevel": "TARGET", + "AttributeName": "LAST_SUCCESSFUL_DATE", + "CommsRouting": "BOOK_NBS" + }, + { + "Type": "R", + "Name": "Actionable Not Vaccinated", + "Description": "Book An Appointment", + "Priority": 1010, + "Operator": "=", + "Comparator": "LS2", + "AttributeLevel": "PERSON", + "AttributeName": "POSTCODE_SECTOR", + "CommsRouting": "BOOK_NBS" + }, + { + "Type": "R", + "Name": "Actionable Future Booked Appointment", + "Description": "Actionable Future Booked Appointment", + "Priority": 1020, + "Operator": ">=", + "Comparator": "0", + "AttributeTarget": "RSV", + "AttributeLevel": "TARGET", + "AttributeName": "BOOKED_APPOINTMENT_DATE", + "CommsRouting": "AMEND_NBS" + }, + { + "Type": "R", + "Name": "Actionable Future Booked Appointment", + "Description": "Actionable Future Booked Appointment", + "Priority": 1020, + "Operator": "=", + "Comparator": "NBS", + "AttributeTarget": "RSV", + "AttributeLevel": "TARGET", + "AttributeName": "BOOKED_APPOINTMENT_PROVIDER", + "CommsRouting": "AMEND_NBS" + }, + { + "Type": "R", + "Name": "Actionable Future Booked Appointment", + "Description": "Actionable Future Booked Appointment", + "Priority": 1030, + "Operator": ">=", + "Comparator": "0", + "AttributeTarget": "RSV", + "AttributeLevel": "TARGET", + "AttributeName": "BOOKED_APPOINTMENT_DATE", + "CommsRouting": "MANAGE_LOCAL" + }, + { + "Type": "R", + "Name": "Actionable Future Booked Appointment", + "Description": "Actionable Future Booked Appointment", + "Priority": 1030, + "Operator": "!=", + "Comparator": "NBS", + "AttributeTarget": "RSV", + "AttributeLevel": "TARGET", + "AttributeName": "BOOKED_APPOINTMENT_PROVIDER", + "CommsRouting": "MANAGE_LOCAL" + } + ], + "ActionsMapper": { + "BOOK_NBS": { + "ExternalRoutingCode": "BookNBS", + "ActionDescription": "", + "ActionType": "ButtonWithAuthLink", + "UrlLink": "http://www.nhs.uk/book-rsv", + "UrlLabel": "Continue to booking" + }, + "AMEND_NBS": { + "ExternalRoutingCode": "AmendNBS", + "ActionDescription": "##You have an RSV vaccination appointment\nYou can view, change or cancel your appointment below.", + "ActionType": "ButtonWithAuthLink", + "UrlLink": "http://www.nhs.uk/book-rsv", + "UrlLabel": "Manage your appointment" + }, + "CONTACT_GP": { + "ExternalRoutingCode": "ContactGP", + "ActionDescription": "Contact your GP", + "ActionType": "InfoText" + }, + "BOOK_LOCAL": { + "ExternalRoutingCode": "BookLocal", + "ActionDescription": "##Getting the vaccine\nYou can get an RSV vaccination at your GP surgery.\nYour GP surgery may contact you about getting the RSV vaccine. This may be by letter, text, phone call, email or through the NHS App. You do not need to wait to be contacted before booking your vaccination.", + "ActionType": "InfoText" + }, + "MANAGE_LOCAL": { + "ExternalRoutingCode": "ManageLocal", + "ActionDescription": "##You have an RSV vaccination appointment\nContact your healthcare provider to change or cancel your appointment.", + "ActionType": "CardWithText" + + }, + "CHECK_CORRECT": { + "ExternalRoutingCode": "CheckCorrect", + "ActionDescription": "##If you think this is incorrect\\nIf you have not had this vaccination and you think you should, speak to your healthcare professional.", + "ActionType": "InfoText" + + } + } + } + ] + } +} diff --git a/tests/e2e/data/dynamoDB/test_data.json b/tests/e2e/data/dynamoDB/test_data.json deleted file mode 100644 index f5c80e4b5..000000000 --- a/tests/e2e/data/dynamoDB/test_data.json +++ /dev/null @@ -1,220 +0,0 @@ -[ - { - "NHS_NUMBER": "0000000001", - "ATTRIBUTE_TYPE": "PERSON", - "DATE_OF_BIRTH": "19960302", - "GENDER": 1, - "POSTCODE": "S18 C1X", - "POSTCODE_SECTOR": "S181", - "POSTCODE_OUTCODE": "S18", - "GP_PRACTICE_CODE": "U69384", - "PCN": "QJ2" - }, - { - "NHS_NUMBER": "0000000001", - "ATTRIBUTE_TYPE": "ELIGIBILITY", - "CONDITION": "Flu", - "STATUS": "Actionable" - }, - { - "NHS_NUMBER": "0000000001", - "ATTRIBUTE_TYPE": "ELIGIBILITY", - "CONDITION": "RSV", - "STATUS": "NotEligible" - }, - { - "NHS_NUMBER": "0000000002", - "ATTRIBUTE_TYPE": "PERSON", - "DATE_OF_BIRTH": "19890518", - "GENDER": 1, - "POSTCODE": "S18 B1X", - "POSTCODE_SECTOR": "S181", - "POSTCODE_OUTCODE": "S18", - "GP_PRACTICE_CODE": "U33827", - "PCN": "RJ3" - }, - { - "NHS_NUMBER": "0000000002", - "ATTRIBUTE_TYPE": "ELIGIBILITY", - "CONDITION": "Flu", - "STATUS": "Actionable" - }, - { - "NHS_NUMBER": "0000000002", - "ATTRIBUTE_TYPE": "ELIGIBILITY", - "CONDITION": "RSV", - "STATUS": "NotActionable" - }, - { - "NHS_NUMBER": "0000000003", - "ATTRIBUTE_TYPE": "PERSON", - "DATE_OF_BIRTH": "19780323", - "GENDER": 1, - "POSTCODE": "S18 X9X", - "POSTCODE_SECTOR": "S189", - "POSTCODE_OUTCODE": "S18", - "GP_PRACTICE_CODE": "U90732", - "PCN": "QJ2" - }, - { - "NHS_NUMBER": "0000000003", - "ATTRIBUTE_TYPE": "ELIGIBILITY", - "CONDITION": "Flu", - "STATUS": "Actionable" - }, - { - "NHS_NUMBER": "0000000004", - "ATTRIBUTE_TYPE": "PERSON", - "DATE_OF_BIRTH": "19570120", - "GENDER": 0, - "POSTCODE": "S18 B6X", - "POSTCODE_SECTOR": "S186", - "POSTCODE_OUTCODE": "S18", - "GP_PRACTICE_CODE": "U52353", - "PCN": "LM8" - }, - { - "NHS_NUMBER": "0000000004", - "ATTRIBUTE_TYPE": "ELIGIBILITY", - "CONDITION": "RSV", - "STATUS": "Actionable" - }, - { - "NHS_NUMBER": "0000000004", - "ATTRIBUTE_TYPE": "ELIGIBILITY", - "CONDITION": "RSV", - "STATUS": "NotEligible" - }, - { - "NHS_NUMBER": "0000000005", - "ATTRIBUTE_TYPE": "PERSON", - "DATE_OF_BIRTH": "19910411", - "GENDER": 1, - "POSTCODE": "S18 B5X", - "POSTCODE_SECTOR": "S185", - "POSTCODE_OUTCODE": "S18", - "GP_PRACTICE_CODE": "U54464", - "PCN": "LM8" - }, - { - "NHS_NUMBER": "0000000005", - "ATTRIBUTE_TYPE": "ELIGIBILITY", - "CONDITION": "RSV", - "STATUS": "NotActionable" - }, - { - "NHS_NUMBER": "0000000005", - "ATTRIBUTE_TYPE": "ELIGIBILITY", - "CONDITION": "Covid", - "STATUS": "NotEligible" - }, - { - "NHS_NUMBER": "0000000006", - "ATTRIBUTE_TYPE": "PERSON", - "DATE_OF_BIRTH": "19450206", - "GENDER": 1, - "POSTCODE": "S18 B9X", - "POSTCODE_SECTOR": "S189", - "POSTCODE_OUTCODE": "S18", - "GP_PRACTICE_CODE": "U97038", - "PCN": "RJ3" - }, - { - "NHS_NUMBER": "0000000006", - "ATTRIBUTE_TYPE": "ELIGIBILITY", - "CONDITION": "RSV", - "STATUS": "Actionable" - }, - { - "NHS_NUMBER": "0000000006", - "ATTRIBUTE_TYPE": "ELIGIBILITY", - "CONDITION": "Flu", - "STATUS": "NotEligible" - }, - { - "NHS_NUMBER": "0000000007", - "ATTRIBUTE_TYPE": "PERSON", - "DATE_OF_BIRTH": "19590705", - "GENDER": 0, - "POSTCODE": "S18 B3X", - "POSTCODE_SECTOR": "S183", - "POSTCODE_OUTCODE": "S18", - "GP_PRACTICE_CODE": "U56174", - "PCN": "TK9" - }, - { - "NHS_NUMBER": "0000000007", - "ATTRIBUTE_TYPE": "ELIGIBILITY", - "CONDITION": "Covid", - "STATUS": "NotEligible" - }, - { - "NHS_NUMBER": "0000000007", - "ATTRIBUTE_TYPE": "ELIGIBILITY", - "CONDITION": "Covid", - "STATUS": "Actionable" - }, - { - "NHS_NUMBER": "0000000008", - "ATTRIBUTE_TYPE": "PERSON", - "DATE_OF_BIRTH": "19990716", - "GENDER": 0, - "POSTCODE": "S18 A5X", - "POSTCODE_SECTOR": "S185", - "POSTCODE_OUTCODE": "S18", - "GP_PRACTICE_CODE": "U33386", - "PCN": "QJ2" - }, - { - "NHS_NUMBER": "0000000008", - "ATTRIBUTE_TYPE": "ELIGIBILITY", - "CONDITION": "Flu", - "STATUS": "NotActionable" - }, - { - "NHS_NUMBER": "0000000009", - "ATTRIBUTE_TYPE": "PERSON", - "DATE_OF_BIRTH": "20100426", - "GENDER": 0, - "POSTCODE": "S18 X4X", - "POSTCODE_SECTOR": "S184", - "POSTCODE_OUTCODE": "S18", - "GP_PRACTICE_CODE": "U94864", - "PCN": "LM8" - }, - { - "NHS_NUMBER": "0000000009", - "ATTRIBUTE_TYPE": "ELIGIBILITY", - "CONDITION": "Flu", - "STATUS": "Actionable" - }, - { - "NHS_NUMBER": "0000000009", - "ATTRIBUTE_TYPE": "ELIGIBILITY", - "CONDITION": "Flu", - "STATUS": "NotActionable" - }, - { - "NHS_NUMBER": "0000000010", - "ATTRIBUTE_TYPE": "PERSON", - "DATE_OF_BIRTH": "19931007", - "GENDER": 1, - "POSTCODE": "S18 B5X", - "POSTCODE_SECTOR": "S185", - "POSTCODE_OUTCODE": "S18", - "GP_PRACTICE_CODE": "U74914", - "PCN": "LM8" - }, - { - "NHS_NUMBER": "0000000010", - "ATTRIBUTE_TYPE": "ELIGIBILITY", - "CONDITION": "RSV", - "STATUS": "NotEligible" - }, - { - "NHS_NUMBER": "0000000010", - "ATTRIBUTE_TYPE": "ELIGIBILITY", - "CONDITION": "Flu", - "STATUS": "NotEligible" - } -] diff --git a/tests/e2e/data/dynamoDB/AUTO_RSV_SB_001.json b/tests/e2e/data/in/dynamoDB/AUTO_RSV_SB_001.json similarity index 86% rename from tests/e2e/data/dynamoDB/AUTO_RSV_SB_001.json rename to tests/e2e/data/in/dynamoDB/AUTO_RSV_SB_001.json index 457437429..e8cba4cf0 100644 --- a/tests/e2e/data/dynamoDB/AUTO_RSV_SB_001.json +++ b/tests/e2e/data/in/dynamoDB/AUTO_RSV_SB_001.json @@ -1,5 +1,9 @@ { "scenario_name": "RSV - Actionable due to membership of an Age Cohort incl. suggested actions (with booking)", + "request_headers": { + "nhs-login-nhs-number": "5000000001" + }, + "config_filename": "AUTO_RSV_SB_001.json", "data": [ { "NHS_NUMBER": "5000000001", @@ -24,7 +28,7 @@ "DATE_OF_BIRTH": "<>", "GENDER": "0", "POSTCODE": "LS1 1AB", - "POSTCODE_SECTOR": "LS1", + "POSTCODE_SECTOR": "LS2", "POSTCODE_OUTCODE": "1AB", "MSOA": "E02001111", "LSOA": "E01005348", diff --git a/tests/e2e/data/dynamoDB/AUTO_RSV_SB_002.json b/tests/e2e/data/in/dynamoDB/AUTO_RSV_SB_002.json similarity index 89% rename from tests/e2e/data/dynamoDB/AUTO_RSV_SB_002.json rename to tests/e2e/data/in/dynamoDB/AUTO_RSV_SB_002.json index 78026a684..f6bda7144 100644 --- a/tests/e2e/data/dynamoDB/AUTO_RSV_SB_002.json +++ b/tests/e2e/data/in/dynamoDB/AUTO_RSV_SB_002.json @@ -1,5 +1,9 @@ { "scenario_name": "RSV - Actionable due to membership of an Age Cohort incl. suggested action (not booking)", + "request_headers": { + "nhs-login-nhs-number": "5000000002" + }, + "config_filename": "AUTO_RSV_SB_001.json", "data": [ { "NHS_NUMBER": "5000000002", diff --git a/tests/e2e/data/dynamoDB/AUTO_RSV_SB_003.json b/tests/e2e/data/in/dynamoDB/AUTO_RSV_SB_003.json similarity index 89% rename from tests/e2e/data/dynamoDB/AUTO_RSV_SB_003.json rename to tests/e2e/data/in/dynamoDB/AUTO_RSV_SB_003.json index df7ceced6..fca3a419b 100644 --- a/tests/e2e/data/dynamoDB/AUTO_RSV_SB_003.json +++ b/tests/e2e/data/in/dynamoDB/AUTO_RSV_SB_003.json @@ -1,5 +1,9 @@ { "scenario_name": "RSV - Actionable due to membership of an alternative Age Cohort incl. suggested action", + "request_headers": { + "nhs-login-nhs-number": "5000000003" + }, + "config_filename": "AUTO_RSV_SB_001.json", "data": [ { "NHS_NUMBER": "5000000003", diff --git a/tests/e2e/data/dynamoDB/AUTO_RSV_SB_004.json b/tests/e2e/data/in/dynamoDB/AUTO_RSV_SB_004.json similarity index 87% rename from tests/e2e/data/dynamoDB/AUTO_RSV_SB_004.json rename to tests/e2e/data/in/dynamoDB/AUTO_RSV_SB_004.json index ed6764ede..5339dfac6 100644 --- a/tests/e2e/data/dynamoDB/AUTO_RSV_SB_004.json +++ b/tests/e2e/data/in/dynamoDB/AUTO_RSV_SB_004.json @@ -1,5 +1,9 @@ { "scenario_name": "RSV - Actionable due to membership of an Age Cohort incl. suggested action (existing national booking)", + "request_headers": { + "nhs-login-nhs-number": "5000000004" + }, + "config_filename": "AUTO_RSV_SB_001.json", "data": [ { "NHS_NUMBER": "5000000004", @@ -7,7 +11,7 @@ "COHORT_MAP": { "cohorts": { "M": { - "rsv_75_rolling": { + "no_group_description": { "M": { "dateJoined": { "S": "20230515" diff --git a/tests/e2e/data/dynamoDB/AUTO_RSV_SB_005.json b/tests/e2e/data/in/dynamoDB/AUTO_RSV_SB_005.json similarity index 87% rename from tests/e2e/data/dynamoDB/AUTO_RSV_SB_005.json rename to tests/e2e/data/in/dynamoDB/AUTO_RSV_SB_005.json index 1892dd217..d4f4265ef 100644 --- a/tests/e2e/data/dynamoDB/AUTO_RSV_SB_005.json +++ b/tests/e2e/data/in/dynamoDB/AUTO_RSV_SB_005.json @@ -1,5 +1,9 @@ { "scenario_name": "RSV - Actionable due to membership of an Age Cohort incl. suggested actions (with local booking)", + "request_headers": { + "nhs-login-nhs-number": "5000000005" + }, + "config_filename": "AUTO_RSV_SB_001.json", "data": [ { "NHS_NUMBER": "5000000005", @@ -7,7 +11,7 @@ "COHORT_MAP": { "cohorts": { "M": { - "rsv_75_rolling": { + "no_group_description": { "M": { "dateJoined": { "S": "20230515" diff --git a/tests/e2e/data/dynamoDB/AUTO_RSV_SB_006.json b/tests/e2e/data/in/dynamoDB/AUTO_RSV_SB_006.json similarity index 78% rename from tests/e2e/data/dynamoDB/AUTO_RSV_SB_006.json rename to tests/e2e/data/in/dynamoDB/AUTO_RSV_SB_006.json index f1101ccc4..ba55e3091 100644 --- a/tests/e2e/data/dynamoDB/AUTO_RSV_SB_006.json +++ b/tests/e2e/data/in/dynamoDB/AUTO_RSV_SB_006.json @@ -1,5 +1,10 @@ { "scenario_name": "RSV - Not Actionable despite membership of an Age Cohort, already vaccinated", + "request_headers": { + "nhs-login-nhs-number": "5000000006" + }, + "config_filename": "AUTO_RSV_SB_001.json", + "notes": "actions need updating in the response when the functionality is delivered to provide actions for not_actionable responses", "data": [ { "NHS_NUMBER": "5000000006", @@ -7,7 +12,7 @@ "COHORT_MAP": { "cohorts": { "M": { - "rsv_75_rolling": { + "no_group_description": { "M": { "dateJoined": { "S": "20230515" diff --git a/tests/e2e/data/dynamoDB/AUTO_RSV_SB_007.json b/tests/e2e/data/in/dynamoDB/AUTO_RSV_SB_007.json similarity index 73% rename from tests/e2e/data/dynamoDB/AUTO_RSV_SB_007.json rename to tests/e2e/data/in/dynamoDB/AUTO_RSV_SB_007.json index 3c9c46c5e..ac8cb72bc 100644 --- a/tests/e2e/data/dynamoDB/AUTO_RSV_SB_007.json +++ b/tests/e2e/data/in/dynamoDB/AUTO_RSV_SB_007.json @@ -1,5 +1,9 @@ { - "scenario_name": "RSV - Not Actionable, membership of Age Cohort, no available vaccinations (not available type 1)", + "scenario_name": "RSV - Not Actionable despite to membership of an Age Cohort with reasoning of no available vaccinations (not available type 1)", + "request_headers": { + "nhs-login-nhs-number": "5000000007" + }, + "config_filename": "AUTO_RSV_SB_001.json", "data": [ { "NHS_NUMBER": "5000000007", @@ -30,16 +34,11 @@ "LSOA": "E01005348", "GP_PRACTICE_CODE": "B87008", "PCN": "U43084", - "ICB": "<>", + "ICB": "SUPPRESSED_ICB", "COMMISSIONING_REGION": "Y63", "13Q_FLAG": "N", "CARE_HOME_FLAG": "N", "DE_FLAG": "N" - }, - { - "NHS_NUMBER": "5000000007", - "ATTRIBUTE_TYPE": "RSV", - "LAST_SUCCESSFUL_DATE": "<>" } ] } diff --git a/tests/e2e/data/dynamoDB/AUTO_RSV_SB_008.json b/tests/e2e/data/in/dynamoDB/AUTO_RSV_SB_008.json similarity index 85% rename from tests/e2e/data/dynamoDB/AUTO_RSV_SB_008.json rename to tests/e2e/data/in/dynamoDB/AUTO_RSV_SB_008.json index 25f7e19fa..8aa367705 100644 --- a/tests/e2e/data/dynamoDB/AUTO_RSV_SB_008.json +++ b/tests/e2e/data/in/dynamoDB/AUTO_RSV_SB_008.json @@ -1,5 +1,9 @@ { "scenario_name": "RSV - No RSV response as no active campaign (not available type 2)", + "request_headers": { + "nhs-login-nhs-number": "5000000008" + }, + "config_filename": "AUTO_RSV_SB_008.json", "data": [ { "NHS_NUMBER": "5000000008", @@ -39,7 +43,7 @@ { "NHS_NUMBER": "5000000008", "ATTRIBUTE_TYPE": "RSV", - "BOOKED_APPOINTMENT_DATE": "2024-07-01" + "BOOKED_APPOINTMENT_DATE": "<>" } ] } diff --git a/tests/e2e/data/dynamoDB/AUTO_RSV_SB_009.json b/tests/e2e/data/in/dynamoDB/AUTO_RSV_SB_009.json similarity index 77% rename from tests/e2e/data/dynamoDB/AUTO_RSV_SB_009.json rename to tests/e2e/data/in/dynamoDB/AUTO_RSV_SB_009.json index 753398d0a..aa661f5a1 100644 --- a/tests/e2e/data/dynamoDB/AUTO_RSV_SB_009.json +++ b/tests/e2e/data/in/dynamoDB/AUTO_RSV_SB_009.json @@ -1,5 +1,9 @@ { - "scenario_name": "RSV - Not Actionable, membership of Age Cohort, dose not yet due", + "scenario_name": "RSV - Not Actionable despite to membership of an Age Cohort with reasoning of dose not yet due", + "request_headers": { + "nhs-login-nhs-number": "5000000009" + }, + "config_filename": "AUTO_RSV_SB_001.json", "data": [ { "NHS_NUMBER": "5000000009", @@ -39,7 +43,7 @@ { "NHS_NUMBER": "5000000009", "ATTRIBUTE_TYPE": "RSV", - "LAST_SUCCESSFUL_DATE": "2023-07-01" + "LAST_SUCCESSFUL_DATE": "20250326" } ] } diff --git a/tests/e2e/data/dynamoDB/AUTO_RSV_SB_010.json b/tests/e2e/data/in/dynamoDB/AUTO_RSV_SB_010.json similarity index 76% rename from tests/e2e/data/dynamoDB/AUTO_RSV_SB_010.json rename to tests/e2e/data/in/dynamoDB/AUTO_RSV_SB_010.json index 7eb7d0488..0bd87ebce 100644 --- a/tests/e2e/data/dynamoDB/AUTO_RSV_SB_010.json +++ b/tests/e2e/data/in/dynamoDB/AUTO_RSV_SB_010.json @@ -1,5 +1,9 @@ { - "scenario_name": "RSV - Not Actionable, membership of Age Cohort, dose not far enough apart", + "scenario_name": "RSV - Not Actionable despite to membership of an Age Cohort with reasoning of dose not far enough apart", + "request_headers": { + "nhs-login-nhs-number": "5000000010" + }, + "config_filename": "AUTO_RSV_SB_001.json", "data": [ { "NHS_NUMBER": "5000000010", @@ -39,7 +43,7 @@ { "NHS_NUMBER": "5000000010", "ATTRIBUTE_TYPE": "RSV", - "LAST_SUCCESSFUL_DATE": "2023-07-01" + "LAST_SUCCESSFUL_DATE": "20250327" } ] } diff --git a/tests/e2e/data/dynamoDB/AUTO_RSV_SB_011.json b/tests/e2e/data/in/dynamoDB/AUTO_RSV_SB_011.json similarity index 89% rename from tests/e2e/data/dynamoDB/AUTO_RSV_SB_011.json rename to tests/e2e/data/in/dynamoDB/AUTO_RSV_SB_011.json index 9626840b3..46f43270a 100644 --- a/tests/e2e/data/dynamoDB/AUTO_RSV_SB_011.json +++ b/tests/e2e/data/in/dynamoDB/AUTO_RSV_SB_011.json @@ -1,5 +1,9 @@ { "scenario_name": "RSV - Not Actionable despite to membership of an Age Cohort with reasoning of vaccination given in other setting (e.g. care home)", + "request_headers": { + "nhs-login-nhs-number": "5000000011" + }, + "config_filename": "AUTO_RSV_SB_001.json", "data": [ { "NHS_NUMBER": "5000000011", diff --git a/tests/e2e/data/dynamoDB/AUTO_RSV_SB_012.json b/tests/e2e/data/in/dynamoDB/AUTO_RSV_SB_012.json similarity index 88% rename from tests/e2e/data/dynamoDB/AUTO_RSV_SB_012.json rename to tests/e2e/data/in/dynamoDB/AUTO_RSV_SB_012.json index b7ff528f2..1c2d8a4a6 100644 --- a/tests/e2e/data/dynamoDB/AUTO_RSV_SB_012.json +++ b/tests/e2e/data/in/dynamoDB/AUTO_RSV_SB_012.json @@ -1,5 +1,9 @@ { "scenario_name": "RSV - Not Actionable despite no cohort membership with reasoning of already vaccinated (type 1 includes unknown cohort)", + "request_headers": { + "nhs-login-nhs-number": "5000000012" + }, + "config_filename": "AUTO_RSV_SB_001.json", "data": [ { "NHS_NUMBER": "5000000012", diff --git a/tests/e2e/data/dynamoDB/AUTO_RSV_SB_013.json b/tests/e2e/data/in/dynamoDB/AUTO_RSV_SB_013.json similarity index 87% rename from tests/e2e/data/dynamoDB/AUTO_RSV_SB_013.json rename to tests/e2e/data/in/dynamoDB/AUTO_RSV_SB_013.json index c0f69b5dc..4f48a06bb 100644 --- a/tests/e2e/data/dynamoDB/AUTO_RSV_SB_013.json +++ b/tests/e2e/data/in/dynamoDB/AUTO_RSV_SB_013.json @@ -1,5 +1,9 @@ { "scenario_name": "RSV - Not Actionable despite no cohort membership with reasoning of already vaccinated (type 2 includes no cohorts)", + "request_headers": { + "nhs-login-nhs-number": "5000000013" + }, + "config_filename": "AUTO_RSV_SB_001.json", "data": [ { "NHS_NUMBER": "5000000013", @@ -37,7 +41,7 @@ "DE_FLAG": "N" }, { - "NHS_NUMBER": "50000000014", + "NHS_NUMBER": "5000000013", "ATTRIBUTE_TYPE": "RSV", "LAST_SUCCESSFUL_DATE": "<>" } diff --git a/tests/e2e/data/dynamoDB/AUTO_RSV_SB_014.json b/tests/e2e/data/in/dynamoDB/AUTO_RSV_SB_014.json similarity index 85% rename from tests/e2e/data/dynamoDB/AUTO_RSV_SB_014.json rename to tests/e2e/data/in/dynamoDB/AUTO_RSV_SB_014.json index 5cd1e21d7..f6945cb28 100644 --- a/tests/e2e/data/dynamoDB/AUTO_RSV_SB_014.json +++ b/tests/e2e/data/in/dynamoDB/AUTO_RSV_SB_014.json @@ -1,5 +1,9 @@ { "scenario_name": "RSV - Not Eligible", + "request_headers": { + "nhs-login-nhs-number": "5000000014" + }, + "config_filename": "AUTO_RSV_SB_001.json", "data": [ { "NHS_NUMBER": "5000000014", @@ -19,7 +23,7 @@ } }, { - "NHS_NUMBER": "50000000014", + "NHS_NUMBER": "5000000014", "ATTRIBUTE_TYPE": "PERSON", "DATE_OF_BIRTH": "<>", "GENDER": "0", diff --git a/tests/e2e/data/responses/AUTO_RSV_SB_001.json b/tests/e2e/data/responses/AUTO_RSV_SB_001.json index fc1326c0a..a45608ae2 100644 --- a/tests/e2e/data/responses/AUTO_RSV_SB_001.json +++ b/tests/e2e/data/responses/AUTO_RSV_SB_001.json @@ -1,30 +1,30 @@ { - "responseId": "<>", "meta": { - "lastUpdated": "<>" + "lastUpdated": "2025-07-15T10:11:33.599540+00:00" }, "processedSuggestions": [ { + "actions": [ + { + "actionCode": "BookNBS", + "actionType": "ButtonWithAuthLink", + "description": "", + "urlLabel": "Continue to booking", + "urlLink": "http://www.nhs.uk/book-rsv" + } + ], "condition": "RSV", - "status": "Actionable", - "statusText": "You should have the RSV vaccine", "eligibilityCohorts": [ { "cohortCode": "rsv_age_rolling", - "cohortText": "You are aged 75 to 79 years old.", - "cohortStatus": "Actionable" + "cohortStatus": "Actionable", + "cohortText": "are aged 75 to 79 years old." } ], - "suitabilityRules": [], - "actions": [ - { - "actionType": "ButtonAuthLink", - "actionCode": "BookNBS", - "description": "", - "urlLink": "http://www.nhs.uk/book-rsv", - "urlLabel": "Continue to booking" - } - ] + "status": "Actionable", + "statusText": "Status.actionable", + "suitabilityRules": [] } - ] -} + ], + "responseId": "dfbf7f95-adc3-46e7-be9a-e08104e8806c" +} \ No newline at end of file diff --git a/tests/e2e/data/responses/AUTO_RSV_SB_002.json b/tests/e2e/data/responses/AUTO_RSV_SB_002.json index e45a371df..b9ddd62ec 100644 --- a/tests/e2e/data/responses/AUTO_RSV_SB_002.json +++ b/tests/e2e/data/responses/AUTO_RSV_SB_002.json @@ -1,28 +1,30 @@ { - "responseId": "<>", "meta": { - "lastUpdated": "<>" + "lastUpdated": "2025-07-15T10:11:36.524070+00:00" }, "processedSuggestions": [ { + "actions": [ + { + "actionCode": "BookLocal", + "actionType": "InfoText", + "description": "##Getting the vaccine\nYou can get an RSV vaccination at your GP surgery.\nYour GP surgery may contact you about getting the RSV vaccine. This may be by letter, text, phone call, email or through the NHS App. You do not need to wait to be contacted before booking your vaccination.", + "urlLabel": "", + "urlLink": "" + } + ], "condition": "RSV", - "status": "Actionable", - "statusText": "You should have the RSV vaccine", "eligibilityCohorts": [ { "cohortCode": "rsv_age_rolling", - "cohortText": "You are aged 75 to 79 years old.", - "cohortStatus": "Actionable" + "cohortStatus": "Actionable", + "cohortText": "are aged 75 to 79 years old." } ], - "suitabilityRules": [], - "actions": [ - { - "actionType": "CareCardWithText", - "actionCode": "BookLocal", - "description": "##Getting the vaccine\nYou can get an RSV vaccination at your GP surgery.\nYour GP surgery may contact you about getting the RSV vaccine. This may be by letter, text, phone call, email or through the NHS App. You do not need to wait to be contacted before booking your vaccination." - } - ] + "status": "Actionable", + "statusText": "Status.actionable", + "suitabilityRules": [] } - ] -} + ], + "responseId": "0ec1b335-129a-42cb-b74e-81c42a07bb3d" +} \ No newline at end of file diff --git a/tests/e2e/data/responses/AUTO_RSV_SB_003.json b/tests/e2e/data/responses/AUTO_RSV_SB_003.json index e814e90b3..345f4fcf0 100644 --- a/tests/e2e/data/responses/AUTO_RSV_SB_003.json +++ b/tests/e2e/data/responses/AUTO_RSV_SB_003.json @@ -1,28 +1,30 @@ { - "responseId": "<>", "meta": { - "lastUpdated": "<>" + "lastUpdated": "2025-07-15T10:11:39.467491+00:00" }, "processedSuggestions": [ { + "actions": [ + { + "actionCode": "BookLocal", + "actionType": "InfoText", + "description": "##Getting the vaccine\nYou can get an RSV vaccination at your GP surgery.\nYour GP surgery may contact you about getting the RSV vaccine. This may be by letter, text, phone call, email or through the NHS App. You do not need to wait to be contacted before booking your vaccination.", + "urlLabel": "", + "urlLink": "" + } + ], "condition": "RSV", - "status": "Actionable", - "statusText": "You should have the RSV vaccine", "eligibilityCohorts": [ { "cohortCode": "rsv_age_catchup", - "cohortText": "You turned 80 between 2nd September 2024 and 31st August 2025", - "cohortStatus": "Actionable" + "cohortStatus": "Actionable", + "cohortText": "turned 80 between 2nd September 2024 and 31st August 2025" } ], - "suitabilityRules": [], - "actions": [ - { - "actionType": "CareCardWithText", - "actionCode": "BookLocal", - "description": "##Getting the vaccine\nYou can get an RSV vaccination at your GP surgery.\nYour GP surgery may contact you about getting the RSV vaccine. This may be by letter, text, phone call, email or through the NHS App. You do not need to wait to be contacted before booking your vaccination." - } - ] + "status": "Actionable", + "statusText": "Status.actionable", + "suitabilityRules": [] } - ] -} + ], + "responseId": "30de7114-1ff9-410f-be9b-7ff8327e78b3" +} \ No newline at end of file diff --git a/tests/e2e/data/responses/AUTO_RSV_SB_004.json b/tests/e2e/data/responses/AUTO_RSV_SB_004.json index ed4663779..3c847a7ca 100644 --- a/tests/e2e/data/responses/AUTO_RSV_SB_004.json +++ b/tests/e2e/data/responses/AUTO_RSV_SB_004.json @@ -1,24 +1,24 @@ { - "responseId": "<>", "meta": { - "lastUpdated": "<>" + "lastUpdated": "2025-07-15T10:11:42.509859+00:00" }, "processedSuggestions": [ { - "condition": "RSV", - "status": "Actionable", - "statusText": "You should have the RSV vaccine", - "eligibilityCohorts": [], - "suitabilityRules": [], "actions": [ { - "actionType": "CardWithAuthButton", "actionCode": "AmendNBS", + "actionType": "ButtonWithAuthLink", "description": "##You have an RSV vaccination appointment\nYou can view, change or cancel your appointment below.", - "urlLink": "http://www.nhs.uk/book-rsv", - "urlLabel": "Manage your appointment" + "urlLabel": "Manage your appointment", + "urlLink": "http://www.nhs.uk/book-rsv" } - ] + ], + "condition": "RSV", + "eligibilityCohorts": [], + "status": "Actionable", + "statusText": "Status.actionable", + "suitabilityRules": [] } - ] -} + ], + "responseId": "139f2328-0695-482b-9e30-2223046ca7e2" +} \ No newline at end of file diff --git a/tests/e2e/data/responses/AUTO_RSV_SB_005.json b/tests/e2e/data/responses/AUTO_RSV_SB_005.json index 687b05405..92bcdd6aa 100644 --- a/tests/e2e/data/responses/AUTO_RSV_SB_005.json +++ b/tests/e2e/data/responses/AUTO_RSV_SB_005.json @@ -1,22 +1,24 @@ { - "responseId": "<>", "meta": { - "lastUpdated": "<>" + "lastUpdated": "2025-07-15T10:11:49.021445+00:00" }, "processedSuggestions": [ { - "condition": "RSV", - "status": "Actionable", - "statusText": "You should have the RSV vaccine", - "eligibilityCohorts": [], - "suitabilityRules": [], "actions": [ { - "actionType": "CardWithText", "actionCode": "ManageLocal", - "description": "##You have an RSV vaccination appointment\nContact your healthcare provider to change or cancel your appointment." + "actionType": "CardWithText", + "description": "##You have an RSV vaccination appointment\nContact your healthcare provider to change or cancel your appointment.", + "urlLabel": "", + "urlLink": "" } - ] + ], + "condition": "RSV", + "eligibilityCohorts": [], + "status": "Actionable", + "statusText": "Status.actionable", + "suitabilityRules": [] } - ] -} + ], + "responseId": "d340ebbd-de36-40d9-8da2-54b2c019225b" +} \ No newline at end of file diff --git a/tests/e2e/data/responses/AUTO_RSV_SB_006.json b/tests/e2e/data/responses/AUTO_RSV_SB_006.json index 3413b999b..f7e0be9ab 100644 --- a/tests/e2e/data/responses/AUTO_RSV_SB_006.json +++ b/tests/e2e/data/responses/AUTO_RSV_SB_006.json @@ -1,22 +1,22 @@ { - "responseId": "<>", "meta": { - "lastUpdated": "<>" + "lastUpdated": "2025-07-15T10:11:52.192023+00:00" }, "processedSuggestions": [ { + "actions": [], "condition": "RSV", - "status": "NotActionable", - "statusText": "You should have the RSV vaccine", "eligibilityCohorts": [], + "status": "NotActionable", + "statusText": "Status.not_actionable", "suitabilityRules": [ { - "ruleType": "S", "ruleCode": "AlreadyVaccinated", - "ruleText": "##You've had your RSV vaccination\nBased on our records, you recently had this vaccination. You do not need to do anything." + "ruleText": "##You've had your RSV vaccination\nWe believe you had your vaccination on <>.", + "ruleType": "S" } - ], - "actions": [] + ] } - ] -} + ], + "responseId": "0d64cba6-96af-49e5-8250-d3471a57da97" +} \ No newline at end of file diff --git a/tests/e2e/data/responses/AUTO_RSV_SB_007.json b/tests/e2e/data/responses/AUTO_RSV_SB_007.json index e292c3719..29ad85800 100644 --- a/tests/e2e/data/responses/AUTO_RSV_SB_007.json +++ b/tests/e2e/data/responses/AUTO_RSV_SB_007.json @@ -1,28 +1,33 @@ { - "responseId": "<>", "meta": { - "lastUpdated": "<>" + "lastUpdated": "2025-07-15T10:11:55.007429+00:00" }, "processedSuggestions": [ { + "actions": [], "condition": "RSV", - "status": "NotActionable", - "statusText": "You should have the RSV vaccine", "eligibilityCohorts": [ { "cohortCode": "rsv_age_rolling", - "cohortText": "You are aged 75 to 79 years old.", - "cohortStatus": "NotActionable" + "cohortStatus": "NotActionable", + "cohortText": "are aged 75 to 79 years old." } ], + "status": "NotActionable", + "statusText": "Status.not_actionable", "suitabilityRules": [ { - "ruleType": "S", "ruleCode": "NotAvailable", - "ruleText": "Vaccinations are not currently available" + "ruleText": "NotAvailable|Vaccinations are not currently available.", + "ruleType": "S" + }, + { + "ruleCode": "AlreadyVaccinated", + "ruleText": "##You've had your RSV vaccination\nWe believe you had your vaccination on <>.", + "ruleType": "S" } - ], - "actions": [] + ] } - ] -} + ], + "responseId": "7b302c89-d3c8-4194-91e8-a9717330e91b" +} \ No newline at end of file diff --git a/tests/e2e/data/responses/AUTO_RSV_SB_008.json b/tests/e2e/data/responses/AUTO_RSV_SB_008.json index 01fef4fb6..3d11dcb87 100644 --- a/tests/e2e/data/responses/AUTO_RSV_SB_008.json +++ b/tests/e2e/data/responses/AUTO_RSV_SB_008.json @@ -1,7 +1,30 @@ { - "responseId": "<>", "meta": { - "lastUpdated": "<>" + "lastUpdated": "2025-07-15T10:11:58.006524+00:00" }, - "processedSuggestions": [] -} + "processedSuggestions": [ + { + "actions": [ + { + "actionCode": "ManageLocal", + "actionType": "CardWithText", + "description": "##You have an RSV vaccination appointment\nContact your healthcare provider to change or cancel your appointment.", + "urlLabel": "", + "urlLink": "" + } + ], + "condition": "RSV", + "eligibilityCohorts": [ + { + "cohortCode": "rsv_age_rolling", + "cohortStatus": "Actionable", + "cohortText": "are aged 75 to 79 years old." + } + ], + "status": "Actionable", + "statusText": "Status.actionable", + "suitabilityRules": [] + } + ], + "responseId": "8090f190-cd7c-4f43-aa77-d0e6f074ac3f" +} \ No newline at end of file diff --git a/tests/e2e/data/responses/AUTO_RSV_SB_009.json b/tests/e2e/data/responses/AUTO_RSV_SB_009.json index 65ecaf7a5..0ad707d50 100644 --- a/tests/e2e/data/responses/AUTO_RSV_SB_009.json +++ b/tests/e2e/data/responses/AUTO_RSV_SB_009.json @@ -1,28 +1,28 @@ { - "responseId": "<>", "meta": { - "lastUpdated": "<>" + "lastUpdated": "2025-07-15T10:12:01.690297+00:00" }, "processedSuggestions": [ { + "actions": [], "condition": "RSV", - "status": "NotActionable", - "statusText": "You should have the RSV vaccine", "eligibilityCohorts": [ { "cohortCode": "rsv_age_rolling", - "cohortText": "You are aged 75 to 79 years old.", - "cohortStatus": "NotActionable" + "cohortStatus": "NotActionable", + "cohortText": "are aged 75 to 79 years old." } ], + "status": "NotActionable", + "statusText": "Status.not_actionable", "suitabilityRules": [ { - "ruleType": "S", "ruleCode": "NotYetDue", - "ruleText": "Your next dose is not yet due." + "ruleText": "NotYetDue|Your next dose is not yet due.", + "ruleType": "S" } - ], - "actions": [] + ] } - ] -} + ], + "responseId": "f70b8f89-42f2-4451-8ef5-cabd542618a7" +} \ No newline at end of file diff --git a/tests/e2e/data/responses/AUTO_RSV_SB_010.json b/tests/e2e/data/responses/AUTO_RSV_SB_010.json index 0b9dc7258..58996d204 100644 --- a/tests/e2e/data/responses/AUTO_RSV_SB_010.json +++ b/tests/e2e/data/responses/AUTO_RSV_SB_010.json @@ -1,28 +1,28 @@ { - "responseId": "<>", "meta": { - "lastUpdated": "<>" + "lastUpdated": "2025-07-15T10:12:04.565581+00:00" }, "processedSuggestions": [ { + "actions": [], "condition": "RSV", - "status": "NotActionable", - "statusText": "You should have the RSV vaccine", "eligibilityCohorts": [ { "cohortCode": "rsv_age_rolling", - "cohortText": "You are aged 75 to 79 years old.", - "cohortStatus": "NotActionable" + "cohortStatus": "NotActionable", + "cohortText": "are aged 75 to 79 years old." } ], + "status": "NotActionable", + "statusText": "Status.not_actionable", "suitabilityRules": [ { - "ruleType": "S", "ruleCode": "TooClose", - "ruleText": "Your previous vaccination was less than 91 days ago." + "ruleText": "TooClose|Your previous vaccination was less than 91 days ago.", + "ruleType": "S" } - ], - "actions": [] + ] } - ] -} + ], + "responseId": "3d3db0f9-ccf8-4e39-a905-37e060317e0b" +} \ No newline at end of file diff --git a/tests/e2e/data/responses/AUTO_RSV_SB_011.json b/tests/e2e/data/responses/AUTO_RSV_SB_011.json index 3070205d0..24706fcf3 100644 --- a/tests/e2e/data/responses/AUTO_RSV_SB_011.json +++ b/tests/e2e/data/responses/AUTO_RSV_SB_011.json @@ -1,28 +1,28 @@ { - "responseId": "<>", "meta": { - "lastUpdated": "<>" + "lastUpdated": "2025-07-15T10:12:07.347052+00:00" }, "processedSuggestions": [ { + "actions": [], "condition": "RSV", - "status": "NotActionable", - "statusText": "You should have the RSV vaccine", "eligibilityCohorts": [ { "cohortCode": "rsv_age_rolling", - "cohortText": "You are aged 75 to 79 years old.", - "cohortStatus": "NotActionable" + "cohortStatus": "NotActionable", + "cohortText": "are aged 75 to 79 years old." } ], + "status": "NotActionable", + "statusText": "Status.not_actionable", "suitabilityRules": [ { - "ruleType": "S", "ruleCode": "OtherSetting", - "ruleText": "##Getting the vaccine\nOur record show you're living in a setting where care is provided.\nIf you think you should have the RSV vaccine, speak to a member of staff where you live." + "ruleText": "OtherSetting|## Getting the vaccine\n\nOur record show you're living in a setting where care is provided.\n\nIf you think you should have the RSV vaccine, speak to a member of staff where you live.", + "ruleType": "S" } - ], - "actions": [] + ] } - ] -} + ], + "responseId": "156cf0b8-e493-4b0d-99cf-805d01930c2e" +} \ No newline at end of file diff --git a/tests/e2e/data/responses/AUTO_RSV_SB_012.json b/tests/e2e/data/responses/AUTO_RSV_SB_012.json index 4996198d2..e6da2c32d 100644 --- a/tests/e2e/data/responses/AUTO_RSV_SB_012.json +++ b/tests/e2e/data/responses/AUTO_RSV_SB_012.json @@ -1,28 +1,22 @@ { - "responseId": "<>", "meta": { - "lastUpdated": "<>" + "lastUpdated": "2025-07-15T10:12:10.102981+00:00" }, "processedSuggestions": [ { + "actions": [], "condition": "RSV", + "eligibilityCohorts": [], "status": "NotActionable", - "statusText": "You should have the RSV vaccine", - "eligibilityCohorts": [ - { - "cohortCode": "unknown_cohort_membership", - "cohortText": "Our records do not say why you are eligible", - "cohortStatus": "NotActionable" - } - ], + "statusText": "Status.not_actionable", "suitabilityRules": [ { - "ruleType": "S", "ruleCode": "AlreadyVaccinated", - "ruleText": "##You've had your RSV vaccination\nBased on our records, you recently had this vaccination.You do not need to do anything." + "ruleText": "##You've had your RSV vaccination\nWe believe you had your vaccination on <>.", + "ruleType": "S" } - ], - "actions": [] + ] } - ] -} + ], + "responseId": "fc41e4b4-818e-4958-a634-31d699592f3a" +} \ No newline at end of file diff --git a/tests/e2e/data/responses/AUTO_RSV_SB_013.json b/tests/e2e/data/responses/AUTO_RSV_SB_013.json index 3413b999b..249a18794 100644 --- a/tests/e2e/data/responses/AUTO_RSV_SB_013.json +++ b/tests/e2e/data/responses/AUTO_RSV_SB_013.json @@ -1,22 +1,22 @@ { - "responseId": "<>", "meta": { - "lastUpdated": "<>" + "lastUpdated": "2025-07-15T10:12:13.045041+00:00" }, "processedSuggestions": [ { + "actions": [], "condition": "RSV", - "status": "NotActionable", - "statusText": "You should have the RSV vaccine", "eligibilityCohorts": [], + "status": "NotActionable", + "statusText": "Status.not_actionable", "suitabilityRules": [ { - "ruleType": "S", "ruleCode": "AlreadyVaccinated", - "ruleText": "##You've had your RSV vaccination\nBased on our records, you recently had this vaccination. You do not need to do anything." + "ruleText": "##You've had your RSV vaccination\nWe believe you had your vaccination on <>.", + "ruleType": "S" } - ], - "actions": [] + ] } - ] -} + ], + "responseId": "a0d01de1-4e56-4b68-a8dc-5cfbca732490" +} \ No newline at end of file diff --git a/tests/e2e/data/responses/AUTO_RSV_SB_014.json b/tests/e2e/data/responses/AUTO_RSV_SB_014.json index 3858968e4..32c7e0c5f 100644 --- a/tests/e2e/data/responses/AUTO_RSV_SB_014.json +++ b/tests/e2e/data/responses/AUTO_RSV_SB_014.json @@ -1,33 +1,27 @@ { - "responseId": "<>", "meta": { - "lastUpdated": "<>" + "lastUpdated": "2025-07-15T10:12:15.890666+00:00" }, "processedSuggestions": [ { + "actions": [], "condition": "RSV", - "status": "NotEligible", - "statusText": "We do not believe you should have this vaccine", "eligibilityCohorts": [ { "cohortCode": "rsv_age_rolling", - "cohortText": "You are not aged 75 to 79 years old.", - "cohortStatus": "NotEligible" + "cohortStatus": "NotEligible", + "cohortText": "are not aged 75 to 79 years old." }, { "cohortCode": "rsv_age_catchup", - "cohortText": "You did not turn 80 between 2nd September 2024 and 31st August 2025", - "cohortStatus": "NotEligible" + "cohortStatus": "NotEligible", + "cohortText": "did not turn 80 between 2nd September 2024 and 31st August 2025" } ], - "suitabilityRules": [], - "actions": [ - { - "actionType": "CardWithText", - "actionCode": "HealtchareProInfo", - "description": "##If you think you need this vaccine\nSpeak to your healthcare professional if you think you should be offered this vaccination." - } - ] + "status": "NotEligible", + "statusText": "Status.not_eligible", + "suitabilityRules": [] } - ] -} + ], + "responseId": "c321bd59-df13-4e06-a61f-b08a82865cca" +} \ No newline at end of file diff --git a/tests/e2e/data/s3/AUTO_RSV_SB_001.json b/tests/e2e/data/s3/AUTO_RSV_SB_001.json new file mode 100644 index 000000000..bcf054503 --- /dev/null +++ b/tests/e2e/data/s3/AUTO_RSV_SB_001.json @@ -0,0 +1,273 @@ +{ + "CampaignConfig": { + "ID": "<>", + "Version": "1", + "Name": "Automation RSV - Smoke Test Config", + "Type": "V", + "Target": "RSV", + "Manager": "person1@nhs.net", + "Approver": "person1@nhs.net", + "Reviewer": "person1@nhs.net", + "IterationFrequency": "X", + "IterationType": "O", + "IterationTime": "07:00:00", + "DefaultCommsRouting": "BOOK_LOCAL", + "StartDate": "20250601", + "EndDate": "20260601", + "ApprovalMinimum": 0, + "ApprovalMaximum": 0, + "Iterations": [ + { + "ID": ",<>", + "DefaultCommsRouting": "BOOK_LOCAL", + "Version": "1", + "Name": "Automation RSV - Smoke Test Config Iteration", + "IterationDate": "20250601", + "IterationNumber": 1, + "CommsType": "I", + "ApprovalMinimum": 0, + "ApprovalMaximum": 0, + "Type": "O", + "IterationCohorts": [ + { + "CohortLabel": "rsv_75_rolling", + "CohortGroup": "rsv_age_rolling", + "PositiveDescription": "are aged 75 to 79 years old.", + "NegativeDescription": "are not aged 75 to 79 years old.", + "Priority": 0 + }, + { + "CohortLabel": "rsv_75to79_2024", + "CohortGroup": "rsv_age_catchup", + "PositiveDescription": "turned 80 between 2nd September 2024 and 31st August 2025", + "NegativeDescription": "did not turn 80 between 2nd September 2024 and 31st August 2025", + "Priority": 10 + }, + { + "CohortLabel": "elid_all_people", + "CohortGroup": "elid_all_people", + "PositiveDescription": "", + "NegativeDescription": "", + "Priority": 20 + }, + { + "CohortLabel": "no_group_description", + "CohortGroup": "", + "PositiveDescription": "", + "NegativeDescription": "", + "Priority": 30 + } + ], + "IterationRules": [ + { + "Type": "F", + "Name": "Assure only already vaccinated taken from magic cohort", + "Description": "Exclude anyone who has NOT been given a dose of RSV Vaccination from the magic cohort", + "Operator": "is_empty", + "Comparator": "", + "AttributeTarget": "RSV", + "AttributeLevel": "TARGET", + "AttributeName": "LAST_SUCCESSFUL_DATE", + "CohortLabel": "elid_all_people", + "Priority": 100 + }, + { + "Type": "F", + "Name": "Under Age - Under 75 Years on day of execution", + "Description": "Ensure anyone who has a PDS date of birth which determines their age to be less than 75 years is filtered out.", + "Priority": 120, + "AttributeLevel": "PERSON", + "AttributeName": "DATE_OF_BIRTH", + "Operator": "Y>", + "Comparator": "-75", + "CohortLabel": "rsv_75to79_2024" + }, + { + "Type": "F", + "Name": "Under Age - Under 75 Years on day of execution", + "Description": "Ensure anyone who has a PDS date of birth which determines their age to be less than 75 years is filtered out.", + "Priority": 125, + "AttributeLevel": "PERSON", + "AttributeName": "DATE_OF_BIRTH", + "Operator": "Y>", + "Comparator": "-75", + "CohortLabel": "rsv_75_rolling" + }, + { + "Type": "F", + "Name": "Exclude Too OLD", + "Description": "Exclude anyone over 80", + "Priority": 130, + "AttributeLevel": "PERSON", + "AttributeName": "DATE_OF_BIRTH", + "Operator": "<", + "Comparator": "-80" + }, + { + "Type": "S", + "Name": "AlreadyVaccinated", + "Description": "##You've had your RSV vaccination\nWe believe you had your vaccination on <>.", + "Priority": 550, + "AttributeLevel": "TARGET", + "AttributeTarget": "RSV", + "AttributeName": "LAST_SUCCESSFUL_DATE", + "Operator": "Y>=", + "Comparator": "-25", + "RuleStop": "Y" + }, + { + "Type": "S", + "Name": "NotAvailable", + "Description": "NotAvailable|Vaccinations are not currently available.", + "Priority": 510, + "AttributeLevel": "PERSON", + "AttributeName": "ICB", + "Operator": "=", + "Comparator": "SUPPRESSED_ICB" + }, + { + "Type": "S", + "Name": "NotYetDue", + "Description": "NotYetDue|Your next dose is not yet due.", + "Priority": 520, + "AttributeTarget": "RSV", + "AttributeLevel": "TARGET", + "AttributeName": "LAST_SUCCESSFUL_DATE", + "Operator": "=", + "Comparator": "20250326", + "RuleStop": "Y" + }, + { + "Type": "S", + "Name": "TooClose", + "Description": "TooClose|Your previous vaccination was less than 91 days ago.", + "Priority": 530, + "AttributeTarget": "RSV", + "AttributeLevel": "TARGET", + "AttributeName": "LAST_SUCCESSFUL_DATE", + "Operator": "=", + "Comparator": "20250327", + "RuleStop": "Y" + }, + { + "Type": "S", + "Name": "OtherSetting", + "Description": "OtherSetting|## Getting the vaccine\n\nOur record show you're living in a setting where care is provided.\n\nIf you think you should have the RSV vaccine, speak to a member of staff where you live.", + "Priority": 540, + "AttributeLevel": "PERSON", + "AttributeName": "CARE_HOME_FLAG", + "Operator": "=", + "Comparator": "Y" + }, + { + "Type": "R", + "Name": "Actionable Not Vaccinated", + "Description": "Book An Appointment", + "Priority": 1010, + "Operator": "is_empty", + "Comparator": "", + "AttributeTarget": "RSV", + "AttributeLevel": "TARGET", + "AttributeName": "LAST_SUCCESSFUL_DATE", + "CommsRouting": "BOOK_NBS" + }, + { + "Type": "R", + "Name": "Actionable Not Vaccinated", + "Description": "Book An Appointment", + "Priority": 1010, + "Operator": "=", + "Comparator": "LS2", + "AttributeLevel": "PERSON", + "AttributeName": "POSTCODE_SECTOR", + "CommsRouting": "BOOK_NBS" + }, + { + "Type": "R", + "Name": "Actionable Future Booked Appointment", + "Description": "Actionable Future Booked Appointment", + "Priority": 1020, + "Operator": ">=", + "Comparator": "0", + "AttributeTarget": "RSV", + "AttributeLevel": "TARGET", + "AttributeName": "BOOKED_APPOINTMENT_DATE", + "CommsRouting": "AMEND_NBS" + }, + { + "Type": "R", + "Name": "Actionable Future Booked Appointment", + "Description": "Actionable Future Booked Appointment", + "Priority": 1020, + "Operator": "=", + "Comparator": "NBS", + "AttributeTarget": "RSV", + "AttributeLevel": "TARGET", + "AttributeName": "BOOKED_APPOINTMENT_PROVIDER", + "CommsRouting": "AMEND_NBS" + }, + { + "Type": "R", + "Name": "Actionable Future Booked Appointment", + "Description": "Actionable Future Booked Appointment", + "Priority": 1030, + "Operator": ">=", + "Comparator": "0", + "AttributeTarget": "RSV", + "AttributeLevel": "TARGET", + "AttributeName": "BOOKED_APPOINTMENT_DATE", + "CommsRouting": "MANAGE_LOCAL" + }, + { + "Type": "R", + "Name": "Actionable Future Booked Appointment", + "Description": "Actionable Future Booked Appointment", + "Priority": 1030, + "Operator": "!=", + "Comparator": "NBS", + "AttributeTarget": "RSV", + "AttributeLevel": "TARGET", + "AttributeName": "BOOKED_APPOINTMENT_PROVIDER", + "CommsRouting": "MANAGE_LOCAL" + } + ], + "ActionsMapper": { + "BOOK_NBS": { + "ExternalRoutingCode": "BookNBS", + "ActionDescription": "", + "ActionType": "ButtonWithAuthLink", + "UrlLink": "http://www.nhs.uk/book-rsv", + "UrlLabel": "Continue to booking" + }, + "AMEND_NBS": { + "ExternalRoutingCode": "AmendNBS", + "ActionDescription": "##You have an RSV vaccination appointment\nYou can view, change or cancel your appointment below.", + "ActionType": "ButtonWithAuthLink", + "UrlLink": "http://www.nhs.uk/book-rsv", + "UrlLabel": "Manage your appointment" + }, + "CONTACT_GP": { + "ExternalRoutingCode": "ContactGP", + "ActionDescription": "Contact your GP", + "ActionType": "InfoText" + }, + "BOOK_LOCAL": { + "ExternalRoutingCode": "BookLocal", + "ActionDescription": "##Getting the vaccine\nYou can get an RSV vaccination at your GP surgery.\nYour GP surgery may contact you about getting the RSV vaccine. This may be by letter, text, phone call, email or through the NHS App. You do not need to wait to be contacted before booking your vaccination.", + "ActionType": "InfoText" + }, + "MANAGE_LOCAL": { + "ExternalRoutingCode": "ManageLocal", + "ActionDescription": "##You have an RSV vaccination appointment\nContact your healthcare provider to change or cancel your appointment.", + "ActionType": "CardWithText" + }, + "CHECK_CORRECT": { + "ExternalRoutingCode": "CheckCorrect", + "ActionDescription": "##If you think this is incorrect\\nIf you have not had this vaccination and you think you should, speak to your healthcare professional.", + "ActionType": "InfoText" + } + } + } + ] + } +} diff --git a/tests/e2e/data/s3/patient_s3_sample.json b/tests/e2e/data/s3/patient_s3_sample.json deleted file mode 100644 index 3e7b93748..000000000 --- a/tests/e2e/data/s3/patient_s3_sample.json +++ /dev/null @@ -1,59 +0,0 @@ -{ - "PATIENT": { - "NHS_NUMBER": "9876543210", - "SCENARIO": "SCN005", - "PERSON_ATTRIBUTES": { - "DATE_OF_BIRTH": "19880615", - "GENDER": "1", - "POSTCODE": "M1 1AA", - "POSTCODE_SECTOR": "M1", - "POSTCODE_OUTCODE": "1AA", - "MSOA": "E05000003", - "LSOA": "E01000003", - "GP_PRACTICE_CODE": "C98765", - "PCN": "PCN003", - "ICB": "ICB002", - "COMMISSIONING_REGION": "RegionZ", - "13Q_FLAG": "Y", - "CARE_HOME_FLAG": "N", - "DE_FLAG": "N" - }, - "TARGET_CONDITIONS": { - "COVID": { - "VALID_DOSES_COUNT": "2", - "INVALID_DOSES_COUNT": "0", - "LAST_SUCCESSFUL_DATE": "20241201", - "LAST_VALID_DOSE_DATE": "20241201", - "BOOKED_APPOINTMENT_DATE": "20250110", - "BOOKED_APPOINTMENT_PROVIDER": "Local Clinic", - "LAST_INVITE_DATE": "20241115", - "LAST_INVITE_STATUS": "Accepted", - "OPTOUT": "N" - }, - "FLU": { - "VALID_DOSES_COUNT": "1", - "INVALID_DOSES_COUNT": "0", - "LAST_SUCCESSFUL_DATE": "20241020", - "LAST_VALID_DOSE_DATE": "20241020", - "LAST_INVITE_DATE": "20240901", - "LAST_INVITE_STATUS": "Delivered", - "OPTOUT": "N" - }, - "RSV": { - "LAST_SUCCESSFUL_DATE": "20241105", - "LAST_VALID_DOSE_DATE": "20241105", - "OPTOUT": "N" - } - }, - "COHORT_MEMBERSHIP": [ - { - "cohort_label": "Trial_Group_A", - "date_joined": "20250210" - }, - { - "cohort_label": "Manchester_Residents_2024", - "date_joined": "20240701" - } - ] - } -} diff --git a/tests/e2e/features/conftest.py b/tests/e2e/features/conftest.py deleted file mode 100644 index d309566c5..000000000 --- a/tests/e2e/features/conftest.py +++ /dev/null @@ -1,32 +0,0 @@ -import os - -import pytest -import requests -from dotenv import load_dotenv - -# Load environment variables -load_dotenv() - -# Constants -BASE_URL = os.getenv("BASE_URL", "https://sandbox.api.service.nhs.uk/eligibility-signposting-api") -API_KEY = os.getenv("API_KEY", "") -VALID_NHS_NUMBER = os.getenv("VALID_NHS_NUMBER", "50000000004") -HTTP_STATUS_SERVER_ERROR = 500 - - -@pytest.fixture(scope="session", autouse=True) -def check_api_accessibility(): - """Check if the API is accessible before running tests.""" - try: - response = requests.get( - f"{BASE_URL}/eligibility-check", - params={"patient": VALID_NHS_NUMBER}, - headers={"apikey": API_KEY, "Accept": "application/json"}, - timeout=5, - ) - # If we get a 4xx response, the API is accessible but our request is invalid - # If we get a 5xx response, the API is having issues - if response.status_code >= HTTP_STATUS_SERVER_ERROR: - pytest.skip("API is returning server errors") - except (requests.RequestException, requests.Timeout): - pytest.skip("API is not accessible") diff --git a/tests/e2e/features/eligibility_check/eligibility_check.feature b/tests/e2e/features/eligibility_check/eligibility_check.feature index e10353c07..d690bbc80 100644 --- a/tests/e2e/features/eligibility_check/eligibility_check.feature +++ b/tests/e2e/features/eligibility_check/eligibility_check.feature @@ -1,44 +1,32 @@ - -Feature: Eligibility Check API - As a consumer of the Eligibility Check API - I want to verify the endpoint's response for various NHS numbers and parameters - So that I can ensure the API behaves as expected for all supported scenarios +Feature: Full mTLS integration with real Eligibility API Background: - Given the Eligibility Check API base URL is configured - - Scenario Outline: Successful eligibility check returns 2xx and valid response - Given I have the NHS number "" - When I request an eligibility check for the NHS number - Then the response status code should be 2xx - And the response content type should be application/json - And the response should have a JSON body - And the response should match the eligibility check schema - - Examples: - | nhs_number | - | 50000000001 | - | 50000000004 | - | 9876543210 | + Given AWS credentials are loaded from the environment + And mTLS certificates are downloaded and available in the out/ directory - Scenario Outline: Eligibility check with invalid or missing NHS number returns error + Scenario Outline: Eligibility check returns 2xx response for NHS number queries + Given I generate the test data files + And I upload the test data files to DynamoDB Given I have the NHS number "" - When I request an eligibility check for the NHS number - Then the response status code should be 4xx or 404 - - Examples: - | nhs_number | - | 00000000000 | - | | - | patient=ABC | + When I query the eligibility API using the headers: - Scenario Outline: Eligibility check with custom Accept header - Given I have the NHS number "" - And I set the Accept header to "" - When I request an eligibility check for the NHS number - Then the response content type should contain "" + Then the response status code should be 200 + And the response should be matching the JSON "" + Then I clean up DynamoDB test data Examples: - | nhs_number | accept_header | expected_content_type | - | 9876543210 | application/json | application/json | - | 9876543210 | application/json | application/json | + | nhs_number | json_response | + | 5000000001 | AUTO_RSV_SB_001.json | + | 5000000002 | AUTO_RSV_SB_002.json | + | 5000000003 | AUTO_RSV_SB_003.json | + | 5000000004 | AUTO_RSV_SB_004.json | + | 5000000005 | AUTO_RSV_SB_005.json | + | 5000000006 | AUTO_RSV_SB_006.json | + | 5000000007 | AUTO_RSV_SB_007.json | + | 5000000008 | AUTO_RSV_SB_008.json | + | 5000000009 | AUTO_RSV_SB_009.json | + | 5000000010 | AUTO_RSV_SB_010.json | + | 5000000011 | AUTO_RSV_SB_011.json | + | 5000000012 | AUTO_RSV_SB_012.json | + | 5000000013 | AUTO_RSV_SB_013.json | + | 5000000014 | AUTO_RSV_SB_014.json | diff --git a/tests/e2e/features/environment.py b/tests/e2e/features/environment.py index cfb2becac..b02481a38 100644 --- a/tests/e2e/features/environment.py +++ b/tests/e2e/features/environment.py @@ -1,4 +1,3 @@ -import json import logging import os from pathlib import Path @@ -12,89 +11,40 @@ def _load_environment_variables(context): - load_dotenv(dotenv_path=".env") - context.base_url = os.getenv("BASE_URL") - context.api_key = os.getenv("API_KEY") + try: + load_dotenv(dotenv_path=".env") + logger.info("Loaded environment variables from .env file") + except OSError as e: + logger.warning("Failed to load .env file: %s", e) + + context.base_url = os.getenv("BASE_URL", "http://localhost:8000") + context.api_key = os.getenv("API_KEY", "test-api-key") context.valid_nhs_number = os.getenv("VALID_NHS_NUMBER", "50000000004") context.aws_region = os.getenv("AWS_REGION", "eu-west-2") - context.inserted_items = [] + context.aws_access_key_id = os.getenv("AWS_ACCESS_KEY_ID") + context.aws_secret_access_key = os.getenv("AWS_SECRET_ACCESS_KEY") + context.aws_session_token = os.getenv("AWS_SESSION_TOKEN") context.abort_on_aws_error = os.getenv("ABORT_ON_AWS_FAILURE", "false").lower() == "true" context.keep_seed = os.getenv("KEEP_SEED", "false").lower() == "true" + context.dynamodb_table_name = os.getenv("DYNAMODB_TABLE_NAME", "eligibilty_data_store") context.s3_bucket = os.getenv("S3_BUCKET_NAME") context.s3_upload_dir = os.getenv("S3_UPLOAD_DIR", "") context.s3_data_path = Path(os.getenv("S3_JSON_SOURCE_DIR", "./data/s3")).resolve() - context.dynamodb_table_name = os.getenv("DYNAMODB_TABLE_NAME", "eligibilty_data_store") - context.dynamo_data_path = Path(os.getenv("DYNAMO_JSON_SOURCE_DIR", "./data/out/dynamoDB")).resolve() + context.api_gateway_url = os.getenv("API_GATEWAY_URL", "https://test.eligibility-signposting-api.nhs.uk") + logger.info("ABORT_ON_AWS_FAILURE=%s", context.abort_on_aws_error) logger.info("KEEP_SEED=%s", context.keep_seed) - - -def _connect_to_dynamodb(context): - try: - context.dynamodb = boto3.resource("dynamodb", region_name=context.aws_region) - context.table = context.dynamodb.Table(context.dynamodb_table_name) - _ = context.table.table_status - except (boto3.exceptions.Boto3Error, BotoCoreError): - logger.exception("DynamoDB not accessible") - return False - else: - logger.info("Connected to DynamoDB table: %s", context.dynamodb_table_name) - return True - - -def _get_dynamo_seed_files(context): - if not context.dynamo_data_path.exists() or not context.dynamo_data_path.is_dir(): - logger.error("Seed directory not found: %s", context.dynamo_data_path) - return [] - return list(context.dynamo_data_path.glob("*.json")) - - -def _load_seed_file(file_path: Path): - try: - with file_path.open() as f: - return json.load(f) - except (OSError, json.JSONDecodeError): - logger.exception("Failed to load seed file: %s", file_path) - return [] - - -def _insert_dynamodb_items(context, items): - for item in items: - try: - context.table.put_item(Item=item) - context.inserted_items.append(item) - except (boto3.exceptions.Boto3Error, BotoCoreError): - logger.exception("Failed to insert item %s", item.get("PK", "")) - - -def _setup_dynamodb(context): - if not _connect_to_dynamodb(context): - if context.abort_on_aws_error: - context.abort_all = True - return False - json_files = _get_dynamo_seed_files(context) - if not json_files: - logger.error("No JSON files found in the directory: %s", context.dynamo_data_path) - if context.abort_on_aws_error: - context.abort_all = True - return False - logger.info("Found %d JSON files to insert into DynamoDB", len(json_files)) - for file_path in json_files: - items = _load_seed_file(file_path) - if not items: - if context.abort_on_aws_error: - context.abort_all = True - continue - logger.info("Inserting %d items from %s...", len(items), file_path.name) - _insert_dynamodb_items(context, items) - logger.info("Inserted %d items from %d files", len(context.inserted_items), len(json_files)) - return True + logger.info("BASE_URL: %s", context.base_url) + logger.info("AWS_REGION: %s", context.aws_region) + logger.info("DYNAMODB_TABLE: %s", context.dynamodb_table_name) + logger.info("S3_BUCKET: %s", context.s3_bucket) def _setup_s3(context): if not context.s3_bucket: logger.info("Skipping S3 upload โ€” no S3_BUCKET_NAME set.") return True + logger.info( "Uploading JSON files from %s to S3 bucket: %s/%s", context.s3_data_path, @@ -106,84 +56,98 @@ def _setup_s3(context): if not context.s3_data_path.exists(): logger.error("S3 source directory not found: %s", context.s3_data_path) return False + json_files = list(context.s3_data_path.glob("*.json")) - upload_success = True for file_path in json_files: key = f"{context.s3_upload_dir}/{file_path.name}" if context.s3_upload_dir else file_path.name try: s3_client.upload_file(str(file_path), context.s3_bucket, key) logger.info("Uploaded %s to s3://%s/%s", file_path.name, context.s3_bucket, key) - except (boto3.exceptions.Boto3Error, BotoCoreError): + except (Exception, BotoCoreError): logger.exception("Failed to upload %s", file_path.name) - upload_success = False - except (boto3.exceptions.Boto3Error, BotoCoreError): + except (Exception, BotoCoreError): logger.exception("S3 upload setup failed") if context.abort_on_aws_error: context.abort_all = True return False - else: - return upload_success + return True def before_all(context): logger.info("Loading .env and initializing AWS fixtures...") _load_environment_variables(context) - _setup_dynamodb(context) - _setup_s3(context) + + context.aws_available = True + try: + logger.info("Setting up S3 (optional)...") + _setup_s3(context) + except OSError as e: + logger.warning("AWS setup failed: %s", e) + context.aws_available = False def before_scenario(context, scenario): if getattr(context, "abort_all", False): + logger.warning("Skipping scenario '%s' due to setup failure", scenario.name) scenario.skip("Skipping scenario due to setup failure") - if "requires_dynamodb" in scenario.tags and not context.inserted_items: - scenario.skip("Skipping due to missing seeded DynamoDB data") + return + logger.info("Running scenario: %s", scenario.name) -def _cleanup_dynamodb(context): - if not context.inserted_items: - logger.info("No items were inserted โ€” skipping DynamoDB cleanup.") - return - logger.info("Cleaning up seeded items from DynamoDB...") - delete_count = 0 - for item in context.inserted_items: - nhs_number = item.get("NHS_NUMBER") - attribute_type = item.get("ATTRIBUTE_TYPE") - if nhs_number and attribute_type: - try: - context.table.delete_item(Key={"NHS_NUMBER": nhs_number, "ATTRIBUTE_TYPE": attribute_type}) - delete_count += 1 - except (boto3.exceptions.Boto3Error, BotoCoreError): - logger.exception("Failed to delete item (%s, %s)", nhs_number, attribute_type) - else: - logger.error("Cannot delete item โ€” missing NHS_NUMBER or ATTRIBUTE_TYPE: %s", item) - logger.info("Deleted %d/%d DynamoDB items", delete_count, len(context.inserted_items)) - - -def _cleanup_s3(context): - if not (context.s3_bucket and context.s3_data_path.exists()): - logger.info("Skipping S3 cleanup โ€” no bucket or source directory not found.") + +def before_feature(context, feature): + """Initialize feature-level context for data setup tracking.""" + context.feature_data_setup_done = False + context.feature_dynamodb_items_count = 0 + context.feature_uploader = None + logger.info("Initialized feature context for: %s", feature.name) + + +def after_feature(context, feature): + """Cleanup feature-level DynamoDB data.""" + if getattr(context, "keep_seed", False): + logger.info( + "KEEP_SEED=true โ€” skipping feature-level DynamoDB cleanup for: %s", + feature.name, + ) return - logger.info("Cleaning up uploaded files from S3...") - try: - s3_client = boto3.client("s3", region_name=context.aws_region) - json_files = list(context.s3_data_path.glob("*.json")) - deleted_files = 0 - for file_path in json_files: - key = f"{context.s3_upload_dir}/{file_path.name}" if context.s3_upload_dir else file_path.name + + if hasattr(context, "feature_uploader") and context.feature_uploader: + if context.feature_dynamodb_items_count > 0: + logger.info( + "Cleaning up %d DynamoDB items for feature: %s", + context.feature_dynamodb_items_count, + feature.name, + ) try: - s3_client.delete_object(Bucket=context.s3_bucket, Key=key) - logger.info("Deleted s3://%s/%s", context.s3_bucket, key) - deleted_files += 1 - except (boto3.exceptions.Boto3Error, BotoCoreError): - logger.exception("Failed to delete s3://%s/%s", context.s3_bucket, key) - logger.info("Deleted %d/%d files from S3", deleted_files, len(json_files)) - except (boto3.exceptions.Boto3Error, BotoCoreError): - logger.exception("S3 cleanup failed") + # Use the uploader's cleanup method if available + if hasattr(context.feature_uploader, "delete_data"): + context.feature_uploader.delete_data() + logger.info( + "Successfully cleaned up DynamoDB data for feature: %s", + feature.name, + ) + except Exception: + logger.exception("Failed to cleanup DynamoDB data for feature: %s", feature.name) def after_all(context): - if getattr(context, "keep_seed", False): + if context.keep_seed: logger.info("KEEP_SEED=true โ€” skipping cleanup.") return - _cleanup_dynamodb(context) - _cleanup_s3(context) + + # Cleanup S3 if necessary (optional) + if context.s3_bucket and context.s3_data_path.exists(): + logger.info("Cleaning up uploaded files from S3...") + try: + s3_client = boto3.client("s3", region_name=context.aws_region) + json_files = list(context.s3_data_path.glob("*.json")) + for file_path in json_files: + key = f"{context.s3_upload_dir}/{file_path.name}" if context.s3_upload_dir else file_path.name + try: + s3_client.delete_object(Bucket=context.s3_bucket, Key=key) + logger.info("Deleted s3://%s/%s", context.s3_bucket, key) + except (Exception, BotoCoreError): + logger.exception("Failed to delete s3://%s/%s", context.s3_bucket, key) + except Exception: + logger.exception("S3 cleanup failed") diff --git a/tests/e2e/features/steps/eligibility_check_steps.py b/tests/e2e/features/steps/eligibility_check_steps.py index 3a0640dab..7b5d90e09 100644 --- a/tests/e2e/features/steps/eligibility_check_steps.py +++ b/tests/e2e/features/steps/eligibility_check_steps.py @@ -1,20 +1,169 @@ -import jsonschema -import pytest +import json +import logging +import os +from difflib import unified_diff +from pathlib import Path + +import boto3 import requests from behave import given, then, when -from utils.config import API_KEY, BASE_URL, ELIGIBILITY_CHECK_SCHEMA +from botocore.exceptions import ClientError +from helpers.dynamodb_data_generator import DateVariableResolver, JsonTestDataProcessor +from helpers.dynamodb_data_uploader import DynamoDBDataUploader + +logger = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + +# API endpoints +API_BASE_URL = os.getenv("API_BASE_URL", "https://" + "test" + ".eligibility-signposting-api.nhs.uk") + +# SSM Parameter paths +SSM_BASE_PATH = "/" + "test" + "/mtls" +CERT_PARAMS = { + "private_key": f"{SSM_BASE_PATH}/api_private_key_cert", + "client_cert": f"{SSM_BASE_PATH}/api_client_cert", + "ca_cert": f"{SSM_BASE_PATH}/api_ca_cert", +} + + +def remove_ignored_properties(obj): + """ + Recursively remove specified properties from JSON objects before comparison. + Creates deep copies to avoid modifying original data. + + Properties to remove: + - meta (and all its sub-properties) + - responseId + + Args: + obj: The JSON object (dict, list, or primitive) to process + + Returns: + A deep copy of the object with specified properties removed + """ + if isinstance(obj, dict): + # Create a new dict excluding the ignored properties + filtered_dict = {} + for key, value in obj.items(): + if key not in ["meta", "responseId"]: + filtered_dict[key] = remove_ignored_properties(value) + return filtered_dict + if isinstance(obj, list): + # Recursively process each item in the list + return [remove_ignored_properties(item) for item in obj] + # Return primitive values as-is + return obj + + +@given("AWS credentials are loaded from the environment") +def step_impl_load_aws_credentials(context): + """Load AWS credentials from environment variables.""" + context.aws_region = os.getenv("AWS_REGION", "eu-west-2") + context.aws_access_key_id = os.getenv("AWS_ACCESS_KEY_ID") + context.aws_secret_access_key = os.getenv("AWS_SECRET_ACCESS_KEY") + context.aws_session_token = os.getenv("AWS_SESSION_TOKEN") + context.dynamodb_table_name = os.getenv("DYNAMODB_TABLE_NAME", "eligibilty_data_store") + + missing = [] + if not context.aws_region: + missing.append("AWS_REGION") + if not context.aws_access_key_id: + missing.append("AWS_ACCESS_KEY_ID") + if not context.aws_secret_access_key: + missing.append("AWS_SECRET_ACCESS_KEY") + + assert not missing, f"Missing required environment variables: {', '.join(missing)}" + + logger.info("AWS credentials loaded successfully") + -# HTTP Status Code Constants -HTTP_STATUS_OK = 200 -HTTP_STATUS_BAD_REQUEST = 400 -HTTP_STATUS_NOT_FOUND = 404 -HTTP_STATUS_SERVER_ERROR = 500 +@given("mTLS certificates are downloaded and available in the out/ directory") +def step_impl_download_certificates(context): + """Retrieve mTLS certs from SSM and write them to local files.""" + cert_dir = Path("./data/out") + cert_dir.mkdir(parents=True, exist_ok=True) -@given("the Eligibility Check API base URL is configured") -def step_impl_base_url(context): - context.base_url = BASE_URL - context.headers = {"apikey": API_KEY} + ssm = boto3.client( + "ssm", + region_name=context.aws_region, + aws_access_key_id=context.aws_access_key_id, + aws_secret_access_key=context.aws_secret_access_key, + aws_session_token=context.aws_session_token, + ) + + context.cert_paths = {} + for cert_type, param_name in CERT_PARAMS.items(): + cert_path = cert_dir / f"{cert_type}.pem" + try: + logger.info("Retrieving SSM parameter: %s", param_name) + response = ssm.get_parameter(Name=param_name, WithDecryption=True) + with cert_path.open("w") as f: + f.write(response["Parameter"]["Value"]) + context.cert_paths[cert_type] = str(cert_path) + except ClientError as e: + msg = f"Failed to retrieve parameter {param_name}: {e}" + raise RuntimeError(msg) from e + + logger.info("mTLS certificates written to local files") + + +@given("I generate the test data files") +def step_impl_generate_data(context): + """Generate test data files with resolved <> placeholders - once per feature.""" + if getattr(context, "feature_data_setup_done", False): + logger.info("Test data already generated for this feature, skipping generation...") + return + + logger.info("Generating test data files for feature...") + input_dir = Path("data/in/dynamoDB").resolve() + output_dir = Path("data/out/dynamoDB").resolve() + + resolver = DateVariableResolver() + processor = JsonTestDataProcessor(input_dir, output_dir, resolver) + + if not input_dir.exists(): + logger.error("Input directory does not exist: %s", input_dir) + return + + logger.info("Scanning for JSON files in directory: %s", input_dir) + count = 0 + for root, _, files in os.walk(input_dir): + for file in files: + if file.endswith(".json"): + full_path = Path(root) / file + processor.process_file(full_path) + count += 1 + + if count == 0: + logger.warning("No .json files found in %s", input_dir) + else: + logger.info("Processed %d test data file(s) for feature.", count) + + +@given("I upload the test data files to DynamoDB") +def step_impl_upload_data(context): + """Upload generated test data to DynamoDB - once per feature.""" + if getattr(context, "feature_data_setup_done", False): + logger.info("Test data already uploaded for this feature, skipping upload...") + return + + logger.info("Uploading test data to DynamoDB for feature...") + uploader = DynamoDBDataUploader( + aws_region=context.aws_region, + access_key=context.aws_access_key_id, + secret_key=context.aws_secret_access_key, + session_token=context.aws_session_token, + ) + inserted = uploader.upload_files_from_path(table_name=context.dynamodb_table_name, path=Path("data/out/dynamoDB")) + assert inserted > 0, "No data uploaded to DynamoDB" + + # Store for feature-level cleanup + context.feature_uploader = uploader + context.feature_dynamodb_items_count = inserted + context.feature_data_setup_done = True + + logger.info("Uploaded %d items to DynamoDB (feature-level)", inserted) @given('I have the NHS number "{nhs_number}"') @@ -22,64 +171,169 @@ def step_impl_nhs_number(context, nhs_number): context.nhs_number = nhs_number -@given('I have the NHS number ""') -def step_impl_empty_nhs_number(context): - context.nhs_number = "" +@then("I clean up DynamoDB test data") +def step_impl_cleanup_dynamo(context): + """Clean up DynamoDB test data - handled at feature level now.""" + logger.info("DynamoDB cleanup will be handled at feature level - skipping scenario-level cleanup") + # This step becomes a no-op since cleanup is handled in after_feature hook -@given('I set the Accept header to "{accept_header}"') -def step_impl_accept_header(context, accept_header): - context.headers["Accept"] = accept_header +def parse_headers_table(table): + """Parse behave table into headers dictionary.""" + headers = {} + if not table: + return headers + for row in table: + if len(row.cells) != 2: + raise ValueError(f"Expected 2 columns in headers table, got {len(row.cells)}") + key, value = row.cells[0], row.cells[1] + headers[key] = value + return headers -@when("I request an eligibility check for the NHS number") -def step_impl_request_eligibility_check(context): - # Use the correct endpoint: /patient-check/{nhs_number} - if context.nhs_number: - url = f"{context.base_url}/patient-check/{context.nhs_number}" - else: - url = f"{context.base_url}/patient-check/" - context.response = requests.get(url, headers=context.headers, timeout=10) +def build_request_headers(context, dynamic_headers=None): + """Build complete headers for API request.""" + # Start with required default header + headers = {"nhs-login-nhs-number": context.nhs_number} -@then("the response status code should be 2xx") -def step_impl_status_code_2xx(context): - assert HTTP_STATUS_OK <= context.response.status_code < HTTP_STATUS_BAD_REQUEST, ( - f"Expected 2xx, got {context.response.status_code}" - ) + # Add dynamic headers if provided + if dynamic_headers: + headers.update(dynamic_headers) + return headers -@then("the response status code should be 4xx or 404") -def step_impl_status_code_4xx_or_404(context): - assert ( - HTTP_STATUS_BAD_REQUEST <= context.response.status_code < HTTP_STATUS_SERVER_ERROR - ) or context.response.status_code == HTTP_STATUS_NOT_FOUND, ( - f"Expected 4xx or 404, got {context.response.status_code}" - ) +def make_eligibility_api_call(context, headers): + """Make mTLS call to Eligibility API with provided headers.""" + if not hasattr(context, "nhs_number"): + msg = "NHS number not set in context." + raise AssertionError(msg) -@then("the response content type should be application/json") -def step_impl_content_type_json(context): - assert "application/json" in context.response.headers.get("Content-Type", ""), ( - f"Content-Type is not application/json, got {context.response.headers.get('Content-Type', '')}" - ) + if not hasattr(context, "cert_paths"): + msg = "mTLS certificate paths not present in context." + raise AssertionError(msg) + api_url = f"{API_BASE_URL}/patient-check/{context.nhs_number}" + cert = (context.cert_paths["client_cert"], context.cert_paths["private_key"]) + verify = False + + # Log headers for debugging (excluding sensitive values) + safe_headers = {k: ("***" if "token" in k.lower() or "key" in k.lower() else v) for k, v in headers.items()} + logger.info("Querying Eligibility API at %s with headers: %s", api_url, safe_headers) -@then("the response should have a JSON body") -def step_impl_has_json_body(context): try: - context.response.json() - except (ValueError, TypeError) as e: - pytest.fail(f"Response does not have a JSON body: {e}") + response = requests.get(api_url, cert=cert, verify=verify, timeout=30, headers=headers) + context.response = response + logger.info( + "Querying Eligibility API response %s - %d", + response.apparent_encoding, + response.status_code, + ) + except requests.exceptions.RequestException as e: + msg = f"API request failed: {e}" + raise RuntimeError(msg) from e -@then("the response should match the eligibility check schema") -def step_impl_schema(context): - jsonschema.validate(instance=context.response.json(), schema=ELIGIBILITY_CHECK_SCHEMA) +@when("I query the eligibility API") +def step_impl_call_eligibility_api(context): + """Make mTLS call to Eligibility API using local certs and context NHS number.""" + headers = build_request_headers(context) + make_eligibility_api_call(context, headers) -@then('the response content type should contain "{expected_content_type}"') -def step_impl_content_type_contains(context, expected_content_type): - assert expected_content_type in context.response.headers.get("Content-Type", ""), ( - f"Content-Type does not contain {expected_content_type}, got {context.response.headers.get('Content-Type', '')}" - ) +@when("I query the eligibility API using the headers:") +def step_impl_call_eligibility_api_with_headers(context): + """Make mTLS call to Eligibility API with dynamic headers from table.""" + try: + # Parse headers table + dynamic_headers = parse_headers_table(context.table) + logger.info("Parsed %d dynamic headers from table", len(dynamic_headers)) + + # Build complete headers + headers = build_request_headers(context, dynamic_headers) + + # Make API call + make_eligibility_api_call(context, headers) + + except ValueError as e: + msg = f"Invalid headers table format: {e}" + raise AssertionError(msg) from e + except Exception as e: + msg = f"Failed to process headers table: {e}" + raise RuntimeError(msg) from e + + +@then("the response status code should be {status_code:d}") +def step_impl_check_status_code(context, status_code): + """Assert response HTTP status code.""" + if not hasattr(context, "response"): + msg = "No HTTP response in context." + raise AssertionError(msg) + actual = context.response.status_code + assert actual == status_code, f"Expected status {status_code}, got {actual}" + + +@then("the response should be valid JSON") +def step_impl_validate_json(context): + """Assert that response content is valid JSON.""" + if not hasattr(context, "response"): + msg = "No HTTP response in context." + raise AssertionError(msg) + + try: + context.json_response = context.response.json() + except ValueError as e: + msg = f"Response is not valid JSON: {e}" + raise AssertionError(msg) from e + + +@then('the response should be matching the JSON "{json_response}"') +def step_impl_match_json_response(context, json_response): + """ + Assert that the API response matches the expected JSON file. + """ + expected_path = os.path.join(Path(__file__).parent.parent.parent, "data", "responses", json_response) + if not os.path.isfile(expected_path): + raise AssertionError(f"Expected JSON file not found: {expected_path}") + + with open(expected_path, encoding="utf-8") as f: + expected = json.load(f) + + try: + actual = context.response.json() + print("> Actual JSON structure: ", actual) + + name, ext = os.path.splitext(json_response) + actual_filename = f"{name}{ext}" + actual_path = os.path.join( + Path(__file__).parent.parent.parent, + "data", + "responses", + actual_filename, + ) + + os.makedirs(os.path.dirname(actual_path), exist_ok=True) + + with open(actual_path, "w", encoding="utf-8") as f: + json.dump(actual, f, indent=2, sort_keys=True) + + except Exception as e: + raise AssertionError(f"Failed to parse API response as JSON: {e}") + + filtered_expected = remove_ignored_properties(expected) + filtered_actual = remove_ignored_properties(actual) + + if filtered_expected != filtered_actual: + expected_str = json.dumps(filtered_expected, indent=2, sort_keys=True) + actual_str = json.dumps(filtered_actual, indent=2, sort_keys=True) + diff = "\n".join( + unified_diff( + expected_str.splitlines(), + actual_str.splitlines(), + fromfile="expected", + tofile="actual", + lineterm="", + ) + ) + raise AssertionError(f"Response JSON does not match expected.\nDiff:\n{diff}") diff --git a/tests/e2e/features/steps/helpers/dynamodb_data_generator.py b/tests/e2e/features/steps/helpers/dynamodb_data_generator.py new file mode 100644 index 000000000..423674f0b --- /dev/null +++ b/tests/e2e/features/steps/helpers/dynamodb_data_generator.py @@ -0,0 +1,92 @@ +import json +import logging +import re +from datetime import UTC, datetime, timedelta +from pathlib import Path +from typing import Any + +DATE_FORMAT = "%Y%m%d" +VAR_PATTERN = re.compile(r"<<([^<>]+)>>") +REQUIRED_TOKEN_PARTS = 3 +logger = logging.getLogger(__name__) + + +class DateVariableResolver: + def __init__(self, today: datetime | None = None): + self.today = today or datetime.now(tz=UTC) + + def resolve(self, token: str) -> str: + parts = token.split("_") + if len(parts) < REQUIRED_TOKEN_PARTS or parts[0].upper() != "DATE": + msg = f"Unsupported variable format: {token}" + raise ValueError(msg) + unit = parts[1].lower() + try: + offset = int(parts[2]) + except ValueError as e: + msg = f"Invalid offset value: {parts[2]}" + raise ValueError(msg) from e + if unit == "day": + return (self.today + timedelta(days=offset)).strftime(DATE_FORMAT) + if unit == "week": + return (self.today + timedelta(weeks=offset)).strftime(DATE_FORMAT) + if unit == "year": + return (self.today.replace(year=self.today.year + offset)).strftime(DATE_FORMAT) + if unit == "age": + try: + birth_date = self.today.replace(year=self.today.year - offset) + except ValueError: + birth_date = self.today.replace(month=2, day=28, year=self.today.year - offset) + return birth_date.strftime(DATE_FORMAT) + msg = f"Unsupported unit: {unit}" + raise ValueError(msg) + + +class JsonTestDataProcessor: + def __init__(self, input_dir: Path, output_dir: Path, resolver: DateVariableResolver): + self.input_dir = input_dir + self.output_dir = output_dir + self.resolver = resolver + + def resolve_placeholders(self, obj: Any) -> Any: + if isinstance(obj, dict): + return {k: self.resolve_placeholders(v) for k, v in obj.items()} + if isinstance(obj, list): + return [self.resolve_placeholders(item) for item in obj] + if isinstance(obj, str): + return VAR_PATTERN.sub(self._replace_token, obj) + return obj + + def _replace_token(self, match: re.Match) -> str: + token = match.group(1) + try: + return self.resolver.resolve(token) + except ValueError: + logger.warning("Failed to resolve variable: %s", token) + return match.group(0) + + def process_file(self, file_path: Path): + # logger.info("Processing file: %s", file_path) + try: + with file_path.open() as f: + content = json.load(f) + except Exception: + logger.exception("Failed to read file: %s", file_path) + return + try: + resolved = self.resolve_placeholders(content) + except Exception: + logger.exception("Failed to resolve placeholders in file: %s", file_path) + return + if "data" not in resolved: + logger.error("Missing 'data' key in file: %s", file_path) + return + relative_path = file_path.relative_to(self.input_dir) + output_path = self.output_dir / relative_path + output_path.parent.mkdir(parents=True, exist_ok=True) + try: + with output_path.open("w") as f: + json.dump(resolved["data"], f, indent=2) + # logger.info("Written resolved file: %s", output_path) + except Exception: + logger.exception("Failed to write output to: %s", output_path) diff --git a/tests/e2e/features/steps/helpers/dynamodb_data_uploader.py b/tests/e2e/features/steps/helpers/dynamodb_data_uploader.py new file mode 100644 index 000000000..46c83bc3b --- /dev/null +++ b/tests/e2e/features/steps/helpers/dynamodb_data_uploader.py @@ -0,0 +1,56 @@ +import json +import logging +from pathlib import Path + +import boto3 + +logger = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + + +class DynamoDBDataUploader: + def __init__(self, aws_region, access_key, secret_key, session_token=None): + self.dynamodb = boto3.resource( + "dynamodb", + region_name=aws_region, + aws_access_key_id=access_key, + aws_secret_access_key=secret_key, + aws_session_token=session_token, + ) + + def upload_files_from_path(self, table_name: str, path: Path): + if not path.exists() or not path.is_dir(): + logger.error("Seed path not found: %s", path) + return 0 + + table = self.dynamodb.Table(table_name) + count = 0 + for file_path in path.glob("*.json"): + try: + with file_path.open() as f: + items = json.load(f) + if not isinstance(items, list): + logger.warning("Skipping non-list file: %s", file_path) + continue + for item in items: + table.put_item(Item=item) + count += 1 + # logger.info("Inserted %d items from %s", len(items), file_path.name) + except Exception: + logger.exception("Failed to insert from file: %s", file_path) + return count + + def delete_data(self): + if not self.inserted_items: + logger.info("No items were inserted โ€” skipping cleanup.") + return + logger.info("Cleaning up seeded items from DynamoDB...") + for item in self.inserted_items: + nhs_number = item.get("NHS_NUMBER") + attribute_type = item.get("ATTRIBUTE_TYPE") + if nhs_number and attribute_type: + try: + self.table.delete_item(Key={"NHS_NUMBER": nhs_number, "ATTRIBUTE_TYPE": attribute_type}) + logger.info("Deleted item: %s - %s", nhs_number, attribute_type) + except Exception: + logger.exception("Failed to delete item: %s - %s", nhs_number, attribute_type) diff --git a/tests/e2e/pytest.ini b/tests/e2e/pytest.ini deleted file mode 100644 index e94f560cc..000000000 --- a/tests/e2e/pytest.ini +++ /dev/null @@ -1,12 +0,0 @@ -[pytest] -testpaths = tests -python_files = test_*.py -python_classes = Test* -python_functions = test_* -markers = - smoke: marks tests as smoke tests - regression: marks tests as regression tests - eligibility: marks tests related to eligibility endpoints - signposting: marks tests related to signposting endpoints - nextactions: marks tests related to next actions endpoints - bdd: marks tests as BDD tests diff --git a/tests/e2e/utils/api_client.py b/tests/e2e/utils/api_client.py deleted file mode 100644 index 53c706168..000000000 --- a/tests/e2e/utils/api_client.py +++ /dev/null @@ -1,63 +0,0 @@ -"""API client module for making HTTP requests to the Eligibility Signposting API.""" - -import requests -from utils.config import API_KEY, BASE_URL - -# Default timeout for API requests in seconds -DEFAULT_TIMEOUT = 10 - - -class ApiClient: - """API client for making HTTP requests to the Eligibility Signposting API.""" - - def __init__(self, base_url=BASE_URL, api_key=API_KEY): - """Initialize the API client with base URL and API key. - - Args: - base_url (str, optional): Base URL for the API. Defaults to BASE_URL from config. - api_key (str, optional): API key for authentication. Defaults to API_KEY from config. - """ - self.base_url = base_url - self.api_key = api_key - self.headers = {"Accept": "application/json", "apikey": self.api_key} - - def get_eligibility_check(self, nhs_number): - """Make a GET request to the eligibility-check endpoint. - - Args: - nhs_number (str): NHS number to check eligibility for. - - Returns: - requests.Response: Response object from the API. - """ - url = f"{BASE_URL}/patient-check/{nhs_number}" - params = {"patient": nhs_number} - - return requests.get(url, headers=self.headers, params=params, timeout=DEFAULT_TIMEOUT) - - def get(self, endpoint, params=None): - """Make a generic GET request to the API. - - Args: - endpoint (str): API endpoint to call. - params (dict, optional): Query parameters. Defaults to None. - - Returns: - requests.Response: Response object from the API. - """ - url = f"{self.base_url}{endpoint}" - return requests.get(url, headers=self.headers, params=params, timeout=DEFAULT_TIMEOUT) - - def post(self, endpoint, data=None, json=None): - """Make a generic POST request to the API. - - Args: - endpoint (str): API endpoint to call. - data (dict, optional): Form data. Defaults to None. - json (dict, optional): JSON data. Defaults to None. - - Returns: - requests.Response: Response object from the API. - """ - url = f"{self.base_url}{endpoint}" - return requests.post(url, headers=self.headers, data=data, json=json, timeout=DEFAULT_TIMEOUT) diff --git a/tests/e2e/utils/config.py b/tests/e2e/utils/config.py index b0666ea7e..ab7b86e86 100644 --- a/tests/e2e/utils/config.py +++ b/tests/e2e/utils/config.py @@ -8,15 +8,15 @@ load_dotenv() # API Configuration -BASE_URL = os.getenv("BASE_URL", "https://sandbox.api.service.nhs.uk/eligibility-signposting-api") -API_KEY = os.getenv("API_KEY", "srgedsrgveg") +BASE_URL = os.getenv("BASE_URL", "https://test.eligibility-signposting-api.nhs.uk") +# API_KEY removed - using mTLS authentication only # Test Data VALID_NHS_NUMBER = os.getenv("VALID_NHS_NUMBER", "50000000004") INVALID_NHS_NUMBER = os.getenv("INVALID_NHS_NUMBER", "9876543210") # API Endpoints -ELIGIBILITY_CHECK_ENDPOINT = "/eligibility-check" +PATIENT_CHECK_ENDPOINT = "/patient-check" # Response Schema ELIGIBILITY_CHECK_SCHEMA = {