package system import ( "context" "kra/internal/biz/system" "testing" "time" "kra/internal/config" ) func newMenuTestData(t *testing.T) *Data { t.Helper() db, err := openWithDriver("sqlite", "file:"+t.Name()+"?mode=memory&cache=shared") if err != nil { t.Fatal(err) } if err = db.AutoMigrate(&menuPO{}, &menuParameterPO{}, &menuButtonPO{}, &authorityButtonPO{}, &authorityPO{}, &authorityMenuPO{}); err != nil { t.Fatal(err) } data := &Data{ gormDB: newReloadableDB(db, nil), runtime: config.NewStore(&config.Config{Data: &config.Data{Database: &config.Database{Driver: "sqlite"}}, Admin: &config.Admin{}}), } t.Cleanup(func() { data.gormDB.close() }) return data } func TestMenuCreatePreservesSubmittedTimestamps(t *testing.T) { data := newMenuTestData(t) ctx := context.Background() createdAt := time.Date(2025, time.January, 2, 3, 4, 5, 0, time.UTC) updatedAt := time.Date(2025, time.February, 3, 4, 5, 6, 0, time.UTC) menu := &system.Menu{CreatedAt: createdAt, UpdatedAt: updatedAt, Name: "created", Path: "created", Component: "view/created.vue", Title: "Created"} if err := (&menuRepo{data: data}).Create(ctx, menu); err != nil { t.Fatal(err) } var stored menuPO if err := data.gormDB.WithContext(ctx).First(&stored, menu.ID).Error; err != nil { t.Fatal(err) } if !stored.CreatedAt.Equal(createdAt) || !stored.UpdatedAt.Equal(updatedAt) { t.Fatalf("stored timestamps = (%v, %v), want (%v, %v)", stored.CreatedAt, stored.UpdatedAt, createdAt, updatedAt) } } func TestSetAuthorityMenusRejectsMissingAuthority(t *testing.T) { data := newMenuTestData(t) err := (&menuRepo{data: data}).SetAuthorityMenus(context.Background(), 999, nil) if err == nil { t.Fatal("SetAuthorityMenus missing authority succeeded") } } func TestSetMenuRolesPreservesDuplicateAuthorityIDs(t *testing.T) { data := newMenuTestData(t) ctx := context.Background() if err := (&menuRepo{data: data}).SetMenuRoles(ctx, 10, []uint{888, 888}); err != nil { t.Fatal(err) } var count int64 if err := data.gormDB.WithContext(ctx).Model(&authorityMenuPO{}). Where("sys_base_menu_id = ? AND sys_authority_authority_id = ?", 10, 888). Count(&count).Error; err != nil { t.Fatal(err) } if count != 2 { t.Fatalf("authority-menu rows = %d, want duplicate role IDs preserved", count) } } func TestMenuUpdateRebuildsRelationsWithoutChangingRequestedIdentity(t *testing.T) { data := newMenuTestData(t) ctx := context.Background() createdAt := time.Date(2025, time.March, 4, 5, 6, 7, 0, time.UTC) updatedAt := time.Date(2025, time.April, 5, 6, 7, 8, 0, time.UTC) if err := data.gormDB.WithContext(ctx).Create(&menuPO{ID: 10, Name: "before", Path: "before", Component: "view/before.vue", Title: "Before"}).Error; err != nil { t.Fatal(err) } if err := data.gormDB.WithContext(ctx).Create(&menuParameterPO{ID: 21, CreatedAt: createdAt, UpdatedAt: updatedAt, MenuID: 10, Type: "query", Key: "old", Value: "old"}).Error; err != nil { t.Fatal(err) } if err := data.gormDB.WithContext(ctx).Create(&menuButtonPO{ID: 31, CreatedAt: createdAt, UpdatedAt: updatedAt, MenuID: 10, Name: "edit", Description: "old"}).Error; err != nil { t.Fatal(err) } if err := data.gormDB.WithContext(ctx).Create(&authorityButtonPO{AuthorityID: 888, MenuID: 10, ButtonID: 31}).Error; err != nil { t.Fatal(err) } repo := &menuRepo{data: data} err := repo.Update(ctx, &system.Menu{ ID: 10, Name: "after", Path: "after", Component: "view/after.vue", Title: "After", Parameters: []*system.MenuParameter{{ ID: 21, CreatedAt: createdAt, UpdatedAt: updatedAt, Type: "params", Key: "new", Value: "value", }}, Buttons: []*system.MenuButton{{ ID: 31, CreatedAt: createdAt, UpdatedAt: updatedAt, Name: "edit", Description: "new", }}, }) if err != nil { t.Fatal(err) } var parameter menuParameterPO if err = data.gormDB.WithContext(ctx).First(¶meter, 21).Error; err != nil { t.Fatal(err) } if parameter.MenuID != 10 || parameter.Type != "params" || parameter.Key != "new" || parameter.Value != "value" { t.Fatalf("parameter = %+v", parameter) } if !parameter.CreatedAt.Equal(createdAt) || !parameter.UpdatedAt.Equal(updatedAt) { t.Fatalf("parameter timestamps = (%v, %v), want (%v, %v)", parameter.CreatedAt, parameter.UpdatedAt, createdAt, updatedAt) } var button menuButtonPO if err = data.gormDB.WithContext(ctx).First(&button, 31).Error; err != nil { t.Fatal(err) } if button.MenuID != 10 || button.Name != "edit" || button.Description != "new" { t.Fatalf("button = %+v", button) } if !button.CreatedAt.Equal(createdAt) || !button.UpdatedAt.Equal(updatedAt) { t.Fatalf("button timestamps = (%v, %v), want (%v, %v)", button.CreatedAt, button.UpdatedAt, createdAt, updatedAt) } var links int64 if err = data.gormDB.WithContext(ctx).Model(&authorityButtonPO{}). Where("authority_id = ? AND sys_menu_id = ? AND sys_base_menu_btn_id = ?", 888, 10, 31). Count(&links).Error; err != nil { t.Fatal(err) } if links != 1 { t.Fatalf("authority-button links = %d, want 1", links) } } func TestSetMenuRolesStrictOnlyChangesManagedAuthorities(t *testing.T) { data := newMenuTestData(t) enableStrictAuthorityTestMode(data) db := data.gormDB.WithContext(context.Background()) rootID, actorID, childID, siblingID := uint(888), uint(4001), uint(4002), uint(5001) if err := db.Create(&[]authorityPO{ {AuthorityID: rootID, ParentID: authorityUintPointer(0)}, {AuthorityID: actorID, ParentID: &rootID}, {AuthorityID: childID, ParentID: &actorID}, {AuthorityID: siblingID, ParentID: &rootID}, }).Error; err != nil { t.Fatal(err) } menu := menuPO{ID: 10, Name: "managed", Path: "managed"} if err := db.Create(&menu).Error; err != nil { t.Fatal(err) } if err := db.Create(&[]authorityMenuPO{ {SysAuthorityAuthorityID: actorID, SysBaseMenuID: menu.ID}, {SysAuthorityAuthorityID: siblingID, SysBaseMenuID: menu.ID}, }).Error; err != nil { t.Fatal(err) } ctx := system.NewActorContext(context.Background(), system.Actor{AuthorityID: actorID}) repo := &menuRepo{data: data} if err := repo.SetMenuRoles(ctx, menu.ID, []uint{siblingID}); err == nil { t.Fatal("SetMenuRoles() accepted an out-of-scope authority") } if err := repo.SetMenuRoles(ctx, menu.ID, []uint{childID}); err != nil { t.Fatalf("SetMenuRoles() rejected a managed authority: %v", err) } var siblingCount, childCount int64 if err := db.Model(&authorityMenuPO{}).Where("sys_authority_authority_id = ? AND sys_base_menu_id = ?", siblingID, menu.ID).Count(&siblingCount).Error; err != nil { t.Fatal(err) } if err := db.Model(&authorityMenuPO{}).Where("sys_authority_authority_id = ? AND sys_base_menu_id = ?", childID, menu.ID).Count(&childCount).Error; err != nil { t.Fatal(err) } if siblingCount != 1 || childCount != 1 { t.Fatalf("menu-role counts sibling/child = %d/%d, want 1/1", siblingCount, childCount) } }