···11# Test Audit Status
2233-As of 2026-03-12, the focused test-correctness and reference-audit pass is complete on rewritten history through `6f181ab`.
33+As of 2026-03-12, the focused test-correctness and reference-audit pass is complete on rewritten history through the current post-`6f181ab` conformance sweep.
4455That does not mean every test has been manually revalidated against every other PDS implementation line by line. It means:
66···1313The current baseline for saying "the audited suite is green" is:
14141515- `prove -lr t`
1616- - latest full green result in the realigned Meridian worktree: `Files=48, Tests=2880`
1616+ - latest full green result in the realigned Meridian worktree: `Files=48, Tests=2898`
1717- `prove -lv t/server-auth.t`
1818- `perl -c script/differential-validate`
1919- `PERLSKY_RUN_REFERENCE_DIFF=1 prove -lv t/reference-differential.t`
···4949- Deactivated accounts should still be able to establish and refresh sessions, but those responses must stay marked `active=false` with `status=deactivated`.
5050- Local `app.bsky.*` emulation must be conservative: only synthesize owner-local feed/thread data when the PDS can answer authoritatively, and proxy upstream instead of inventing partial global state.
5151- Account email handling needs consistent normalization on write, lookup, session creation, and confirmation checks; treating email case inconsistently leaves both tests and user-facing auth behavior brittle.
5252+- `com.atproto.server.requestEmailConfirmation`, `requestEmailUpdate`, and `requestAccountDelete` should reject accounts with no stored email using the official `400 InvalidRequest` / `account does not have an email address` shape, and `updateEmail` should reject unsupported syntax with the official `This email address is not supported, please use a different email.` message.
5253- `app.bsky.actor.putPreferences` and `app.bsky.notification.putPreferencesV2` now have explicit shape validation plus focused regression coverage, turning an earlier hardening concern into a pinned contract.
5354- `com.atproto.identity.resolveHandle` should reject malformed handles with `400 InvalidRequest`, not quietly treat them as misses or return a local `InvalidHandle` variant.
5455- `com.atproto.identity.resolveHandle` should treat well-formed but unresolved handles as `400 InvalidRequest` with `Unable to resolve handle`, matching the official runtime instead of returning a local `404 HandleNotFound`.
···9899| `t/crawlers.t` | audited local regression | outbound crawl notification semantics |
99100| `t/crypto-interop.t` | direct reference differential | pinned upstream crypto fixture coverage |
100101| `t/delete-account.t` | audited local regression | reference-style account deletion flow using DID, password, and action token without a live bearer session |
101101-| `t/email-confirmation.t` | audited local regression | intentionally testing-friendly email flow |
102102+| `t/email-confirmation.t` | audited local regression | intentionally testing-friendly email flow plus strict missing-email and invalid-email validation semantics |
102103| `t/event-stream.t` | audited local regression | wire-format, malformed frame, and event decoding coverage |
103104| `t/extended-api.t` | audited local regression | broad XRPC behavior including invites and moderation-adjacent flows; still intentionally mixes conformance-ish happy paths with local-policy coverage |
104105| `t/external-surface.t` | audited local regression | external repo/account surface including missing-blob behavior; intentionally broad, with order-insensitive assertions for label presence rather than brittle label ordering |
+18-2
lib/ATProto/PDS/API/Server.pm
···473473 },
474474 );
475475 _assert_full_non_oauth_access($claims);
476476- return {} unless $account->{email};
476476+ xrpc_error(400, 'InvalidRequest', 'account does not have an email address')
477477+ unless defined($account->{email}) && length($account->{email});
477478 issue_account_action_token(
478479 $c,
479480 $account,
···527528 },
528529 );
529530 _assert_full_non_oauth_access($claims);
531531+ xrpc_error(400, 'InvalidRequest', 'account does not have an email address')
532532+ unless defined($account->{email}) && length($account->{email});
530533 my $token_required = defined $account->{email_confirmed_at} ? 1 : 0;
531534 if ($token_required) {
532535 issue_account_action_token(
···550553 disallow_oauth => 1,
551554 );
552555 my $body = $c->req->json || {};
556556+ my $email = _supported_email($body->{email});
557557+ xrpc_error(400, 'InvalidRequest', 'This email address is not supported, please use a different email.')
558558+ unless defined $email;
553559 if (defined $account->{email_confirmed_at}) {
554560 xrpc_error(400, 'TokenRequired', 'A confirmation token is required to update email')
555561 unless defined($body->{token}) && length($body->{token});
···561567 unless ($token->{did} // q()) eq $account->{did};
562568 $c->store->consume_action_token($token->{token});
563569 }
564564- update_account_email($c, $account->{did}, $body->{email});
570570+ update_account_email($c, $account->{did}, $email);
565571 return {};
566572 });
567573···572578 required_scope => 'full',
573579 disallow_oauth => 1,
574580 );
581581+ xrpc_error(400, 'InvalidRequest', 'account does not have an email address')
582582+ unless defined($account->{email}) && length($account->{email});
575583 issue_account_action_token(
576584 $c,
577585 $account,
···11491157sub _normalize_email ($email) {
11501158 return undef unless defined $email;
11511159 return lc $email;
11601160+}
11611161+11621162+sub _supported_email ($email) {
11631163+ my $normalized = _normalize_email($email);
11641164+ return undef unless defined($normalized) && length($normalized);
11651165+ return undef if $normalized =~ /\s/;
11661166+ return undef unless $normalized =~ /\A[^\s\@]+\@[A-Za-z0-9](?:[A-Za-z0-9\-]{0,61}[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9\-]{0,61}[A-Za-z0-9])?)+\z/;
11671167+ return $normalized;
11521168}
1153116911541170sub _require_action_token ($c, %args) {
+32
t/email-confirmation.t
···124124 'stale confirmation tokens cannot confirm a changed email address',
125125);
126126127127+$t->post_ok('/xrpc/com.atproto.server.updateEmail' => {
128128+ Authorization => "Bearer $alice->{accessJwt}",
129129+} => json => {
130130+ email => 'not-an-email',
131131+})->status_is(400)
132132+ ->json_is('/error' => 'InvalidRequest')
133133+ ->json_is('/message' => 'This email address is not supported, please use a different email.');
134134+127135$t->post_ok('/xrpc/com.atproto.server.createAccount' => json => {
128136 handle => 'bob.example.test',
129137 email => 'bob@example.test',
···190198 token => $carol_token->{token},
191199})->status_is(200)
192200 ->json_is({});
201201+202202+$bypass_t->post_ok('/xrpc/com.atproto.server.createAccount' => json => {
203203+ handle => 'noemail.example.test',
204204+ password => 'hunter22',
205205+})->status_is(200);
206206+my $noemail = $bypass_t->tx->res->json;
207207+208208+$bypass_t->post_ok('/xrpc/com.atproto.server.requestEmailConfirmation' => {
209209+ Authorization => "Bearer $noemail->{accessJwt}",
210210+} => json => {})->status_is(400)
211211+ ->json_is('/error' => 'InvalidRequest')
212212+ ->json_is('/message' => 'account does not have an email address');
213213+214214+$bypass_t->post_ok('/xrpc/com.atproto.server.requestEmailUpdate' => {
215215+ Authorization => "Bearer $noemail->{accessJwt}",
216216+} => json => {})->status_is(400)
217217+ ->json_is('/error' => 'InvalidRequest')
218218+ ->json_is('/message' => 'account does not have an email address');
219219+220220+$bypass_t->post_ok('/xrpc/com.atproto.server.requestAccountDelete' => {
221221+ Authorization => "Bearer $noemail->{accessJwt}",
222222+} => json => {})->status_is(400)
223223+ ->json_is('/error' => 'InvalidRequest')
224224+ ->json_is('/message' => 'account does not have an email address');
193225194226done_testing;