发布 0.1.0:通知渠道、告警规则、令牌版本与安全加固
This commit is contained in:
+111
-78
@@ -13,115 +13,124 @@ import (
|
||||
// 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
|
||||
gotPage string
|
||||
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, f.gotPage = region, start, end, page
|
||||
f.gotRegion, f.gotStart, f.gotEnd = region, start, end
|
||||
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},
|
||||
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,
|
||||
}
|
||||
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})
|
||||
got, err := decodeAuditCursor(encodeAuditCursor(cur))
|
||||
if err != nil {
|
||||
t.Fatalf("AuditEvents: %v", err)
|
||||
t.Fatalf("roundtrip: %v", err)
|
||||
}
|
||||
if client.calls != 1 {
|
||||
t.Fatalf("calls = %d, want 1", client.calls)
|
||||
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)
|
||||
}
|
||||
if client.gotRegion != "ap-tokyo-1" {
|
||||
t.Errorf("region = %q, want %q", client.gotRegion, "ap-tokyo-1")
|
||||
|
||||
// 空串 → 自当前时刻首查:24h 窗、分钟粒度、无窗内游标
|
||||
first, err := decodeAuditCursor("")
|
||||
if err != nil {
|
||||
t.Fatalf("first cursor: %v", err)
|
||||
}
|
||||
if window := client.gotEnd.Sub(client.gotStart); window != 6*time.Hour {
|
||||
t.Errorf("window = %v, want %v", window, 6*time.Hour)
|
||||
if first.End.Sub(first.Start) != 24*time.Hour || first.Page != "" || first.End.Second() != 0 {
|
||||
t.Fatalf("first cursor = %+v, 应为 24h 分钟粒度首窗", first)
|
||||
}
|
||||
// 响应回传分钟粒度的绝对窗,续查据此原样带回
|
||||
if got.Start.Second() != 0 || !got.End.After(got.Start) {
|
||||
t.Errorf("响应窗口 = [%v, %v), 应为分钟粒度且有序", got.Start, got.End)
|
||||
|
||||
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 TestAuditEventsResume(t *testing.T) {
|
||||
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"}},
|
||||
result: oci.AuditEventsResult{Items: []oci.AuditEvent{}, Truncated: true, NextPage: "tok-2"},
|
||||
batch: oci.AuditBatchResult{
|
||||
Items: []oci.AuditEvent{{EventId: "e1", EventName: "GetInstance"}},
|
||||
Cursor: &next,
|
||||
},
|
||||
}
|
||||
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",
|
||||
})
|
||||
// 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.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.gotCursor.Start.Equal(next.Start) || client.gotCursor.WindowHours != 24 {
|
||||
t.Fatalf("续查游标透传 = %+v, want %+v", client.gotCursor, next)
|
||||
}
|
||||
if !client.gotStart.Equal(time.Date(2026, 7, 8, 10, 0, 0, 0, time.UTC)) {
|
||||
t.Errorf("续查未用绝对窗: start=%v", client.gotStart)
|
||||
|
||||
// 尽头: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)
|
||||
}
|
||||
// 非法窗口逐项拒绝
|
||||
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)
|
||||
}
|
||||
|
||||
// 非法游标 → ErrInvalidAuditCursor
|
||||
if _, err := svc.AuditEvents(ctx, cfg.ID, AuditQuery{Cursor: "!!!"}); !errors.Is(err, ErrInvalidAuditCursor) {
|
||||
t.Fatalf("非法游标 err = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,7 +139,7 @@ func TestAuditRawStrippedAndDetail(t *testing.T) {
|
||||
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{
|
||||
batch: oci.AuditBatchResult{Items: []oci.AuditEvent{
|
||||
{EventId: "evt-1", EventTime: &eventTime, EventName: "TerminateInstance", Raw: raw},
|
||||
}},
|
||||
}
|
||||
@@ -138,7 +147,7 @@ func TestAuditRawStrippedAndDetail(t *testing.T) {
|
||||
cfg := importAliveConfig(t, svc)
|
||||
ctx := context.Background()
|
||||
|
||||
got, err := svc.AuditEvents(ctx, cfg.ID, AuditQuery{Hours: 1})
|
||||
got, err := svc.AuditEvents(ctx, cfg.ID, AuditQuery{})
|
||||
if err != nil {
|
||||
t.Fatalf("AuditEvents: %v", err)
|
||||
}
|
||||
@@ -184,3 +193,27 @@ func TestAuditDetailRequeryFallback(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user