package main import ( "context" "fmt" "io" "net/http" "net/url" "os" "strings" "testing" "time" _ "ppgo_job/boot" "github.com/gogf/gf/v2/frame/g" ) const testPort = ":18082" const testBaseURL = "http://127.0.0.1" + testPort func TestMain(m *testing.M) { // Switch the server to a test port so we don't conflict with a dev instance s := g.Server() s.SetPort(18082) // Start server non-blocking go s.Start() // Wait for the server to be ready ready := false for i := 0; i < 40; i++ { resp, err := http.Get(testBaseURL + "/") if err == nil { resp.Body.Close() ready = true break } time.Sleep(250 * time.Millisecond) } if !ready { fmt.Println("WARNING: Server did not start within timeout. Integration tests may fail.") } code := m.Run() s.Shutdown() os.Exit(code) } // --- HTTP test helpers --- func httpGet(path string, cookies []*http.Cookie) (int, string, error) { req, _ := http.NewRequest("GET", testBaseURL+path, nil) if cookies != nil { for _, c := range cookies { req.AddCookie(c) } } client := &http.Client{ Timeout: 5 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse // don't follow redirects }, } resp, err := client.Do(req) if err != nil { return 0, "", err } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) return resp.StatusCode, string(body), nil } func httpPost(path string, data url.Values, cookies []*http.Cookie) (int, string, error) { bodyReader := strings.NewReader(data.Encode()) req, _ := http.NewRequest("POST", testBaseURL+path, bodyReader) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") if cookies != nil { for _, c := range cookies { req.AddCookie(c) } } client := &http.Client{ Timeout: 5 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } resp, err := client.Do(req) if err != nil { return 0, "", err } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) return resp.StatusCode, string(body), nil } func loginCookies(t *testing.T) []*http.Cookie { req, _ := http.NewRequest("POST", testBaseURL+"/login_in", strings.NewReader(url.Values{"username": {"admin"}, "password": {"123456"}}.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") client := &http.Client{ Timeout: 5 * time.Second, CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }, } resp, err := client.Do(req) if err != nil { t.Fatalf("login failed: %v", err) return nil } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) if !strings.Contains(string(body), `"status":0`) { t.Fatalf("login failed, body: %s", body) } return resp.Cookies() } // ===================== Tests ===================== func TestLoginPage(t *testing.T) { code, body, err := httpGet("/", nil) if err != nil { t.Fatalf("GET /: %v", err) } if code != http.StatusOK { t.Errorf("status = %d, want %d", code, http.StatusOK) } if !strings.Contains(body, "登录") && !strings.Contains(body, "login") { t.Error("body should contain login text") } } func TestLogin(t *testing.T) { tests := []struct { name string username string password string wantCode int }{ {"correct credentials", "admin", "123456", 0}, {"wrong password", "admin", "wrongpass", -1}, {"wrong username", "nonexistent", "123456", -1}, {"empty username", "", "123456", -1}, {"empty password", "admin", "", -1}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { _, body, err := httpPost("/login_in", url.Values{ "username": {tt.username}, "password": {tt.password}, }, nil) if err != nil { t.Fatalf("POST /login_in: %v", err) } if !strings.Contains(body, fmt.Sprintf(`"status":%d`, tt.wantCode)) { t.Errorf("body does not contain status:%d — body: %s", tt.wantCode, body) } }) } } func TestAuthRequired(t *testing.T) { paths := []string{"/home", "/home/start", "/task/list", "/server/list", "/group/list", "/admin/list", "/role/list"} for _, p := range paths { t.Run(p, func(t *testing.T) { code, _, err := httpGet(p, nil) if err != nil { t.Fatalf("GET %s: %v", p, err) } if code != http.StatusFound { t.Errorf("GET %s (no auth) = %d, want redirect 302", p, code) } }) } } func TestHomePage(t *testing.T) { cookies := loginCookies(t) code, body, err := httpGet("/home", cookies) if err != nil { t.Fatalf("GET /home: %v", err) } if code != http.StatusOK { t.Errorf("status = %d, want %d", code, http.StatusOK) } if !strings.Contains(body, "首页") { t.Error("home page should contain '首页'") } } func TestHomeStart(t *testing.T) { cookies := loginCookies(t) code, body, err := httpGet("/home/start", cookies) if err != nil { t.Fatalf("GET /home/start: %v", err) } if code != http.StatusOK { t.Errorf("status = %d, want %d", code, http.StatusOK) } // Rendered template substitutes {{.startJob}} etc. with integer values, // so we check for static UI text and structural content instead for _, s := range []string{ "即将执行的任务", "待审核任务数量", "近期执行成功", "近期执行失败", "当前用户总数", "定时任务总数量", "累计运行次数", "当前日志总量", "系统概况", "运行概况", } { if !strings.Contains(body, s) { t.Errorf("body missing expected text: %s", s) } } } func TestTaskTable(t *testing.T) { cookies := loginCookies(t) code, body, err := httpGet("/task/table", cookies) if err != nil { t.Fatalf("GET /task/table: %v", err) } if code != http.StatusOK { t.Errorf("status = %d, want %d", code, http.StatusOK) } if !strings.Contains(body, `"code":`) || !strings.Contains(body, `"data"`) { t.Errorf("response missing JSON fields: %s", body) } } func TestServerTable(t *testing.T) { cookies := loginCookies(t) code, body, err := httpGet("/server/table", cookies) if err != nil { t.Fatalf("GET /server/table: %v", err) } if code != http.StatusOK { t.Errorf("status = %d, want %d", code, http.StatusOK) } if !strings.Contains(body, `"code":`) { t.Errorf("response missing JSON fields: %s", body) } } func TestTaskCRUD(t *testing.T) { cookies := loginCookies(t) ctx := context.Background() // Create _, body, err := httpPost("/task/ajax_save", url.Values{ "task_name": {"test-crud-task"}, "cron_spec": {"0 */5 * * * *"}, "command": {"echo ok"}, "timeout": {"30"}, }, cookies) if err != nil { t.Fatalf("POST /task/ajax_save: %v", err) } if !strings.Contains(body, `"status":0`) { t.Errorf("create failed: %s", body) } // Cleanup if _, err := g.DB().Exec(ctx, "DELETE FROM pp_task WHERE task_name = ?", "test-crud-task"); err != nil { //nolint t.Logf("cleanup: %v", err) } } func TestTaskStartStop(t *testing.T) { cookies := loginCookies(t) // Start a non-existent task => error _, body, err := httpPost("/task/ajax_start", url.Values{"id": {"99999"}}, cookies) if err != nil { t.Fatalf("POST /task/ajax_start: %v", err) } // Should still succeed (it's a soft action that just updates DB) if !strings.Contains(body, `"status":0`) { t.Logf("start non-existent task response: %s", body) } // Pause with invalid id _, body, err = httpPost("/task/ajax_pause", url.Values{"id": {"99999"}}, cookies) if err != nil { t.Fatalf("POST /task/ajax_pause: %v", err) } // Should succeed (soft action) if !strings.Contains(body, `"status":0`) { t.Logf("pause non-existent task response: %s", body) } } func TestGroupCRUD(t *testing.T) { cookies := loginCookies(t) ctx := context.Background() // Create _, body, err := httpPost("/group/ajax_save", url.Values{ "group_name": {"test-group"}, "description": {"created by test"}, }, cookies) if err != nil { t.Fatalf("POST /group/ajax_save: %v", err) } if !strings.Contains(body, `"status":0`) { t.Errorf("create group failed: %s", body) } // Cleanup g.DB().Exec(ctx, "DELETE FROM pp_task_group WHERE group_name = ?", "test-group") //nolint } func TestBanCRUD(t *testing.T) { cookies := loginCookies(t) ctx := context.Background() _, body, err := httpPost("/ban/ajax_save", url.Values{ "code": {"rm -rf"}, }, cookies) if err != nil { t.Fatalf("POST /ban/ajax_save: %v", err) } if !strings.Contains(body, `"status":0`) { t.Errorf("create ban failed: %s", body) } g.DB().Exec(ctx, "DELETE FROM pp_task_ban WHERE code = ?", "rm -rf") //nolint } func TestAllTemplatesRender(t *testing.T) { cookies := loginCookies(t) pages := []string{ "/task/list", "/task/add", "/task/audit_list", "/server/list", "/server/add", "/group/list", "/group/add", "/server_group/list", "/server_group/add", "/ban/list", "/ban/add", "/notify_tpl/list", "/notify_tpl/add", "/auth/index", "/role/list", "/role/add", "/admin/list", "/admin/add", "/task_log/list", "/user/edit", "/help", } for _, p := range pages { t.Run(p, func(t *testing.T) { code, body, err := httpGet(p, cookies) if err != nil { t.Fatalf("GET %s: %v", p, err) } if code != http.StatusOK { t.Errorf("GET %s = %d, want 200", p, code) } if strings.Contains(body, "template not found") || strings.Contains(body, "ERROR") { t.Errorf("GET %s has template error", p) } }) } } func TestAdminEditPage(t *testing.T) { cookies := loginCookies(t) code, body, err := httpGet("/admin/edit?id=1", cookies) if err != nil { t.Fatalf("GET /admin/edit: %v", err) } if code != http.StatusOK { t.Errorf("status = %d, want %d", code, http.StatusOK) } if !strings.Contains(body, "admin") { t.Error("edit page should contain user info") } } func TestStaticFiles(t *testing.T) { files := []string{ "/static/layui/css/layui.css", "/static/layui/layui.js", "/static/admin/css/main.css", "/static/admin/images/default.png", "/static/admin/images/favicon.ico", "/static/font-awesome/css/font-awesome.min.css", } for _, f := range files { t.Run(f, func(t *testing.T) { code, _, err := httpGet(f, nil) if err != nil { t.Fatalf("GET %s: %v", f, err) } if code != http.StatusOK { t.Errorf("GET %s = %d, want 200", f, code) } }) } } func TestDatabaseHasSeedData(t *testing.T) { ctx := context.Background() // Use Count (supported by all GoFrame versions) instead of GetScan with basic types count, err := g.DB().Model("pp_uc_admin").Ctx(ctx).Count() if err != nil { t.Fatalf("admin count: %v", err) } if count < 1 { t.Error("admin table should have seed data") } count, err = g.DB().Model("pp_uc_auth").Ctx(ctx).Count() if err != nil { t.Fatalf("auth count: %v", err) } if count < 1 { t.Error("auth table should have seed data") } } func TestCORSHeader(t *testing.T) { req, _ := http.NewRequest("OPTIONS", testBaseURL+"/", nil) client := &http.Client{Timeout: 5 * time.Second} resp, err := client.Do(req) if err != nil { t.Fatalf("OPTIONS /: %v", err) } defer resp.Body.Close() if resp.Header.Get("Access-Control-Allow-Origin") == "" { t.Error("CORS Access-Control-Allow-Origin header not set") } } // ==================== 日志管理 ==================== // TestTaskLogTable tests the task log list JSON endpoint func TestTaskLogTable(t *testing.T) { cookies := loginCookies(t) code, body, err := httpGet("/task_log/table", cookies) if err != nil { t.Fatalf("GET /task_log/table: %v", err) } if code != http.StatusOK { t.Errorf("status = %d, want %d", code, http.StatusOK) } if !strings.Contains(body, `"code":`) || !strings.Contains(body, `"data"`) { t.Errorf("response missing JSON fields: %s", body) } // Filter by task_id code2, body2, err2 := httpGet("/task_log/table?task_id=1", cookies) if err2 != nil { t.Fatalf("GET /task_log/table?task_id=1: %v", err2) } if code2 != http.StatusOK { t.Errorf("status = %d, want %d", code2, http.StatusOK) } if !strings.Contains(body2, `"code":`) { t.Errorf("response missing code field: %s", body2) } } // TestTaskLogDetail tests the log detail page rendering (with real log data if available) func TestTaskLogDetail(t *testing.T) { cookies := loginCookies(t) // Try viewing a specific log (id=1 may or may not exist; either 200 or error msg is OK) code, body, err := httpGet("/task_log/detail?id=1", cookies) if err != nil { t.Fatalf("GET /task_log/detail: %v", err) } if code == http.StatusOK { // Rendered successfully — check for expected template elements if !strings.Contains(body, "日志") && !strings.Contains(body, "任务") { t.Log("log detail page rendered (no specific content check)") } } // Non-existent log should return error code2, body2, err2 := httpGet("/task_log/detail?id=99999", cookies) if err2 != nil { t.Fatalf("GET /task_log/detail?id=99999: %v", err2) } if code2 != http.StatusOK { t.Errorf("status for missing log = %d, want 200", code2) } // Should show error message in flash if strings.Contains(body2, "不存在") { t.Log("non-existent log correctly reported") } } // ==================== 权限管理 ==================== // TestAuthGetNodes tests the auth tree node (zTree format) endpoint func TestAuthGetNodes(t *testing.T) { cookies := loginCookies(t) code, body, err := httpGet("/auth/get_nodes", cookies) if err != nil { t.Fatalf("GET /auth/get_nodes: %v", err) } if code != http.StatusOK { t.Errorf("status = %d, want %d", code, http.StatusOK) } // Should return JSON array with id, name, pId fields if !strings.Contains(body, `"id"`) || !strings.Contains(body, `"name"`) || !strings.Contains(body, `"pId"`) { t.Errorf("auth nodes missing required fields: %s", body[:min(len(body), 200)]) } } // TestRoleTable tests the role list JSON endpoint func TestRoleTable(t *testing.T) { cookies := loginCookies(t) code, body, err := httpGet("/role/table", cookies) if err != nil { t.Fatalf("GET /role/table: %v", err) } if code != http.StatusOK { t.Errorf("status = %d, want %d", code, http.StatusOK) } if !strings.Contains(body, `"code":`) || !strings.Contains(body, `"data"`) { t.Errorf("response missing JSON fields: %s", body) } } // TestAdminTable tests the admin list JSON endpoint func TestAdminTable(t *testing.T) { cookies := loginCookies(t) code, body, err := httpGet("/admin/table", cookies) if err != nil { t.Fatalf("GET /admin/table: %v", err) } if code != http.StatusOK { t.Errorf("status = %d, want %d", code, http.StatusOK) } if !strings.Contains(body, `"code":`) || !strings.Contains(body, `"data"`) { t.Errorf("response missing JSON fields: %s", body) } // Should include admin user data if !strings.Contains(body, "admin") { t.Log("admin table body: " + body[:min(len(body), 200)]) } } // TestRoleCRUD tests role creation and cleanup func TestRoleCRUD(t *testing.T) { cookies := loginCookies(t) ctx := context.Background() // Create a role _, body, err := httpPost("/role/ajax_save", url.Values{ "role_name": {"test-role"}, "detail": {"created by test"}, "auth_ids": {"1,2,3"}, }, cookies) if err != nil { t.Fatalf("POST /role/ajax_save: %v", err) } if !strings.Contains(body, `"status":0`) { t.Errorf("create role failed: %s", body) } // Cleanup g.DB().Exec(ctx, "DELETE FROM pp_uc_role WHERE role_name = ?", "test-role") //nolint } // TestRoleEdit tests the role edit page func TestRoleEdit(t *testing.T) { cookies := loginCookies(t) code, body, err := httpGet("/role/edit?id=1", cookies) if err != nil { t.Fatalf("GET /role/edit: %v", err) } if code != http.StatusOK { t.Errorf("status = %d, want %d", code, http.StatusOK) } if strings.Contains(body, "模板错误") || strings.Contains(body, "ERROR") { t.Error("role edit page has template errors") } } // ==================== 首页仪表盘 ==================== // TestDashboardStats tests the dashboard statistical data func TestDashboardStats(t *testing.T) { cookies := loginCookies(t) code, body, err := httpGet("/home/start", cookies) if err != nil { t.Fatalf("GET /home/start: %v", err) } if code != http.StatusOK { t.Errorf("status = %d, want %d", code, http.StatusOK) } // Check that echarts library loads code2, _, err2 := httpGet("/static/echarts/echarts.min.js", nil) if err2 != nil { t.Fatalf("GET echarts: %v", err2) } if code2 != http.StatusOK { t.Errorf("echarts static file status = %d, want 200", code2) } // Check chart data is present in the rendered page if !strings.Contains(body, "运行概况") { t.Error("dashboard missing chart title") } } // TestBanTable tests the ban list JSON endpoint func TestBanTable(t *testing.T) { cookies := loginCookies(t) code, body, err := httpGet("/ban/table", cookies) if err != nil { t.Fatalf("GET /ban/table: %v", err) } if code != http.StatusOK { t.Errorf("status = %d, want %d", code, http.StatusOK) } if !strings.Contains(body, `"code":`) || !strings.Contains(body, `"data"`) { t.Errorf("response missing JSON fields: %s", body) } } // TestNotifyTplTable tests the notify template list JSON endpoint func TestNotifyTplTable(t *testing.T) { cookies := loginCookies(t) code, body, err := httpGet("/notify_tpl/table", cookies) if err != nil { t.Fatalf("GET /notify_tpl/table: %v", err) } if code != http.StatusOK { t.Errorf("status = %d, want %d", code, http.StatusOK) } if !strings.Contains(body, `"code":`) || !strings.Contains(body, `"data"`) { t.Errorf("response missing JSON fields: %s", body) } } func min(a, b int) int { if a < b { return a } return b }