Files
oci-portal/internal/service/audit_test.go
T

187 lines
6.2 KiB
Go

package service
import (
"context"
"encoding/json"
"errors"
"testing"
"time"
"oci-portal/internal/oci"
)
// auditStubClient 覆写审计查询并记录透传参数,其余行为沿用 fakeClient。
type auditStubClient struct {
*fakeClient
result oci.AuditEventsResult
gotRegion string
gotStart time.Time
gotEnd time.Time
gotPage string
calls int
}
func (f *auditStubClient) ListAuditEvents(ctx context.Context, cred oci.Credentials, region string, start, end time.Time, page string) (oci.AuditEventsResult, error) {
f.calls++
f.gotRegion, f.gotStart, f.gotEnd, f.gotPage = region, start, end, page
return f.result, nil
}
func TestAuditEventsHoursValidation(t *testing.T) {
tests := []struct {
name string
hours int
wantErr bool
}{
{name: "下界 1 小时", hours: 1},
{name: "默认 24 小时", hours: 24},
{name: "上界 72 小时", hours: 72},
{name: "0 越界", hours: 0, wantErr: true},
{name: "负数越界", hours: -3, wantErr: true},
{name: "100 越界", hours: 100, wantErr: true},
}
client := &auditStubClient{
fakeClient: &fakeClient{tenancy: oci.TenancyInfo{Name: "t"}},
result: oci.AuditEventsResult{Items: []oci.AuditEvent{{EventName: "GetInstance"}}},
}
svc := newTestService(t, client)
cfg := importAliveConfig(t, svc)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := svc.AuditEvents(context.Background(), cfg.ID, AuditQuery{Hours: tt.hours})
if tt.wantErr {
if !errors.Is(err, ErrInvalidAuditHours) {
t.Fatalf("AuditEvents(hours=%d) error = %v, want ErrInvalidAuditHours", tt.hours, err)
}
return
}
if err != nil {
t.Fatalf("AuditEvents(hours=%d): %v", tt.hours, err)
}
if len(got.Items) != 1 {
t.Errorf("items = %d, want 1", len(got.Items))
}
})
}
}
func TestAuditEventsWindowAndRegion(t *testing.T) {
client := &auditStubClient{fakeClient: &fakeClient{tenancy: oci.TenancyInfo{Name: "t"}}}
svc := newTestService(t, client)
cfg := importAliveConfig(t, svc)
got, err := svc.AuditEvents(context.Background(), cfg.ID, AuditQuery{Region: "ap-tokyo-1", Hours: 6})
if err != nil {
t.Fatalf("AuditEvents: %v", err)
}
if client.calls != 1 {
t.Fatalf("calls = %d, want 1", client.calls)
}
if client.gotRegion != "ap-tokyo-1" {
t.Errorf("region = %q, want %q", client.gotRegion, "ap-tokyo-1")
}
if window := client.gotEnd.Sub(client.gotStart); window != 6*time.Hour {
t.Errorf("window = %v, want %v", window, 6*time.Hour)
}
// 响应回传分钟粒度的绝对窗,续查据此原样带回
if got.Start.Second() != 0 || !got.End.After(got.Start) {
t.Errorf("响应窗口 = [%v, %v), 应为分钟粒度且有序", got.Start, got.End)
}
}
func TestAuditEventsResume(t *testing.T) {
client := &auditStubClient{
fakeClient: &fakeClient{tenancy: oci.TenancyInfo{Name: "t"}},
result: oci.AuditEventsResult{Items: []oci.AuditEvent{}, Truncated: true, NextPage: "tok-2"},
}
svc := newTestService(t, client)
cfg := importAliveConfig(t, svc)
ctx := context.Background()
// 续查:绝对窗 + 游标透传;NextPage 原样回传
got, err := svc.AuditEvents(ctx, cfg.ID, AuditQuery{
Start: "2026-07-08T10:00:00Z", End: "2026-07-09T10:00:00Z", Page: "tok-1",
})
if err != nil {
t.Fatalf("续查: %v", err)
}
if client.gotPage != "tok-1" || !got.Truncated || got.NextPage != "tok-2" {
t.Errorf("游标透传 page=%q next=%q truncated=%v", client.gotPage, got.NextPage, got.Truncated)
}
if !client.gotStart.Equal(time.Date(2026, 7, 8, 10, 0, 0, 0, time.UTC)) {
t.Errorf("续查未用绝对窗: start=%v", client.gotStart)
}
// 非法窗口逐项拒绝
bad := []AuditQuery{
{Start: "not-a-time", End: "2026-07-09T10:00:00Z"},
{Start: "2026-07-09T10:00:00Z", End: "2026-07-08T10:00:00Z"}, // 倒序
{Start: "2026-07-01T00:00:00Z", End: "2026-07-09T10:00:00Z"}, // 超 72h
{Page: "tok-only"}, // 带游标缺窗口
}
for i, q := range bad {
if _, err := svc.AuditEvents(ctx, cfg.ID, q); !errors.Is(err, ErrInvalidAuditWindow) {
t.Errorf("bad[%d] err = %v, want ErrInvalidAuditWindow", i, err)
}
}
}
func TestAuditRawStrippedAndDetail(t *testing.T) {
raw := json.RawMessage(`{"eventId":"evt-1","full":true}`)
eventTime := time.Date(2026, 7, 9, 10, 30, 40, 0, time.UTC)
client := &auditStubClient{
fakeClient: &fakeClient{tenancy: oci.TenancyInfo{Name: "t"}},
result: oci.AuditEventsResult{Items: []oci.AuditEvent{
{EventId: "evt-1", EventTime: &eventTime, EventName: "TerminateInstance", Raw: raw},
}},
}
svc := newTestService(t, client)
cfg := importAliveConfig(t, svc)
ctx := context.Background()
got, err := svc.AuditEvents(ctx, cfg.ID, AuditQuery{Hours: 1})
if err != nil {
t.Fatalf("AuditEvents: %v", err)
}
if got.Items[0].Raw != nil {
t.Error("列表响应应剥离 raw")
}
// 详情走缓存,不再回源
detail, err := svc.AuditEventDetail(ctx, cfg.ID, "", "evt-1", eventTime)
if err != nil || string(detail) != string(raw) {
t.Fatalf("AuditEventDetail = %s, %v", detail, err)
}
if client.calls != 1 {
t.Errorf("缓存命中不应回源, calls = %d", client.calls)
}
}
func TestAuditDetailRequeryFallback(t *testing.T) {
raw := json.RawMessage(`{"eventId":"evt-2"}`)
eventTime := time.Date(2026, 7, 9, 10, 30, 40, 0, time.UTC)
client := &auditStubClient{
fakeClient: &fakeClient{tenancy: oci.TenancyInfo{Name: "t"}},
result: oci.AuditEventsResult{Items: []oci.AuditEvent{
{EventId: "evt-2", EventTime: &eventTime, Raw: raw},
}},
}
svc := newTestService(t, client)
cfg := importAliveConfig(t, svc)
ctx := context.Background()
// 缓存未预热:miss 走事件时间小窗重查找回
detail, err := svc.AuditEventDetail(ctx, cfg.ID, "", "evt-2", eventTime)
if err != nil || string(detail) != string(raw) {
t.Fatalf("兜底重查 = %s, %v", detail, err)
}
if client.calls != 1 {
t.Fatalf("应回源一次, calls = %d", client.calls)
}
if win := client.gotEnd.Sub(client.gotStart); win != 2*time.Minute || client.gotStart.Second() != 0 {
t.Errorf("兜底窗口 = [%v, +%v), 应为事件分钟起 2 分钟", client.gotStart, win)
}
// 重查也找不到 → ErrAuditEventGone
if _, err := svc.AuditEventDetail(ctx, cfg.ID, "", "no-such", eventTime); !errors.Is(err, ErrAuditEventGone) {
t.Errorf("未找回 err = %v, want ErrAuditEventGone", err)
}
}