Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions .github/workflows/fabric-integration-tests.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
name: Fabric integration tests

on:
workflow_dispatch:
inputs:
env_url:
description: "Fabric base URL the tests run against"
required: true
default: "https://uatapi.equinix.com"
pull_request:
types: [labeled]

permissions:
contents: read

jobs:
tests:
if: >-
${{ github.event_name == 'workflow_dispatch'
|| (github.event_name == 'pull_request'
&& github.event.label.name == 'run-fabric-tests'
&& startsWith(github.head_ref, 'sync/gh')) }}
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
ref: ${{ github.event.pull_request.head.sha || github.ref }}

- name: Set up Python
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6
with:
python-version-file: .python-version

- name: Install SDK and test dependencies
run: |
python -m pip install --upgrade pip
python -m pip install -e .
python -m pip install -r tests/requirements.txt

- name: Write test data
env:
TEST_DATA_UAT: ${{ secrets.TEST_DATA_UAT }}
run: |
if [ -z "$TEST_DATA_UAT" ]; then
echo "::error::TEST_DATA_UAT secret is not set"
exit 1
fi
printf '%s' "$TEST_DATA_UAT" > env.json

- name: Run integration tests
env:
ENV_URL: ${{ inputs.env_url || 'https://uatapi.equinix.com' }}
run: |
# The suite auto-loads env.json from the repo root (written above).
./run_fabric_tests.sh

- name: Upload test reports
if: always()
uses: actions/upload-artifact@v4
with:
name: fabric-test-reports
path: reports/
if-no-files-found: warn
retention-days: 30

- name: Summarize results
if: always()
id: summary
run: |
passed=0; failed=0; total=0
if [ -f reports/fabric-tests.json ]; then
passed=$(python -c "import json;s=json.load(open('reports/fabric-tests.json'))['summary'];print(s.get('passed',0))")
failed=$(python -c "import json;s=json.load(open('reports/fabric-tests.json'))['summary'];print(s.get('failed',0)+s.get('error',0))")
total=$(python -c "import json;s=json.load(open('reports/fabric-tests.json'))['summary'];print(s.get('total',0))")
fi
echo "passed=$passed" >> "$GITHUB_OUTPUT"
echo "failed=$failed" >> "$GITHUB_OUTPUT"
echo "total=$total" >> "$GITHUB_OUTPUT"

- name: Notify Slack
if: always()
uses: slackapi/slack-github-action@v2
with:
method: chat.postMessage
token: ${{ secrets.SLACK_ACCESS_TOKEN }}
payload: |
channel: digin-panthers-gha-automation
attachments:
- color: ${{ job.status == 'success' && 'good' || job.status == 'failure' && 'danger' || 'warning' }}
text: |
*Repository:* <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|${{ github.repository }}>
*Workflow:* ${{ github.workflow }}
*Status:* ${{ job.status }}
*Test Runs:* ${{ steps.summary.outputs.total }}
*Results:* ${{ steps.summary.outputs.passed }} Passed, ${{ steps.summary.outputs.failed }} Failed
*Triggered by:* <@${{ github.actor }}>

- name: Clean up test data
if: always()
run: rm -f env.json
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,8 @@ target/
#Ipython Notebook
.ipynb_checkpoints
.openapi-generator

# Fabric integration tests — test data / reports (never commit)
env.json
file.json
reports/
193 changes: 193 additions & 0 deletions RUNNING_TESTS_LOCALLY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
# Running the Fabric tests locally (step by step)

How to run the Fabric integration tests on your own machine from scratch.
The tests are **on-demand** — a plain `pytest` does not run them.

> Run every command from the repo root:
> `~/PycharmProjects/equinix-sdk-python`

---

## 0. Prerequisites

- **Python 3.10** (see `.python-version`).
- Network access (corporate VPN) to the Fabric environment you test against
(`ENV_URL`, e.g. `https://uatapi.equinix.com`).
- The **test-data JSON** (credentials per user). See step 3.

---

## 1. Virtualenv + dependencies

```bash
# venv (a .venv already exists in the repo; from scratch:)
python3 -m venv .venv
source .venv/bin/activate

# the SDK in editable mode + test deps (pytest, requests)
pip install --upgrade pip
pip install -e .
pip install -r tests/requirements.txt
```

