//go:build integration package integration import ( "context" "os" "strings" "testing" "github.com/jackc/pgx/v5" "github.com/shopspring/decimal" "github.com/missionworkforce/missionmatch/internal/modules/matching" "github.com/missionworkforce/missionmatch/internal/modules/matching/engine" "github.com/missionworkforce/missionmatch/internal/platform/config" "github.com/missionworkforce/missionmatch/internal/platform/database/dbgen" "github.com/missionworkforce/missionmatch/internal/platform/embeddings" ) // TestCrossTenantDatabaseIsolation proves the database per tenant boundary: a connection bound to one // tenant database physically cannot read another tenant's database. Postgres refuses a cross database // reference, so isolation does not depend on RLS or on application code being correct. Skips unless two // tenant DSNs are provided (set MM_TENANT_A_DSN and MM_TENANT_B_DSN to two provisioned databases). func TestCrossTenantDatabaseIsolation(t *testing.T) { aDSN, bDSN := os.Getenv("MM_TENANT_A_DSN"), os.Getenv("MM_TENANT_B_DSN") if aDSN == "" || bDSN == "" { t.Skip("MM_TENANT_A_DSN / MM_TENANT_B_DSN unset; skipping the database per tenant isolation test") } ctx := context.Background() a, err := pgx.Connect(ctx, aDSN) if err != nil { t.Fatalf("connect A: %v", err) } defer a.Close(ctx) // The name of database A, so we can attempt a cross database reference from B. var aName string if err := a.QueryRow(ctx, "SELECT current_database()").Scan(&aName); err != nil { t.Fatalf("current_database A: %v", err) } b, err := pgx.Connect(ctx, bDSN) if err != nil { t.Fatalf("connect B: %v", err) } defer b.Close(ctx) var bName string _ = b.QueryRow(ctx, "SELECT current_database()").Scan(&bName) if aName == bName { t.Fatalf("A and B resolve to the same database %q; they must be separate tenant databases", aName) } // From a connection bound to B, reaching into A's tables must fail at the Postgres level. _, err = b.Exec(ctx, "SELECT 1 FROM "+aName+".public.candidates LIMIT 1") if err == nil || !strings.Contains(strings.ToLower(err.Error()), "cross-database") { t.Fatalf("a cross database read from B into A must be refused by Postgres, got: %v", err) } // No cross database bridge (dblink / postgres_fdw) may be installed in B. var bridges int if err := b.QueryRow(ctx, "SELECT count(*) FROM pg_extension WHERE extname IN ('dblink','postgres_fdw')").Scan(&bridges); err != nil { t.Fatalf("bridge check: %v", err) } if bridges != 0 { t.Fatalf("a tenant database must have no cross database bridge, found %d", bridges) } } // TestErasureWipesResumeVector proves the right to erasure also wipes the AI vector: after a candidate is // embedded, anonymize must null resume_embedding so no semantic trace of the person survives. func TestErasureWipesResumeVector(t *testing.T) { org := seedOrg(t) cand := seedCandidate(t, org, "secret", "active", true) ctx := context.Background() // Give the candidate a non null embedding (as a backfill would). vec := "[" + strings.TrimSuffix(strings.Repeat("0.05,", 384), ",") + "]" if _, err := migrator.Exec(ctx, "UPDATE candidates SET resume_embedding=$1::vector WHERE id=$2", vec, cand.ID); err != nil { t.Fatalf("set embedding: %v", err) } var present bool _ = migrator.QueryRow(ctx, "SELECT resume_embedding IS NOT NULL FROM candidates WHERE id=$1", cand.ID).Scan(&present) if !present { t.Fatal("precondition: the candidate should have an embedding before erasure") } // Anonymize (the erasure path) and confirm the vector is gone. if err := appDB.RunInTenantTx(ctxOrg(org), func(ctx context.Context, tx pgx.Tx) error { _, e := dbgen.New(tx).AnonymizeCandidate(ctx, cand.ID) return e }); err != nil { t.Fatalf("anonymize: %v", err) } var stillThere bool _ = migrator.QueryRow(ctx, "SELECT resume_embedding IS NOT NULL FROM candidates WHERE id=$1", cand.ID).Scan(&stillThere) if stillThere { t.Fatal("ERASURE LEAK: resume_embedding survived anonymize; the person's semantic vector was not wiped") } } // TestEmbedderUsesRealSemantics proves the skill matcher now uses real semantics through the MEGAMIND // plane, not the deterministic hash fallback: a true synonym pair scores meaningfully higher with the // real embedder than with the fake one, so the engine's Skills component is semantic. Skips unless the // inference plane is configured. func TestEmbedderUsesRealSemantics(t *testing.T) { url := os.Getenv("MM_MEGAMIND_URL") if url == "" { t.Skip("MM_MEGAMIND_URL unset; skipping the semantic skill match test") } ctx := context.Background() required := engine.RequiredSkill{Name: "Amazon Web Services"} cands := []engine.CandidateSkill{{Name: "AWS cloud"}} threshold := decimal.RequireFromString("0.50") real, err := embeddings.New(config.Embeddings{Provider: config.ProviderMegamind, MegamindURL: url, Model: "all-minilm", Dimension: 384}) if err != nil { t.Fatalf("megamind embedder: %v", err) } fake, _ := embeddings.New(config.Embeddings{Provider: config.ProviderFake, Dimension: 384}) realMatch, realOK := matching.NewEmbeddingSkillMatcher(real).Match(ctx, required, cands, threshold) _, fakeOK := matching.NewEmbeddingSkillMatcher(fake).Match(ctx, required, cands, threshold) if !realOK { t.Fatalf("the real embedder should semantically match %q to %q above %s, got similarity %s", required.Name, cands[0].Name, threshold, realMatch.Similarity) } if fakeOK { t.Fatal("the fake (hash) embedder should NOT semantically match these distinct strings; if it does the test is not proving real semantics") } if realMatch.Exact { t.Fatal("this is a semantic match, not an exact one; Exact must be false") } }