Files
ppgo_job/integration_test.go
lmk 610b85e3a5 refactor: 从 Beego 迁移到 GoFrame v2
- 替换 Beego 框架为 GoFrame v2
- 重构项目结构: controller/service/dao/middleware 分层
- 替换自定义 crons 包为 gcron
- 模板从 views/ 迁移到 resource/template/
- 配置从 conf/app.conf 迁移到 config.yml
- 数据库从 MySQL 切换为 SQLite (modernc.org/sqlite)
- 移除 agent/ 远程执行器(待后续迁移)
- 移除 crons/ 自定义定时器包
- 静态资源整理到 resource/static/
2026-07-09 11:06:12 +08:00

631 lines
17 KiB
Go

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
}