Files
pdbmux/source_test.go
T
unkin-agent c228597fb9
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
Scope the extract gate to subqueries and correct the suppression doc
Suppression matches a record's own `name` field, so a projection that
filters on `name` without returning it carries an upstream value through.
The README claimed the record was always dropped on every query shape.
State the rule the code implements and pin the shape with a test.

`hasExtract` exempted only `in`. openvoxdb's `valid-operator?`
(src/puppetlabs/puppetdb/query_eng/engine.clj:2779-2784) lists `subquery`
separately, and the AST-rewrite stage (:2111-2123) expands
["subquery" entity expr] into ["in" cols ["extract" cols ["select_x" expr]]]
before any plan node is built, so its operand is projected into a subquery
exactly like `in`'s (:2705-2712). Exempt `subquery` and the explicit
`select_<entity>` forms (:1889-1911).

Signed-off-by: unkin-agent <unkin-agent@unkin.net>
2026-09-05 21:49:14 +10:00

579 lines
23 KiB
Go

package main
import (
"bytes"
"encoding/json"
"io"
"log"
"net/http"
"slices"
"strings"
"testing"
)
// sourceValues returns certname -> value of the synthetic fact record, and the
// number of records carrying that fact name.
func sourceValues(t *testing.T, body []byte, factName string) (map[string]string, int) {
t.Helper()
var raws []json.RawMessage
if err := json.Unmarshal(body, &raws); err != nil {
t.Fatalf("unmarshal %s: %v", body, err)
}
out := map[string]string{}
n := 0
for _, raw := range raws {
var m struct {
Certname string `json:"certname"`
Name string `json:"name"`
Value string `json:"value"`
}
if json.Unmarshal(raw, &m) != nil || m.Name != factName {
continue
}
out[m.Certname] = m.Value
n++
}
return out, n
}
// nodeSources returns certname -> the stamped provenance field on /nodes records.
func nodeSources(t *testing.T, body []byte, field string) map[string]string {
t.Helper()
var raws []json.RawMessage
if err := json.Unmarshal(body, &raws); err != nil {
t.Fatalf("unmarshal %s: %v", body, err)
}
out := map[string]string{}
for _, raw := range raws {
var obj map[string]json.RawMessage
if err := json.Unmarshal(raw, &obj); err != nil {
t.Fatalf("unmarshal record %s: %v", raw, err)
}
var m recordMeta
_ = json.Unmarshal(raw, &m)
v, ok := obj[field]
if !ok {
continue
}
var s string
if err := json.Unmarshal(v, &s); err != nil {
t.Fatalf("provenance field of %s is not a string: %v", raw, err)
}
out[m.Certname] = s
}
return out
}
func factEnv(cn, name, val, env string) string {
return `{"certname":"` + cn + `","name":"` + name + `","value":"` + val + `","environment":"` + env + `"}`
}
// Both backends hold h1; a holds its newer report, so h1's facts and its
// provenance fact must both come from a.
func TestHandler_FactsSourceFollowsMergeOwner(t *testing.T) {
a := newFakeBackend(t,
`[`+node("h1", "2026-07-20T00:00:00Z")+`,`+node("h2", "2026-07-01T00:00:00Z")+`]`,
`[`+factEnv("h1", "role", "web-a", "production")+`,`+factEnv("h2", "role", "db-a", "production")+`]`)
b := newFakeBackend(t,
`[`+node("h1", "2026-07-01T00:00:00Z")+`,`+node("h2", "2026-07-20T00:00:00Z")+`]`,
`[`+factEnv("h1", "role", "web-b", "staging")+`,`+factEnv("h2", "role", "db-b", "staging")+`]`)
srv := newTestServer(testConfig(a.srv.URL, b.srv.URL, mergeFreshness))
rec := doGet(t, srv.Handler(), factsPath, "")
if rec.Code != http.StatusOK {
t.Fatalf("status %d: %s", rec.Code, rec.Body.String())
}
got, n := sourceValues(t, rec.Body.Bytes(), defaultSourceFact)
if n != 2 {
t.Fatalf("expected one %s record per certname, got %d: %s", defaultSourceFact, n, rec.Body.String())
}
if got["h1"] != "a" || got["h2"] != "b" {
t.Errorf("provenance must name the backend that won the merge, got %v", got)
}
}
// The synthetic record carries the node's own environment so it groups with the
// real facts rather than landing in an unrelated environment.
func TestHandler_FactsSourceCopiesEnvironment(t *testing.T) {
a := newFakeBackend(t, `[`+node("h1", "2026-07-20T00:00:00Z")+`]`,
`[`+factEnv("h1", "role", "web", "staging")+`]`)
b := newFakeBackend(t, `[]`, `[]`)
srv := newTestServer(testConfig(a.srv.URL, b.srv.URL, mergeStatic))
rec := doGet(t, srv.Handler(), factsPath, "")
var raws []json.RawMessage
if err := json.Unmarshal(rec.Body.Bytes(), &raws); err != nil {
t.Fatal(err)
}
var found bool
for _, raw := range raws {
var m recordMeta
if json.Unmarshal(raw, &m) != nil || m.Name != defaultSourceFact {
continue
}
found = true
if m.Environment != "staging" {
t.Errorf("environment = %q, want staging: %s", m.Environment, raw)
}
}
if !found {
t.Fatalf("no %s record: %s", defaultSourceFact, rec.Body.String())
}
}
// A node record's provenance names the backend whose node record won /nodes'
// own report_timestamp merge.
func TestHandler_NodesSourceStamped(t *testing.T) {
a := newFakeBackend(t,
`[`+node("h1", "2026-07-01T00:00:00Z")+`,`+node("h2", "2026-07-20T00:00:00Z")+`]`, `[]`)
b := newFakeBackend(t, `[`+node("h1", "2026-07-20T00:00:00Z")+`]`, `[]`)
srv := newTestServer(testConfig(a.srv.URL, b.srv.URL, mergeStatic))
rec := doGet(t, srv.Handler(), nodesPath, "")
if rec.Code != http.StatusOK {
t.Fatalf("status %d: %s", rec.Code, rec.Body.String())
}
got := nodeSources(t, rec.Body.Bytes(), defaultSourceFact)
if got["h1"] != "b" || got["h2"] != "a" {
t.Errorf("node provenance = %v, want h1=b h2=a", got)
}
}
// Stamping must not drop unknown upstream fields.
func TestHandler_NodesSourceKeepsUpstreamFields(t *testing.T) {
a := newFakeBackend(t, `[`+node("h1", "2026-07-20T00:00:00Z")+`]`, `[]`)
b := newFakeBackend(t, `[]`, `[]`)
srv := newTestServer(testConfig(a.srv.URL, b.srv.URL, mergeStatic))
rec := doGet(t, srv.Handler(), nodesPath, "")
var raws []json.RawMessage
if err := json.Unmarshal(rec.Body.Bytes(), &raws); err != nil {
t.Fatal(err)
}
if len(raws) != 1 {
t.Fatalf("expected 1 node, got %d", len(raws))
}
var obj map[string]json.RawMessage
if err := json.Unmarshal(raws[0], &obj); err != nil {
t.Fatal(err)
}
for _, k := range []string{"certname", "report_timestamp", "latest_report_status", defaultSourceFact} {
if _, ok := obj[k]; !ok {
t.Errorf("field %q missing from stamped record: %s", k, raws[0])
}
}
}
func TestHandler_SourceDisabled(t *testing.T) {
a := newFakeBackend(t, `[`+node("h1", "2026-07-20T00:00:00Z")+`]`,
`[`+fact("h1", "role", "web", "")+`]`)
b := newFakeBackend(t, `[]`, `[]`)
cfg := testConfig(a.srv.URL, b.srv.URL, mergeStatic)
cfg.SourceFactEnabled = false
srv := newTestServer(cfg)
facts := doGet(t, srv.Handler(), factsPath, "")
if _, n := sourceValues(t, facts.Body.Bytes(), defaultSourceFact); n != 0 {
t.Errorf("disabled injection still produced %d records: %s", n, facts.Body.String())
}
nodes := doGet(t, srv.Handler(), nodesPath, "")
if got := nodeSources(t, nodes.Body.Bytes(), defaultSourceFact); len(got) != 0 {
t.Errorf("disabled injection still stamped nodes: %v", got)
}
}
func TestHandler_SourceFactNameOverride(t *testing.T) {
a := newFakeBackend(t, `[`+node("h1", "2026-07-20T00:00:00Z")+`]`,
`[`+fact("h1", "role", "web", "")+`]`)
b := newFakeBackend(t, `[]`, `[]`)
cfg := testConfig(a.srv.URL, b.srv.URL, mergeStatic)
cfg.SourceFact = "origin_pdb"
srv := newTestServer(cfg)
facts := doGet(t, srv.Handler(), factsPath, "")
got, n := sourceValues(t, facts.Body.Bytes(), "origin_pdb")
if n != 1 || got["h1"] != "a" {
t.Errorf("override name not honoured: %s", facts.Body.String())
}
if _, n := sourceValues(t, facts.Body.Bytes(), defaultSourceFact); n != 0 {
t.Errorf("default name still emitted alongside the override: %s", facts.Body.String())
}
nodes := doGet(t, srv.Handler(), nodesPath, "")
if got := nodeSources(t, nodes.Body.Bytes(), "origin_pdb"); got["h1"] != "a" {
t.Errorf("override name not honoured on /nodes: %v", got)
}
}
// An upstream fact of the configured name is replaced, not duplicated: pdbmux's
// own attribution is authoritative.
func TestHandler_UpstreamSourceFactOverridden(t *testing.T) {
a := newFakeBackend(t, `[`+node("h1", "2026-07-20T00:00:00Z")+`]`,
`[`+fact("h1", "role", "web", "")+`,`+fact("h1", defaultSourceFact, "stale-value", "")+`]`)
b := newFakeBackend(t, `[]`, `[]`)
srv := newTestServer(testConfig(a.srv.URL, b.srv.URL, mergeStatic))
rec := doGet(t, srv.Handler(), factsPath, "")
got, n := sourceValues(t, rec.Body.Bytes(), defaultSourceFact)
if n != 1 {
t.Fatalf("expected exactly 1 %s record, got %d: %s", defaultSourceFact, n, rec.Body.String())
}
if got["h1"] != "a" {
t.Errorf("upstream value survived: %v", got)
}
}
// The configured name means one thing on every query shape: an upstream fact of
// that name is dropped whether or not the query gate allows synthesis.
func TestHandler_UpstreamSourceFactSuppressedOnEveryGateState(t *testing.T) {
const upstream = "REAL-UPSTREAM-VALUE"
tests := []struct {
name string
query string
want string // synthetic value, or "" when the gate blocks injection
}{
{"injection on", "", "a"},
{"gated by extract", `["extract",["certname","name","value"],["=","certname","h1"]]`, ""},
{"gated by name filter", `["=","name","` + defaultSourceFact + `"]`, ""},
{"gated by nested extract", `["and",["=","certname","h1"],["extract",["certname"]]]`, ""},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
a := newFakeBackend(t, `[`+node("h1", "2026-07-20T00:00:00Z")+`]`,
`[`+fact("h1", "role", "web", "")+`,`+fact("h1", defaultSourceFact, upstream, "")+`]`)
b := newFakeBackend(t, `[]`, `[]`)
srv := newTestServer(testConfig(a.srv.URL, b.srv.URL, mergeStatic))
rec := doGet(t, srv.Handler(), factsPath, tc.query)
if rec.Code != http.StatusOK {
t.Fatalf("status %d: %s", rec.Code, rec.Body.String())
}
if strings.Contains(rec.Body.String(), upstream) {
t.Fatalf("upstream value survived: %s", rec.Body.String())
}
got, n := sourceValues(t, rec.Body.Bytes(), defaultSourceFact)
if tc.want == "" {
if n != 0 {
t.Errorf("gated query produced %d %s records: %s", n, defaultSourceFact, rec.Body.String())
}
} else if n != 1 || got["h1"] != tc.want {
t.Errorf("provenance = %v (%d records), want h1=%s: %s", got, n, tc.want, rec.Body.String())
}
if !strings.Contains(rec.Body.String(), `"role"`) {
t.Errorf("real facts were dropped: %s", rec.Body.String())
}
})
}
}
// Dropping an upstream record is invisible in the response, so it is logged —
// once per request, not once per record.
func TestHandler_SuppressedUpstreamFactLoggedOncePerRequest(t *testing.T) {
a := newFakeBackend(t,
`[`+node("h1", "2026-07-20T00:00:00Z")+`,`+node("h2", "2026-07-20T00:00:00Z")+`]`,
`[`+fact("h1", defaultSourceFact, "old-h1", "")+`,`+fact("h2", defaultSourceFact, "old-h2", "")+`]`)
b := newFakeBackend(t, `[]`, `[]`)
var buf bytes.Buffer
srv := NewServer(testConfig(a.srv.URL, b.srv.URL, mergeStatic), log.New(&buf, "", 0))
doGet(t, srv.Handler(), factsPath, "")
if n := strings.Count(buf.String(), defaultSourceFact); n != 1 {
t.Fatalf("expected 1 log line naming the fact, got %d: %s", n, buf.String())
}
if !strings.Contains(buf.String(), "dropped 2") {
t.Errorf("log does not report the number dropped: %s", buf.String())
}
}
// Disabled means untouched: an upstream fact of the configured name is served as
// the backend reported it.
func TestHandler_SourceDisabledKeepsUpstreamFact(t *testing.T) {
a := newFakeBackend(t, `[`+node("h1", "2026-07-20T00:00:00Z")+`]`,
`[`+fact("h1", defaultSourceFact, "upstream-value", "")+`]`)
b := newFakeBackend(t, `[]`, `[]`)
cfg := testConfig(a.srv.URL, b.srv.URL, mergeStatic)
cfg.SourceFactEnabled = false
srv := newTestServer(cfg)
rec := doGet(t, srv.Handler(), factsPath, "")
got, n := sourceValues(t, rec.Body.Bytes(), defaultSourceFact)
if n != 1 || got["h1"] != "upstream-value" {
t.Errorf("disabled injection altered the upstream fact: %s", rec.Body.String())
}
}
// Suppression matches a record's own name field, so rows from a projection that
// filters on name without returning it are not self-identifying and pass
// through. Pinned as a known limit of the guarantee, and documented as one.
func TestHandler_ProjectionWithoutNameColumnCarriesUpstreamValue(t *testing.T) {
a := newFakeBackend(t, `[`+node("h1", "2026-07-20T00:00:00Z")+`]`, `[]`)
b := newFakeBackend(t, `[]`, `[]`)
a.bodies[factsPath] = `[{"certname":"h1","value":"upstream-value"}]`
var buf bytes.Buffer
srv := NewServer(testConfig(a.srv.URL, b.srv.URL, mergeStatic), log.New(&buf, "", 0))
rec := doGet(t, srv.Handler(), factsPath,
`["extract",["certname","value"],["=","name","`+defaultSourceFact+`"]]`)
if rec.Code != http.StatusOK {
t.Fatalf("status %d: %s", rec.Code, rec.Body.String())
}
if got := rec.Body.String(); !strings.Contains(got, "upstream-value") {
t.Errorf("unidentifiable row was dropped: %s", got)
}
if strings.Contains(buf.String(), "dropped") {
t.Errorf("a row with no name field was counted as suppressed: %s", buf.String())
}
if _, n := sourceValues(t, rec.Body.Bytes(), defaultSourceFact); n != 0 {
t.Errorf("gated projection gained %d synthetic records: %s", n, rec.Body.String())
}
}
// Covers only that an aggregate /facts response gains no synthetic record and no
// rewritten row. It does not cover whether the aggregate rows are correct:
// /facts has no parseAggregate branch, so its rows take the certname merge
// instead of serveSummed and are not summed across backends.
func TestHandler_SourceNotInjectedOnFactsAggregate(t *testing.T) {
a := newFakeBackend(t, `[`+node("h1", "2026-07-20T00:00:00Z")+`]`, `[]`)
b := newFakeBackend(t, `[]`, `[]`)
a.bodies[factsPath] = `[{"count":3}]`
b.bodies[factsPath] = `[{"count":2}]`
srv := newTestServer(testConfig(a.srv.URL, b.srv.URL, mergeStatic))
rec := doGet(t, srv.Handler(), factsPath, `["extract",[["function","count"]]]`)
if _, n := sourceValues(t, rec.Body.Bytes(), defaultSourceFact); n != 0 {
t.Errorf("aggregate response gained %d synthetic records: %s", n, rec.Body.String())
}
// Every row must be one an upstream actually returned: an aggregate row pdbmux
// invented or rewrote would change the count the client sees.
var raws []json.RawMessage
if err := json.Unmarshal(rec.Body.Bytes(), &raws); err != nil {
t.Fatal(err)
}
upstream := []string{`{"count":3}`, `{"count":2}`}
for _, raw := range raws {
if !slices.Contains(upstream, string(raw)) {
t.Errorf("aggregate row %s is not an upstream row", raw)
}
}
}
// /nodes aggregates are summed rather than merged, so nothing may stamp them.
func TestHandler_NodesAggregateNotStamped(t *testing.T) {
a := newFakeBackend(t, `[]`, `[]`)
b := newFakeBackend(t, `[]`, `[]`)
a.bodies[nodesPath] = `[{"count":3}]`
b.bodies[nodesPath] = `[{"count":2}]`
srv := newTestServer(testConfig(a.srv.URL, b.srv.URL, mergeStatic))
rec := doGet(t, srv.Handler(), nodesPath, `["extract",[["function","count"]]]`)
if strings.Contains(rec.Body.String(), defaultSourceFact) {
t.Errorf("aggregate rows were stamped: %s", rec.Body.String())
}
if got := rec.Body.String(); !strings.Contains(got, `"count":5`) {
t.Errorf("count = %s, want the summed 5", got)
}
}
// A plain extract projects columns and skips the aggregate path, so the stamp
// must not add a key the client did not ask for.
func TestHandler_NodesProjectionNotStamped(t *testing.T) {
a := newFakeBackend(t, `[]`, `[]`)
b := newFakeBackend(t, `[]`, `[]`)
a.bodies[nodesPath] = `[{"certname":"h1"}]`
b.bodies[nodesPath] = `[]`
srv := newTestServer(testConfig(a.srv.URL, b.srv.URL, mergeStatic))
rec := doGet(t, srv.Handler(), nodesPath, `["extract",["certname"]]`)
if strings.Contains(rec.Body.String(), defaultSourceFact) {
t.Errorf("projection gained a stamp: %s", rec.Body.String())
}
}
// A query naming a specific fact asked for that fact only.
func TestHandler_SourceNotInjectedWhenNameFiltered(t *testing.T) {
a := newFakeBackend(t, `[`+node("h1", "2026-07-20T00:00:00Z")+`]`,
`[`+fact("h1", "role", "web", "")+`]`)
b := newFakeBackend(t, `[]`, `[]`)
srv := newTestServer(testConfig(a.srv.URL, b.srv.URL, mergeStatic))
for _, q := range []string{
`["=","name","role"]`,
`["and",["=","certname","h1"],["=","name","role"]]`,
`["=","name","` + defaultSourceFact + `"]`,
} {
rec := doGet(t, srv.Handler(), factsPath, q)
if _, n := sourceValues(t, rec.Body.Bytes(), defaultSourceFact); n != 0 {
t.Errorf("query %s gained %d synthetic records: %s", q, n, rec.Body.String())
}
}
}
// A certname filter selects nodes, not facts, so the full fact set — synthetic
// record included — is still the right answer.
func TestHandler_SourceInjectedWhenOnlyCertnameFiltered(t *testing.T) {
a := newFakeBackend(t, `[`+node("h1", "2026-07-20T00:00:00Z")+`]`,
`[`+fact("h1", "role", "web", "")+`]`)
b := newFakeBackend(t, `[]`, `[]`)
srv := newTestServer(testConfig(a.srv.URL, b.srv.URL, mergeStatic))
rec := doGet(t, srv.Handler(), factsPath, `["=","certname","h1"]`)
if _, n := sourceValues(t, rec.Body.Bytes(), defaultSourceFact); n != 1 {
t.Errorf("expected 1 synthetic record, got %d: %s", n, rec.Body.String())
}
}
func TestInjectable(t *testing.T) {
tests := []struct {
name string
query string
factEntity bool
want bool
}{
{"empty query", "", true, true},
{"certname filter", `["=","certname","h1"]`, true, true},
{"regex certname filter", `["~","certname","^web"]`, true, true},
{"name filter", `["=","name","os"]`, true, false},
{"name regex filter", `["~","name","^net"]`, true, false},
{"name under and", `["and",["=","certname","h1"],["=","name","os"]]`, true, false},
{"name under or", `["or",["=","name","os"],["=","name","kernel"]]`, true, false},
{"name under not", `["not",["=","name","os"]]`, true, false},
{"name in list", `["in","name",["array",["os"]]]`, true, false},
// A select_facts subquery narrows which nodes match; the outer response is
// still whole fact sets, so the synthetic record belongs in it.
{"name only inside subquery", `["in","certname",["extract",["certname"],["select_facts",["=","name","os"]]]]`, true, true},
{"top-level extract", `["extract",["certname","value"],["=","certname","h1"]]`, true, false},
{"aggregate extract", `["extract",[["function","count"]]]`, true, false},
// openvoxdb hands each boolean operand back to user-node->plan-node, which
// builds an extract node from it, so a nested extract can reshape the rows.
{"extract under and", `["and",["=","certname","h1"],["extract",["certname"]]]`, true, false},
{"extract under or", `["or",["extract",["certname"]],["=","certname","h1"]]`, true, false},
{"extract under not", `["not",["extract",["certname"]]]`, true, false},
{"extract nested two deep", `["and",["or",["extract",[["function","count"]]]]]`, true, false},
{"extract under from", `["from","facts",["extract",["certname"]]]`, true, false},
{"nested extract on nodes", `["and",["extract",["certname"]]]`, false, false},
// An extract inside an `in` operand projects the subquery, not the response.
{"extract under in stays injectable", `["in","certname",["extract",["certname"],["select_facts",["=","name","os"]]]]`, true, true},
// `subquery` is rewritten to ["in" ... ["extract" ... ["select_x" ...]]]
// before any plan node is built, so its operand is subquery-scoped too.
{"subquery operand stays injectable", `["and",["=","certname","h1"],["subquery","facts",["extract",["certname"],["=","name","os"]]]]`, true, true},
{"bare subquery stays injectable", `["subquery","facts",["extract",["certname"]]]`, true, true},
{"extract under select_facts stays injectable", `["and",["select_facts",["extract",["certname"]]]]`, true, true},
{"nodes name filter is not a fact filter", `["=","name","os"]`, false, true},
{"nodes extract", `["extract",["certname"]]`, false, false},
{"unparseable query", `not json`, true, false},
{"non-array query", `{"a":1}`, true, false},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
if got := injectable(tc.query, tc.factEntity); got != tc.want {
t.Errorf("injectable(%s, %v) = %v, want %v", tc.query, tc.factEntity, got, tc.want)
}
})
}
}
// pypuppetdb reads certname/name/value/environment by direct index, so a
// missing key is a KeyError there — all four are always present.
func TestSourceInjector_FactRecordHasEveryFactKey(t *testing.T) {
si := &sourceInjector{name: defaultSourceFact, inject: true}
for _, env := range []string{"production", ""} {
var obj map[string]json.RawMessage
if err := json.Unmarshal(si.factRecord("h1", "a", env), &obj); err != nil {
t.Fatal(err)
}
for _, k := range []string{"certname", "name", "value", "environment"} {
if _, ok := obj[k]; !ok {
t.Errorf("environment=%q: key %q missing from synthetic fact", env, k)
}
}
if len(obj) != 4 {
t.Errorf("synthetic fact has %d keys, want the 4 of a real fact record: %v", len(obj), obj)
}
}
}
// A nil injector is the disabled path and must leave every input untouched.
func TestSourceInjector_NilIsInert(t *testing.T) {
var si *sourceInjector
if si.claims(defaultSourceFact) {
t.Error("nil injector claims a fact name")
}
if si.injects() {
t.Error("nil injector injects")
}
si.logSuppressed(log.New(io.Discard, "", 0))
if si.factRecord("h1", "a", "production") != nil {
t.Error("nil injector produced a record")
}
raw := json.RawMessage(`{"certname":"h1"}`)
if got := si.stamp(raw, "a"); string(got) != string(raw) {
t.Errorf("nil injector rewrote %s to %s", raw, got)
}
}
// A response element that is not a JSON object cannot be stamped, and must be
// passed through rather than dropped or mangled.
func TestSourceInjector_StampNonObject(t *testing.T) {
si := &sourceInjector{name: defaultSourceFact, inject: true}
for _, raw := range []string{`"scalar"`, `[1,2]`, `null`} {
if got := si.stamp(json.RawMessage(raw), "a"); string(got) != raw {
t.Errorf("stamp(%s) = %s, want unchanged", raw, got)
}
}
}
// A gated injector suppresses without synthesising anything, on either endpoint.
func TestSourceInjector_GatedSuppressesButDoesNotInject(t *testing.T) {
si := &sourceInjector{name: defaultSourceFact}
if !si.claims(defaultSourceFact) {
t.Error("gated injector does not claim its own fact name")
}
if si.factRecord("h1", "a", "production") != nil {
t.Error("gated injector produced a record")
}
raw := json.RawMessage(`{"certname":"h1"}`)
if got := si.stamp(raw, "a"); string(got) != string(raw) {
t.Errorf("gated injector rewrote %s to %s", raw, got)
}
}
// The disabled path must return the backends' records verbatim, source fact
// included.
func TestMergeFacts_DisabledIsUntouched(t *testing.T) {
a := recs(t, "a", fact("h1", "role", "web-a", ""), fact("h1", defaultSourceFact, "upstream", ""))
merged := mergeFacts([]backendResult{a}, nil, nil)
got := factValues(t, merged)
want := []string{"h1:role=web-a", "h1:" + defaultSourceFact + "=upstream"}
if !slices.Equal(got, want) {
t.Errorf("merged = %v, want %v", got, want)
}
}
// A gated query still gets the upstream record removed, and nothing added.
func TestMergeFacts_GatedSuppressesUpstream(t *testing.T) {
a := recs(t, "a", fact("h1", "role", "web-a", ""), fact("h1", defaultSourceFact, "upstream", ""))
si := &sourceInjector{name: defaultSourceFact}
merged := mergeFacts([]backendResult{a}, nil, si)
got := factValues(t, merged)
if !slices.Equal(got, []string{"h1:role=web-a"}) {
t.Errorf("merged = %v, want only the real fact", got)
}
if si.suppressed != 1 {
t.Errorf("suppressed = %d, want 1", si.suppressed)
}
}
func TestMergeFacts_SourceOrderedAfterOwnersFacts(t *testing.T) {
a := recs(t, "a", fact("h1", "role", "web-a", ""), fact("h1", "kernel", "Linux", ""))
b := recs(t, "b", fact("h1", "role", "web-b", ""))
merged := mergeFacts([]backendResult{a, b}, nil, &sourceInjector{name: defaultSourceFact, inject: true})
got := factValues(t, merged)
want := []string{"h1:role=web-a", "h1:kernel=Linux", "h1:" + defaultSourceFact + "=a"}
if !slices.Equal(got, want) {
t.Errorf("merged = %v, want %v", got, want)
}
}