package service import ( "context" "errors" "fmt" "testing" "github.com/owncord/server/db" "github.com/owncord/server/permissions" ) // newTestModerationService seeds a real in-memory DB with a role hierarchy: // owner (pos 100, Administrator) > mod (pos 80, BanMembers) > member (pos 40). // Users: 1=owner, 2=mod, 3=member, 4=member, 5=mod (equal rank to 2). func newTestModerationService(t *testing.T) (*ModerationService, *db.DB) { t.Helper() database := newTestDB(t) seedRole(t, database, &db.Role{ID: 1, Name: "owner", Permissions: permissions.Administrator, Position: 100}) seedRole(t, database, &db.Role{ID: 2, Name: "mod", Permissions: permissions.BanMembers, Position: 80}) seedRole(t, database, &db.Role{ID: 3, Name: "member", Permissions: permissions.SendMessages, Position: 40}) for userID, roleID := range map[int64]int64{1: 1, 2: 2, 3: 3, 4: 3, 5: 2} { seedUser(t, database, &db.User{ID: userID, Username: fmt.Sprintf("u%d", userID), Status: "offline"}) seedUserRole(t, database, userID, roleID) } checker := permissions.NewChecker(database) return NewModerationService(database, NewPermissionService(database, checker)), database } func TestBanUser_RequiresBanPermission(t *testing.T) { svc, _ := newTestModerationService(t) // A member without BAN_MEMBERS is refused. if err := svc.BanUser(context.Background(), 3, 4, "nope", nil); !errors.Is(err, ErrForbidden) { t.Fatalf("member ban attempt: want ErrForbidden, got %v", err) } // And gets Forbidden — not NotFound — for a nonexistent target, so the // ban path cannot be used to enumerate user ids. if err := svc.BanUser(context.Background(), 3, 999, "probe", nil); !errors.Is(err, ErrForbidden) { t.Fatalf("unauthorized probe of missing id: want ErrForbidden, got %v", err) } } func TestBanUser_HierarchyEnforced(t *testing.T) { svc, database := newTestModerationService(t) // Equal rank: mod cannot ban mod. if err := svc.BanUser(context.Background(), 2, 5, "peer", nil); !errors.Is(err, ErrForbidden) { t.Fatalf("equal-rank ban: want ErrForbidden, got %v", err) } // Higher rank target: mod cannot ban the owner. if err := svc.BanUser(context.Background(), 2, 1, "coup", nil); !errors.Is(err, ErrForbidden) { t.Fatalf("ban owner: want ErrForbidden, got %v", err) } owner, _ := database.GetUserByID(1) if owner.Banned { t.Fatal("owner must not be banned") } } func TestBanUser_AuthorizedSucceeds(t *testing.T) { svc, database := newTestModerationService(t) if err := svc.BanUser(context.Background(), 2, 3, "spam", nil); err != nil { t.Fatalf("authorized ban: %v", err) } target, _ := database.GetUserByID(3) if !target.Banned { t.Fatal("target should be banned") } if target.BanReason == nil || *target.BanReason != "spam" { t.Fatalf("ban reason not recorded: %v", target.BanReason) } // Authorized actor gets a real NotFound for a missing target. if err := svc.BanUser(context.Background(), 2, 999, "gone", nil); !errors.Is(err, ErrNotFound) { t.Fatalf("authorized ban of missing id: want ErrNotFound, got %v", err) } // Self-ban is a bad request regardless of authority. if err := svc.BanUser(context.Background(), 2, 2, "self", nil); !errors.Is(err, ErrBadRequest) { t.Fatalf("self ban: want ErrBadRequest, got %v", err) } } func TestUnbanUser_AuthorizationMatrix(t *testing.T) { svc, database := newTestModerationService(t) if err := svc.BanUser(context.Background(), 1, 3, "setup", nil); err != nil { t.Fatalf("setup ban: %v", err) } // No BAN_MEMBERS → Forbidden (member 4 trying to unban member 3). if err := svc.UnbanUser(context.Background(), 4, 3); !errors.Is(err, ErrForbidden) { t.Fatalf("member unban: want ErrForbidden, got %v", err) } // Equal rank → Forbidden. if err := svc.UnbanUser(context.Background(), 2, 5); !errors.Is(err, ErrForbidden) { t.Fatalf("equal-rank unban: want ErrForbidden, got %v", err) } // Authorized → succeeds. if err := svc.UnbanUser(context.Background(), 2, 3); err != nil { t.Fatalf("authorized unban: %v", err) } target, _ := database.GetUserByID(3) if target.Banned { t.Fatal("target should be unbanned") } }