Smail/test/backend/logs_api_test.go
smail 3795c34218
Some checks are pending
Security Checks / security (push) Waiting to run
限制系统邮件记录仅超管可见并校验唯一超级管理员
2026-09-15 21:00:31 +08:00

141 lines
5.6 KiB
Go

package controllers
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"net/http/httptest"
"net/url"
"path/filepath"
"testing"
"time"
"git.pcux.cn/xuans/Smail/db/migration"
"git.pcux.cn/xuans/Smail/dto/response"
"git.pcux.cn/xuans/Smail/services/access"
"git.pcux.cn/xuans/Smail/services/logstore"
"git.pcux.cn/xuans/Smail/services/runtimelog"
appcontext "git.pcux.cn/xuans/Smail/utils/context"
_ "modernc.org/sqlite"
)
func TestLogsAPIRejectsUnscopedAccessAndValidatesQueries(t *testing.T) {
database, err := sql.Open("sqlite", filepath.Join(t.TempDir(), "logs.db"))
if err != nil {
t.Fatal(err)
}
defer database.Close()
// 与正式 SQLite 连接池一致,避免测试自行引入多写连接锁冲突。
database.SetMaxOpenConns(1)
runner := migration.Runner{DB: database, Dialect: migration.DialectSQLite, Migrations: migration.DefaultMigrations()}
if _, err := runner.Run(context.Background()); err != nil {
t.Fatal(err)
}
store, err := logstore.New(database, migration.DialectSQLite)
if err != nil {
t.Fatal(err)
}
runtime := logstore.Start(store)
defer runtime.Stop()
writer, err := runtimelog.Start(t.TempDir())
if err != nil {
t.Fatal(err)
}
defer writer.Close()
if err := store.Write(context.Background(), logstore.Entry{Kind: "operation", CreatedAt: time.Now(), ActorID: "42", Action: "probe", Result: "success", Payload: []byte(`{}`)}); err != nil {
t.Fatal(err)
}
call := func(role access.Role, query string) response.Response {
t.Helper()
request := httptest.NewRequest("GET", "/api/v1/logs?"+query, nil)
recorder := httptest.NewRecorder()
GetLogs(&appcontext.Context{Role: role, UserID: 42}, recorder, request)
if recorder.Header().Get("Cache-Control") != "no-store" {
t.Fatal("sensitive log response can be cached")
}
var result response.Response
if err := json.Unmarshal(recorder.Body.Bytes(), &result); err != nil {
t.Fatal(err)
}
return result
}
for _, kind := range []string{"operation", "mail", "runtime"} {
if got := call(access.RoleUser, "kind="+kind+"&actorId=42"); got.ErrorNo != 0 {
t.Fatalf("普通用户无法查询本人日志:%s => %d", kind, got.ErrorNo)
}
}
for _, role := range []access.Role{access.RoleUser, access.RoleAdmin} {
if got := call(role, "kind=mail&systemOnly=true"); got.ErrorNo != response.NoAccessPrivileges {
t.Fatalf("低权限账号能够查询系统邮件:%s", role)
}
if got := call(role, "kind=runtime&listFiles=true"); got.ErrorNo != response.NoAccessPrivileges {
t.Fatalf("低权限用户能读取全局文件列表:%s", role)
}
}
for _, query := range []string{"kind=operation&systemOnly=true", "kind=mail&systemOnly=1", "kind=config", "kind=unknown", "kind=mail&limit=501", "kind=mail&offset=-1", "kind=mail&from=invalid", "kind=mail&from=2026-09-08T00:00:00Z&to=2026-09-01T00:00:00Z"} {
if got := call(access.RoleSuperAdmin, query); got.ErrorNo != response.ParamsError {
t.Fatalf("invalid query accepted: %s => %d", query, got.ErrorNo)
}
}
for _, query := range []string{"kind=runtime&from=invalid", "kind=runtime&to=", "kind=runtime&file=../config/config.json", "kind=runtime&from=2026-09-08T00:00:00Z&to=2026-09-01T00:00:00Z"} {
if got := call(access.RoleSuperAdmin, query); got.ErrorNo != response.ParamsError {
t.Fatalf("运行日志未校验参数:%s => %d", query, got.ErrorNo)
}
}
result := call(access.RoleSuperAdmin, "kind=operation&actorId=42&limit=1")
if result.ErrorNo != 0 {
t.Fatalf("administrator query failed: %d", result.ErrorNo)
}
data := result.Data.(map[string]any)
if len(data["items"].([]any)) != 1 || data["retentionDays"] != float64(30) {
t.Fatal("log query lost records or retention policy")
}
now := time.Now().UTC().Add(-time.Minute)
for index := 0; index < 14; index++ {
at := now.Add(time.Duration(index) * time.Second)
if index == 13 {
at = now.Add(-25 * time.Hour)
}
if err := writer.WriteAt(at, fmt.Sprintf("运行事件%d", index)); err != nil {
t.Fatal(err)
}
if err := store.Write(context.Background(), logstore.Entry{Kind: "mail", CreatedAt: at, Action: "投递邮件", Result: "success", Payload: []byte(`{}`)}); err != nil {
t.Fatal(err)
}
}
for _, kind := range []string{"mail", "runtime"} {
key := "items"
if kind == "runtime" {
key = "lines"
}
first := call(access.RoleSuperAdmin, "kind="+kind)
data := first.Data.(map[string]any)
if first.ErrorNo != 0 || len(data[key].([]any)) != 10 || data["hasMore"] != true {
t.Fatalf("默认十条:%+v", first)
}
second := call(access.RoleSuperAdmin, "kind="+kind+"&offset=10")
data = second.Data.(map[string]any)
if len(data[key].([]any)) != 3 || data["hasMore"] != false {
t.Fatalf("默认时段不是最近二十四小时:%+v", second)
}
values := url.Values{"kind": {kind}, "from": {now.Add(-26 * time.Hour).Format(time.RFC3339Nano)}, "to": {now.Add(-24 * time.Hour).Format(time.RFC3339Nano)}}
history := call(access.RoleSuperAdmin, values.Encode())
if history.ErrorNo != 0 || len(history.Data.(map[string]any)[key].([]any)) != 1 {
t.Fatalf("指定时段:%+v", history)
}
}
filtered := call(access.RoleSuperAdmin, "kind=mail&systemOnly=true")
if filtered.ErrorNo != 0 || len(filtered.Data.(map[string]any)["items"].([]any)) != 10 {
t.Fatal("超级管理员无法筛选系统邮件")
}
for _, role := range []access.Role{access.RoleAdmin, access.RoleUser} {
for _, query := range []string{"kind=mail", "kind=mail&actorId=0", "kind=mail&systemOnly=false"} {
got := call(role, query)
if got.ErrorNo != 0 || len(got.Data.(map[string]any)["items"].([]any)) != 0 {
t.Fatal("低权限账号读取到了系统邮件")
}
}
}
}