TestExportUserData verifies the right-of-access flow. The export should contain the user's profile, every domain/agent/key/message they own, and exclude internal identifiers (google_subject, key hashes, session tokens).
(t *testing.T)
| 91 | // they own, and exclude internal identifiers (google_subject, key |
| 92 | // hashes, session tokens). |
| 93 | func TestExportUserData(t *testing.T) { |
| 94 | pool := testutil.TestDB(t) |
| 95 | store := identity.NewStore(pool) |
| 96 | ctx := context.Background() |
| 97 | |
| 98 | user := seedUserData(t, store, ctx, "exporter") |
| 99 | |
| 100 | dump, err := store.ExportUserData(ctx, user.ID) |
| 101 | if err != nil { |
| 102 | t.Fatalf("ExportUserData: %v", err) |
| 103 | } |
| 104 | |
| 105 | if dump.User.ID != user.ID || dump.User.Email != user.Email { |
| 106 | t.Errorf("user mismatch: got id=%s email=%s, want %s/%s", |
| 107 | dump.User.ID, dump.User.Email, user.ID, user.Email) |
| 108 | } |
| 109 | if len(dump.Domains) != 1 { |
| 110 | t.Errorf("domains: got %d, want 1", len(dump.Domains)) |
| 111 | } |
| 112 | if len(dump.Agents) != 1 { |
| 113 | t.Errorf("agents: got %d, want 1", len(dump.Agents)) |
| 114 | } |
| 115 | if len(dump.APIKeys) != 2 { |
| 116 | t.Errorf("api_keys: got %d, want 2", len(dump.APIKeys)) |
| 117 | } |
| 118 | if len(dump.Messages) != 2 { |
| 119 | t.Errorf("messages: got %d, want 2 (1 inbound + 1 outbound)", len(dump.Messages)) |
| 120 | } |
| 121 | if len(dump.Suppressions) != 1 || dump.Suppressions[0].Address != "blocked@spam.com" { |
| 122 | t.Errorf("suppressions: got %+v, want 1 (blocked@spam.com)", dump.Suppressions) |
| 123 | } |
| 124 | if len(dump.ProtectionEvents) != 1 || dump.ProtectionEvents[0].SubjectAddr != "alice@gmail.com" { |
| 125 | t.Errorf("protection_events: got %+v, want 1 (subject alice@gmail.com)", dump.ProtectionEvents) |
| 126 | } |
| 127 | |
| 128 | // Right-of-access requires every stored header field to round-trip |
| 129 | // through the export. Reply-To regression-guard: if a future SELECT |
| 130 | // drops the column, the user's export silently loses data — exactly |
| 131 | // the kind of gap that fails a data-rights audit. |
| 132 | var inbound *identity.Message |
| 133 | for i := range dump.Messages { |
| 134 | if dump.Messages[i].Direction == "inbound" { |
| 135 | inbound = &dump.Messages[i] |
| 136 | break |
| 137 | } |
| 138 | } |
| 139 | if inbound == nil { |
| 140 | t.Fatal("no inbound message in export") |
| 141 | } |
| 142 | wantReplyTo := []string{"real-alice@example.com"} |
| 143 | if !reflect.DeepEqual(inbound.ReplyTo, wantReplyTo) { |
| 144 | t.Errorf("inbound ReplyTo in export = %v, want %v", inbound.ReplyTo, wantReplyTo) |
| 145 | } |
| 146 | |
| 147 | // Confirm the export doesn't leak internal identifiers. We marshal |
| 148 | // to JSON because the most likely accidental leak path is a struct |
| 149 | // field with a `json:` tag we forgot. |
| 150 | raw, err := json.Marshal(dump) |
nothing calls this directly
no test coverage detected