CLI app for developers prototyping atproto functionality
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add HTTP stage snapshot tests and fixtures covering AC3

Five insta snapshot tests exercise queryLabels success, empty-label
advisory, malformed schema with precise NamedSource span, pagination
round-trip, ignored-cursor spec violation, and transport-error no-
cascade (AC3.1-AC3.6). Fixtures live under tests/fixtures/labeler/http/
and are loaded by the existing FakeRawHttpTee from tests/common/mod.rs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+246
+1
tests/fixtures/labeler/http/empty/first_page.json
··· 1 + {"cursor":null,"labels":[]}
+1
tests/fixtures/labeler/http/healthy/second_page.json
··· 1 + {"cursor":null,"labels":[{"ver":1,"src":"did:plc:test123456789abcdefghijklmnop","uri":"at://did:plc:test123456789abcdefghijklmnop/app.bsky.feed.post/def1","val":"inappropriate","neg":false,"cts":"2026-01-03T00:00:00.000Z"}]}
+1
tests/fixtures/labeler/http/ignored_cursor/first_page.json
··· 1 + {"cursor":"cursor1","labels":[{"ver":1,"src":"did:plc:test123456789abcdefghijklmnop","uri":"at://did:plc:test123456789abcdefghijklmnop/app.bsky.feed.post/abc1","val":"spam","neg":false,"cts":"2026-01-01T00:00:00.000Z"}]}
+1
tests/fixtures/labeler/http/ignored_cursor/second_page.json
··· 1 + {"cursor":null,"labels":[{"ver":1,"src":"did:plc:test123456789abcdefghijklmnop","uri":"at://did:plc:test123456789abcdefghijklmnop/app.bsky.feed.post/abc1","val":"spam","neg":false,"cts":"2026-01-01T00:00:00.000Z"}]}
+1
tests/fixtures/labeler/http/malformed/first_page.json
··· 1 + {"cursor":null,"labels":42}
+162
tests/labeler_http.rs
··· 1 + //! Integration tests for the labeler HTTP stage using snapshot tests. 2 + 3 + mod common; 4 + 5 + use atproto_devtool::commands::test::labeler::http::run; 6 + use common::FakeRawHttpTee; 7 + 8 + /// Helper to render a report to a string for snapshot testing. 9 + fn render_report_to_string( 10 + report: &atproto_devtool::commands::test::labeler::report::LabelerReport, 11 + ) -> String { 12 + let mut buf = Vec::new(); 13 + report 14 + .render( 15 + &mut buf, 16 + &atproto_devtool::commands::test::labeler::report::RenderConfig { no_color: true }, 17 + ) 18 + .expect("render failed"); 19 + String::from_utf8(buf).expect("invalid utf-8") 20 + } 21 + 22 + #[tokio::test] 23 + async fn http_healthy_renders_all_ok() { 24 + let tee = FakeRawHttpTee::new(); 25 + // Load fixture pages with real labels and pagination. 26 + let first_page = include_bytes!("fixtures/labeler/http/healthy/first_page.json").to_vec(); 27 + let second_page = include_bytes!("fixtures/labeler/http/healthy/second_page.json").to_vec(); 28 + 29 + tee.add_response(None, 200, first_page); 30 + tee.add_response(Some("cursor1"), 200, second_page); 31 + 32 + let output = run(&tee).await; 33 + 34 + // Build a minimal report for snapshot testing. 35 + let mut report = atproto_devtool::commands::test::labeler::report::LabelerReport::new( 36 + atproto_devtool::commands::test::labeler::report::ReportHeader { 37 + target: "https://example.com".to_string(), 38 + resolved_did: None, 39 + pds_endpoint: None, 40 + labeler_endpoint: Some("https://example.com".to_string()), 41 + }, 42 + ); 43 + 44 + for result in output.results { 45 + report.record(result); 46 + } 47 + report.finish(); 48 + 49 + let rendered = render_report_to_string(&report); 50 + insta::assert_snapshot!(rendered); 51 + } 52 + 53 + #[tokio::test] 54 + async fn http_empty_labeler_emits_advisory() { 55 + let tee = FakeRawHttpTee::new(); 56 + let empty_page = include_bytes!("fixtures/labeler/http/empty/first_page.json").to_vec(); 57 + 58 + tee.add_response(None, 200, empty_page); 59 + 60 + let output = run(&tee).await; 61 + 62 + let mut report = atproto_devtool::commands::test::labeler::report::LabelerReport::new( 63 + atproto_devtool::commands::test::labeler::report::ReportHeader { 64 + target: "https://example.com".to_string(), 65 + resolved_did: None, 66 + pds_endpoint: None, 67 + labeler_endpoint: Some("https://example.com".to_string()), 68 + }, 69 + ); 70 + 71 + for result in output.results { 72 + report.record(result); 73 + } 74 + report.finish(); 75 + 76 + let rendered = render_report_to_string(&report); 77 + insta::assert_snapshot!(rendered); 78 + } 79 + 80 + #[tokio::test] 81 + async fn http_malformed_schema_fails_with_source_span() { 82 + let tee = FakeRawHttpTee::new(); 83 + let malformed = include_bytes!("fixtures/labeler/http/malformed/first_page.json").to_vec(); 84 + 85 + tee.add_response(None, 200, malformed.clone()); 86 + 87 + let output = run(&tee).await; 88 + 89 + let mut report = atproto_devtool::commands::test::labeler::report::LabelerReport::new( 90 + atproto_devtool::commands::test::labeler::report::ReportHeader { 91 + target: "https://example.com".to_string(), 92 + resolved_did: None, 93 + pds_endpoint: None, 94 + labeler_endpoint: Some("https://example.com".to_string()), 95 + }, 96 + ); 97 + 98 + for result in output.results { 99 + report.record(result); 100 + } 101 + report.finish(); 102 + 103 + let rendered = render_report_to_string(&report); 104 + insta::assert_snapshot!(rendered); 105 + } 106 + 107 + #[tokio::test] 108 + async fn http_ignored_cursor_fails() { 109 + let tee = FakeRawHttpTee::new(); 110 + // Load fixture with identical pages (cursor ignored). 111 + let first_page = 112 + include_bytes!("fixtures/labeler/http/ignored_cursor/first_page.json").to_vec(); 113 + let second_page = 114 + include_bytes!("fixtures/labeler/http/ignored_cursor/second_page.json").to_vec(); 115 + 116 + tee.add_response(None, 200, first_page); 117 + tee.add_response(Some("cursor1"), 200, second_page); 118 + 119 + let output = run(&tee).await; 120 + 121 + let mut report = atproto_devtool::commands::test::labeler::report::LabelerReport::new( 122 + atproto_devtool::commands::test::labeler::report::ReportHeader { 123 + target: "https://example.com".to_string(), 124 + resolved_did: None, 125 + pds_endpoint: None, 126 + labeler_endpoint: Some("https://example.com".to_string()), 127 + }, 128 + ); 129 + 130 + for result in output.results { 131 + report.record(result); 132 + } 133 + report.finish(); 134 + 135 + let rendered = render_report_to_string(&report); 136 + insta::assert_snapshot!(rendered); 137 + } 138 + 139 + #[tokio::test] 140 + async fn http_transport_error_renders_network_error() { 141 + let tee = FakeRawHttpTee::new(); 142 + tee.set_transport_error(); 143 + 144 + let output = run(&tee).await; 145 + 146 + let mut report = atproto_devtool::commands::test::labeler::report::LabelerReport::new( 147 + atproto_devtool::commands::test::labeler::report::ReportHeader { 148 + target: "https://example.com".to_string(), 149 + resolved_did: None, 150 + pds_endpoint: None, 151 + labeler_endpoint: Some("https://example.com".to_string()), 152 + }, 153 + ); 154 + 155 + for result in output.results { 156 + report.record(result); 157 + } 158 + report.finish(); 159 + 160 + let rendered = render_report_to_string(&report); 161 + insta::assert_snapshot!(rendered); 162 + }
+15
tests/snapshots/labeler_http__http_empty_labeler_emits_advisory.snap
··· 1 + --- 2 + source: tests/labeler_http.rs 3 + expression: rendered 4 + --- 5 + Target: https://example.com 6 + Labeler endpoint: https://example.com 7 + elapsed: 0ms 8 + 9 + == HTTP == 10 + [OK] Labeler endpoint is reachable 11 + [OK] First page schema is valid 12 + [WARN] Labeler has no published labels 13 + [OK] First page was complete; pagination not exercised 14 + 15 + Summary: 3 passed, 0 failed (spec), 0 network errors, 1 advisories, 0 skipped. Exit code: 0
+15
tests/snapshots/labeler_http__http_healthy_renders_all_ok.snap
··· 1 + --- 2 + source: tests/labeler_http.rs 3 + expression: rendered 4 + --- 5 + Target: https://example.com 6 + Labeler endpoint: https://example.com 7 + elapsed: 0ms 8 + 9 + == HTTP == 10 + [OK] Labeler endpoint is reachable 11 + [OK] First page schema is valid 12 + [OK] Second page schema is valid 13 + [OK] Pagination round-trip successful 14 + 15 + Summary: 4 passed, 0 failed (spec), 0 network errors, 0 advisories, 0 skipped. Exit code: 0
+15
tests/snapshots/labeler_http__http_ignored_cursor_fails.snap
··· 1 + --- 2 + source: tests/labeler_http.rs 3 + expression: rendered 4 + --- 5 + Target: https://example.com 6 + Labeler endpoint: https://example.com 7 + elapsed: 0ms 8 + 9 + == HTTP == 10 + [OK] Labeler endpoint is reachable 11 + [OK] First page schema is valid 12 + [OK] Second page schema is valid 13 + [FAIL] Labeler ignored the cursor parameter 14 + 15 + Summary: 3 passed, 1 failed (spec), 0 network errors, 0 advisories, 0 skipped. Exit code: 1
+21
tests/snapshots/labeler_http__http_malformed_schema_fails_with_source_span.snap
··· 1 + --- 2 + source: tests/labeler_http.rs 3 + expression: rendered 4 + --- 5 + Target: https://example.com 6 + Labeler endpoint: https://example.com 7 + elapsed: 0ms 8 + 9 + == HTTP == 10 + [OK] Labeler endpoint is reachable 11 + [FAIL] Schema validation failed 12 + labeler::http::schema_failure 13 + 14 + × Failed to decode query_labels response: invalid type: integer `42`, expected a sequence at line 1 column 27 15 + ╭─[https://example.com/xrpc/com.atproto.label.queryLabels:1:27] 16 + 1 │ {"cursor":null,"labels":42} 17 + · ┬ 18 + · ╰── JSON error 19 + ╰──── 20 + 21 + Summary: 1 passed, 1 failed (spec), 0 network errors, 0 advisories, 0 skipped. Exit code: 1
+13
tests/snapshots/labeler_http__http_transport_error_renders_network_error.snap
··· 1 + --- 2 + source: tests/labeler_http.rs 3 + assertion_line: 162 4 + expression: rendered 5 + --- 6 + Target: https://example.com 7 + Labeler endpoint: https://example.com 8 + elapsed: 0ms 9 + 10 + == HTTP == 11 + [NET] Network error: tcp connect: connection refused 12 + 13 + Summary: 0 passed, 0 failed (spec), 1 network errors, 0 advisories, 0 skipped. Exit code: 0