220 lines
7.2 KiB
Go
220 lines
7.2 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
"oci-portal/internal/oci"
|
|
)
|
|
|
|
// auditStubClient 覆写审计查询并记录透传参数,其余行为沿用 fakeClient。
|
|
type auditStubClient struct {
|
|
*fakeClient
|
|
batch oci.AuditBatchResult
|
|
result oci.AuditEventsResult
|
|
gotRegion string
|
|
gotCursor oci.AuditCursor
|
|
gotLimit int
|
|
gotStart time.Time
|
|
gotEnd time.Time
|
|
calls int
|
|
}
|
|
|
|
func (f *auditStubClient) ListAuditEventsBatch(ctx context.Context, cred oci.Credentials, region string, cur oci.AuditCursor, limit int) (oci.AuditBatchResult, error) {
|
|
f.calls++
|
|
f.gotRegion, f.gotCursor, f.gotLimit = region, cur, limit
|
|
return f.batch, nil
|
|
}
|
|
|
|
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 = region, start, end
|
|
return f.result, nil
|
|
}
|
|
|
|
func TestAuditCursorCodec(t *testing.T) {
|
|
cur := oci.AuditCursor{
|
|
Start: time.Date(2026, 7, 9, 10, 0, 0, 0, time.UTC),
|
|
End: time.Date(2026, 7, 10, 10, 0, 0, 0, time.UTC),
|
|
Page: "tok-1",
|
|
WindowHours: 48,
|
|
}
|
|
got, err := decodeAuditCursor(encodeAuditCursor(cur))
|
|
if err != nil {
|
|
t.Fatalf("roundtrip: %v", err)
|
|
}
|
|
if !got.Start.Equal(cur.Start) || !got.End.Equal(cur.End) || got.Page != cur.Page || got.WindowHours != cur.WindowHours {
|
|
t.Fatalf("roundtrip = %+v, want %+v", got, cur)
|
|
}
|
|
|
|
// 空串 → 自当前时刻首查:24h 窗、分钟粒度、无窗内游标
|
|
first, err := decodeAuditCursor("")
|
|
if err != nil {
|
|
t.Fatalf("first cursor: %v", err)
|
|
}
|
|
if first.End.Sub(first.Start) != 24*time.Hour || first.Page != "" || first.End.Second() != 0 {
|
|
t.Fatalf("first cursor = %+v, 应为 24h 分钟粒度首窗", first)
|
|
}
|
|
|
|
bad := []string{"!!!", "bm90LWpzb24", encodeAuditCursor(oci.AuditCursor{})}
|
|
for i, s := range bad {
|
|
if _, err := decodeAuditCursor(s); !errors.Is(err, ErrInvalidAuditCursor) {
|
|
t.Errorf("bad[%d] err = %v, want ErrInvalidAuditCursor", i, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAuditEventsBatchParams(t *testing.T) {
|
|
next := oci.AuditCursor{
|
|
Start: time.Date(2026, 7, 8, 10, 0, 0, 0, time.UTC),
|
|
End: time.Date(2026, 7, 9, 10, 0, 0, 0, time.UTC),
|
|
WindowHours: 24,
|
|
}
|
|
client := &auditStubClient{
|
|
fakeClient: &fakeClient{tenancy: oci.TenancyInfo{Name: "t"}},
|
|
batch: oci.AuditBatchResult{
|
|
Items: []oci.AuditEvent{{EventId: "e1", EventName: "GetInstance"}},
|
|
Cursor: &next,
|
|
},
|
|
}
|
|
svc := newTestService(t, client)
|
|
cfg := importAliveConfig(t, svc)
|
|
ctx := context.Background()
|
|
|
|
// limit 归一:0 → 100;超上限截到 200;region 透传
|
|
cases := []struct {
|
|
name string
|
|
limit int
|
|
wantLimit int
|
|
}{
|
|
{"缺省 100", 0, 100},
|
|
{"正常透传", 50, 50},
|
|
{"超限截断", 999, 200},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got, err := svc.AuditEvents(ctx, cfg.ID, AuditQuery{Region: "ap-tokyo-1", Limit: tc.limit})
|
|
if err != nil {
|
|
t.Fatalf("AuditEvents: %v", err)
|
|
}
|
|
if client.gotLimit != tc.wantLimit || client.gotRegion != "ap-tokyo-1" {
|
|
t.Fatalf("limit = %d(want %d), region = %q", client.gotLimit, tc.wantLimit, client.gotRegion)
|
|
}
|
|
if got.Cursor == "" || got.Exhausted {
|
|
t.Fatalf("响应应携带续查游标: %+v", got)
|
|
}
|
|
})
|
|
}
|
|
|
|
// 续查:响应游标原样带回可解析,并透传到 oci 层
|
|
got, err := svc.AuditEvents(ctx, cfg.ID, AuditQuery{})
|
|
if err != nil {
|
|
t.Fatalf("首查: %v", err)
|
|
}
|
|
if _, err := svc.AuditEvents(ctx, cfg.ID, AuditQuery{Cursor: got.Cursor}); err != nil {
|
|
t.Fatalf("续查: %v", err)
|
|
}
|
|
if !client.gotCursor.Start.Equal(next.Start) || client.gotCursor.WindowHours != 24 {
|
|
t.Fatalf("续查游标透传 = %+v, want %+v", client.gotCursor, next)
|
|
}
|
|
|
|
// 尽头:Cursor 为 nil → 响应空游标 + exhausted
|
|
client.batch = oci.AuditBatchResult{Items: []oci.AuditEvent{}, Exhausted: true}
|
|
end, err := svc.AuditEvents(ctx, cfg.ID, AuditQuery{})
|
|
if err != nil || end.Cursor != "" || !end.Exhausted {
|
|
t.Fatalf("尽头响应 = %+v, %v", end, err)
|
|
}
|
|
|
|
// 非法游标 → ErrInvalidAuditCursor
|
|
if _, err := svc.AuditEvents(ctx, cfg.ID, AuditQuery{Cursor: "!!!"}); !errors.Is(err, ErrInvalidAuditCursor) {
|
|
t.Fatalf("非法游标 err = %v", 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"}},
|
|
batch: oci.AuditBatchResult{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{})
|
|
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)
|
|
}
|
|
}
|
|
|
|
func TestAuditRawCacheTenantIsolationAndInvalidation(t *testing.T) {
|
|
svc := newTestService(t, &fakeClient{})
|
|
raw1 := json.RawMessage(`{"tenant":1}`)
|
|
raw2 := json.RawMessage(`{"tenant":2}`)
|
|
svc.stripAuditRaw(1, []oci.AuditEvent{{EventId: "same", Raw: raw1}})
|
|
svc.stripAuditRaw(2, []oci.AuditEvent{{EventId: "same", Raw: raw2}})
|
|
|
|
assertAuditRaw(t, svc, 1, raw1)
|
|
assertAuditRaw(t, svc, 2, raw2)
|
|
svc.InvalidateAuditCache(1)
|
|
if _, ok := svc.auditRaw.Get(auditRawKey(1, "same")); ok {
|
|
t.Fatal("tenant 1 raw cache still exists after invalidation")
|
|
}
|
|
assertAuditRaw(t, svc, 2, raw2)
|
|
}
|
|
|
|
func assertAuditRaw(t *testing.T, svc *OciConfigService, configID uint, want json.RawMessage) {
|
|
t.Helper()
|
|
got, ok := svc.auditRaw.Get(auditRawKey(configID, "same"))
|
|
if !ok || string(got.(json.RawMessage)) != string(want) {
|
|
t.Fatalf("tenant %d raw = %v, want %s", configID, got, want)
|
|
}
|
|
}
|