This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/server/handlers/team_roles_test.go
Jeffrey Smith 0661e1d768
All checks were successful
CI/CD / detect-changes (push) Successful in 4s
CI/CD / e2e-smoke (push) Has been skipped
CI/CD / test-runners (push) Has been skipped
CI/CD / test-frontend (push) Successful in 6s
CI/CD / test-go-pg (push) Successful in 2m51s
CI/CD / test-sqlite (push) Successful in 3m5s
CI/CD / build-and-deploy (push) Successful in 27s
Feat v0.9.3 team user roles (#77)
Co-authored-by: Jeffrey Smith <jasafpro@gmail.com>
Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
2026-04-03 15:51:31 +00:00

274 lines
7.8 KiB
Go

package handlers
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"armature/database"
"armature/middleware"
"armature/models"
"armature/store"
)
// ── Manifest: requires_roles parsing ──────────
func TestValidateManifest_RequiresRoles(t *testing.T) {
m := map[string]any{
"id": "role-gated",
"title": "Role Gated Ext",
"type": "surface",
"requires_roles": []any{"approver", "reviewer"},
}
info, err := ValidateManifest(m)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(info.RequiresRoles) != 2 {
t.Fatalf("expected 2 requires_roles, got %d", len(info.RequiresRoles))
}
if info.RequiresRoles[0] != "approver" || info.RequiresRoles[1] != "reviewer" {
t.Errorf("unexpected roles: %v", info.RequiresRoles)
}
}
func TestValidateManifest_RequiresRoles_Empty(t *testing.T) {
m := map[string]any{
"id": "no-roles",
"title": "No Roles",
"type": "surface",
}
info, err := ValidateManifest(m)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(info.RequiresRoles) != 0 {
t.Errorf("expected empty requires_roles, got %v", info.RequiresRoles)
}
}
func TestValidateManifest_RequiresRoles_SkipsInvalid(t *testing.T) {
m := map[string]any{
"id": "bad-roles",
"title": "Bad Roles",
"type": "surface",
"requires_roles": []any{"ok", "", 42, "also-ok"},
}
info, err := ValidateManifest(m)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(info.RequiresRoles) != 2 {
t.Fatalf("expected 2 valid roles, got %d: %v", len(info.RequiresRoles), info.RequiresRoles)
}
}
// ── Store: team_user_roles CRUD ──────────────
func seedTeamAndMember(t *testing.T, stores store.Stores) (teamID, userID, memberID string) {
t.Helper()
ctx := context.Background()
userID = seedRoleUser(t, "roleuser", "roleuser@test.com")
creatorID := seedRoleUser(t, "creator", "creator@test.com")
team := &models.Team{Name: "role-test-" + store.NewID()[:8], CreatedBy: creatorID, IsActive: true}
if err := stores.Teams.Create(ctx, team); err != nil {
t.Fatalf("create team: %v", err)
}
teamID = team.ID
mid, err := stores.Teams.AddMemberReturningID(ctx, teamID, userID, "member")
if err != nil {
t.Fatalf("add member: %v", err)
}
memberID = mid
return
}
func TestUserRoles_AddAndList(t *testing.T) {
database.RequireTestDB(t)
stores := testStores(t)
ctx := context.Background()
teamID, userID, _ := seedTeamAndMember(t, stores)
// Initially: only the primary role
roles, err := stores.Teams.GetMemberRoles(ctx, teamID, userID)
if err != nil {
t.Fatalf("GetMemberRoles: %v", err)
}
if len(roles) != 1 || roles[0] != "member" {
t.Errorf("expected [member], got %v", roles)
}
// Add two additional roles
if err := stores.Teams.AddUserRole(ctx, teamID, userID, "reviewer", userID); err != nil {
t.Fatalf("AddUserRole: %v", err)
}
if err := stores.Teams.AddUserRole(ctx, teamID, userID, "approver", userID); err != nil {
t.Fatalf("AddUserRole: %v", err)
}
roles, _ = stores.Teams.GetMemberRoles(ctx, teamID, userID)
if len(roles) != 3 {
t.Fatalf("expected 3 roles, got %d: %v", len(roles), roles)
}
// ListUserRoles returns only additional
extra, _ := stores.Teams.ListUserRoles(ctx, teamID, userID)
if len(extra) != 2 {
t.Fatalf("expected 2 extra roles, got %d: %v", len(extra), extra)
}
}
func TestUserRoles_AddIdempotent(t *testing.T) {
database.RequireTestDB(t)
stores := testStores(t)
ctx := context.Background()
teamID, userID, _ := seedTeamAndMember(t, stores)
// Add same role twice — should not error
if err := stores.Teams.AddUserRole(ctx, teamID, userID, "reviewer", userID); err != nil {
t.Fatalf("first add: %v", err)
}
if err := stores.Teams.AddUserRole(ctx, teamID, userID, "reviewer", userID); err != nil {
t.Fatalf("idempotent add should not error: %v", err)
}
extra, _ := stores.Teams.ListUserRoles(ctx, teamID, userID)
if len(extra) != 1 {
t.Errorf("expected 1 extra role after idempotent add, got %d", len(extra))
}
}
func TestUserRoles_HasRole(t *testing.T) {
database.RequireTestDB(t)
stores := testStores(t)
ctx := context.Background()
teamID, userID, _ := seedTeamAndMember(t, stores)
// Primary role
has, _ := stores.Teams.HasRole(ctx, teamID, userID, "member")
if !has {
t.Error("expected HasRole=true for primary role 'member'")
}
// Non-existent role
has, _ = stores.Teams.HasRole(ctx, teamID, userID, "reviewer")
if has {
t.Error("expected HasRole=false for unassigned role")
}
// Add and check
stores.Teams.AddUserRole(ctx, teamID, userID, "reviewer", userID)
has, _ = stores.Teams.HasRole(ctx, teamID, userID, "reviewer")
if !has {
t.Error("expected HasRole=true after adding role")
}
}
func TestUserRoles_Remove(t *testing.T) {
database.RequireTestDB(t)
stores := testStores(t)
ctx := context.Background()
teamID, userID, _ := seedTeamAndMember(t, stores)
stores.Teams.AddUserRole(ctx, teamID, userID, "reviewer", userID)
stores.Teams.RemoveUserRole(ctx, teamID, userID, "reviewer")
extra, _ := stores.Teams.ListUserRoles(ctx, teamID, userID)
if len(extra) != 0 {
t.Errorf("expected 0 extra roles after remove, got %d", len(extra))
}
}
func TestUserRoles_RemoveAllOnMemberDelete(t *testing.T) {
database.RequireTestDB(t)
stores := testStores(t)
ctx := context.Background()
teamID, userID, _ := seedTeamAndMember(t, stores)
stores.Teams.AddUserRole(ctx, teamID, userID, "reviewer", userID)
stores.Teams.AddUserRole(ctx, teamID, userID, "approver", userID)
// Cleanup (as the handler does)
stores.Teams.RemoveAllUserRoles(ctx, teamID, userID)
extra, _ := stores.Teams.ListUserRoles(ctx, teamID, userID)
if len(extra) != 0 {
t.Errorf("expected 0 extra roles after RemoveAll, got %d", len(extra))
}
}
// ── Middleware: RequireRole ───────────────────
func TestRequireRole_Allowed(t *testing.T) {
database.RequireTestDB(t)
stores := testStores(t)
ctx := context.Background()
teamID, userID, _ := seedTeamAndMember(t, stores)
stores.Teams.AddUserRole(ctx, teamID, userID, "reviewer", userID)
gin.SetMode(gin.TestMode)
w := httptest.NewRecorder()
_, r := gin.CreateTestContext(w)
called := false
r.Use(func(c *gin.Context) { c.Set("user_id", userID); c.Next() })
r.GET("/teams/:teamId/test",
middleware.RequireRole(stores.Teams, []string{"reviewer"}, stores),
func(c *gin.Context) {
called = true
c.Status(http.StatusOK)
},
)
req := httptest.NewRequest("GET", "/teams/"+teamID+"/test", nil)
r.ServeHTTP(w, req)
if w.Code != http.StatusOK || !called {
t.Errorf("expected 200 + handler called, got %d called=%v", w.Code, called)
}
}
func TestRequireRole_Denied(t *testing.T) {
database.RequireTestDB(t)
stores := testStores(t)
teamID, userID, _ := seedTeamAndMember(t, stores)
gin.SetMode(gin.TestMode)
w := httptest.NewRecorder()
c, r := gin.CreateTestContext(w)
r.Use(func(c *gin.Context) { c.Set("user_id", userID); c.Next() })
r.GET("/teams/:teamId/test",
middleware.RequireRole(stores.Teams, []string{"approver"}, stores),
func(c *gin.Context) { c.Status(http.StatusOK) },
)
c.Request = httptest.NewRequest("GET", "/teams/"+teamID+"/test", nil)
r.ServeHTTP(w, c.Request)
if w.Code != http.StatusForbidden {
t.Errorf("expected 403 for missing role, got %d", w.Code)
}
}
// ── helpers ──────────────────────────────────
func seedRoleUser(t *testing.T, username, email string) string {
t.Helper()
uname := username + store.NewID()[:6]
q := `INSERT INTO users (username, email, password_hash, display_name, is_active, auth_source, handle)
VALUES ($1, $2, 'hash', $3, true, 'builtin', $4) RETURNING id`
return seedInsertReturningID(t, q, uname, email+store.NewID()[:6], uname, uname)
}