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
170 lines
5.5 KiB
Go
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")
|
|
}
|
|
}
|