| 1 | package sheets |
| 2 | |
| 3 | import "testing" |
| 4 | |
| 5 | func TestMemberForIsIdempotent(t *testing.T) { |
| 6 | app := newTestApp(t) |
| 7 | a, err := app.memberFor("Ada@example.com") |
| 8 | if err != nil { |
| 9 | t.Fatalf("memberFor: %v", err) |
| 10 | } |
| 11 | if a.ID == 0 { |
| 12 | t.Fatal("memberFor returned id 0 — the uid-0 trap AGENTS.md names") |
| 13 | } |
| 14 | b, err := app.memberFor("ada@example.com") |
| 15 | if err != nil { |
| 16 | t.Fatalf("memberFor again: %v", err) |
| 17 | } |
| 18 | if b.ID != a.ID { |
| 19 | t.Fatalf("the same person got two ids (%d then %d): the address "+ |
| 20 | "comparison is case-sensitive, but the allowlist's is not", a.ID, b.ID) |
| 21 | } |
| 22 | } |
| 23 | |
| 24 | func TestMemberForSurroundingSpaceIsTheSamePerson(t *testing.T) { |
| 25 | app := newTestApp(t) |
| 26 | a, err := app.memberFor("ada@example.com") |
| 27 | if err != nil { |
| 28 | t.Fatalf("memberFor: %v", err) |
| 29 | } |
| 30 | b, err := app.memberFor(" ada@example.com ") |
| 31 | if err != nil { |
| 32 | t.Fatalf("memberFor padded: %v", err) |
| 33 | } |
| 34 | if a.ID != b.ID { |
| 35 | t.Fatalf("a padded address minted a second member: %d then %d", a.ID, b.ID) |
| 36 | } |
| 37 | } |
| 38 | |
| 39 | func TestMemberForRejectsAnEmptyAddress(t *testing.T) { |
| 40 | app := newTestApp(t) |
| 41 | if _, err := app.memberFor(""); err == nil { |
| 42 | t.Fatal(`memberFor("") = nil error; an empty address must never mint a member`) |
| 43 | } |
| 44 | if _, err := app.memberFor(" "); err == nil { |
| 45 | t.Fatal("memberFor with only whitespace = nil error, want a refusal") |
| 46 | } |
| 47 | } |
| 48 | |
| 49 | func TestTwoPeopleGetTwoIDs(t *testing.T) { |
| 50 | app := newTestApp(t) |
| 51 | ada, err := app.memberFor("ada@example.com") |
| 52 | if err != nil { |
| 53 | t.Fatalf("memberFor ada: %v", err) |
| 54 | } |
| 55 | bob, err := app.memberFor("bob@example.com") |
| 56 | if err != nil { |
| 57 | t.Fatalf("memberFor bob: %v", err) |
| 58 | } |
| 59 | if ada.ID == bob.ID { |
| 60 | t.Fatalf("two addresses share one actor id (%d); every edit would be misattributed", ada.ID) |
| 61 | } |
| 62 | } |
| 63 | |
| 64 | // A concurrent first sight is the realistic case — two tabs, one |
| 65 | // person, both arriving on the same magic link — and read-then-create |
| 66 | // loses it. |
| 67 | func TestMemberForIsSafeUnderConcurrentFirstSight(t *testing.T) { |
| 68 | app := newTestApp(t) |
| 69 | const n = 8 |
| 70 | ids := make(chan uint64, n) |
| 71 | errs := make(chan error, n) |
| 72 | start := make(chan struct{}) |
| 73 | for i := 0; i < n; i++ { |
| 74 | go func() { |
| 75 | <-start |
| 76 | m, err := app.memberFor("ada@example.com") |
| 77 | if err != nil { |
| 78 | errs <- err |
| 79 | return |
| 80 | } |
| 81 | ids <- m.ID |
| 82 | }() |
| 83 | } |
| 84 | close(start) |
| 85 | var first uint64 |
| 86 | for i := 0; i < n; i++ { |
| 87 | select { |
| 88 | case err := <-errs: |
| 89 | t.Fatalf("concurrent memberFor: %v", err) |
| 90 | case id := <-ids: |
| 91 | if first == 0 { |
| 92 | first = id |
| 93 | } else if id != first { |
| 94 | t.Fatalf("concurrent first sight minted two members: %d and %d", first, id) |
| 95 | } |
| 96 | } |
| 97 | } |
| 98 | } |
| 99 | |