Skip to content

Commit e8c372b

Browse files
KI7MTclaude
andcommitted
Update testing page — full 4-layer fleet test battery (12 servers, 82 tools)
Added L2 unit test summary (474+ tests), L3 live integration results (74/74 across 6 public servers), L4 fleet composition results (20/20), and known findings. Preserved existing L1 security and adif-mcp sections. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent a832faa commit e8c372b

1 file changed

Lines changed: 135 additions & 84 deletions

File tree

docs/testing.md

Lines changed: 135 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,137 @@
11
# Testing & Validation
22

3-
**Every QSO-Graph package ships with automated security tests and must pass an independent security audit before PyPI publication.**
3+
**Every QSO-Graph server is tested across four independent layers before release.** Each layer catches different failure modes. All four must pass before a fleet-wide release.
4+
5+
| Layer | Name | What It Catches | Blocking? |
6+
|-------|------|-----------------|-----------|
7+
| **L1** | Security Audit | Credential leaks, injection, unsafe patterns | Hard stop |
8+
| **L2** | Unit Tests (Mock Mode) | Tool logic, parameter handling, return shapes | Hard stop |
9+
| **L3** | Integration Tests (Live) | API connectivity, auth flows, data correctness | Pre-release gate |
10+
| **L4** | Fleet Composition | Tool name collisions, schema conflicts, cross-server consistency | Fleet releases |
411

512
---
613

