material_manage_system/internal/repository/organization_access_test.go
2026-10-08 17:04:09 +08:00

187 lines
7.2 KiB
Go

package repository
import (
"encoding/json"
"testing"
"github.com/wmms/wmms/internal/model"
)
// Removing the role enabled predicate, trusting a previous login role, or
// omitting the organization status filter must expose unauthorized companies.
func TestOrganizationAccessUsesCurrentEnabledRolesAndGrants(t *testing.T) {
db := isolationDB(t, false)
mustSQL(t, db, `INSERT INTO organizations(id,code,name,status) VALUES(2,'TWO','Two','active'),(3,'THREE','Three','inactive');
INSERT INTO users(id,username,password) VALUES(1,'ordinary','x'),(2,'admin','x'),(3,'disabled-admin','x'),(4,'no-grants','x');
INSERT INTO user_roles(user_id,role_id) SELECT 2,id FROM roles WHERE code='super_admin';
INSERT INTO user_roles(user_id,role_id) SELECT 3,id FROM roles WHERE code='super_admin';
INSERT INTO roles(id,code,name,enabled) VALUES(20,'old_admin','Old',0);
INSERT INTO user_organizations(user_id,organization_id,is_default) VALUES(1,2,1),(1,3,0),(3,2,1);`)
repo := NewOrganizationRepository()
access, err := repo.ListAccessible(1)
if err != nil {
t.Fatal(err)
}
if len(access.Organizations) != 1 || access.Organizations[0].ID != 2 || access.DefaultOrganizationID == nil || *access.DefaultOrganizationID != 2 {
t.Fatalf("ordinary access=%+v", access)
}
allowed, err := repo.CanAccess(1, 1)
if err != nil || allowed {
t.Fatalf("ungranted organization allowed=%v err=%v", allowed, err)
}
allowed, err = repo.CanAccess(1, 3)
if err != nil || allowed {
t.Fatalf("inactive organization allowed=%v err=%v", allowed, err)
}
access, err = repo.ListAccessible(2)
if err != nil || len(access.Organizations) != 2 || access.DefaultOrganizationID != nil {
t.Fatalf("ungranted admin access=%+v err=%v", access, err)
}
allowed, err = repo.CanAccess(2, 2)
if err != nil || !allowed {
t.Fatalf("enabled admin access=%v err=%v", allowed, err)
}
mustSQL(t, db, `DELETE FROM user_organizations WHERE user_id=1`)
access, err = repo.ListAccessible(1)
if err != nil || len(access.Organizations) != 0 || access.DefaultOrganizationID != nil {
t.Fatalf("revoked grant access=%+v err=%v", access, err)
}
allowed, err = repo.CanAccess(1, 2)
if err != nil || allowed {
t.Fatalf("revoked grant allowed=%v err=%v", allowed, err)
}
mustSQL(t, db, `UPDATE roles SET enabled=0 WHERE code='super_admin'`)
access, err = repo.ListAccessible(2)
if err != nil || len(access.Organizations) != 0 {
t.Fatalf("disabled admin access=%+v err=%v", access, err)
}
allowed, err = repo.CanAccess(2, 2)
if err != nil || allowed {
t.Fatalf("disabled admin allowed=%v err=%v", allowed, err)
}
access, err = repo.ListAccessible(3)
if err != nil || len(access.Organizations) != 1 || access.Organizations[0].ID != 2 {
t.Fatalf("ordinary grant with disabled role access=%+v err=%v", access, err)
}
allowed, err = repo.CanAccess(999, 2)
if err != nil || allowed {
t.Fatalf("nonexistent user allowed=%v err=%v", allowed, err)
}
}
// An audit failure must reach the caller so its business mutation can roll back;
// a rollback must remove both records, and reads must require an organization.
func TestOrganizationAuditTransactionAndScope(t *testing.T) {
db := isolationDB(t, false)
mustSQL(t, db, `INSERT INTO users(id,username,password) VALUES(1,'actor','x'); INSERT INTO organizations(id,code,name) VALUES(2,'TWO','Two')`)
repo := NewOrganizationAuditRepository()
org1, org2 := int64(1), int64(2)
entry := &model.OrganizationAuditLog{ActorUserID: 1, OrganizationID: &org1, Action: "organization.switch", OldValue: json.RawMessage(`{"organization_id":2}`), NewValue: json.RawMessage(`{"organization_id":1}`)}
tx, err := db.Begin()
if err != nil {
t.Fatal(err)
}
defer tx.Rollback()
mustSQL(t, db, `UPDATE organizations SET name='Before' WHERE id=1`)
if _, err = tx.Exec(`UPDATE organizations SET name='Rolled back' WHERE id=1`); err != nil {
t.Fatal(err)
}
if err = repo.CreateTx(tx, entry); err != nil {
t.Fatal(err)
}
if err = tx.Rollback(); err != nil {
t.Fatal(err)
}
logs, err := repo.ListByOrganization(1, 20, 0)
if err != nil || len(logs) != 0 {
t.Fatalf("rolled back audit=%+v err=%v", logs, err)
}
var organizationName string
if err = db.QueryRow(`SELECT name FROM organizations WHERE id=1`).Scan(&organizationName); err != nil || organizationName != "Before" {
t.Fatalf("business rollback=%s err=%v", organizationName, err)
}
tx, err = db.Begin()
if err != nil {
t.Fatal(err)
}
defer tx.Rollback()
if _, err = tx.Exec(`UPDATE organizations SET name='Audit failed' WHERE id=1`); err != nil {
t.Fatal(err)
}
entry.NewValue = json.RawMessage(`not-json`)
if err = repo.CreateTx(tx, entry); err == nil {
t.Fatal("invalid audit JSON was accepted")
}
tx.Rollback()
if err = db.QueryRow(`SELECT name FROM organizations WHERE id=1`).Scan(&organizationName); err != nil || organizationName != "Before" {
t.Fatalf("audit failure business rollback=%s err=%v", organizationName, err)
}
entry.NewValue = json.RawMessage(`{"organization_id":1}`)
tx, err = db.Begin()
if err != nil {
t.Fatal(err)
}
defer tx.Rollback()
if _, err = tx.Exec(`UPDATE organizations SET name='Committed' WHERE id=1`); err != nil {
t.Fatal(err)
}
if err = repo.CreateTx(tx, entry); err != nil {
t.Fatal(err)
}
if err = tx.Commit(); err != nil {
t.Fatal(err)
}
if err = db.QueryRow(`SELECT name FROM organizations WHERE id=1`).Scan(&organizationName); err != nil || organizationName != "Committed" {
t.Fatalf("business commit=%s err=%v", organizationName, err)
}
entry.OrganizationID = &org2
entry.Action = "organization.grants.update"
entry.OldValue = json.RawMessage(`{"organization_ids":[1],"default_organization_id":1}`)
entry.NewValue = json.RawMessage(`{"organization_ids":[2],"default_organization_id":2}`)
if err = repo.Create(entry); err != nil {
t.Fatal(err)
}
entry.OrganizationID = nil
entry.Action = "organization.grants.update"
if err = repo.Create(entry); err != nil {
t.Fatal(err)
}
logs, err = repo.ListByOrganization(1, 20, 0)
if err != nil || len(logs) != 1 || logs[0].Action != "organization.switch" {
t.Fatalf("scoped audit=%+v err=%v", logs, err)
}
var newValue struct {
OrganizationID int64 `json:"organization_id"`
}
if err = json.Unmarshal(logs[0].NewValue, &newValue); err != nil || newValue.OrganizationID != 1 {
t.Fatalf("audit value=%s err=%v", logs[0].NewValue, err)
}
if _, err = repo.ListByOrganization(0, 20, 0); err == nil {
t.Fatal("unscoped audit read accepted")
}
if err = repo.CreateTx(nil, entry); err == nil {
t.Fatal("nil transaction accepted")
}
}
func TestOrganizationRepositoryPropagatesDatabaseFailures(t *testing.T) {
db := isolationDB(t, false)
if err := db.Close(); err != nil {
t.Fatal(err)
}
repo := NewOrganizationRepository()
if _, err := repo.ListAccessible(1); err == nil {
t.Fatal("ListAccessible swallowed database failure")
}
if allowed, err := repo.CanAccess(1, 1); err == nil || allowed {
t.Fatalf("CanAccess allowed=%v err=%v", allowed, err)
}
audit := NewOrganizationAuditRepository()
if err := audit.Create(&model.OrganizationAuditLog{ActorUserID: 1, Action: "test"}); err == nil {
t.Fatal("Create swallowed database failure")
}
if _, err := audit.ListByOrganization(1, 20, 0); err == nil {
t.Fatal("audit read swallowed database failure")
}
}