Confirm the SDK imports:

```bash
python -c "from equinix.services import fabricv4; print('SDK import OK')"
```

---

## 2. Quick sanity check (no network)

Confirms the tests are collected correctly and **skipped by default**:

```bash
pytest -q # -> deselected (not run on a build)
pytest -m integration --collect-only -q # -> lists the tests
```

---

## 3. Provide the test data (`env.json`)

The tests read user credentials from a JSON document. Save it as **`env.json`**
(or `file.json`) in the repo root — the suite auto-loads it. This is the same
content stored in CI as the `TEST_DATA_UAT` GitHub secret.

Preferred shape — keyed by user name (`fcr` / `fnv`). You may put `envUrl`
next to the users instead of exporting `ENV_URL`:

```json
{
"envUrl": "https://uatapi.equinix.com",
"fnv": {
"client_id": "...",
"client_secret": "...",
"projectId": "...",
"accountNumberEIA": "...",
"iaProfileUuid": "..."
},
"fcr": {
"client_id": "...",
"client_secret": "...",
"projectId": "..."
}
}
```

The legacy shape `{"users": [{"name": "fnv", ...}]}` is also accepted. A user
value may be a JSON-encoded string, and field names may be snake_case or
camelCase (`client_id`/`clientId`, etc.).

How the tests resolve data (in order):
1. the `TEST_DATA_UAT_USERS` env var (if set), otherwise
2. a JSON file — `TEST_DATA_FILE`, else `env.json`/`file.json` in the cwd, then
the repo root.

> The user key in the JSON (`fnv`/`fcr`) must match the one the test uses.
> `test_internet_access_api.py` uses `UserName.PANTHERS_FNV` → key `"fnv"`.

---

## 4. Environment base URL

If you didn't put `envUrl` in the data file, export it:

```bash
export ENV_URL="https://uatapi.equinix.com"
```

---

## 5. Run the tests

```bash
./run_fabric_tests.sh
```

The script activates `.venv`, checks that test data (env var or data file) and
a base URL are available, then runs `pytest -m integration -v`.

Filtering / extra args pass through to pytest:

```bash
./run_fabric_tests.sh -k test_3 # one scenario
./run_fabric_tests.sh tests/services/fabricv4/test_internet_access_api.py
./run_fabric_tests.sh -s # show print()/logs live
```

Or without the script:

```bash
pytest -m integration -v
```

---

## See the HTTP requests (logging)

Set `FABRIC_DEBUG` to log every request, its **body**, and the **response**:

```bash
FABRIC_DEBUG=1 ./run_fabric_tests.sh
```

Example output (run script adds `-s` so logs stream live):

```
fabric.http INFO --> POST https://uatapi.equinix.com/fabric/v4/ports/search
fabric.http INFO request body: {'filter': {'or': [...]}, 'pagination': {...}}
fabric.http INFO <-- POST .../fabric/v4/ports/search -> HTTP 200
fabric.http INFO response body: {"pagination":{...},"data":[...]}
```

- `FABRIC_DEBUG=1` — request URL + request body + response status + response
body. Headers are **not** logged, so the auth token never appears.
- `FABRIC_LOG_MAXLEN` — truncate logged bodies to N chars (default 2000;
`0` = no truncation), e.g. `FABRIC_LOG_MAXLEN=0 FABRIC_DEBUG=1 ./run_fabric_tests.sh`.
- `FABRIC_DEBUG=body` — additionally dumps full headers. **Debugging only** —
this prints the `Authorization: Bearer ...` header.

A non-2xx status in the log is a failed request; the test assertion then
reports the failure (`with_http_info` calls assert on the status code).

---

## Test reports

`run_fabric_tests.sh` always writes three reports into `./reports/` (git-ignored):

| File | Format | Use |
|---|---|---|
| `reports/fabric-tests.xml` | JUnit XML | CI (GitHub Actions, Jenkins) |
| `reports/fabric-tests.html` | HTML (self-contained) | open in a browser |
| `reports/fabric-tests.json` | JSON | machine-readable / dashboards |

Change the directory with `REPORTS_DIR=out ./run_fabric_tests.sh`.

---

## All in one (shortcut)