7-
## Fleet Test Summary
8-
9-
| Package | Version | Security Tests | CI Gate | Audit |
10-
|---------|---------|:--------------:|:-------:|:-----:|
11-
| [qso-graph-auth](servers/qso-graph-auth.md) | v0.1.0 | 6 | Yes | PASS |
12-
| [adif-mcp](servers/adif-mcp.md) | v1.0.0 | 6 | Yes | PASS |
13-
| [eqsl-mcp](servers/eqsl.md) | v0.3.0 | 6 | Yes | PASS |
14-
| [qrz-mcp](servers/qrz.md) | v0.3.0 | 6 | Yes | PASS |
15-
| [lotw-mcp](servers/lotw.md) | v0.3.0 | 6 | Yes | PASS |
16-
| [hamqth-mcp](servers/hamqth.md) | v0.3.0 | 6 | Yes | PASS |
17-
| [pota-mcp](servers/pota.md) | v0.1.1 | 6 | Yes | PASS |
18-
| [sota-mcp](servers/sota.md) | v0.1.4 | 6 | Yes | PASS |
19-
| [solar-mcp](servers/solar.md) | v0.2.0 | 6 | Yes | PASS |
20-
| [wspr-mcp](servers/wspr.md) | v0.2.0 | 6 | Yes | PASS |
21-
| [iota-mcp](servers/iota.md) | v0.1.0 | 6 | Yes | PASS |
22-
| **Total** || **66** | **11/11** | **11/11 PASS** |
14+
## Fleet Overview
15+
16+
**12 servers, 82 tools, 4 test layers.**
17+
18+
| Package | Version | Tools | L1 Security | L2 Unit | L3 Live | L4 Fleet |
19+
|---------|---------|:-----:|:-----------:|:-------:|:-------:|:--------:|
20+
| [adif-mcp](servers/adif-mcp.md) | 1.0.0 | 8 | 6 PASS | 48 PASS | CI/CD | PASS |
21+
| [eqsl-mcp](servers/eqsl.md) | 0.3.0 | 5 | 6 PASS | 45 PASS | Auth | PASS |
22+
| [qrz-mcp](servers/qrz.md) | 0.3.0 | 5 | 6 PASS | 38 PASS | Auth | PASS |
23+
| [lotw-mcp](servers/lotw.md) | 0.3.0 | 5 | 6 PASS | 38 PASS | Auth | PASS |
24+
| [hamqth-mcp](servers/hamqth.md) | 0.3.0 | 7 | 6 PASS | 39 PASS | 10 PASS | PASS |
25+
| [pota-mcp](servers/pota.md) | 0.1.1 | 7 | 6 PASS | 45 PASS | 15 PASS | PASS |
26+
| [sota-mcp](servers/sota.md) | 0.1.4 | 4 | 6 PASS | 33 PASS | 10 PASS | PASS |
27+
| [solar-mcp](servers/solar.md) | 0.2.0 | 6 | 6 PASS | 43 PASS | 15 PASS | PASS |
28+
| [wspr-mcp](servers/wspr.md) | 0.2.0 | 8 | 6 PASS | 40 PASS | 12 PASS | PASS |
29+
| [iota-mcp](servers/iota.md) | 0.1.0 | 6 | 6 PASS | 46 PASS | 12 PASS | PASS |
30+
| [n1mm-mcp](servers/n1mm-mcp.md) | 0.1.4 | 8 | 6 PASS | 59 PASS | Local | PASS |
31+
| [ionis-mcp](https://github.com/qso-graph/ionis-mcp) | 1.2.6 | 11 | 6 PASS || Local | PASS |
32+
| **Total** || **82** | **72** | **474+** | **74** | **20** |
33+
34+
!!! note "L3 Live column notes"
35+
- **Auth** — requires OS keyring credentials (eQSL, QRZ, LoTW accounts)
36+
- **Local** — requires local infrastructure (N1MM Logger+, SQLite datasets)
37+
- **CI/CD** — tested in GitHub Actions pipeline
2338

2439
---
2540

26-
## Security Test Suite (All 11 Packages)
41+
## L1: Security Audit
2742

28-
Every package includes `test_security.py` with 6 source-code audit tests. These are not runtime tests — they scan all Python source files for forbidden patterns:
43+
Every package includes `test_security.py` with 6 source-code audit tests. These scan all Python source files for forbidden patterns — they are not runtime tests.
2944

3045
| # | Test | What It Catches |
3146
|---|------|-----------------|
32-
| 1 | `test_no_print_credentials` | `print()` calls containing password/secret/api_key/token |
33-
| 2 | `test_no_logging_credentials` | `logging.*()` calls containing credential keywords |
34-
| 3 | `test_no_subprocess` | Any use of `subprocess` or `shell=True` (command injection) |
35-
| 4 | `test_all_urls_https` | Hardcoded `http://` URLs (except localhost) |
36-
| 5 | `test_error_messages_safe` | Exception messages that could expose credentials |
37-
| 6 | `test_no_eval_exec` | Any use of `eval()` or `exec()` (code injection) |
47+
| S1 | `test_no_print_credentials` | `print()` calls containing password, secret, api_key, or token |
48+
| S2 | `test_no_logging_credentials` | `logging.*()` calls containing credential keywords |
49+
| S3 | `test_no_subprocess` | Any use of `subprocess` or `shell=True` (command injection) |
50+
| S4 | `test_all_urls_https` | Hardcoded `http://` URLs (except localhost) |
51+
| S5 | `test_error_messages_safe` | Exception messages that could expose credentials |
52+
| S6 | `test_no_eval_exec` | Any use of `eval()` or `exec()` (code injection) |
3853

39-
These tests run in CI on every push and must pass before any PyPI publish.
54+
These tests run in CI on every push and must pass before any PyPI publish. If the security gate fails, the publish job is **blocked**. No exceptions.
4055

4156
---
4257

43-
## CI Security Gate
58+
## L2: Unit Tests (Mock Mode)
4459

45-
Every package's GitHub Actions publish workflow includes a mandatory security job:
60+
Each server supports a mock mode (`{SERVER}_MCP_MOCK=1`) that replaces HTTP calls with embedded test fixtures. L2 tests verify tool logic, parameter handling, return shapes, parser correctness, and helper functions without making any API calls.
4661

47-
```yaml
48-
jobs:
49-
security:
50-
name: Security gate
51-
steps:
52-
- Security tests (pytest test_security.py)
53-
- Static analysis (grep for forbidden patterns)
62+
| Category | What's Tested | Example |
63+
|----------|---------------|---------|
64+
| **Parser/Helper Functions** | ADIF parsing, frequency conversion, date normalization, grid validation | `parse_adif()`, `freq_to_band()`, `to_yyyymmddhhmm()` |
65+
| **Tool Return Shapes** | Every tool returns expected fields, types, and structures | `eqsl_inbox()` returns `total`, `records`, `by_band` |
66+
| **Parameter Handling** | Filters, defaults, edge cases, invalid input | Band filter, callsign uppercase, empty string handling |
67+
| **Caching** | TTL expiry, cache hits, overwrites | `_cache_set()` / `_cache_get()` with timed expiry |
68+
| **Data Models** | Dataclass immutability, field defaults, type conversions | `FetchResult(records=[])` is frozen |
5469

55-
publish:
56-
needs: security # blocked until security passes
57-
steps:
58-
- Build and publish to PyPI
70+
```bash
71+
# Run L2 tests for any server (no network needed)
72+
cd solar-mcp
73+
pytest tests/test_tools.py -v
5974
```
6075

61-
If the security gate fails, the publish job is **blocked**. No exceptions.
76+
---
77+
78+
## L3: Live Integration Tests
79+
80+
L3 tests hit real APIs with known-good reference values. They verify that external services are responding correctly and that our client code handles real-world responses.
81+
82+
Tests are gated behind a `--live` flag and skipped by default. This keeps CI fast and avoids hammering volunteer-run services.
83+
84+
| Server | Tests | APIs Hit | Reference Values |
85+
|--------|:-----:|----------|------------------|
86+
| solar-mcp | 15 | NOAA SWPC | SFI 50-400, Kp 0-9, flare class A-X, 10 HF bands |
87+
| pota-mcp | 15 | POTA API | US-0001 (Acadia NP), K4SWL, US-ME parks |
88+
| sota-mcp | 10 | SOTA API | W7I/CU-001 (Borah Peak, Idaho) |
89+
| wspr-mcp | 12 | wspr.live ClickHouse | DN13→JN48 path, JO62 grid, 20m band activity |
90+
| iota-mcp | 12 | iota-world.org | OC-001 (Australia), 1000+ groups in programme |
91+
| hamqth-mcp | 10 | HamQTH (public) | W1AW DXCC=291, DX cluster spots, RBN decodes |
92+
93+
```bash
94+
# Run L3 live tests (requires network)
95+
cd solar-mcp
96+
pytest tests/test_live.py --live -v
97+
```
98+
99+
!!! warning "Rate Limiting"
100+
WSPR and HamQTH L3 tests include a 1-second pause between requests to respect volunteer-run services. Tests take longer but avoid API bans.
101+
102+
---
103+
104+
## L4: Fleet Composition Tests
105+
106+
L4 tests verify that all 12 servers work correctly when loaded together. They import every server's MCP object, enumerate all tools, and check for cross-server conflicts.
107+
108+
| Category | Tests | What's Verified |
109+
|----------|:-----:|-----------------|
110+
| **F1: Tool Name Uniqueness** | 5 | No unexpected name collisions, snake_case convention, server namespacing, tool counts |
111+
| **F2: Schema Validity** | 7 | Non-empty descriptions, typed properties, required fields exist, description length bounds |
112+
| **F3: Fleet Inventory** | 5 | All 12 servers loaded, expected tools present, no empty servers |
113+
| **F4: Cross-Server Consistency** | 3 | Band parameter types, callsign naming, limit parameter types |
114+
115+
### Known Findings
116+
117+
| Finding | Status | Detail |
118+
|---------|--------|--------|
119+
| `solar_conditions` name collision | Documented | Exists in both solar-mcp (live NOAA) and ionis-mcp (historical SQLite). MCP clients disambiguate by server prefix. |
120+
| Null defaults from `Optional` params | Tracked | FastMCP generates `{"default": null}` from Python `Optional[str] = None`. Valid JSON Schema but may affect some local LLM tool parsers. |
121+
| Band parameter type split | By design | qso-graph servers use string band names (`"20M"`), ionis-mcp uses integer ADIF band IDs (`107`). |
122+
123+
```bash
124+
# Run L4 fleet tests (all 12 servers must be installed)
125+
cd ionis-devel
126+
EQSL_MCP_MOCK=1 HAMQTH_MCP_MOCK=1 LOTW_MCP_MOCK=1 QRZ_MCP_MOCK=1 \
127+
pytest tests/test_fleet.py -v
128+
```
62129

63130
---
64131

65132
## adif-mcp Validation (v1.0.0)
66133

67-
adif-mcp is the foundation package. Beyond the standard 6 security tests, it carries a comprehensive validation test suite against the ADIF 3.1.6 specification:
134+
adif-mcp is the foundation package. Beyond the standard security and unit tests, it carries a comprehensive validation suite against the ADIF 3.1.6 specification:
68135

69136
### Test Matrix — 48/48 PASS
70137

@@ -114,66 +181,49 @@ The gold standard for ADIF validation. The [official test file](https://adif.org
114181
| FRN-011 | SUBMODE without MODE field | eQSL — incomplete records | Graceful handling of missing parent field |
115182
| FRN-012 | EQSL_AG=Y (Authenticity Guaranteed) | eQSL — AG status for DXCC | 3-value enum critical for DXCC credit eligibility |
116183

117-
### Enumeration Coverage
118-
119-
adif-mcp v1.0.0 validates all 26 ADIF 3.1.6 enumerations across 43 enum-typed fields:
120-
121-
| Enumeration | Records | Import-Only | Fields Using It |
122-
|-------------|--------:|:-----------:|-----------------|
123-
| Mode | 90 | 42 | MODE |
124-
| Submode | 108 | 0 | SUBMODE (conditional on MODE) |
125-
| Band | 33 | 0 | BAND, BAND_RX |
126-
| DXCC Entity Code | 395 | 0 | DXCC, MY_DXCC |
127-
| Contest_ID | 431 | 0 | CONTEST_ID |
128-
| Continent | 6 | 0 | CONT, MY_CONT |
129-
| Credit | 25 | 0 | CREDIT_SUBMITTED, CREDIT_GRANTED |
130-
| ARRL Section | 84 | 0 | ARRL_SECT, MY_ARRL_SECT |
131-
| Propagation Mode | 19 | 0 | PROP_MODE |
132-
| QSL_Rcvd | 5 | 0 | QSL_RCVD, EQSL_QSL_RCVD, LOTW_QSL_RCVD |
133-
| QSL_Sent | 4 | 0 | QSL_SENT, EQSL_QSL_SENT, LOTW_QSL_SENT |
134-
| QSL_Via | 5 | 2 | QSL_SENT_VIA, QSL_RCVD_VIA |
135-
| QSL Medium | 4 | 0 | Used in CreditList format |
136-
| QSO_Complete | 6 | 0 | QSO_COMPLETE |
137-
| EQSL_AG | 3 | 0 | APP_EQSL_AG |
138-
| + 10 more | — | — | See ADIF 3.1.6 spec |
139-
140-
### Validation Logic
141-
142-
Enum validation handles several complex ADIF patterns:
143-
144-
- **Simple membership**: Uppercase-normalized lookup (e.g., `cw` → `CW` → valid Mode)
145-
- **Compound CreditList**: `CREDIT_SUBMITTED=DXCC:CARD&LOTW` — split on comma, validate credit name against Credit enum, validate each medium against QSL_Medium enum
146-
- **Conditional Submode**: `SUBMODE=USB` checks membership in Submode enum, then warns if parent mode (SSB) doesn't match the record's MODE field
147-
- **Import-only detection**: Deprecated values produce warnings, not errors — historical QSO data is preserved
148-
- **Empty value rejection**: Empty or whitespace-only values for enum fields produce errors
149-
150184
---
151185

152186
## Running Tests
153187

154-
### adif-mcp (full suite)
188+
### Single server — security only
155189

156190
```bash
157-
cd adif-mcp
158-
.venv/bin/python -m pytest test/ -v
191+
cd eqsl-mcp
192+
pytest tests/test_security.py -v
159193
```
160194

161-
### Any server (security tests)
195+
### Single server — full mock suite
162196

163197
```bash
164-
cd eqsl-mcp # or any server
165-
.venv/bin/python -m pytest tests/test_security.py -v
198+
cd solar-mcp
199+
pytest tests/ -v
166200
```
167201

168-
### All servers at once
202+
### Single server — including live API tests
169203

170204
```bash
171-
for pkg in qso-graph-auth eqsl-mcp qrz-mcp lotw-mcp hamqth-mcp pota-mcp sota-mcp iota-mcp solar-mcp wspr-mcp; do
172-
echo "=== $pkg ==="
173-
cd /path/to/$pkg && .venv/bin/python -m pytest tests/test_security.py -v
205+
cd solar-mcp
206+
pytest tests/ -v --live
207+
```
208+
209+
### All servers — security sweep
210+
211+
```bash
212+
for repo in adif-mcp eqsl-mcp qrz-mcp lotw-mcp hamqth-mcp pota-mcp \
213+
sota-mcp solar-mcp wspr-mcp iota-mcp n1mm-mcp ionis-mcp; do
214+
echo "=== $repo ==="
215+
(cd $repo && pytest tests/test_security.py -v) 2>&1
174216
done
175217
```
176218

219+
### Fleet composition tests
220+
221+
```bash
222+
cd ionis-devel
223+
EQSL_MCP_MOCK=1 HAMQTH_MCP_MOCK=1 LOTW_MCP_MOCK=1 QRZ_MCP_MOCK=1 \
224+
pytest tests/test_fleet.py -v
225+
```
226+
177227
---
178228

179229
## Audit Process
@@ -198,3 +248,4 @@ All three must pass before the tag is created.
198248
| K1MU ADIF Validator | [rickmurphy.net/adifvalidator.html](https://www.rickmurphy.net/adifvalidator.html) |
199249
| adif-multitool (flwyd) | [github.com/flwyd/adif-multitool](https://github.com/flwyd/adif-multitool) |
200250
| MCP Security Best Practices | [modelcontextprotocol.io](https://modelcontextprotocol.io/docs/tutorials/security/security_best_practices) |
251+
| QSO-Graph Test Framework (internal) | [ionis-devel/planning/QSO-GRAPH-TEST-FRAMEWORK.md](https://github.com/IONIS-AI/ionis-devel) |

0 commit comments

Comments
 (0)