- 替换 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/
631 lines
17 KiB
Go
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
|
|
}
|