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) } }