Files
OwnCord/Server/service/permission_test.go
T
Claude 5f1d6fc287 refactor(server): remove the store abstraction layer (D3)
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
2026-07-19 16:33:58 +00:00

193 lines
6.6 KiB
Go

package service
import (
"testing"
"time"
"github.com/owncord/server/db"
"github.com/owncord/server/permissions"
)
// newTestPermService creates a PermissionService backed by a real in-memory DB
// pre-populated with a single role and user.
func newTestPermService(t *testing.T) (*PermissionService, *db.DB) {
t.Helper()
database := newTestDB(t)
seedRole(t, database, &db.Role{
ID: permissions.MemberRoleID,
Name: "member",
Permissions: permissions.SendMessages | permissions.ReadMessages | permissions.AddReactions,
Position: 1,
})
seedUserRole(t, database, 1, permissions.MemberRoleID)
checker := permissions.NewChecker(database)
return NewPermissionService(database, checker), database
}
func TestHasChannelPerm_Allowed(t *testing.T) {
svc, database := newTestPermService(t)
seedChannel(t, database, &db.Channel{ID: 10, Name: "general", Type: "text"})
// Member has SendMessages | ReadMessages; no overrides exist, so base role perms apply.
if !svc.HasChannelPerm(1, 10, permissions.SendMessages) {
t.Fatal("expected user to have SendMessages permission")
}
if !svc.HasChannelPerm(1, 10, permissions.ReadMessages) {
t.Fatal("expected user to have ReadMessages permission")
}
}
func TestHasChannelPerm_Denied(t *testing.T) {
svc, database := newTestPermService(t)
seedChannel(t, database, &db.Channel{ID: 10, Name: "general", Type: "text"})
// ManageMessages is NOT in the member role.
if svc.HasChannelPerm(1, 10, permissions.ManageMessages) {
t.Fatal("expected user to NOT have ManageMessages permission")
}
}
func TestHasChannelPerm_OverrideDeny(t *testing.T) {
svc, database := newTestPermService(t)
seedChannel(t, database, &db.Channel{ID: 10, Name: "readonly", Type: "text"})
// Deny SendMessages for this channel.
seedChannelOverride(t, database, permissions.MemberRoleID, 10, 0, permissions.SendMessages)
// Invalidate so next check re-populates cache.
svc.InvalidateAll()
if svc.HasChannelPerm(1, 10, permissions.SendMessages) {
t.Fatal("expected SendMessages to be denied via channel override")
}
// ReadMessages should still be allowed.
if !svc.HasChannelPerm(1, 10, permissions.ReadMessages) {
t.Fatal("expected ReadMessages to remain allowed")
}
}
func TestHasChannelPerm_OverrideAllow(t *testing.T) {
svc, database := newTestPermService(t)
seedChannel(t, database, &db.Channel{ID: 10, Name: "special", Type: "text"})
// Allow ManageMessages (not in base role) via channel override.
seedChannelOverride(t, database, permissions.MemberRoleID, 10, permissions.ManageMessages, 0)
svc.InvalidateAll()
if !svc.HasChannelPerm(1, 10, permissions.ManageMessages) {
t.Fatal("expected ManageMessages to be allowed via channel override")
}
}
func TestHasChannelPerm_AdminBypass(t *testing.T) {
database := newTestDB(t)
seedRole(t, database, &db.Role{
ID: permissions.AdminRoleID,
Name: "admin",
Permissions: permissions.Administrator,
Position: 90,
})
seedUserRole(t, database, 1, permissions.AdminRoleID)
checker := permissions.NewChecker(database)
svc := NewPermissionService(database, checker)
seedChannel(t, database, &db.Channel{ID: 10, Name: "locked", Type: "text"})
// Deny everything via override; admin should still bypass.
seedChannelOverride(t, database, permissions.AdminRoleID, 10, 0, permissions.SendMessages|permissions.ReadMessages)
if !svc.HasChannelPerm(1, 10, permissions.SendMessages) {
t.Fatal("admin should bypass all permission checks")
}
if !svc.HasChannelPerm(1, 10, permissions.ManageMessages) {
t.Fatal("admin should bypass all permission checks")
}
}
func TestInvalidateUser_ClearsCacheForUser(t *testing.T) {
svc, database := newTestPermService(t)
seedChannel(t, database, &db.Channel{ID: 10, Name: "general", Type: "text"})
// Populate cache.
svc.HasChannelPerm(1, 10, permissions.SendMessages)
// Now add a deny override.
seedChannelOverride(t, database, permissions.MemberRoleID, 10, 0, permissions.SendMessages)
// Without invalidation, cache still says allowed.
if !svc.HasChannelPerm(1, 10, permissions.SendMessages) {
t.Fatal("expected cached value to still allow SendMessages")
}
// After invalidation, should pick up the override.
svc.InvalidateUser(1)
if svc.HasChannelPerm(1, 10, permissions.SendMessages) {
t.Fatal("expected SendMessages to be denied after cache invalidation")
}
}
func TestInvalidateAll_ClearsEntireCache(t *testing.T) {
svc, database := newTestPermService(t)
// Add a second user.
seedUserRole(t, database, 2, permissions.MemberRoleID)
seedChannel(t, database, &db.Channel{ID: 10, Name: "general", Type: "text"})
// Populate cache for both users.
svc.HasChannelPerm(1, 10, permissions.SendMessages)
svc.HasChannelPerm(2, 10, permissions.SendMessages)
// Add deny override.
seedChannelOverride(t, database, permissions.MemberRoleID, 10, 0, permissions.SendMessages)
// Both still cached as allowed.
if !svc.HasChannelPerm(1, 10, permissions.SendMessages) {
t.Fatal("expected cached allow for user 1")
}
if !svc.HasChannelPerm(2, 10, permissions.SendMessages) {
t.Fatal("expected cached allow for user 2")
}
svc.InvalidateAll()
// Both should now see the deny.
if svc.HasChannelPerm(1, 10, permissions.SendMessages) {
t.Fatal("expected deny for user 1 after InvalidateAll")
}
if svc.HasChannelPerm(2, 10, permissions.SendMessages) {
t.Fatal("expected deny for user 2 after InvalidateAll")
}
}
func TestPermCacheTTLExpiry(t *testing.T) {
// This test verifies the cache TTL mechanism. We cannot easily wait 30s
// in a unit test, so we verify the structural behavior: after manually
// backdating the populatedAt field the cache should be stale and the
// next check should re-populate from the store.
svc, database := newTestPermService(t)
seedChannel(t, database, &db.Channel{ID: 10, Name: "general", Type: "text"})
// Populate cache.
svc.HasChannelPerm(1, 10, permissions.SendMessages)
// Add deny override.
seedChannelOverride(t, database, permissions.MemberRoleID, 10, 0, permissions.SendMessages)
// Manually expire the cache entry by backdating populatedAt.
svc.mu.Lock()
if cp, ok := svc.cache[int64(1)]; ok {
cp.populatedAt = time.Now().Add(-permCacheTTL - time.Second)
}
svc.mu.Unlock()
// The next call should re-populate and pick up the deny.
if svc.HasChannelPerm(1, 10, permissions.SendMessages) {
t.Fatal("expected cache TTL expiry to cause re-population with deny override")
}
}
func TestHasChannelPerm_UnknownUserReturnsFalse(t *testing.T) {
svc, database := newTestPermService(t)
seedChannel(t, database, &db.Channel{ID: 10, Name: "general", Type: "text"})
// User 999 has no role assigned.
if svc.HasChannelPerm(999, 10, permissions.SendMessages) {
t.Fatal("expected false for unknown user")
}
}