mirror of
https://github.com/J3vb/OwnCord.git
synced 2026-09-03 03:50:00 +03:00
Deletes Server/store (SQLiteStore, MemStore, the composed Store interface) and collapses to a single sqlc-backed db package, executing the prior audit's P4 "single data layer" direction (finding #6). SQLiteStore was a pure pass-through to *db.DB, so consumers now depend on narrow interfaces that *db.DB satisfies directly: - service.Store (service/datastore.go, renamed from store/store.go) - ws.EventStore (ws/eventstore.go) - plugin.PluginStore (plugin/pluginstore.go) The event- and plugin-KV methods that lived in the store's SQLite implementation move into the db package (db/event_queries.go, db/plugin_queries.go), keeping their raw-SQL form. Tests: the MemStore-based unit tests now run against a real in-memory SQLite db opened per-test with migrations applied, via package-local seed helpers. Fault-injection tests embed a real *db.DB and override the single method under test, preserving error-path coverage. Full server suite and sqlc-verify are green. Docs: audit finding #6 and A-2026-07-06 marked resolved; decisions D3 updated; architecture server.md / data-model.md diagrams and prose updated to the api -> service -> db layering. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UA17KPvqGBX3XbXYnMf1rA
109 lines
4.0 KiB
Go
109 lines
4.0 KiB
Go
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")
|
|
}
|
|
}
|