```bash
source .venv/bin/activate
# put the test-data JSON in env.json (repo root), then:
export ENV_URL="https://uatapi.equinix.com" # or add "envUrl" to env.json
./run_fabric_tests.sh # data file auto-loaded
```

---

## Troubleshooting

| Symptom | Cause / fix |
|---|---|
| `skipped` instead of running | no test data or base URL — provide `env.json` and `ENV_URL` (steps 3–4) |
| `User 'fnv' not found` | the user key in the JSON doesn't match the `UserName` used by the test |
| `401/403` from the Fabric API | wrong `client_id`/`client_secret` or wrong `ENV_URL` |
| timeout / no connection to `ENV_URL` | not on the corporate VPN |
| import error `equinix.services.fabricv4` | running on code before the SDK fixes — update your branch |

> `env.json` / `file.json` contain secrets — do not commit them (they are
> git-ignored).
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ def create_eia_service(

_response_types_map: Dict[str, Optional[str]] = {
'202': "InternetAccessService",
'201': "InternetAccessService",
'400': "List[Error]",
'403': "List[Error]",
'500': "List[Error]",
Expand Down Expand Up @@ -164,6 +165,7 @@ def create_eia_service_with_http_info(

_response_types_map: Dict[str, Optional[str]] = {
'202': "InternetAccessService",
'201': "InternetAccessService",
'400': "List[Error]",
'403': "List[Error]",
'500': "List[Error]",
Expand Down Expand Up @@ -234,6 +236,7 @@ def create_eia_service_without_preload_content(

_response_types_map: Dict[str, Optional[str]] = {
'202': "InternetAccessService",
'201': "InternetAccessService",
'400': "List[Error]",
'403': "List[Error]",
'500': "List[Error]",
Expand Down Expand Up @@ -376,7 +379,7 @@ def delete_eia_service(
)

_response_types_map: Dict[str, Optional[str]] = {
'202': "InternetAccessService",
'202': None,
'404': "List[Error]",
'409': "List[Error]",
}
Expand Down Expand Up @@ -445,7 +448,7 @@ def delete_eia_service_with_http_info(
)

_response_types_map: Dict[str, Optional[str]] = {
'202': "InternetAccessService",
'202': None,
'404': "List[Error]",
'409': "List[Error]",
}
Expand Down Expand Up @@ -514,7 +517,7 @@ def delete_eia_service_without_preload_content(
)

_response_types_map: Dict[str, Optional[str]] = {
'202': "InternetAccessService",
'202': None,
'404': "List[Error]",
'409': "List[Error]",
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ def equinix_peer_ip_validate_regular_expression(cls, value):
if value is None:
return value

if not re.match(r"^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$", value):
raise ValueError(r"must validate the regular expression /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/")
if not re.match(r"^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(?:/\d{1,2})?$", value):
raise ValueError(r"must validate the regular expression /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(?:/\d{1,2})?$/")
return value

@field_validator('customer_peer_ip')
Expand All @@ -58,8 +58,8 @@ def customer_peer_ip_validate_regular_expression(cls, value):
if value is None:
return value

if not re.match(r"^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$", value):
raise ValueError(r"must validate the regular expression /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/")
if not re.match(r"^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(?:/\d{1,2})?$", value):
raise ValueError(r"must validate the regular expression /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(?:/\d{1,2})?$/")
return value

@field_validator('equinix_vrrp_ip')
Expand All @@ -68,8 +68,8 @@ def equinix_vrrp_ip_validate_regular_expression(cls, value):
if value is None:
return value

if not re.match(r"^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$", value):
raise ValueError(r"must validate the regular expression /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/")
if not re.match(r"^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(?:/\d{1,2})?$", value):
raise ValueError(r"must validate the regular expression /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(?:/\d{1,2})?$/")
return value

@field_validator('customer_vrrp_ip')
Expand All @@ -78,8 +78,8 @@ def customer_vrrp_ip_validate_regular_expression(cls, value):
if value is None:
return value

if not re.match(r"^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$", value):
raise ValueError(r"must validate the regular expression /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/")
if not re.match(r"^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(?:/\d{1,2})?$", value):
raise ValueError(r"must validate the regular expression /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(?:/\d{1,2})?$/")
return value

model_config = ConfigDict(
Expand Down
Loading
Loading