187 lines
6.2 KiB
Go
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)
|
|
}
|
|
}
|