Smail/test/backend/logs_api_test.go
smail b77b272a5e
Some checks are pending
Security Checks / security (push) Waiting to run
保存日志权限分页与归档并发修正,继续运行验收
2026-09-08 22:44:21 +08:00

78 lines
2.7 KiB
Go

package controllers
import (
"context"
"database/sql"
"encoding/json"
"net/http/httptest"
"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"
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)
items := migration.DefaultMigrations()
for _, statement := range items[len(items)-1].StatementsFor(migration.DialectSQLite) {
if _, err := database.Exec(statement); 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()
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 _, role := range []access.Role{access.RoleUser, access.RoleAdmin} {
for _, kind := range []string{"operation", "config", "mail", "runtime"} {
if got := call(role, "kind="+kind+"&actorId=42"); got.ErrorNo != response.NoAccessPrivileges {
t.Fatalf("unscoped role %s accessed %s: %d", role, kind, got.ErrorNo)
}
}
}
for _, query := range []string{"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)
}
}
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")
}
}