CI / ci (push) Successful in 1m56s
Co-authored-by: dev_agent <dev_agent@cubecraftlabs.com>
248 lines
8.3 KiB
Go
248 lines
8.3 KiB
Go
package recipes
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/db"
|
|
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/secretbox"
|
|
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/store"
|
|
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/migrations"
|
|
)
|
|
|
|
const testBraveKey = "BSA-secret-brave-key-9f3c"
|
|
|
|
func newTestSearchSettings(t *testing.T) *SearchSettingsService {
|
|
t.Helper()
|
|
return newTestSearchSettingsWithDefault(t, "")
|
|
}
|
|
|
|
// newTestSearchSettingsWithDefault is newTestSearchSettings with
|
|
// defaultBaseURL as the deployment's SearXNG URL.
|
|
func newTestSearchSettingsWithDefault(t *testing.T, defaultBaseURL string) *SearchSettingsService {
|
|
t.Helper()
|
|
st, err := store.Open(t.Context(), t.TempDir())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() {
|
|
if err := st.Close(); err != nil {
|
|
t.Error(err)
|
|
}
|
|
})
|
|
if err := store.Migrate(t.Context(), st.Writer, migrations.FS); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
key := make([]byte, secretbox.KeySize)
|
|
rand.Read(key)
|
|
box, err := secretbox.New(base64.StdEncoding.EncodeToString(key))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
svc, err := NewSearchSettingsService(st, box, defaultBaseURL)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return svc
|
|
}
|
|
|
|
func ptr(s string) *string { return &s }
|
|
|
|
// noKey fails the test when v, marshalled as the API would, holds the
|
|
// Brave key.
|
|
func noKey(t *testing.T, what string, v any) {
|
|
t.Helper()
|
|
b, err := json.Marshal(v)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.Contains(string(b), testBraveKey) || strings.Contains(string(b), testBraveKey[:12]) {
|
|
t.Errorf("%s holds the Brave key: %s", what, b)
|
|
}
|
|
}
|
|
|
|
func TestSearchSettingsUpdate(t *testing.T) {
|
|
t.Parallel()
|
|
svc := newTestSearchSettings(t)
|
|
got, err := svc.Get(t.Context())
|
|
if err != nil || got != (SearchSettings{}) {
|
|
t.Fatalf("Get before any update = %+v, %v; want every field unset", got, err)
|
|
}
|
|
if _, err := svc.Provider(t.Context()); !errors.Is(err, ErrSearchNotConfigured) {
|
|
t.Errorf("Provider before any update error = %v, want ErrSearchNotConfigured", err)
|
|
}
|
|
|
|
for name, patch := range map[string]SearchSettingsPatch{
|
|
"no provider": {BaseURL: ptr("http://searxng:8080")},
|
|
"unknown provider": {Provider: ptr("google")},
|
|
"searxng without URL": {Provider: ptr("searxng")},
|
|
"bad URL": {Provider: ptr("searxng"), BaseURL: ptr("ftp://searxng")},
|
|
"brave without key": {Provider: ptr("brave")},
|
|
"short key": {Provider: ptr("brave"), APIKey: ptr("abc12")},
|
|
"key with space": {Provider: ptr("brave"), APIKey: ptr("abc 12345678")},
|
|
} {
|
|
var invalid *SearchSettingsError
|
|
if _, err := svc.Update(t.Context(), patch); !errors.As(err, &invalid) {
|
|
t.Errorf("%s: error = %v, want *SearchSettingsError", name, err)
|
|
}
|
|
}
|
|
|
|
got, err = svc.Update(t.Context(), SearchSettingsPatch{Provider: ptr("searxng"), BaseURL: ptr("http://searxng:8080")})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if *got.Provider != "searxng" || *got.BaseURL != "http://searxng:8080" || got.APIKeySet || got.APIKeyLast4 != nil || got.UpdatedAt == nil {
|
|
t.Errorf("SearXNG settings = %+v", got)
|
|
}
|
|
|
|
got, err = svc.Update(t.Context(), SearchSettingsPatch{Provider: ptr("brave"), APIKey: ptr(testBraveKey)})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if *got.Provider != "brave" || !got.APIKeySet || *got.APIKeyLast4 != "9f3c" || *got.BaseURL != "http://searxng:8080" {
|
|
t.Errorf("Brave settings = %+v", got)
|
|
}
|
|
noKey(t, "Update result", got)
|
|
got, err = svc.Get(t.Context())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
noKey(t, "Get result", got)
|
|
row, err := db.New(svc.st.Reader).GetRecipeSearchSettings(t.Context())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.Contains(string(row.ApiKeyCt), testBraveKey) {
|
|
t.Error("Brave key stored in plain text")
|
|
}
|
|
|
|
if _, err := svc.Update(t.Context(), SearchSettingsPatch{APIKey: ptr("")}); err == nil {
|
|
t.Error("removing the key while Brave is chosen succeeded")
|
|
}
|
|
got, err = svc.Update(t.Context(), SearchSettingsPatch{Provider: ptr("searxng"), APIKey: ptr("")})
|
|
if err != nil || got.APIKeySet || got.APIKeyLast4 != nil {
|
|
t.Errorf("switch to SearXNG removing the key = %+v, %v", got, err)
|
|
}
|
|
}
|
|
|
|
func TestSearchSettingsDefaultBaseURL(t *testing.T) {
|
|
t.Parallel()
|
|
svc := newTestSearchSettingsWithDefault(t, "http://searxng:8080")
|
|
got, err := svc.Get(t.Context())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got.DefaultBaseURL == nil || *got.DefaultBaseURL != "http://searxng:8080" || got.BaseURL != nil || got.Provider != nil {
|
|
t.Errorf("Get before any update = %+v, want only default_base_url set", got)
|
|
}
|
|
if _, err := svc.Provider(t.Context()); !errors.Is(err, ErrSearchNotConfigured) {
|
|
t.Errorf("Provider with only a default error = %v, want ErrSearchNotConfigured", err)
|
|
}
|
|
|
|
// The default is a suggestion: SearXNG still needs a saved URL.
|
|
if _, err := svc.Update(t.Context(), SearchSettingsPatch{Provider: ptr("searxng")}); err == nil {
|
|
t.Error("choosing SearXNG without a URL succeeded with a default set")
|
|
}
|
|
|
|
got, err = svc.Update(t.Context(), SearchSettingsPatch{Provider: ptr("searxng"), BaseURL: ptr("https://search.example")})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if *got.BaseURL != "https://search.example" || got.DefaultBaseURL == nil || *got.DefaultBaseURL != "http://searxng:8080" {
|
|
t.Errorf("Update result = %+v, want the saved URL beside the default", got)
|
|
}
|
|
got, err = svc.Get(t.Context())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if *got.BaseURL != "https://search.example" {
|
|
t.Errorf("Get base_url = %q, want the saved URL, not the default", *got.BaseURL)
|
|
}
|
|
row, err := db.New(svc.st.Reader).GetRecipeSearchSettings(t.Context())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if row.BaseUrl.String != "https://search.example" {
|
|
t.Errorf("stored base_url = %q", row.BaseUrl.String)
|
|
}
|
|
}
|
|
|
|
func TestNewSearchSettingsServiceRejectsBadDefault(t *testing.T) {
|
|
t.Parallel()
|
|
st, err := store.Open(t.Context(), t.TempDir())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() {
|
|
if err := st.Close(); err != nil {
|
|
t.Error(err)
|
|
}
|
|
})
|
|
key := make([]byte, secretbox.KeySize)
|
|
rand.Read(key)
|
|
box, err := secretbox.New(base64.StdEncoding.EncodeToString(key))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, u := range []string{"searxng:8080", "ftp://searxng", "http://user:pw@searxng:8080"} {
|
|
if _, err := NewSearchSettingsService(st, box, u); err == nil {
|
|
t.Errorf("NewSearchSettingsService(%q) succeeded", u)
|
|
} else if strings.Contains(err.Error(), "pw") {
|
|
t.Errorf("error for %q echoes the URL: %v", u, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSearchSettingsTestSearch(t *testing.T) {
|
|
t.Parallel()
|
|
searx := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Query().Get("q") != SearchTestQuery+" recipe" {
|
|
t.Errorf("SearXNG query = %q", r.URL.Query().Get("q"))
|
|
}
|
|
w.Write([]byte(`{"results":[{"url":"https://a.example/1"},{"url":"https://b.example/2"}]}`))
|
|
}))
|
|
t.Cleanup(searx.Close)
|
|
svc := newTestSearchSettings(t)
|
|
if _, err := svc.TestSearch(t.Context()); !errors.Is(err, ErrSearchNotConfigured) {
|
|
t.Errorf("TestSearch unconfigured error = %v, want ErrSearchNotConfigured", err)
|
|
}
|
|
if _, err := svc.Update(t.Context(), SearchSettingsPatch{Provider: ptr("searxng"), BaseURL: ptr(searx.URL)}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if n, err := svc.TestSearch(t.Context()); err != nil || n != 2 {
|
|
t.Errorf("TestSearch = %d, %v; want 2", n, err)
|
|
}
|
|
|
|
// Brave refuses the key; the error must not carry it.
|
|
brave := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Header.Get("X-Subscription-Token") != testBraveKey {
|
|
t.Errorf("Brave token = %q", r.Header.Get("X-Subscription-Token"))
|
|
}
|
|
http.Error(w, "bad key "+testBraveKey, http.StatusUnauthorized)
|
|
}))
|
|
t.Cleanup(brave.Close)
|
|
svc.newProvider = func(s db.RecipeSearchSetting, box *secretbox.Box) (SearchProvider, error) {
|
|
p, err := NewSearchProvider(s, box)
|
|
if b, ok := p.(*Brave); ok {
|
|
b.endpoint = brave.URL
|
|
}
|
|
return p, err
|
|
}
|
|
if _, err := svc.Update(t.Context(), SearchSettingsPatch{Provider: ptr("brave"), APIKey: ptr(testBraveKey)}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, err := svc.TestSearch(t.Context())
|
|
if !errors.Is(err, ErrSearchRefused) {
|
|
t.Fatalf("TestSearch error = %v, want ErrSearchRefused", err)
|
|
}
|
|
if strings.Contains(err.Error(), testBraveKey) {
|
|
t.Errorf("TestSearch error holds the Brave key: %v", err)
|
|
}
|
|
}
|