//go:build integration package integration import ( "context" "testing" "time" "github.com/jackc/pgx/v5/pgtype" "github.com/shopspring/decimal" oppintel "github.com/missionworkforce/missionmatch/internal/modules/opportunity_intelligence" "github.com/missionworkforce/missionmatch/internal/platform/database/dbgen" ) // TestOpportunityRecordsGlobalCatalog proves opportunity_records behaves like the other global public // reference tables (sam_opportunities, usaspending_awards): the migrator writes it, the app role reads // it with NO tenant context (it is outside RLS, so a read returns rows without an org GUC set), and the // app role cannot write it (SELECT only). It also round trips the decimal estimated_value exactly and // the is_planning flag. func TestOpportunityRecordsGlobalCatalog(t *testing.T) { ctx := context.Background() srcID := "INT-" + time.Now().UTC().Format("20060102150405.000000") amt := decimal.RequireFromString("2500000.75") if err := dbgen.New(migrator).UpsertOpportunityRecord(ctx, dbgen.UpsertOpportunityRecordParams{ SourceName: "SAM.gov", SourceRecordID: srcID, DedupeKey: "INTDEDUPE", RecordKind: "active_solicitation", Title: "Integration cyber support", Agency: "DEPT OF DEFENSE", SubAgency: "AIR FORCE", Naics: "541512", PopState: "HI", SetAside: "8(a) Set Aside", SetAsideCode: "8A", EstimatedValue: &amt, ValueSource: "operator", IsPlanning: false, PostedDate: pgtype.Date{Time: time.Now().UTC(), Valid: true}, FetchedAt: time.Now().UTC(), RawObjectKey: "opportunity_records/sam.gov/" + srcID + ".json", RawSha256: "deadbeef", }); err != nil { t.Fatalf("migrator upsert: %v", err) } // Read through the service on the APP pool with NO tenant transaction. A global catalog outside RLS // must return rows even though no org GUC is set. svc := oppintel.NewService(appDB) views, err := svc.List(ctx, oppintel.Filters{NAICS: []string{"541512"}, Limit: 500}) if err != nil { t.Fatalf("app read: %v", err) } var found *oppintel.View for i := range views { if views[i].SourceRecordID == srcID { found = &views[i] break } } if found == nil { t.Fatal("app role could not read the global opportunity_records catalog") } if found.EstimatedValue != "2500000.75" { t.Fatalf("decimal round trip = %q, want 2500000.75", found.EstimatedValue) } if found.Kind != "active_solicitation" || found.IsPlanning { t.Fatalf("fields wrong: %+v", found) } // The app role must be read only on this catalog: SELECT is granted, INSERT is not. _, werr := appDB.Pool.Exec(ctx, `INSERT INTO opportunity_records (source_name, source_record_id, record_kind, fetched_at) VALUES ('x', 'y', 'forecast', now())`) if werr == nil { t.Fatal("app role must NOT be able to write opportunity_records (read only catalog)") } } // TestOpportunityRecordsPlanningLabel confirms a forecast row reads back labeled as planning data, so // the UI can never present a forecast as a posted solicitation. func TestOpportunityRecordsPlanningLabel(t *testing.T) { ctx := context.Background() srcID := "INTFC-" + time.Now().UTC().Format("20060102150405.000000") if err := dbgen.New(migrator).UpsertOpportunityRecord(ctx, dbgen.UpsertOpportunityRecordParams{ SourceName: "GSA Forecast", SourceRecordID: srcID, RecordKind: "forecast", Title: "Planned IT recompete", Agency: "GSA", Naics: "541512", IsPlanning: true, EstimatedAwardDate: pgtype.Date{Time: time.Now().UTC().AddDate(0, 6, 0), Valid: true}, FetchedAt: time.Now().UTC(), }); err != nil { t.Fatalf("migrator upsert forecast: %v", err) } svc := oppintel.NewService(appDB) views, err := svc.List(ctx, oppintel.Filters{Kinds: []string{"forecast"}, Limit: 500}) if err != nil { t.Fatalf("read: %v", err) } for _, v := range views { if v.SourceRecordID == srcID { if !v.IsPlanning || v.PlanningNote != "Planning data subject to change" { t.Fatalf("forecast not labeled as planning data: %+v", v) } return } } t.Fatal("forecast row not found") }