package idear import ( "testing" "time" ) // TestMember_Active_Nil checks that a nil Member is never active — the // contract Active() documents so callers can pass a lookup's zero value // straight in without a separate nil check. func TestMember_Active_Nil(t *testing.T) { var m *Member if m.Active() { t.Fatalf("nil.Active() = true, want false") } } // TestMember_Active_Deactivated checks that a member with a non-nil // DeactivatedAt is not active, regardless of how long ago. func TestMember_Active_Deactivated(t *testing.T) { at := time.Unix(0, 0) m := &Member{ID: 1, Role: RoleMember, DeactivatedAt: &at} if m.Active() { t.Fatalf("Active() = true for a deactivated member, want false") } } // TestMember_Active_True checks the remaining case: a real member with // no DeactivatedAt is active. func TestMember_Active_True(t *testing.T) { m := &Member{ID: 1, Role: RoleMember} if !m.Active() { t.Fatalf("Active() = false for a live member, want true") } } // The four Pending tests below each flip exactly one field away from // the "would otherwise be pending" baseline, so each one independently // proves its own reason for refusal. A single case combining all three // reasons would pass even if two of the three checks were missing from // the implementation. // TestInvitation_Pending_Accepted checks that an accepted invitation // is never pending, even though it is neither revoked nor expired. func TestInvitation_Pending_Accepted(t *testing.T) { now := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) acceptedAt := now.Add(-time.Hour) inv := &Invitation{ ExpiresAt: now.Add(24 * time.Hour), AcceptedAt: &acceptedAt, } if inv.Pending(now) { t.Fatalf("Pending(now) = true for an accepted invitation, want false") } } // TestInvitation_Pending_Revoked checks that a revoked invitation is // never pending, even though it is neither accepted nor expired. func TestInvitation_Pending_Revoked(t *testing.T) { now := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) revokedAt := now.Add(-time.Hour) inv := &Invitation{ ExpiresAt: now.Add(24 * time.Hour), RevokedAt: &revokedAt, } if inv.Pending(now) { t.Fatalf("Pending(now) = true for a revoked invitation, want false") } } // TestInvitation_Pending_Expired checks that an expired invitation is // never pending, even though it is neither accepted nor revoked. func TestInvitation_Pending_Expired(t *testing.T) { now := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) inv := &Invitation{ ExpiresAt: now.Add(-time.Second), } if inv.Pending(now) { t.Fatalf("Pending(now) = true for an expired invitation, want false") } } // TestInvitation_Pending_ExpiresAtNow pins the exact boundary: // ExpiresAt == now is not pending. Pending uses ExpiresAt.After(now), // so an invitation expiring at exactly now must read as already // expired — matching the CAS SQL Task 3 writes, "expires_at > ?", // which also excludes the boundary. Left unpinned, a future edit to // use !now.After(ExpiresAt) (or similar) would flip this one instant // without any other test noticing. func TestInvitation_Pending_ExpiresAtNow(t *testing.T) { now := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) inv := &Invitation{ ExpiresAt: now, } if inv.Pending(now) { t.Fatalf("Pending(now) = true when ExpiresAt == now exactly, want false") } } // TestInvitation_Pending_True checks the remaining case: unaccepted, // unrevoked, unexpired is pending. func TestInvitation_Pending_True(t *testing.T) { now := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) inv := &Invitation{ ExpiresAt: now.Add(24 * time.Hour), } if !inv.Pending(now) { t.Fatalf("Pending(now) = false for a live invitation, want true") } }