package e2e
import (
"net/http"
"net/http/httptest"
"strings"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/conf/configtest"
"github.com/navidrome/navidrome/server/jellyfin/dto"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("QuickConnect", func() {
BeforeEach(func() {
setupTestDB()
DeferCleanup(configtest.SetupConfig())
conf.Server.Jellyfin.QuickConnect = true
})
clientReq := func(deviceID, method, path, body string) *httptest.ResponseRecorder {
w := httptest.NewRecorder()
r := httptest.NewRequest(method, path, strings.NewReader(body))
r.Header.Set("Authorization", `MediaBrowser Client="Finamp", Device="Pixel 7", DeviceId="`+deviceID+`", Version="1.0"`)
r.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, r)
return w
}
initiate := func() dto.QuickConnectResult {
var pending dto.QuickConnectResult
parseInto(clientReq("new-device", "POST", "/QuickConnect/Initiate", ""), &pending)
Expect(pending.Authenticated).To(BeFalse())
return pending
}
redeem := func(deviceID, secret string) *httptest.ResponseRecorder {
return clientReq(deviceID, "POST", "/Users/AuthenticateWithQuickConnect", `{"Secret":"`+secret+`"}`)
}
It("signs a new client in with a code approved by a signed-in user", func() {
Expect(rawReq("GET", "/QuickConnect/Enabled", "").Body.String()).To(MatchJSON("true"))
pending := initiate()
Expect(postAs(regularUser, "/QuickConnect/Authorize?Code="+pending.Code, "").Code).To(Equal(http.StatusOK))
var status dto.QuickConnectResult
parseInto(rawReq("GET", "/QuickConnect/Connect?Secret="+pending.Secret, ""), &status)
Expect(status.Authenticated).To(BeTrue())
w := redeem("other-device", pending.Secret)
Expect(w.Code).To(Equal(http.StatusOK))
var res dto.AuthenticationResult
parseInto(w, &res)
Expect(res.User.Name).To(Equal(regularUser.UserName))
Expect(res.SessionInfo).ToNot(BeNil())
Expect(res.SessionInfo.DeviceId).To(Equal("other-device"))
r := httptest.NewRequest("GET", "/Users/Me", nil)
r.Header.Set("X-Emby-Token", res.AccessToken)
me := httptest.NewRecorder()
router.ServeHTTP(me, r)
Expect(me.Code).To(Equal(http.StatusOK))
Expect(redeem("new-device", pending.Secret).Code).To(Equal(http.StatusNotFound))
})
It("lets an admin approve a code for another user", func() {
pending := initiate()
Expect(post("/QuickConnect/Authorize?Code="+pending.Code+"&UserId="+enc(regularUser.ID), "").Code).
To(Equal(http.StatusOK))
var res dto.AuthenticationResult
parseInto(redeem("new-device", pending.Secret), &res)
Expect(res.User.Name).To(Equal(regularUser.UserName))
})
It("requires authentication to approve a code", func() {
pending := initiate()
Expect(rawReq("POST", "/QuickConnect/Authorize?Code="+pending.Code, "").Code).To(Equal(http.StatusUnauthorized))
})
It("answers 401 on every Quick Connect call when disabled", func() {
conf.Server.Jellyfin.QuickConnect = false
Expect(rawReq("GET", "/QuickConnect/Enabled", "").Body.String()).To(MatchJSON("false"))
Expect(clientReq("new-device", "POST", "/QuickConnect/Initiate", "").Code).To(Equal(http.StatusUnauthorized))
Expect(rawReq("GET", "/QuickConnect/Connect?Secret=x", "").Code).To(Equal(http.StatusUnauthorized))
Expect(post("/QuickConnect/Authorize?Code=123456", "").Code).To(Equal(http.StatusUnauthorized))
Expect(redeem("new-device", "x").Code).To(Equal(http.StatusUnauthorized))
})
})