package oppintel import ( "context" "encoding/json" "errors" "testing" "time" "github.com/missionworkforce/missionmatch/internal/platform/audit" "github.com/missionworkforce/missionmatch/internal/platform/storage" ) // fakeSource yields a fixed set of records, then returns its err (used to inject a throttle). type fakeSource struct { name string recs []OpportunityRecord err error } func (f *fakeSource) Name() string { return f.name } func (f *fakeSource) Fetch(_ context.Context, yield func(OpportunityRecord) error) error { for _, r := range f.recs { if err := yield(r); err != nil { return err } } return f.err } // fakeWriter records upserts and can fail on a chosen record id. type fakeWriter struct { upserts []OpportunityRecord keys []string failOn string } func (w *fakeWriter) Upsert(_ context.Context, rec OpportunityRecord, key, _ string) error { if rec.SourceRecordID == w.failOn { return errors.New("db down") } w.upserts = append(w.upserts, rec) w.keys = append(w.keys, key) return nil } // fakeAuditor records events and can be made to fail. type fakeAuditor struct { events []audit.Event fail bool } func (a *fakeAuditor) System(_ context.Context, e audit.Event) error { if a.fail { return errors.New("audit down") } a.events = append(a.events, e) return nil } func sample(id string, kind RecordKind) OpportunityRecord { return OpportunityRecord{ SourceName: "Test", SourceRecordID: id, Kind: kind, FetchedAt: time.Unix(1700000000, 0).UTC(), Raw: json.RawMessage(`{"id":"` + id + `"}`), } } func TestIngestHappyPath(t *testing.T) { store := storage.NewMemory() w := &fakeWriter{} a := &fakeAuditor{} ing := NewIngester(store, w, a) src := &fakeSource{name: "Test", recs: []OpportunityRecord{ sample("a", KindActiveSolicitation), sample("b", KindContractAward), }} res, err := ing.Ingest(context.Background(), src) if err != nil { t.Fatalf("Ingest: %v", err) } if res.Fetched != 2 || res.Upserted != 2 { t.Fatalf("counts = %+v", res) } if res.ByKind["active_solicitation"] != 1 || res.ByKind["contract_award"] != 1 { t.Fatalf("by kind = %v", res.ByKind) } if len(w.upserts) != 2 { t.Fatalf("upserts = %d", len(w.upserts)) } for _, k := range w.keys { if ok, _ := store.Exists(context.Background(), k); !ok { t.Fatalf("raw payload not retained at %s", k) } } if len(a.events) != 1 || a.events[0].Action != "opportunity.ingest" { t.Fatalf("audit = %+v", a.events) } } func TestIngestInvalidKindFailsClosed(t *testing.T) { ing := NewIngester(storage.NewMemory(), &fakeWriter{}, &fakeAuditor{}) src := &fakeSource{name: "Test", recs: []OpportunityRecord{sample("a", RecordKind("bogus"))}} if _, err := ing.Ingest(context.Background(), src); err == nil { t.Fatal("an invalid record kind must abort the run") } } func TestIngestWriterErrorFailsClosed(t *testing.T) { a := &fakeAuditor{} ing := NewIngester(storage.NewMemory(), &fakeWriter{failOn: "b"}, a) src := &fakeSource{name: "Test", recs: []OpportunityRecord{ sample("a", KindActiveSolicitation), sample("b", KindActiveSolicitation), }} if _, err := ing.Ingest(context.Background(), src); err == nil { t.Fatal("a writer error must abort the run") } if len(a.events) != 0 { t.Fatal("a failed run must not be audited as a success") } } func TestIngestAuditFailureFailsClosed(t *testing.T) { ing := NewIngester(storage.NewMemory(), &fakeWriter{}, &fakeAuditor{fail: true}) src := &fakeSource{name: "Test", recs: []OpportunityRecord{sample("a", KindActiveSolicitation)}} if _, err := ing.Ingest(context.Background(), src); err == nil { t.Fatal("an audit write failure must surface as an error") } } func TestIngestThrottleIsSoftStop(t *testing.T) { a := &fakeAuditor{} ing := NewIngester(storage.NewMemory(), &fakeWriter{}, a) src := &fakeSource{name: "Test", recs: []OpportunityRecord{sample("a", KindActiveSolicitation)}, err: ErrThrottled} res, err := ing.Ingest(context.Background(), src) if err != nil { t.Fatalf("a throttle should be a soft stop, got %v", err) } if res.Upserted != 1 { t.Fatalf("the pre throttle record should be kept, got %d", res.Upserted) } if len(a.events) != 1 { t.Fatal("a throttled run is still audited") } } func TestIngestEmptySourceRecordIDFailsClosed(t *testing.T) { ing := NewIngester(storage.NewMemory(), &fakeWriter{}, &fakeAuditor{}) src := &fakeSource{name: "Test", recs: []OpportunityRecord{ {SourceName: "Test", Kind: KindActiveSolicitation, FetchedAt: time.Now()}, }} if _, err := ing.Ingest(context.Background(), src); err == nil { t.Fatal("an empty source record id must abort the run") } }