Files
OwnCord/Server/db/role_invite_queries_test.go
J3vbandClaude Fable 5 6afa9e974c refactor(server): thread context.Context through the db layer and all callers
Fixes all 109 golangci-lint findings (106 contextcheck, 1 gocritic,
2 gosec) that accumulated after D2 wired dbgen (whose queries take ctx)
under ctx-less db.DB wrappers while CI lint was quota-dead. No nolint
comments added; every finding fixed by genuinely threading context.

- db: all 138 hand-written db.DB methods take ctx first; the dbCtx()
  Background shim is deleted; raw Query/QueryRow/Exec/Begin use their
  Context variants; the four redundant ctx-less passthroughs removed.
  db.Auditor/WriteAudit gain ctx.
- Seams: permissions.Checker (DB iface, HasChannelPerm,
  RequireChannelAccess) and the service.Store interface mirror the new
  signatures (ws.EventStore and plugin.PluginStore already did).
- Callers: api/admin handlers use r.Context(); ws per-message paths use
  the connection ctx via DispatchV2; hub loops and startup wiring use
  context.Background(); service methods thread ctx where they have one
  and Background where no ctx exists. Public service surface reached by
  ctx-holding chains (PermissionService.HasChannelPerm/GetRoleForUser/
  RequireChannelAccess, message/dm/block/invite/profile methods) is now
  ctx-first.
- Detached (context.WithoutCancel) where cancellation would break an
  invariant, found by a 3-lens adversarial review of the diff:
  * voice-leave background retries (a dead webhook/connection ctx killed
    retry 2 before it ran, leaving ghost capacity-holding voice rows)
  * rollbackVoiceJoin's compensating delete (its trigger IS the cancel)
  * post-2FA-change DeleteOtherSessions and logout DeleteSession (the
    security tail of a committed change must not die with the request)
  * all api/ws audit writes (a banned user could suppress their own
    login_blocked_banned row by aborting the request mid-bcrypt)
  * admin backup VACUUM INTO (an interrupt left a truncated .db that
    the backup list presented as restorable)
  * post-commit message/edit refetches (a committed message must still
    fan out when the sender disconnects)
  * hub settings-cache refresh (one dead connection could pin stale
    values for the 30s TTL)
- gocritic rangeValCopy fixed (index iteration); gosec G306 excluded in
  config with justification (generated source must stay world-readable)
  instead of flipping genprotocol output to 0o600.

Verified: gofmt/vet, all four build-tag variants, full suite, deadlock
pass, full -race pass, golangci-lint 0 issues uncapped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 17:03:52 +02:00

221 lines
6.4 KiB
Go

