perlsky is a Perl 5 implementation of an AT Protocol Personal Data Server.
13
fork

Configure Feed

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

Split missing blob checks from external surface suite

alice 450e2c4c 9f7e4eae

+164 -71
+5 -4
docs/TEST_AUDIT.md
··· 13 13 The current baseline for saying "the audited suite is green" is: 14 14 15 15 - `prove -lr t` 16 - - latest full green result in the realigned Meridian worktree: `Files=60, Tests=3061` 16 + - latest full green result in the realigned Meridian worktree: `Files=61, Tests=3069` 17 17 - `prove -lv t/server-auth.t` 18 18 - `perl -c script/differential-validate` 19 19 - `PERLSKY_RUN_REFERENCE_DIFF=1 prove -lv t/reference-differential.t` ··· 116 116 Current suite counts by bucket: 117 117 118 118 - `direct reference differential`: `5` 119 - - `audited local regression`: `42` 119 + - `audited local regression`: `43` 120 120 - `local correctness/infrastructure`: `13` 121 121 122 122 | Test file | Bucket | Current note | ··· 145 145 | `t/event-stream.t` | audited local regression | wire-format, malformed frame, and event decoding coverage | 146 146 | `t/applywrites-surfaces.t` | audited local regression | focused `applyWrites` happy-path and missing-delete behavior after the self-service invite, identity/email, label, and blob/sync happy paths were split out | 147 147 | `t/external-handle-update.t` | audited local regression | external-handle update semantics, including DID-resolution checks and empty-body success for external handle adoption | 148 - | `t/external-surface.t` | audited local regression | focused external-surface coverage for repo/blob export and missing-blob behavior after splitting discovery, label RPC, and account-status checks into dedicated suites | 148 + | `t/external-surface.t` | audited local regression | focused external-surface coverage for repo/blob export after splitting discovery, label RPC, account-status, and missing-blob checks into dedicated suites | 149 149 | `t/firehose.t` | audited local regression | repo subscription lifecycle, cursor, and CAR behavior | 150 150 | `t/identity.t` | local correctness/infrastructure | lower-level handle and DID helper coverage, including DNS-over-well-known preference and malformed-handle rejection | 151 151 | `t/import-repo.t` | audited local regression | focused `importRepo` snapshot-restore and rollback behavior, now cleaner after splitting the disabled-import policy gate into its own suite | ··· 158 158 | `t/labels.t` | audited local regression | label persistence, replay, negation, and cursor behavior | 159 159 | `t/local-service-surfaces.t` | audited local regression | isolated local-only coverage for reserved handles and crawler host tracking surfaces | 160 160 | `t/metrics.t` | audited local regression | metrics endpoint, token-gating smoke, and instrumentation contract for local appview behavior | 161 + | `t/missing-blob-surfaces.t` | audited local regression | isolated `listMissingBlobs` pagination and record-ownership coverage for direct and nested blob references | 161 162 | `t/moderation.t` | audited local regression | takedown visibility and moderation behavior | 162 163 | `t/oauth-include.t` | audited local regression | permission-set scope expansion and least-privilege enforcement from `include:<nsid>` scopes | 163 164 | `t/oauth-permissions.t` | audited local regression | granular OAuth permission enforcement across account/email, identity, repo, blob, and rpc scope families | ··· 188 189 - `t/applywrites-surfaces.t` 189 190 Carries real conformance value for `applyWrites`, but it is now much narrower and mostly acts as a focused repo-write regression suite. 190 191 - `t/external-surface.t` 191 - Carries strong external-surface coverage for repo/blob export and missing-blob listing. It is cleaner after moving discovery, label-RPC, and account-status checks into dedicated suites, but still remains broader than a single-endpoint conformance file. 192 + Carries strong external-surface coverage for repo/blob export. It is much cleaner after moving discovery, label-RPC, account-status, and missing-blob checks into dedicated suites, but still remains broader than a single-endpoint conformance file. 192 193 193 194 ## What This Audit Does Not Yet Claim 194 195
+22 -67
t/external-surface.t
··· 112 112 rkey => 'missing-blob-ref', 113 113 record => { 114 114 '$type' => 'com.example.record', 115 - note => 'blob reference for missing-blob listing', 115 + note => 'blob reference for sync/blob surface listing', 116 116 image => $blob, 117 117 }, 118 118 })->status_is(200); ··· 156 156 } => 'blob-two')->status_is(200); 157 157 158 158 my $blob_two_cid = $t->tx->res->json->{blob}{ref}{'$link'}; 159 - my @sorted_blob_cids = sort ($blob_cid); 159 + my @sorted_blob_cids = sort ($blob_cid, $blob_two_cid); 160 + 161 + $t->post_ok('/xrpc/com.atproto.repo.createRecord' => { 162 + Authorization => "Bearer $access", 163 + } => json => { 164 + repo => $did, 165 + collection => 'com.example.record', 166 + rkey => 'second-sync-blob-ref', 167 + record => { 168 + '$type' => 'com.example.record', 169 + note => 'second blob reference for sync/blob surface listing', 170 + image => { 171 + '$type' => 'blob', 172 + ref => { '$link' => $blob_two_cid }, 173 + mimeType => 'text/plain', 174 + size => length('blob-two'), 175 + }, 176 + }, 177 + })->status_is(200); 160 178 161 179 $t->get_ok(Mojo::URL->new('/xrpc/com.atproto.sync.listBlobs')->query( 162 180 did => $did, ··· 170 188 limit => 1, 171 189 cursor => $sorted_blob_cids[0], 172 190 ))->status_is(200) 173 - ->json_is('/cids' => []); 191 + ->json_is('/cids/0' => $sorted_blob_cids[1]); 174 192 175 193 $t->get_ok(Mojo::URL->new('/xrpc/com.atproto.sync.getBlob')->query( 176 194 did => $second_did, ··· 182 200 ->content_type_is('text/plain') 183 201 ->content_is('blob-bytes'); 184 202 185 - $t->post_ok('/xrpc/com.atproto.repo.uploadBlob' => { 186 - Authorization => "Bearer $access", 187 - 'Content-Type' => 'text/plain', 188 - } => 'nested-blob-bytes')->status_is(200); 189 - 190 - my $nested_blob = $t->tx->res->json->{blob}; 191 - my $nested_blob_cid = $nested_blob->{ref}{'$link'}; 192 - 193 - $t->post_ok('/xrpc/com.atproto.repo.createRecord' => { 194 - Authorization => "Bearer $access", 195 - } => json => { 196 - repo => $did, 197 - collection => 'com.example.record', 198 - rkey => 'nested-missing-blob-ref', 199 - record => { 200 - '$type' => 'com.example.record', 201 - note => 'nested blob reference for missing-blob listing', 202 - attachments => [{ 203 - kind => 'image', 204 - image => $nested_blob, 205 - }], 206 - }, 207 - })->status_is(200); 208 - 209 - my $nested_record_uri = $t->tx->res->json->{uri}; 210 - my @since_sorted_blob_cids = sort ($blob_cid, $nested_blob_cid); 203 + my @since_sorted_blob_cids = sort ($blob_cid, $blob_two_cid); 211 204 212 205 $t->get_ok(Mojo::URL->new('/xrpc/com.atproto.sync.listBlobs')->query( 213 206 did => $did, ··· 215 208 ))->status_is(200) 216 209 ->json_is('/cids/0' => $since_sorted_blob_cids[0]) 217 210 ->json_is('/cids/1' => $since_sorted_blob_cids[1]); 218 - 219 - for my $cid ($blob_cid, $nested_blob_cid) { 220 - $app->store->dbh->do( 221 - q{DELETE FROM blob_owners WHERE cid = ?}, 222 - undef, 223 - $cid, 224 - ); 225 - $app->store->dbh->do( 226 - q{DELETE FROM blobs WHERE cid = ?}, 227 - undef, 228 - $cid, 229 - ); 230 - } 231 - 232 - my %expected_missing = ( 233 - $blob_cid => "at://$did/com.example.record/missing-blob-ref", 234 - $nested_blob_cid => $nested_record_uri, 235 - ); 236 - my @missing_cids = sort keys %expected_missing; 237 - 238 - $t->get_ok(Mojo::URL->new('/xrpc/com.atproto.repo.listMissingBlobs')->query( 239 - limit => 1, 240 - ), { 241 - Authorization => "Bearer $access", 242 - })->status_is(200) 243 - ->json_is('/blobs/0/cid' => $missing_cids[0]) 244 - ->json_is('/blobs/0/recordUri' => $expected_missing{$missing_cids[0]}) 245 - ->json_is('/cursor' => $missing_cids[0]); 246 - 247 - $t->get_ok(Mojo::URL->new('/xrpc/com.atproto.repo.listMissingBlobs')->query( 248 - limit => 1, 249 - cursor => $missing_cids[0], 250 - ), { 251 - Authorization => "Bearer $access", 252 - })->status_is(200) 253 - ->json_is('/blobs/0/cid' => $missing_cids[1]) 254 - ->json_is('/blobs/0/recordUri' => $expected_missing{$missing_cids[1]}) 255 - ->json_is('/cursor' => $missing_cids[1]); 256 211 257 212 done_testing;
+137
t/missing-blob-surfaces.t
··· 1 + use v5.34; 2 + use warnings; 3 + 4 + use Config (); 5 + use File::Spec; 6 + use File::Temp qw(tempdir); 7 + use FindBin qw($Bin); 8 + use Test::More; 9 + 10 + BEGIN { 11 + require lib; 12 + my $root = File::Spec->rel2abs(File::Spec->catdir($Bin, '..')); 13 + lib->import( 14 + File::Spec->catdir($root, 'lib'), 15 + File::Spec->catdir($root, 'local', 'lib', 'perl5'), 16 + File::Spec->catdir($root, 'local', 'lib', 'perl5', $Config::Config{archname}), 17 + ); 18 + } 19 + 20 + use Test::Mojo; 21 + use Mojo::URL; 22 + use ATProto::PDS; 23 + 24 + my $root = File::Spec->rel2abs(File::Spec->catdir($Bin, '..')); 25 + my $tmp = tempdir(CLEANUP => 1); 26 + 27 + my $app = ATProto::PDS->new( 28 + project_root => $root, 29 + settings => { 30 + base_url => 'http://127.0.0.1:7755', 31 + service_handle_domain => 'example.test', 32 + service_did_method => 'did:web', 33 + jwt_secret => 'missing-blob-secret', 34 + admin_password => 'admin-secret', 35 + data_dir => File::Spec->catdir($tmp, 'data'), 36 + db_path => File::Spec->catfile($tmp, 'perlsky.sqlite'), 37 + }, 38 + ); 39 + 40 + my $t = Test::Mojo->new($app); 41 + 42 + $t->post_ok('/xrpc/com.atproto.server.createAccount' => json => { 43 + handle => 'alice.example.test', 44 + email => 'alice@example.test', 45 + password => 'hunter22', 46 + })->status_is(200); 47 + 48 + my $session = $t->tx->res->json; 49 + my $did = $session->{did}; 50 + my $access = $session->{accessJwt}; 51 + 52 + $t->post_ok('/xrpc/com.atproto.repo.uploadBlob' => { 53 + Authorization => "Bearer $access", 54 + 'Content-Type' => 'text/plain', 55 + } => 'blob-bytes')->status_is(200); 56 + 57 + my $blob = $t->tx->res->json->{blob}; 58 + my $blob_cid = $blob->{ref}{'$link'}; 59 + 60 + $t->post_ok('/xrpc/com.atproto.repo.createRecord' => { 61 + Authorization => "Bearer $access", 62 + } => json => { 63 + repo => $did, 64 + collection => 'com.example.record', 65 + rkey => 'missing-blob-ref', 66 + record => { 67 + '$type' => 'com.example.record', 68 + note => 'blob reference for missing-blob listing', 69 + image => $blob, 70 + }, 71 + })->status_is(200); 72 + 73 + $t->post_ok('/xrpc/com.atproto.repo.uploadBlob' => { 74 + Authorization => "Bearer $access", 75 + 'Content-Type' => 'text/plain', 76 + } => 'nested-blob-bytes')->status_is(200); 77 + 78 + my $nested_blob = $t->tx->res->json->{blob}; 79 + my $nested_blob_cid = $nested_blob->{ref}{'$link'}; 80 + 81 + $t->post_ok('/xrpc/com.atproto.repo.createRecord' => { 82 + Authorization => "Bearer $access", 83 + } => json => { 84 + repo => $did, 85 + collection => 'com.example.record', 86 + rkey => 'nested-missing-blob-ref', 87 + record => { 88 + '$type' => 'com.example.record', 89 + note => 'nested blob reference for missing-blob listing', 90 + attachments => [{ 91 + kind => 'image', 92 + image => $nested_blob, 93 + }], 94 + }, 95 + })->status_is(200); 96 + 97 + my $nested_record_uri = $t->tx->res->json->{uri}; 98 + 99 + for my $cid ($blob_cid, $nested_blob_cid) { 100 + $app->store->dbh->do( 101 + q{DELETE FROM blob_owners WHERE cid = ?}, 102 + undef, 103 + $cid, 104 + ); 105 + $app->store->dbh->do( 106 + q{DELETE FROM blobs WHERE cid = ?}, 107 + undef, 108 + $cid, 109 + ); 110 + } 111 + 112 + my %expected_missing = ( 113 + $blob_cid => "at://$did/com.example.record/missing-blob-ref", 114 + $nested_blob_cid => $nested_record_uri, 115 + ); 116 + my @missing_cids = sort keys %expected_missing; 117 + 118 + $t->get_ok(Mojo::URL->new('/xrpc/com.atproto.repo.listMissingBlobs')->query( 119 + limit => 1, 120 + ), { 121 + Authorization => "Bearer $access", 122 + })->status_is(200) 123 + ->json_is('/blobs/0/cid' => $missing_cids[0]) 124 + ->json_is('/blobs/0/recordUri' => $expected_missing{$missing_cids[0]}) 125 + ->json_is('/cursor' => $missing_cids[0]); 126 + 127 + $t->get_ok(Mojo::URL->new('/xrpc/com.atproto.repo.listMissingBlobs')->query( 128 + limit => 1, 129 + cursor => $missing_cids[0], 130 + ), { 131 + Authorization => "Bearer $access", 132 + })->status_is(200) 133 + ->json_is('/blobs/0/cid' => $missing_cids[1]) 134 + ->json_is('/blobs/0/recordUri' => $expected_missing{$missing_cids[1]}) 135 + ->json_is('/cursor' => $missing_cids[1]); 136 + 137 + done_testing;