Iter 12: High-value API coverage gaps#16
Conversation
…d metrics Close the highest-value API coverage gaps between hoppy and bunny.net: - URL Purge: `hoppy purge --url <url>` (POST /purge) - Pull Zone Hostnames: add/remove hostname, load free cert, force SSL, add/remove custom certificate (6 new API methods, 6 CLI commands) - DNS Export/Import: export zone as BIND file, import from file/stdin - Stream Captions: add/delete SRT caption tracks on videos - Shield Metrics: overview metrics for Shield Zones Adds 16 new API client methods, 16 new CLI commands, 29 fixtures, and 30+ new wiremock/E2E tests (428 total tests passing). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add iteration plan files for iterations 0-11, reorganize test plan into testing/ subfolder, add release checklist and bug tracker docs. Update CLAUDE.md with expanded documentation instructions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR closes several “high-value” bunny.net API coverage gaps by adding new Core/Stream/Shield client methods and wiring corresponding CLI subcommands, with accompanying wiremock-based CLI E2E tests and snapshots.
Changes:
- Add
hoppy purge --url <url>and Core client support forPOST /purge. - Add pull zone hostname/SSL management subcommands and Core client endpoints.
- Add DNS zone export/import, stream caption add/delete, and Shield metrics overview (client + CLI + tests/snapshots).
Reviewed changes
Copilot reviewed 82 out of 83 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/snapshots/cli_stream__stream_video_caption_add_json.snap | Adds snapshot for stream caption add JSON output. |
| tests/snapshots/cli_shield__shield_metrics_overview_table.snap | Adds snapshot for shield metrics overview table output. |
| tests/snapshots/cli_shield__shield_metrics_overview_json.snap | Adds snapshot for shield metrics overview JSON output. |
| tests/cli_stream.rs | Adds CLI E2E tests for stream caption add/delete. |
| tests/cli_shield.rs | Adds CLI E2E tests for shield metrics overview (json/table). |
| tests/cli_pull_zone.rs | Adds CLI E2E tests for URL purge and pull-zone hostname/SSL operations. |
| tests/cli_dns.rs | Adds CLI E2E tests for DNS export/import. |
| src/main.rs | Wires new top-level purge command into the command dispatcher. |
| src/commands/stream.rs | Adds stream video caption add/delete handling. |
| src/commands/shield.rs | Adds shield metrics overview handling and table output mapping. |
| src/commands/purge.rs | Implements the purge command handler. |
| src/commands/pull_zone.rs | Adds pull-zone hostname ... handler wiring. |
| src/commands/mod.rs | Exposes new purge command module. |
| src/commands/dns.rs | Adds dns zone export/import handlers (stdout export, stdin/file import). |
| src/cli.rs | Adds clap subcommands/flags for purge, pull-zone hostname, dns import/export, stream captions, shield metrics. |
| hoppy-knowledgebase/testing/test-plan-v0.1.0.md | Adds “Related” links section. |
| hoppy-knowledgebase/testing/test-environment-needed.md | New doc: requirements for a safe live-test environment. |
| hoppy-knowledgebase/testing/e2e-test-report.md | New generated E2E test report document. |
| hoppy-knowledgebase/research/rust-code-review-skill-research.md | Adds “Related” links section. |
| hoppy-knowledgebase/research/rust-cli-best-practices.md | Adds “Related” links section. |
| hoppy-knowledgebase/research/release-engineering-research.md | Adds “Related” links section. |
| hoppy-knowledgebase/research/openapi-codegen-rust.md | Adds “Related” links section. |
| hoppy-knowledgebase/research/hand-written-experiment-results.md | Adds “Related” links section. |
| hoppy-knowledgebase/research/cli-e2e-testing-research.md | Adds “Related” links section. |
| hoppy-knowledgebase/research/cli-design-patterns.md | Adds “Related” links section. |
| hoppy-knowledgebase/release/release-setup-checklist.md | New release setup checklist doc. |
| hoppy-knowledgebase/iterations/rust-e2e-rewrite-plan.md | New detailed plan doc for Rust-based E2E tests rewrite. |
| hoppy-knowledgebase/iterations/iteration-9-gap-analysis.md | New iteration doc (gap analysis). |
| hoppy-knowledgebase/iterations/iteration-8-release.md | New iteration doc (release readiness). |
| hoppy-knowledgebase/iterations/iteration-7-cleanup.md | New iteration doc (cleanup). |
| hoppy-knowledgebase/iterations/iteration-6-scripting-containers.md | New iteration doc (scripting + containers). |
| hoppy-knowledgebase/iterations/iteration-5-shield.md | New iteration doc (Shield). |
| hoppy-knowledgebase/iterations/iteration-4-stream.md | New iteration doc (Stream). |
| hoppy-knowledgebase/iterations/iteration-3-dns.md | New iteration doc (DNS). |
| hoppy-knowledgebase/iterations/iteration-2-storage.md | New iteration doc (Storage). |
| hoppy-knowledgebase/iterations/iteration-12-api-coverage.md | New iteration doc describing the PR scope and deliverables. |
| hoppy-knowledgebase/iterations/iteration-11-e2e-lifecycle.md | New iteration doc (live lifecycle tests). |
| hoppy-knowledgebase/iterations/iteration-10-e2e-test-harness.md | New iteration doc (mock-based harness). |
| hoppy-knowledgebase/iterations/iteration-1-pull-zones.md | New iteration doc (pull zones). |
| hoppy-knowledgebase/iterations/iteration-1-code-review.md | New iteration doc capturing code review findings. |
| hoppy-knowledgebase/iterations/iteration-0.5-codegen-experiment.md | New iteration doc (codegen experiment). |
| hoppy-knowledgebase/iterations/iteration-0-skeleton.md | New iteration doc (project skeleton). |
| hoppy-knowledgebase/iterations/e2e-test-harness-plan.md | New E2E harness plan doc. |
| hoppy-knowledgebase/development-roadmap.md | Updates roadmap to link iteration docs and include iteration 12. |
| hoppy-knowledgebase/decision-log.md | Adds API-specific decisions section and additional entries. |
| hoppy-knowledgebase/api/magic-containers/magic-containers-applications-api.md | Adds “Related” links section. |
| hoppy-knowledgebase/api/bunny-stream-api-research.md | Adds “Related” links section. |
| hoppy-knowledgebase/api/bunny-api-quirks.md | Adds “Related” links section. |
| hoppy-knowledgebase/api/bunny-api-overview.md | Adds “Related” links section. |
| hoppy-knowledgebase/api/bunny-api-client-patterns.md | Adds “Related” links section. |
| hoppy-knowledgebase/adding-a-feature.md | Adds “Related” links section. |
| hoppy-knowledgebase/Seed.md | Adds frontmatter + “Related” links section and fixes formatting. |
| hoppy-knowledgebase/Bugs.md | New bug doc capturing a containers deserialization issue. |
| fixtures/stream/video_caption_delete.json | Adds fixture for stream caption delete response. |
| fixtures/stream/video_caption_add.json | Adds fixture for stream caption add response. |
| fixtures/shield/metrics_waf_rule.json | Adds fixture for Shield WAF rule metrics. |
| fixtures/shield/metrics_rate_limits.json | Adds fixture for Shield rate limit metrics. |
| fixtures/shield/metrics_overview_detailed.json | Adds fixture for Shield overview metrics (detailed). |
| fixtures/shield/metrics_overview.json | Adds fixture for Shield metrics overview. |
| fixtures/shield/metrics_bot_detection.json | Adds fixture for Shield bot detection metrics. |
| fixtures/core/purge_url.json | Adds fixture/documentation stub for URL purge response semantics. |
| fixtures/core/pullzone_setforcessl.json | Adds fixture comment for empty-body success for force SSL endpoint. |
| fixtures/core/pullzone_removehostname.json | Adds fixture for pull zone remove-hostname response. |
| fixtures/core/pullzone_removecertificate.json | Adds fixture comment for empty-body success for remove-certificate endpoint. |
| fixtures/core/pullzone_loadfreecertificate.json | Adds fixture comment for empty-body success for load-free-certificate endpoint. |
| fixtures/core/pullzone_addhostname.json | Adds fixture for pull zone add-hostname response. |
| fixtures/core/pullzone_addcertificate.json | Adds fixture comment for empty-body success for add-certificate endpoint. |
| fixtures/core/dnszone_import.json | Adds fixture for DNS zone import result. |
| fixtures/core/dnszone_export.txt | Adds fixture for DNS zone export BIND file. |
| crates/bunny-api-stream/tests/stream_api.rs | Adds Stream client wiremock tests for caption add/delete. |
| crates/bunny-api-stream/src/client.rs | Adds Stream client methods for caption add/delete. |
| crates/bunny-api-shield/tests/shield_api.rs | Adds Shield client wiremock test for metrics overview. |
| crates/bunny-api-shield/src/types.rs | Adds Shield metrics response types. |
| crates/bunny-api-shield/src/lib.rs | Re-exports Shield metrics types. |
| crates/bunny-api-shield/src/client.rs | Adds Shield client method for metrics overview. |
| crates/bunny-api-core/tests/pullzone_api.rs | Adds Core client wiremock tests for purge URL and hostname/SSL endpoints. |
| crates/bunny-api-core/tests/dns_api.rs | Adds Core client wiremock tests for DNS export/import. |
| crates/bunny-api-core/src/types.rs | Adds DnsImportResult type. |
| crates/bunny-api-core/src/lib.rs | Re-exports DnsImportResult. |
| crates/bunny-api-core/src/client.rs | Adds Core client methods for purge URL, pull-zone hostname/SSL, and DNS export/import. |
| Cargo.toml | Adds tempfile dev-dependency for tests. |
| Cargo.lock | Locks tempfile dependency addition. |
| CLAUDE.md | Updates repo documentation guidance for Obsidian knowledgebase usage. |
| None => { | ||
| let mut buf = String::new(); | ||
| io::stdin().lock().read_to_string(&mut buf)?; | ||
| buf | ||
| } | ||
| }; | ||
| let result = client.import_dns_zone(*id, &zone_data).await?; | ||
| if let OutputFormat::Json = format { | ||
| let json = | ||
| serde_json::to_string_pretty(&result).expect("failed to serialize to JSON"); | ||
| println!("{json}"); |
There was a problem hiding this comment.
This reads from stdin with read_to_string without adding context, and JSON output uses .expect("failed to serialize to JSON"), which can panic. Prefer .context("failed to read zone file from stdin")? and serde_json::to_string_pretty(&result).context("failed to serialize to JSON")? to keep errors user-friendly and non-panicking.
| if let OutputFormat::Json = format { | ||
| let json = | ||
| serde_json::to_string_pretty(&metrics).expect("failed to serialize to JSON"); | ||
| println!("{json}"); |
There was a problem hiding this comment.
serde_json::to_string_pretty(&metrics).expect(...) will panic on serialization errors and bypass normal CLI error handling. Prefer propagating the error with .context("failed to serialize to JSON")? (consistent with other handlers like commands/auth.rs).
| // Write a temporary SRT file to the system temp directory | ||
| let srt_path = std::env::temp_dir().join("hoppy_test_captions.srt"); | ||
| std::fs::write(&srt_path, "1\n00:00:00,000 --> 00:00:05,000\nHello\n").unwrap(); | ||
|
|
||
| let output = support::hoppy_mock_cmd_full( | ||
| "test-api-key", | ||
| &server.uri(), | ||
| None, | ||
| Some(&server.uri()), | ||
| None, | ||
| ) | ||
| .args([ | ||
| "--format", | ||
| "json", | ||
| "stream", | ||
| "video", | ||
| "caption", | ||
| "add", | ||
| "--library-id", | ||
| "10001", | ||
| "--video-id", | ||
| "vid-guid-0001", | ||
| "--srclang", | ||
| "en", | ||
| "--file", | ||
| srt_path.to_str().unwrap(), | ||
| ]) |
There was a problem hiding this comment.
This test writes a fixed filename into the global temp directory ("hoppy_test_captions.srt"), which can cause cross-test interference when tests run in parallel and can leave stale files behind. Prefer tempfile::NamedTempFile (or a per-test unique path) and keep the file handle alive for the duration of the command.
| #[tokio::test] | ||
| async fn purge_url_sends_correct_request() { | ||
| let server = MockServer::start().await; | ||
| Mock::given(method("POST")) | ||
| .and(path("/purge")) | ||
| .and(query_param("url", "https://cdn.example.com/index.html")) | ||
| .respond_with(ResponseTemplate::new(204)) | ||
| .mount(&server) | ||
| .await; |
There was a problem hiding this comment.
This E2E test doesn’t assert the AccessKey header (and also doesn’t set .expect(1)), so it can pass even if authentication is accidentally dropped or the request is sent multiple times. Add a header("AccessKey", ...) matcher and .expect(1) to ensure the CLI sends the correct authenticated request exactly once.
| You may organize content in suitable subfolders. | ||
| Keep all the documentation in the folder ./hoppy-knowledgebase as *.md markdown files with frontmatter in yaml format with properties of type text, numbers, checkboxes, dates, dates and times, or lists. | ||
|
|
||
| Use is as your second brain and document there: |
There was a problem hiding this comment.
Grammar typo: “Use is as your second brain” should be “Use it as your second brain”.
| Use is as your second brain and document there: | |
| Use it as your second brain and document there: |
| pub async fn import_dns_zone(&self, id: i64, zone_file: &str) -> Result<DnsImportResult> { | ||
| use reqwest::header::CONTENT_TYPE; | ||
| let url = format!("{}/dnszone/{id}/import", self.base_url); | ||
| // Build a minimal multipart body manually: bunny.net expects a field named "file". | ||
| let boundary = "HoppyBoundary"; | ||
| let body = format!( | ||
| "--{boundary}\r\nContent-Disposition: form-data; name=\"file\"; filename=\"zone.txt\"\r\nContent-Type: text/plain\r\n\r\n{zone_file}\r\n--{boundary}--\r\n" | ||
| ); | ||
| let content_type = format!("multipart/form-data; boundary={boundary}"); | ||
| let rb = self | ||
| .auth(self.http.post(&url)) | ||
| .header(CONTENT_TYPE, content_type) | ||
| .body(body); | ||
| let response = self.send(rb).await?; |
There was a problem hiding this comment.
import_dns_zone builds multipart/form-data manually with a fixed boundary ("HoppyBoundary") and interpolates the zone file content directly. Multipart boundaries should be unique/unpredictable; with a fixed boundary, valid zone content containing that boundary sequence can corrupt the request. Prefer enabling reqwest’s multipart feature and using reqwest::multipart::Form (or at least generate a per-request boundary and build a bytes body safely).
| if let OutputFormat::Json = format { | ||
| let json = | ||
| serde_json::to_string_pretty(&result).expect("failed to serialize to JSON"); | ||
| println!("{json}"); |
There was a problem hiding this comment.
This uses serde_json::to_string_pretty(...).expect(...) in non-test code, which will panic on serialization errors and bypass the CLI’s normal error reporting. Use anyhow::Context (or ?) to propagate a structured error instead of panicking.
- Generate unique multipart boundary per request in DNS import
- Add error context for stdin reads in DNS import
- Use tempfile::NamedTempFile in caption test for isolation
- Add AccessKey header matcher and expect(1) to purge URL test
- Fix CLAUDE.md typo ("Use is" → "Use it")
- Fix iteration-12 checklist: mark unimplemented metrics as deferred
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Addresses remaining Copilot review comments: use anyhow::Context instead of .expect() for serde_json serialization in the new dns import, shield metrics, and stream caption handlers. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
hoppy purge --url <url>— purge individual URLs from CDN cache (POST /purge)hoppy pull-zone hostname)hoppy dns zone exportoutputs BIND zone file to stdout;hoppy dns zone importreads from file or stdin (GET /dnszone/{id}/export,POST /dnszone/{id}/import)hoppy stream video caption add/delete— manage SRT caption tracks on videoshoppy shield metrics overview— security metrics overview for Shield ZonesAdds 16 new API client methods, 16 new CLI commands, 29 test fixtures, and 30+ new wiremock/E2E tests (428 total tests passing).
Test plan
cargo test --workspace— 428 tests passingcargo clippy --workspace --tests -- -D warnings— cleancargo fmt --all— cleanBUNNY_API_KEY=xxx cargo test --features live-api -- --test-threads=1 live_— run live E2E tests against real API🤖 Generated with Claude Code