package db_test
import (
"context"
"testing"
)
// ─── GetRoleByID tests ────────────────────────────────────────────────────────
func TestGetRoleByID_Found(t *testing.T) {
database := newTestDB(t)
role, err := database.GetRoleByID(context.Background(), 4) // Member — inserted by migration
if err != nil {
t.Fatalf("GetRoleByID: %v", err)
}
if role == nil {
t.Fatal("GetRoleByID returned nil for Member role")
}
if role.Name != "Member" {
t.Errorf("Name = %q, want %q", role.Name, "Member")
}
if role.Permissions == 0 {
t.Error("Member permissions = 0, want non-zero")
}
}
func TestGetRoleByID_NotFound(t *testing.T) {
database := newTestDB(t)
role, err := database.GetRoleByID(context.Background(), 9999)
if err != nil {
t.Fatalf("GetRoleByID(not found): %v", err)
}
if role != nil {
t.Error("GetRoleByID returned non-nil for missing role")
}
}
func TestGetRoleByID_OwnerHasAllPermissions(t *testing.T) {
database := newTestDB(t)
role, err := database.GetRoleByID(context.Background(), 1) // Owner
if err != nil {
t.Fatalf("GetRoleByID Owner: %v", err)
}
if role == nil {
t.Fatal("GetRoleByID returned nil for Owner role")
}
// Owner has permissions = 0x7FFFFFFF = 2147483647
if role.Permissions != 2147483647 {
t.Errorf("Owner Permissions = %d, want 2147483647", role.Permissions)
}
}
func TestGetRoleByID_IsDefaultField(t *testing.T) {
database := newTestDB(t)
owner, _ := database.GetRoleByID(context.Background(), 1)
member, _ := database.GetRoleByID(context.Background(), 4)
if owner.IsDefault {
t.Error("Owner.IsDefault = true, want false")
}
// Member is the default role (is_default=1 in the migration).
if !member.IsDefault {
t.Error("Member.IsDefault = false, want true (Member is the default role for new users)")
}
}
// ─── ListRoles tests ──────────────────────────────────────────────────────────
func TestListRoles_ReturnsFourDefaultRoles(t *testing.T) {
database := newTestDB(t)
roles, err := database.ListRoles(context.Background())
if err != nil {
t.Fatalf("ListRoles: %v", err)
}
if len(roles) != 4 {
t.Errorf("ListRoles count = %d, want 4", len(roles))
}
}
func TestListRoles_OrderedByPositionDesc(t *testing.T) {
database := newTestDB(t)
roles, err := database.ListRoles(context.Background())
if err != nil {
t.Fatalf("ListRoles: %v", err)
}
for i := 1; i < len(roles); i++ {
if roles[i].Position > roles[i-1].Position {
t.Errorf("ListRoles not ordered by position DESC: index %d (%d) > index %d (%d)",
i, roles[i].Position, i-1, roles[i-1].Position)
}
}
}
// ─── GetUserWithRole tests ────────────────────────────────────────────────────
func TestGetUserWithRole_Found(t *testing.T) {
database := newTestDB(t)
uid, err := database.CreateUser(context.Background(), "joinuser", "hash", 4) // Member role
if err != nil {
t.Fatalf("CreateUser: %v", err)
}
user, role, err := database.GetUserWithRole(context.Background(), uid)
if err != nil {
t.Fatalf("GetUserWithRole: %v", err)
}
if user == nil || role == nil {
t.Fatal("GetUserWithRole returned nil user or role")
}
if user.ID != uid {
t.Errorf("user.ID = %d, want %d", user.ID, uid)
}
if user.Username != "joinuser" {
t.Errorf("user.Username = %q, want %q", user.Username, "joinuser")
}
if role.ID != 4 {
t.Errorf("role.ID = %d, want 4 (Member)", role.ID)
}
if role.Name != "Member" {
t.Errorf("role.Name = %q, want %q", role.Name, "Member")
}
if role.Permissions == 0 {
t.Error("role.Permissions = 0, want non-zero for Member")
}
}
func TestGetUserWithRole_NotFound(t *testing.T) {
database := newTestDB(t)
user, role, err := database.GetUserWithRole(context.Background(), 9999)
if err != nil {
t.Fatalf("GetUserWithRole(not found): %v", err)
}
if user != nil || role != nil {
t.Error("GetUserWithRole returned non-nil for missing user")
}
}
func TestGetUserWithRole_BoolConversions(t *testing.T) {
database := newTestDB(t)
uid, _ := database.CreateUser(context.Background(), "booluser", "hash", 4)
user, role, err := database.GetUserWithRole(context.Background(), uid)
if err != nil {
t.Fatalf("GetUserWithRole: %v", err)
}
// Fresh user should not be banned.
if user.Banned {
t.Error("user.Banned = true, want false for new user")
}
// Member role has is_default=1.
if !role.IsDefault {
t.Error("role.IsDefault = false, want true for Member")
}
}
// ─── ListInvites tests ────────────────────────────────────────────────────────
func TestListInvites_Empty(t *testing.T) {
database := newTestDB(t)
invites, err := database.ListInvites(context.Background())
if err != nil {
t.Fatalf("ListInvites empty: %v", err)
}
if len(invites) != 0 {
t.Errorf("ListInvites empty = %d items, want 0", len(invites))
}
}
func TestListInvites_Multiple(t *testing.T) {
database := newTestDB(t)
uid, _ := database.CreateUser(context.Background(), "listowner", "hash", 4)
_, _ = database.CreateInvite(context.Background(), uid, 1, nil)
_, _ = database.CreateInvite(context.Background(), uid, 5, nil)
_, _ = database.CreateInvite(context.Background(), uid, 0, nil)
invites, err := database.ListInvites(context.Background())
if err != nil {
t.Fatalf("ListInvites multiple: %v", err)
}
if len(invites) != 3 {
t.Errorf("ListInvites count = %d, want 3", len(invites))
}
}
func TestListInvites_IncludesRevokedInvites(t *testing.T) {
database := newTestDB(t)
uid, _ := database.CreateUser(context.Background(), "revokelistowner", "hash", 4)
code, _ := database.CreateInvite(context.Background(), uid, 1, nil)
_ = database.RevokeInvite(context.Background(), code)
_, _ = database.CreateInvite(context.Background(), uid, 0, nil) // active
invites, err := database.ListInvites(context.Background())
if err != nil {
t.Fatalf("ListInvites with revoked: %v", err)
}
if len(invites) != 2 {
t.Errorf("ListInvites count = %d, want 2", len(invites))
}
var revokedCount int
for _, inv := range invites {
if inv.Revoked {
revokedCount++
}
}
if revokedCount != 1 {
t.Errorf("ListInvites revoked count = %d, want 1", revokedCount)
}
}