package secrets import ( "context" "encoding/json" "net/http" "net/http/httptest" "strings" "testing" ) func testStore(t *testing.T, h http.Handler) *Store { t.Helper() srv := httptest.NewServer(h) t.Cleanup(srv.Close) t.Setenv("BAO_ADDR", srv.URL) t.Setenv("BAO_TOKEN", "test-token") s, err := NewFromEnv("hall-of-helix") if err != nil { t.Fatal(err) } return s } // The salt rule. Rotating it silently invalidates every longitudinal comparison // the interface has made, with no visible failure -- so the tool must have no // path that replaces an existing one. func TestCreateIfAbsentNeverOverwrites(t *testing.T) { var writes int existing := map[string]string{"salt": "the-original"} s := testStore(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodPost { writes++ w.WriteHeader(http.StatusOK) return } json.NewEncoder(w).Encode(map[string]any{"data": map[string]any{"data": existing}}) })) created, err := s.CreateIfAbsent(context.Background(), KeyRedactionSalt, map[string]string{"salt": "a-replacement"}) if err != nil { t.Fatal(err) } if created { t.Error("reported creating a salt that already existed") } if writes != 0 { t.Errorf("wrote over an existing salt (%d writes)", writes) } } func TestCreateIfAbsentWritesWhenMissing(t *testing.T) { var got map[string]any s := testStore(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodPost { json.NewDecoder(r.Body).Decode(&got) w.WriteHeader(http.StatusOK) return } w.WriteHeader(http.StatusNotFound) })) created, err := s.CreateIfAbsent(context.Background(), KeyRedactionSalt, map[string]string{"salt": "fresh"}) if err != nil { t.Fatal(err) } if !created { t.Fatal("did not create a salt when none existed") } if got["data"].(map[string]any)["salt"] != "fresh" { t.Errorf("wrote %v", got) } } // A missing path is an ordinary first-run state, not an error. func TestGetMissingIsNotAnError(t *testing.T) { s := testStore(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotFound) })) _, found, err := s.Get(context.Background(), KeyBotToken) if err != nil || found { t.Fatalf("found=%v err=%v", found, err) } } // Ref is printed in plans and errors, so it must name a location and never // carry a value. func TestRefIsSafeToPrint(t *testing.T) { s := testStore(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(map[string]any{ "data": map[string]any{"data": map[string]string{"token": "123:SECRET"}}}) })) ref := s.Ref(KeyBotToken) if strings.Contains(ref, "SECRET") || strings.Contains(ref, "test-token") { t.Fatalf("Ref leaked a secret: %q", ref) } if !strings.Contains(ref, "hall-of-helix") || !strings.Contains(ref, KeyBotToken) { t.Errorf("Ref should locate the secret: %q", ref) } } // Errors name the path that failed, never what was at it. func TestErrorsDoNotCarryValues(t *testing.T) { s := testStore(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusInternalServerError) })) _, _, err := s.Get(context.Background(), KeyOperatorSession) if err == nil { t.Fatal("expected an error") } if strings.Contains(err.Error(), "test-token") { t.Fatalf("error leaked the bao token: %v", err) } }