package store import ( "context" "path/filepath" "testing" "time" "git.dern.ovh/infrastructure/caddy-opnsense-blocker/internal/model" ) func TestStoreRecordsEventsAndState(t *testing.T) { t.Parallel() dbPath := filepath.Join(t.TempDir(), "blocker.db") db, err := Open(dbPath) if err != nil { t.Fatalf("open store: %v", err) } defer db.Close() ctx := context.Background() occurredAt := time.Date(2025, 3, 11, 12, 0, 0, 0, time.UTC) event := &model.Event{ SourceName: "main", ProfileName: "main", OccurredAt: occurredAt, RemoteIP: "198.51.100.10", ClientIP: "203.0.113.10", Host: "example.test", Method: "GET", URI: "/wp-login.php", Path: "/wp-login.php", Status: 404, UserAgent: "curl/8.0", Decision: model.DecisionActionBlock, DecisionReason: "php_path", DecisionReasons: []string{"php_path"}, Enforced: true, RawJSON: `{"status":404}`, } if err := db.RecordEvent(ctx, event); err != nil { t.Fatalf("record event: %v", err) } if event.ID == 0 { t.Fatalf("expected inserted event ID") } state, found, err := db.GetIPState(ctx, "203.0.113.10") if err != nil { t.Fatalf("get ip state: %v", err) } if !found { t.Fatalf("expected IP state to exist") } if state.State != model.IPStateBlocked { t.Fatalf("unexpected ip state: %+v", state) } if state.TotalEvents != 1 { t.Fatalf("unexpected total events: %d", state.TotalEvents) } if _, err := db.SetManualOverride(ctx, "203.0.113.10", model.ManualOverrideForceAllow, model.IPStateAllowed, "manual allow"); err != nil { t.Fatalf("set manual override: %v", err) } state, found, err = db.GetIPState(ctx, "203.0.113.10") if err != nil || !found { t.Fatalf("get overridden ip state: found=%v err=%v", found, err) } if state.ManualOverride != model.ManualOverrideForceAllow { t.Fatalf("unexpected override after set: %+v", state) } if _, err := db.ClearManualOverride(ctx, "203.0.113.10", "reset"); err != nil { t.Fatalf("clear manual override: %v", err) } state, found, err = db.GetIPState(ctx, "203.0.113.10") if err != nil || !found { t.Fatalf("get reset ip state: found=%v err=%v", found, err) } if state.ManualOverride != model.ManualOverrideNone { t.Fatalf("expected cleared override, got %+v", state) } if err := db.AddDecision(ctx, &model.DecisionRecord{EventID: event.ID, IP: event.ClientIP, SourceName: event.SourceName, Kind: "automatic", Action: model.DecisionActionBlock, Reason: "php_path", Actor: "engine", Enforced: true}); err != nil { t.Fatalf("add decision: %v", err) } if err := db.AddBackendAction(ctx, &model.OPNsenseAction{IP: event.ClientIP, Action: "block", Result: "added", Message: "php_path"}); err != nil { t.Fatalf("add backend action: %v", err) } if err := db.SaveSourceOffset(ctx, model.SourceOffset{SourceName: "main", Path: "/tmp/main.log", Inode: "1:2", Offset: 42, UpdatedAt: occurredAt}); err != nil { t.Fatalf("save source offset: %v", err) } offset, found, err := db.GetSourceOffset(ctx, "main") if err != nil { t.Fatalf("get source offset: %v", err) } if !found || offset.Offset != 42 { t.Fatalf("unexpected source offset: found=%v offset=%+v", found, offset) } overview, err := db.GetOverview(ctx, 10) if err != nil { t.Fatalf("get overview: %v", err) } if overview.TotalEvents != 1 || overview.TotalIPs != 1 { t.Fatalf("unexpected overview counters: %+v", overview) } recentIPs, err := db.ListRecentIPRows(ctx, occurredAt.Add(-time.Hour), 10) if err != nil { t.Fatalf("list recent ip rows: %v", err) } if len(recentIPs) != 1 { t.Fatalf("unexpected recent ip rows count: %d", len(recentIPs)) } if recentIPs[0].IP != event.ClientIP || recentIPs[0].SourceName != event.SourceName || recentIPs[0].Events != 1 { t.Fatalf("unexpected recent ip row: %+v", recentIPs[0]) } details, err := db.GetIPDetails(ctx, event.ClientIP, 10, 10, 10) if err != nil { t.Fatalf("get ip details: %v", err) } if len(details.RecentEvents) != 1 || len(details.Decisions) != 1 || len(details.BackendActions) != 1 { t.Fatalf("unexpected ip details: %+v", details) } investigation := model.IPInvestigation{ IP: event.ClientIP, UpdatedAt: occurredAt, Bot: &model.BotMatch{Name: "Googlebot", ProviderID: "google_official", Icon: "🤖", Method: "published_ranges", Verified: true}, } if err := db.SaveInvestigation(ctx, investigation); err != nil { t.Fatalf("save investigation: %v", err) } loadedInvestigation, found, err := db.GetInvestigation(ctx, event.ClientIP) if err != nil { t.Fatalf("get investigation: %v", err) } if !found || loadedInvestigation.Bot == nil || loadedInvestigation.Bot.Name != "Googlebot" { t.Fatalf("unexpected investigation payload: found=%v investigation=%+v", found, loadedInvestigation) } investigations, err := db.GetInvestigationsForIPs(ctx, []string{event.ClientIP, "198.51.100.99"}) if err != nil { t.Fatalf("get investigations for ips: %v", err) } if len(investigations) != 1 || investigations[event.ClientIP].Bot == nil { t.Fatalf("unexpected investigations map: %+v", investigations) } userAgents, err := db.ListRecentUserAgentsForIP(ctx, event.ClientIP, 10) if err != nil { t.Fatalf("list recent user agents: %v", err) } if len(userAgents) != 1 || userAgents[0] != event.UserAgent { t.Fatalf("unexpected user agents: %#v", userAgents) } }