package api import ( "context" "encoding/json" "net/http" "net/http/httptest" "os" "testing" "github.com/go-chi/chi/v5" "git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq" ) func newMeSubsonicPasswordRouter(h *handlers) chi.Router { r := chi.NewRouter() r.Get("/api/me/subsonic-password", h.handleGetMySubsonicPassword) r.Post("/api/me/subsonic-password", h.handleGenerateMySubsonicPassword) r.Delete("/api/me/subsonic-password", h.handleClearMySubsonicPassword) return r } func readSubsonicPassword(t *testing.T, h *handlers, user dbq.User) *string { t.Helper() var pw *string if err := h.pool.QueryRow(context.Background(), "SELECT subsonic_password FROM users WHERE id = $1", user.ID).Scan(&pw); err != nil { t.Fatalf("read subsonic_password: %v", err) } return pw } func TestSubsonicPassword_GenerateIsRandomAndNotTheLoginPassword(t *testing.T) { if os.Getenv("MINSTREL_TEST_DATABASE_URL") == "" { t.Skip("MINSTREL_TEST_DATABASE_URL not set") } h, pool := testHandlers(t) user := seedUser(t, pool, "sspw1", "login-pw", false) router := newMeSubsonicPasswordRouter(h) generate := func() string { req := withUser(httptest.NewRequest(http.MethodPost, "/api/me/subsonic-password", nil), user) rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status = %d, want 200; body=%s", rec.Code, rec.Body.String()) } var resp subsonicPasswordResp if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil { t.Fatalf("decode: %v", err) } return resp.Password } first := generate() if len(first) != 24 { t.Errorf("password length = %d, want 24", len(first)) } if first == "login-pw" { t.Errorf("generated password equals the login password") } if got := readSubsonicPassword(t, h, user); got == nil || *got != first { t.Errorf("stored = %v, want the returned password", got) } second := generate() if second == first { t.Errorf("regenerate returned the same password") } if got := readSubsonicPassword(t, h, user); got == nil || *got != second { t.Errorf("stored after regenerate = %v, want the new password", got) } } func TestSubsonicPassword_StatusAndClear(t *testing.T) { if os.Getenv("MINSTREL_TEST_DATABASE_URL") == "" { t.Skip("MINSTREL_TEST_DATABASE_URL not set") } h, pool := testHandlers(t) user := seedUser(t, pool, "sspw2", "login-pw", false) router := newMeSubsonicPasswordRouter(h) status := func(u dbq.User) bool { req := withUser(httptest.NewRequest(http.MethodGet, "/api/me/subsonic-password", nil), u) rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("GET status = %d, want 200; body=%s", rec.Code, rec.Body.String()) } var resp subsonicPasswordStatusResp if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil { t.Fatalf("decode: %v", err) } return resp.Enabled } if status(user) { t.Errorf("enabled = true for a new account, want false") } pw := "set-by-test" user.SubsonicPassword = &pw if !status(user) { t.Errorf("enabled = false with a password set, want true") } // The status response never carries the value itself. req := withUser(httptest.NewRequest(http.MethodGet, "/api/me/subsonic-password", nil), user) rec := httptest.NewRecorder() router.ServeHTTP(rec, req) var raw map[string]any if err := json.Unmarshal(rec.Body.Bytes(), &raw); err != nil { t.Fatalf("decode: %v", err) } if _, has := raw["password"]; has { t.Errorf("GET response includes the password: %s", rec.Body.String()) } if err := dbq.New(pool).SetSubsonicPassword(context.Background(), dbq.SetSubsonicPasswordParams{ ID: user.ID, SubsonicPassword: &pw, }); err != nil { t.Fatalf("seed subsonic_password: %v", err) } req = withUser(httptest.NewRequest(http.MethodDelete, "/api/me/subsonic-password", nil), user) rec = httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusNoContent { t.Fatalf("DELETE status = %d, want 204; body=%s", rec.Code, rec.Body.String()) } if got := readSubsonicPassword(t, h, user); got != nil { t.Errorf("stored after clear = %q, want NULL", *got) } }