Files
goauto/server/app/goauto/access/modules_test.go
T
QiuSWandClaude Opus 5 ab292d6a17 feat(yeeke): add admin read-only return-package/sync-run modules (#337)
Add read-only server endpoints for yeeke return packages (paginated list
with order/tracking/shop/item/variation/claim-status/create-time filters,
plus package detail with items) and extend the existing sync-runs endpoint
with trigger/status/started-at filters, backward compatible. Split the
admin menu into two modules per the ticket's confirmed scope adjustment:
yeeke_returns (packages) and the new yeeke_sync_runs, both under the
采集采购 menu group, wired via a new sys_menu migration mirroring #237's
precedent. Add the corresponding Vue pages, API wrappers and Go tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
2026-09-23 14:23:51 +08:00

170 lines
5.5 KiB
Go

package access
import (
"os"
"path/filepath"
"runtime"
"strings"
"testing"
)
func TestGoAutoModulesOwnEveryAdminAPIExactlyOnce(t *testing.T) {
modules := GoAutoModules()
if len(modules) != 15 {
t.Fatalf("got %d modules, want 15", len(modules))
}
owners := make(map[string]int)
for _, module := range modules {
if module.Key == "" || module.Path == "" || module.RouteName == "" || module.Component == "" {
t.Fatalf("module metadata is incomplete: %#v", module)
}
for _, permission := range module.APIs {
owners[permission.Method+" "+permission.Path]++
}
}
for _, permission := range AdminAPIs {
key := permission.Method + " " + permission.Path
if owners[key] != 1 {
t.Fatalf("%s belongs to %d modules, want exactly one", key, owners[key])
}
}
}
func TestYeekeSyncRunsModuleIsRegisteredAndPurchaserVisible(t *testing.T) {
module, ok := ModuleByRouteName("GoAutoYeekeSyncRuns")
if !ok {
t.Fatal("yeeke sync-runs module not found")
}
if module.Key != ModuleYeekeSyncRuns {
t.Fatalf("unexpected key: %s", module.Key)
}
if module.Path != "/yeeke-sync-runs" || module.Component != "/goauto/yeeke-sync-runs/index" {
t.Fatalf("unexpected route wiring: %#v", module)
}
if !module.PurchaserDefault {
t.Fatal("yeeke sync-runs module must be purchaser-visible per #337")
}
if len(module.APIs) == 0 {
t.Fatal("yeeke sync-runs module must own its APIs")
}
for _, permission := range module.APIs {
if permission.Path == "/api/admin/v1/yeeke-returns" && permission.Method == "GET" {
t.Fatal("the return-package list API belongs to yeeke_returns, not yeeke_sync_runs")
}
}
}
func TestAIMatchingIsHardHiddenFromPurchaser(t *testing.T) {
module, ok := ModuleByRouteName("GoAutoAiMatchingSettings")
if !ok {
t.Fatal("AI matching module not found")
}
if module.PurchaserDefault || !module.PurchaserHardHidden {
t.Fatalf("unexpected AI purchaser flags: %#v", module)
}
if len(module.APIs) == 0 {
t.Fatal("AI matching module must own its APIs")
}
}
func TestGoAutoMenuSortsFitMySQLSignedTinyInt(t *testing.T) {
modules := GoAutoModules()
previous := 40
for _, module := range modules {
if module.Sort <= previous {
t.Fatalf("module %s sort %d must be greater than %d", module.Key, module.Sort, previous)
}
if module.Sort > 127 {
t.Fatalf("module %s sort %d exceeds MySQL signed TINYINT", module.Key, module.Sort)
}
previous = module.Sort
}
if modules[0].Sort != 50 || modules[len(modules)-1].Sort != 64 {
t.Fatalf("GoAuto menu sort range = %d..%d, want 50..64", modules[0].Sort, modules[len(modules)-1].Sort)
}
}
func TestGoAutoMenuGroupsCoverModulesExactlyOnce(t *testing.T) {
groups := GoAutoMenuGroups()
if len(groups) != 2 {
t.Fatalf("got %d groups, want 2", len(groups))
}
if groups[0].Title != "采集采购" || groups[1].Title != "采采管理" {
t.Fatalf("unexpected group titles: %#v", groups)
}
wantOrder := [][]string{
{ModuleSYBProducts, ModuleSYBSyncRuns, ModuleSYBInnerCodes, ModuleShopeeProducts, ModulePDDProducts, ModuleCollectionTasks, ModulePurchaseTasks, ModuleYeekeReturns, ModuleYeekeSyncRuns},
{ModuleSYBShops, ModuleSYBProductFilters, ModuleCollectionRules, ModulePurchaseRules, ModuleDevices, ModuleAIMatching},
}
seen := make(map[string]int)
for groupIndex, group := range groups {
if group.Sort < 0 || group.Sort > 127 {
t.Fatalf("group %s sort %d exceeds MySQL signed TINYINT", group.Key, group.Sort)
}
if len(group.ModuleKeys) != len(wantOrder[groupIndex]) {
t.Fatalf("group %s modules = %#v, want %#v", group.Key, group.ModuleKeys, wantOrder[groupIndex])
}
for index, key := range group.ModuleKeys {
if key != wantOrder[groupIndex][index] {
t.Fatalf("group %s module %d = %s, want %s", group.Key, index, key, wantOrder[groupIndex][index])
}
seen[key]++
}
}
for _, module := range GoAutoModules() {
if seen[module.Key] != 1 {
t.Fatalf("module %s belongs to %d groups, want exactly one", module.Key, seen[module.Key])
}
}
groups[0].ModuleKeys[0] = "mutated"
if GoAutoMenuGroups()[0].ModuleKeys[0] != ModuleSYBProducts {
t.Fatal("GoAutoMenuGroups must return deep copies")
}
}
func TestGoAutoRoutesAreGeneratedFromServerMenus(t *testing.T) {
_, fileName, _, ok := runtime.Caller(0)
if !ok {
t.Fatal("cannot locate test source")
}
routerPath := filepath.Join(filepath.Dir(fileName), "..", "..", "..", "..", "web", "src", "router", "index.js")
content, err := os.ReadFile(routerPath)
if err != nil {
t.Fatal(err)
}
router := string(content)
for _, module := range GoAutoModules() {
if strings.Contains(router, "name: '"+module.RouteName+"'") {
t.Fatalf("GoAuto route %s must not be a public constant route", module.RouteName)
}
}
permissionPath := filepath.Join(filepath.Dir(fileName), "..", "..", "..", "..", "web", "src", "store", "modules", "permission.js")
content, err = os.ReadFile(permissionPath)
if err != nil {
t.Fatal(err)
}
permission := string(content)
for _, fragment := range []string{
"getRoutes()",
"generaMenu(dynamicRoutes, loadMenuData)",
"commit('SET_SIDEBAR_ROUTERS', constantRoutes.concat(sidebarRoutes))",
} {
if !strings.Contains(permission, fragment) {
t.Fatalf("Web permission routing is missing %q", fragment)
}
}
userPath := filepath.Join(filepath.Dir(fileName), "..", "..", "..", "..", "web", "src", "store", "modules", "user.js")
content, err = os.ReadFile(userPath)
if err != nil {
t.Fatal(err)
}
if strings.Count(string(content), "resetRouter()") < 3 {
t.Fatal("login role changes must clear previously registered dynamic routes")
}
}