| 1 | package idear |
| 2 | |
| 3 | import ( |
| 4 | "testing" |
| 5 | "time" |
| 6 | ) |
| 7 | |
| 8 | // TestMember_Active_Nil checks that a nil Member is never active — the |
| 9 | // contract Active() documents so callers can pass a lookup's zero value |
| 10 | // straight in without a separate nil check. |
| 11 | func TestMember_Active_Nil(t *testing.T) { |
| 12 | var m *Member |
| 13 | if m.Active() { |
| 14 | t.Fatalf("nil.Active() = true, want false") |
| 15 | } |
| 16 | } |
| 17 | |
| 18 | // TestMember_Active_Deactivated checks that a member with a non-nil |
| 19 | // DeactivatedAt is not active, regardless of how long ago. |
| 20 | func TestMember_Active_Deactivated(t *testing.T) { |
| 21 | at := time.Unix(0, 0) |
| 22 | m := &Member{ID: 1, Role: RoleMember, DeactivatedAt: &at} |
| 23 | if m.Active() { |
| 24 | t.Fatalf("Active() = true for a deactivated member, want false") |
| 25 | } |
| 26 | } |
| 27 | |
| 28 | // TestMember_Active_True checks the remaining case: a real member with |
| 29 | // no DeactivatedAt is active. |
| 30 | func TestMember_Active_True(t *testing.T) { |
| 31 | m := &Member{ID: 1, Role: RoleMember} |
| 32 | if !m.Active() { |
| 33 | t.Fatalf("Active() = false for a live member, want true") |
| 34 | } |
| 35 | } |
| 36 | |
| 37 | // The four Pending tests below each flip exactly one field away from |
| 38 | // the "would otherwise be pending" baseline, so each one independently |
| 39 | // proves its own reason for refusal. A single case combining all three |
| 40 | // reasons would pass even if two of the three checks were missing from |
| 41 | // the implementation. |
| 42 | |
| 43 | // TestInvitation_Pending_Accepted checks that an accepted invitation |
| 44 | // is never pending, even though it is neither revoked nor expired. |
| 45 | func TestInvitation_Pending_Accepted(t *testing.T) { |
| 46 | now := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) |
| 47 | acceptedAt := now.Add(-time.Hour) |
| 48 | inv := &Invitation{ |
| 49 | ExpiresAt: now.Add(24 * time.Hour), |
| 50 | AcceptedAt: &acceptedAt, |
| 51 | } |
| 52 | if inv.Pending(now) { |
| 53 | t.Fatalf("Pending(now) = true for an accepted invitation, want false") |
| 54 | } |
| 55 | } |
| 56 | |
| 57 | // TestInvitation_Pending_Revoked checks that a revoked invitation is |
| 58 | // never pending, even though it is neither accepted nor expired. |
| 59 | func TestInvitation_Pending_Revoked(t *testing.T) { |
| 60 | now := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) |
| 61 | revokedAt := now.Add(-time.Hour) |
| 62 | inv := &Invitation{ |
| 63 | ExpiresAt: now.Add(24 * time.Hour), |
| 64 | RevokedAt: &revokedAt, |
| 65 | } |
| 66 | if inv.Pending(now) { |
| 67 | t.Fatalf("Pending(now) = true for a revoked invitation, want false") |
| 68 | } |
| 69 | } |
| 70 | |
| 71 | // TestInvitation_Pending_Expired checks that an expired invitation is |
| 72 | // never pending, even though it is neither accepted nor revoked. |
| 73 | func TestInvitation_Pending_Expired(t *testing.T) { |
| 74 | now := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) |
| 75 | inv := &Invitation{ |
| 76 | ExpiresAt: now.Add(-time.Second), |
| 77 | } |
| 78 | if inv.Pending(now) { |
| 79 | t.Fatalf("Pending(now) = true for an expired invitation, want false") |
| 80 | } |
| 81 | } |
| 82 | |
| 83 | // TestInvitation_Pending_ExpiresAtNow pins the exact boundary: |
| 84 | // ExpiresAt == now is not pending. Pending uses ExpiresAt.After(now), |
| 85 | // so an invitation expiring at exactly now must read as already |
| 86 | // expired — matching the CAS SQL Task 3 writes, "expires_at > ?", |
| 87 | // which also excludes the boundary. Left unpinned, a future edit to |
| 88 | // use !now.After(ExpiresAt) (or similar) would flip this one instant |
| 89 | // without any other test noticing. |
| 90 | func TestInvitation_Pending_ExpiresAtNow(t *testing.T) { |
| 91 | now := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) |
| 92 | inv := &Invitation{ |
| 93 | ExpiresAt: now, |
| 94 | } |
| 95 | if inv.Pending(now) { |
| 96 | t.Fatalf("Pending(now) = true when ExpiresAt == now exactly, want false") |
| 97 | } |
| 98 | } |
| 99 | |
| 100 | // TestInvitation_Pending_True checks the remaining case: unaccepted, |
| 101 | // unrevoked, unexpired is pending. |
| 102 | func TestInvitation_Pending_True(t *testing.T) { |
| 103 | now := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) |
| 104 | inv := &Invitation{ |
| 105 | ExpiresAt: now.Add(24 * time.Hour), |
| 106 | } |
| 107 | if !inv.Pending(now) { |
| 108 | t.Fatalf("Pending(now) = false for a live invitation, want true") |
| 109 | } |
| 110 | } |
| 111 | |