package federalsignal import ( "context" "net/http" "net/http/httptest" "strings" "testing" "time" "github.com/missionworkforce/missionmatch/internal/platform/storage" ) const fakeKey = "FAKE_SAM_KEY_do_not_log_0123456789" // newTestServer serves a plain text body at /desc and an HTML body at /res. It records, per path, // whether the request carried the api_key query parameter, so the test can assert the key is sent to // SAM and never elsewhere. func newTestServer(t *testing.T, sawKey map[string]bool) *httptest.Server { t.Helper() mux := http.NewServeMux() mux.HandleFunc("/desc.txt", func(w http.ResponseWriter, r *http.Request) { sawKey["/desc.txt"] = r.URL.Query().Get("api_key") != "" w.Header().Set("Content-Type", "text/plain") _, _ = w.Write([]byte("SIGINT Analyst x3 and one ISSO")) }) mux.HandleFunc("/res.html", func(w http.ResponseWriter, r *http.Request) { sawKey["/res.html"] = r.URL.Query().Get("api_key") != "" w.Header().Set("Content-Type", "text/html") _, _ = w.Write([]byte("

COMSEC Custodian needed

")) }) mux.HandleFunc("/broken.txt", func(w http.ResponseWriter, r *http.Request) { http.Error(w, "boom", http.StatusInternalServerError) }) srv := httptest.NewServer(mux) t.Cleanup(srv.Close) return srv } func TestFetchSolicitationText(t *testing.T) { sawKey := map[string]bool{} srv := newTestServer(t, sawKey) // The host is treated as a SAM host for the test so the key is appended; see WithSAMHosts. host := strings.TrimPrefix(srv.URL, "http://") f := New(fakeKey, WithSAMHosts(host), WithTimeout(5*time.Second), WithMaxRetries(1)) got, err := f.FetchSolicitationText(context.Background(), srv.URL+"/desc.txt", []string{srv.URL + "/res.html"}) if err != nil { t.Fatalf("FetchSolicitationText: %v", err) } if !strings.Contains(got, "SIGINT Analyst") { t.Fatalf("missing description text: %q", got) } if !strings.Contains(got, "COMSEC Custodian") { t.Fatalf("missing resource link text: %q", got) } // The api_key must NEVER appear in the returned text. if strings.Contains(got, fakeKey) { t.Fatalf("api_key leaked into returned text: %q", got) } if strings.Contains(got, "api_key") { t.Fatalf("api_key parameter name leaked into returned text: %q", got) } // The key WAS sent to the SAM host on both URLs. if !sawKey["/desc.txt"] || !sawKey["/res.html"] { t.Fatalf("api_key was not sent to the SAM host: %+v", sawKey) } } // TestFetchBestEffortOnError proves one failing attachment does not fail the whole record: the good // attachment's text still comes back. func TestFetchBestEffortOnError(t *testing.T) { sawKey := map[string]bool{} srv := newTestServer(t, sawKey) host := strings.TrimPrefix(srv.URL, "http://") f := New(fakeKey, WithSAMHosts(host), WithTimeout(5*time.Second), WithMaxRetries(0)) got, err := f.FetchSolicitationText(context.Background(), srv.URL+"/desc.txt", []string{srv.URL + "/broken.txt"}) // 500s every time if err != nil { t.Fatalf("a per attachment failure must not fail the record: %v", err) } if !strings.Contains(got, "SIGINT Analyst") { t.Fatalf("good attachment text missing after a sibling failed: %q", got) } } // TestFetchKeyNotSentToNonSAMHost proves the api_key is [credential removed] ONLY to SAM hosts, never to a foreign // resource link, so the secret cannot leak to a third party. func TestFetchKeyNotSentToNonSAMHost(t *testing.T) { foreignSaw := false foreign := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Query().Get("api_key") != "" { foreignSaw = true } _, _ = w.Write([]byte("

External Role

")) })) defer foreign.Close() samSaw := map[string]bool{} sam := newTestServer(t, samSaw) samHost := strings.TrimPrefix(sam.URL, "http://") // Only the SAM server's host is registered, the foreign host is not. f := New(fakeKey, WithSAMHosts(samHost), WithTimeout(5*time.Second)) got, err := f.FetchSolicitationText(context.Background(), sam.URL+"/desc.txt", []string{foreign.URL}) // a non SAM resource link if err != nil { t.Fatalf("FetchSolicitationText: %v", err) } if foreignSaw { t.Fatal("api_key was sent to a non SAM host") } if !strings.Contains(got, "External Role") { t.Fatalf("foreign resource text should still be fetched (without the key): %q", got) } } // TestFetchStoresRawBytes proves the optional Storer receives the raw bytes under a deterministic key, // and that passing a nil Storer skips storage without error. func TestFetchStoresRawBytes(t *testing.T) { sawKey := map[string]bool{} srv := newTestServer(t, sawKey) host := strings.TrimPrefix(srv.URL, "http://") mem := storage.NewMemory() f := New(fakeKey, WithSAMHosts(host), WithStore(mem, "fedsig")) descURL := srv.URL + "/desc.txt" if _, err := f.FetchSolicitationText(context.Background(), descURL, nil); err != nil { t.Fatalf("FetchSolicitationText: %v", err) } // The deterministic key is the prefix plus the sha256 of the URL, so two runs of the same URL collide // on one object rather than piling up duplicates. key := rawKey("fedsig", descURL) raw, err := mem.Get(context.Background(), key) if err != nil { t.Fatalf("expected raw bytes stored under %q: %v", key, err) } if !strings.Contains(string(raw), "SIGINT Analyst") { t.Fatalf("stored bytes are not the body: %q", string(raw)) } // The stored key MUST NOT contain the api_key. if strings.Contains(key, fakeKey) { t.Fatalf("api_key leaked into the storage key: %q", key) } // A nil Storer is allowed and simply skips storage. fNil := New(fakeKey, WithSAMHosts(host)) if _, err := fNil.FetchSolicitationText(context.Background(), descURL, nil); err != nil { t.Fatalf("nil Storer should be allowed: %v", err) } } // TestFetchEmptyInputs proves no URLs yields empty text and no error. func TestFetchEmptyInputs(t *testing.T) { f := New(fakeKey) got, err := f.FetchSolicitationText(context.Background(), "", nil) if err != nil { t.Fatalf("empty inputs should not error: %v", err) } if got != "" { t.Fatalf("empty inputs should yield empty text, got %q", got) } }