mirror of
https://github.com/navidrome/navidrome.git
synced 2026-10-09 10:57:08 +02:00
148 lines
4.5 KiB
Go
148 lines
4.5 KiB
Go
|
|
package nativeapi
|
||
|
|
|
||
|
|
import (
|
||
|
|
"encoding/json"
|
||
|
|
"net/http"
|
||
|
|
"net/http/httptest"
|
||
|
|
"strings"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"github.com/go-chi/chi/v5"
|
||
|
|
"github.com/navidrome/navidrome/conf"
|
||
|
|
"github.com/navidrome/navidrome/conf/configtest"
|
||
|
|
"github.com/navidrome/navidrome/core/quickconnect"
|
||
|
|
"github.com/navidrome/navidrome/model"
|
||
|
|
"github.com/navidrome/navidrome/model/request"
|
||
|
|
. "github.com/onsi/ginkgo/v2"
|
||
|
|
. "github.com/onsi/gomega"
|
||
|
|
)
|
||
|
|
|
||
|
|
var _ = Describe("Quick Connect endpoints", func() {
|
||
|
|
var (
|
||
|
|
qc quickconnect.QuickConnect
|
||
|
|
pending quickconnect.Request
|
||
|
|
user = model.User{ID: "u1", UserName: "alice"}
|
||
|
|
)
|
||
|
|
|
||
|
|
BeforeEach(func() {
|
||
|
|
qc = quickconnect.New()
|
||
|
|
var err error
|
||
|
|
pending, err = qc.Initiate(quickconnect.Device{ID: "dev-1", Name: "Pixel 7", App: "Finamp", AppVersion: "1.0.0"})
|
||
|
|
Expect(err).ToNot(HaveOccurred())
|
||
|
|
})
|
||
|
|
|
||
|
|
asUser := func(r *http.Request) *http.Request {
|
||
|
|
return r.WithContext(request.WithUser(r.Context(), user))
|
||
|
|
}
|
||
|
|
decode := func(w *httptest.ResponseRecorder) quickConnectDevice {
|
||
|
|
var res quickConnectDevice
|
||
|
|
Expect(json.Unmarshal(w.Body.Bytes(), &res)).To(Succeed())
|
||
|
|
return res
|
||
|
|
}
|
||
|
|
finamp := quickConnectDevice{AppName: "Finamp", AppVersion: "1.0.0", DeviceName: "Pixel 7"}
|
||
|
|
|
||
|
|
Describe("GET /quickconnect", func() {
|
||
|
|
lookup := func(code string) *httptest.ResponseRecorder {
|
||
|
|
w := httptest.NewRecorder()
|
||
|
|
lookupQuickConnect(qc)(w, asUser(httptest.NewRequest("GET", "/quickconnect?code="+code, nil)))
|
||
|
|
return w
|
||
|
|
}
|
||
|
|
|
||
|
|
It("describes the device waiting for the code", func() {
|
||
|
|
w := lookup(pending.Code)
|
||
|
|
Expect(w.Code).To(Equal(http.StatusOK))
|
||
|
|
Expect(decode(w)).To(Equal(finamp))
|
||
|
|
})
|
||
|
|
|
||
|
|
It("does not approve the code", func() {
|
||
|
|
lookup(pending.Code)
|
||
|
|
got, _ := qc.Status(pending.Secret)
|
||
|
|
Expect(got.Authorized()).To(BeFalse())
|
||
|
|
})
|
||
|
|
|
||
|
|
It("returns 404 for an unknown code", func() {
|
||
|
|
Expect(lookup("000000").Code).To(Equal(http.StatusNotFound))
|
||
|
|
})
|
||
|
|
|
||
|
|
It("returns 409 for a code that is already approved", func() {
|
||
|
|
_, _ = qc.Authorize(pending.Code, "someone")
|
||
|
|
Expect(lookup(pending.Code).Code).To(Equal(http.StatusConflict))
|
||
|
|
})
|
||
|
|
})
|
||
|
|
|
||
|
|
Describe("POST /quickconnect/authorize", func() {
|
||
|
|
authorize := func(body string) *httptest.ResponseRecorder {
|
||
|
|
w := httptest.NewRecorder()
|
||
|
|
authorizeQuickConnect(qc)(w, asUser(httptest.NewRequest("POST", "/quickconnect/authorize", strings.NewReader(body))))
|
||
|
|
return w
|
||
|
|
}
|
||
|
|
|
||
|
|
It("approves the code for the signed-in user", func() {
|
||
|
|
w := authorize(`{"code":"` + pending.Code + `"}`)
|
||
|
|
Expect(w.Code).To(Equal(http.StatusOK))
|
||
|
|
Expect(decode(w)).To(Equal(finamp))
|
||
|
|
got, _ := qc.Status(pending.Secret)
|
||
|
|
Expect(got.UserID).To(Equal(user.ID))
|
||
|
|
})
|
||
|
|
|
||
|
|
It("returns 404 for an unknown code", func() {
|
||
|
|
Expect(authorize(`{"code":"000000"}`).Code).To(Equal(http.StatusNotFound))
|
||
|
|
})
|
||
|
|
|
||
|
|
It("returns 409 for a code that is already approved", func() {
|
||
|
|
_, _ = qc.Authorize(pending.Code, "someone")
|
||
|
|
Expect(authorize(`{"code":"` + pending.Code + `"}`).Code).To(Equal(http.StatusConflict))
|
||
|
|
})
|
||
|
|
|
||
|
|
It("returns 400 for a bad body", func() {
|
||
|
|
Expect(authorize(`nope`).Code).To(Equal(http.StatusBadRequest))
|
||
|
|
})
|
||
|
|
})
|
||
|
|
|
||
|
|
Describe("route registration", func() {
|
||
|
|
serve := func() int {
|
||
|
|
r := chi.NewRouter()
|
||
|
|
(&Router{quickConnect: qc}).addQuickConnectRoute(r)
|
||
|
|
w := httptest.NewRecorder()
|
||
|
|
r.ServeHTTP(w, asUser(httptest.NewRequest("GET", "/quickconnect?code="+pending.Code, nil)))
|
||
|
|
return w.Code
|
||
|
|
}
|
||
|
|
|
||
|
|
BeforeEach(func() {
|
||
|
|
DeferCleanup(configtest.SetupConfig())
|
||
|
|
conf.Server.Jellyfin.Enabled = true
|
||
|
|
conf.Server.Jellyfin.QuickConnect = true
|
||
|
|
})
|
||
|
|
|
||
|
|
It("mounts the routes when Jellyfin Quick Connect is on", func() {
|
||
|
|
Expect(serve()).To(Equal(http.StatusOK))
|
||
|
|
})
|
||
|
|
|
||
|
|
It("rate-limits code attempts when a login limit is configured", func() {
|
||
|
|
conf.Server.AuthRequestLimit = 2
|
||
|
|
conf.Server.AuthWindowLength = time.Minute
|
||
|
|
r := chi.NewRouter()
|
||
|
|
(&Router{quickConnect: qc}).addQuickConnectRoute(r)
|
||
|
|
attempt := func() int {
|
||
|
|
w := httptest.NewRecorder()
|
||
|
|
req := httptest.NewRequest("GET", "/quickconnect?code=000000", nil)
|
||
|
|
req.RemoteAddr = "10.0.0.1:1234"
|
||
|
|
r.ServeHTTP(w, asUser(req))
|
||
|
|
return w.Code
|
||
|
|
}
|
||
|
|
Expect(attempt()).To(Equal(http.StatusNotFound))
|
||
|
|
Expect(attempt()).To(Equal(http.StatusNotFound))
|
||
|
|
Expect(attempt()).To(Equal(http.StatusTooManyRequests))
|
||
|
|
})
|
||
|
|
|
||
|
|
It("skips the routes when the Jellyfin API is off", func() {
|
||
|
|
conf.Server.Jellyfin.Enabled = false
|
||
|
|
Expect(serve()).To(Equal(http.StatusNotFound))
|
||
|
|
})
|
||
|
|
|
||
|
|
It("skips the routes when Quick Connect is off", func() {
|
||
|
|
conf.Server.Jellyfin.QuickConnect = false
|
||
|
|
Expect(serve()).To(Equal(http.StatusNotFound))
|
||
|
|
})
|
||
|
|
})
|
||
|
|
})
|