170 lines
6.6 KiB
Go
170 lines
6.6 KiB
Go
package skills
|
|
|
|
import (
|
|
"encoding/json"
|
|
"gorm.io/gorm"
|
|
"reflect"
|
|
"testing"
|
|
"yingce/backend/internal/model"
|
|
)
|
|
|
|
func TestSkillCurationManagementAvailable(t *testing.T) {
|
|
if _, ok := reflect.TypeOf((*Service)(nil)).MethodByName("UpdateSkillCuration"); !ok {
|
|
t.Fatal("missing platform curation management: skill categories are fixed and cannot be maintained by an administrator")
|
|
}
|
|
}
|
|
|
|
func newCurationTest(t *testing.T) (*Service, *gorm.DB, *model.User) {
|
|
t.Helper()
|
|
svc, db := newSkillLibraryCategoryTestService(t)
|
|
if err := db.AutoMigrate(&model.SkillCurationRoot{}, &model.SkillCurationRootAssignment{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := db.AutoMigrate(&model.SkillCurationSetting{}, &model.SkillCurationCategory{}, &model.SkillCurationAssignment{}, &model.AdminAuditEvent{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := db.Create(&model.SkillCurationSetting{ID: 1}).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
actor := &model.User{ID: "curator", Role: model.UserRoleAdmin, Status: model.UserStatusActive, Username: "curator"}
|
|
if err := db.Create(actor).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return svc, db, actor
|
|
}
|
|
|
|
func TestSkillCurationDisabledSnapshot(t *testing.T) {
|
|
svc, db, actor := newCurationTest(t)
|
|
// Snapshot of the original empty /skills data and fixed category order.
|
|
const original = `{"skills":[],"totalCount":0,"hasMore":false,"nextOffset":0,"page":1,"pageSize":20,"categories":[{"value":"drama","label":"短剧影视"},{"value":"ecommerce","label":"电商营销"},{"value":"creative","label":"创意设计"},{"value":"social","label":"社媒内容"},{"value":"others","label":"其他"}]}`
|
|
if err := db.Create(&model.SkillCurationCategory{ID: "hidden", RootTag: "drama", Name: "Hidden", NormalizedName: "hidden", Enabled: true}).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, req := range []SkillListRequest{{}, {PlatformCategoryID: "invalid", PlatformUncategorized: true}} {
|
|
result, err := svc.Skills(actor.ID, req)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
data, _ := json.Marshal(result)
|
|
if string(data) != original {
|
|
t.Fatalf("legacy response changed:\n%s", data)
|
|
}
|
|
}
|
|
result, err := svc.SkillCuration(actor, false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if result.Enabled || len(result.Categories) != 0 || len(result.Assignments) != 0 {
|
|
t.Fatalf("disabled mode leaked curation: %#v", result)
|
|
}
|
|
}
|
|
|
|
func TestSkillCurationLifecycleAndGuards(t *testing.T) {
|
|
svc, db, actor := newCurationTest(t)
|
|
update := func(req SkillCurationUpdate) *SkillCuration {
|
|
t.Helper()
|
|
state, err := svc.SkillCuration(actor, true)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
req.ExpectedRevision = &state.Revision
|
|
out, err := svc.UpdateSkillCuration(actor, req, "test-request")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return out
|
|
}
|
|
on := true
|
|
update(SkillCurationUpdate{Enabled: &on})
|
|
state := update(SkillCurationUpdate{Category: &model.SkillCurationCategory{RootTag: "drama", Name: "Story", SortOrder: 2, Enabled: true}})
|
|
c := state.Categories[0]
|
|
for _, s := range []model.Skill{{ID: "public", Name: "Story", Tag: "drama", Status: 1}, {ID: "other", Name: "Other", Tag: "social", Status: 1}, {ID: "private", Name: "Secret", Tag: "drama", Status: 1, IsPrivate: true, OwnerID: actor.ID}} {
|
|
if err := db.Create(&s).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
state = update(SkillCurationUpdate{Assignment: &SkillCurationAssignmentInput{SkillID: "public", CategoryIDs: []string{c.ID}}})
|
|
list, err := svc.Skills(actor.ID, SkillListRequest{PlatformCategoryID: c.ID})
|
|
if err != nil || list.TotalCount != 1 {
|
|
t.Fatalf("filtered: %#v %v", list, err)
|
|
}
|
|
list, err = svc.Skills(actor.ID, SkillListRequest{PlatformUncategorized: true})
|
|
if err != nil || list.TotalCount != 1 {
|
|
t.Fatalf("uncategorized: %#v %v", list, err)
|
|
}
|
|
for _, id := range []string{"other", "private"} {
|
|
_, err := svc.UpdateSkillCuration(actor, SkillCurationUpdate{ExpectedRevision: &state.Revision, Assignment: &SkillCurationAssignmentInput{SkillID: id, CategoryIDs: []string{c.ID}}}, "")
|
|
if err == nil {
|
|
t.Fatalf("accepted invalid assignment: %s", id)
|
|
}
|
|
}
|
|
stale := int64(0)
|
|
if _, err := svc.UpdateSkillCuration(actor, SkillCurationUpdate{ExpectedRevision: &stale, Enabled: &on}, ""); appErrorStatus(err) != 409 {
|
|
t.Fatalf("stale update: %v", err)
|
|
}
|
|
c.Name = "Renamed"
|
|
c.SortOrder = -1
|
|
c.Enabled = false
|
|
state = update(SkillCurationUpdate{Category: &c})
|
|
public, err := svc.SkillCuration(actor, false)
|
|
if err != nil || len(public.Categories) != 0 || len(public.Assignments) != 0 {
|
|
t.Fatalf("disabled category: %#v %v", public, err)
|
|
}
|
|
var assignments int64
|
|
db.Model(&model.SkillCurationAssignment{}).Count(&assignments)
|
|
if assignments != 1 {
|
|
t.Fatal("disable deleted assignments")
|
|
}
|
|
c.Enabled = true
|
|
update(SkillCurationUpdate{Category: &c})
|
|
off := false
|
|
update(SkillCurationUpdate{Enabled: &off})
|
|
list, err = svc.Skills(actor.ID, SkillListRequest{PlatformCategoryID: c.ID})
|
|
if err != nil || list.TotalCount != 2 {
|
|
t.Fatalf("disabled switch must ignore filters: %#v %v", list, err)
|
|
}
|
|
// A stale in-memory administrator cannot keep writing after revocation.
|
|
db.Model(&model.User{}).Where("id = ?", actor.ID).Update("role", model.UserRoleUser)
|
|
if _, err := svc.UpdateSkillCuration(actor, SkillCurationUpdate{ExpectedRevision: &state.Revision, Enabled: &on}, ""); appErrorStatus(err) != 403 {
|
|
t.Fatalf("revoked administrator: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSkillCurationAuditFailureRollsBack(t *testing.T) {
|
|
svc, db, actor := newCurationTest(t)
|
|
if err := db.Exec(`CREATE TRIGGER reject_curation_audit BEFORE INSERT ON admin_audit_events BEGIN SELECT RAISE(ABORT, 'audit unavailable'); END`).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
revision := int64(0)
|
|
on := true
|
|
if _, err := svc.UpdateSkillCuration(actor, SkillCurationUpdate{ExpectedRevision: &revision, Enabled: &on}, ""); err == nil {
|
|
t.Fatal("audit failure accepted")
|
|
}
|
|
state, err := svc.SkillCuration(actor, true)
|
|
if err != nil || state.Revision != 0 || state.Enabled {
|
|
t.Fatalf("partial commit: %#v %v", state, err)
|
|
}
|
|
}
|
|
|
|
func TestSkillCurationDeletionCleanup(t *testing.T) {
|
|
svc, db, _ := newCurationTest(t)
|
|
if err := db.AutoMigrate(&model.SkillVersion{}, &model.SkillFile{}, &model.BuiltinSkillTombstone{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, id := range []string{"normal", "builtin"} {
|
|
db.Create(&model.Skill{ID: id, Status: 1})
|
|
db.Create(&model.SkillCurationAssignment{SkillID: id, CategoryID: "category"})
|
|
}
|
|
if err := svc.repo.DeleteSkill("normal"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := svc.repo.DeleteBuiltinSkill("builtin", "admin"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var count int64
|
|
db.Model(&model.SkillCurationAssignment{}).Count(&count)
|
|
if count != 0 {
|
|
t.Fatal("deleted skills retain assignments")
|
|
}
|
|
}
|