misaki/server/internal_stream_test.go
q e37b35d48b
All checks were successful
Build & push Docker image / build (push) Successful in 1m27s
copyparty and radio support
2026-06-20 16:47:36 -04:00

167 lines
5.7 KiB
Go

package server_test
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"misaki/db"
"misaki/server"
)
func newSecretServer(t *testing.T, secret string) (http.Handler, *db.DB) {
t.Helper()
d := newTestDB(t)
srv := server.New(d, server.ServiceURLs{}, nil, server.SMTPConfig{},
server.Config{InternalSecret: secret})
return srv, d
}
func streamAuth(srv http.Handler, secret, user, pass, mount string) *httptest.ResponseRecorder {
body, _ := json.Marshal(map[string]string{
"username": user, "password": pass, "mount": mount,
})
req := httptest.NewRequest(http.MethodPost, "/internal/stream-auth", bytes.NewReader(body))
if secret != "" {
req.Header.Set("Authorization", "Bearer "+secret)
}
rr := httptest.NewRecorder()
srv.ServeHTTP(rr, req)
return rr
}
func TestInternalStreamAuth(t *testing.T) {
const secret = "test-secret"
srv, d := newSecretServer(t, secret)
id := makeApprovedUser(t, d, "dj_fm", "hunter2hunter2")
if err := d.SetUserServiceEnabled(id, "radio", true); err != nil {
t.Fatalf("grant streaming: %v", err)
}
if err := d.SetUserServiceData(id, "radio", "fmradio,live"); err != nil {
t.Fatalf("set stations: %v", err)
}
cases := []struct {
name string
secret string
user, pass, mt string
want int
}{
{"allowed mount", secret, "dj_fm", "hunter2hunter2", "fmradio", http.StatusOK},
{"other allowed", secret, "dj_fm", "hunter2hunter2", "live", http.StatusOK},
{"disallowed mount", secret, "dj_fm", "hunter2hunter2", "angelcast", http.StatusForbidden},
{"wrong password", secret, "dj_fm", "nope", "fmradio", http.StatusUnauthorized},
{"unknown user", secret, "ghost", "whatever12345", "fmradio", http.StatusUnauthorized},
{"bad bearer", "wrong", "dj_fm", "hunter2hunter2", "fmradio", http.StatusUnauthorized},
{"no bearer", "", "dj_fm", "hunter2hunter2", "fmradio", http.StatusUnauthorized},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
rr := streamAuth(srv, tc.secret, tc.user, tc.pass, tc.mt)
if rr.Code != tc.want {
t.Errorf("status: got %d, want %d", rr.Code, tc.want)
}
})
}
}
func TestInternalStreamAuth_NotGranted(t *testing.T) {
const secret = "test-secret"
srv, d := newSecretServer(t, secret)
makeApprovedUser(t, d, "rando", "hunter2hunter2") // approved but no streaming grant
rr := streamAuth(srv, secret, "rando", "hunter2hunter2", "fmradio")
if rr.Code != http.StatusForbidden {
t.Errorf("status: got %d, want %d", rr.Code, http.StatusForbidden)
}
}
func TestInternalStreamAuth_AdminAnyMount(t *testing.T) {
const secret = "test-secret"
srv, d := newSecretServer(t, secret)
makeAdminUser(t, d, "boss", "hunter2hunter2") // admin, no streaming grant needed
rr := streamAuth(srv, secret, "boss", "hunter2hunter2", "angelcast")
if rr.Code != http.StatusOK {
t.Errorf("status: got %d, want %d", rr.Code, http.StatusOK)
}
}
func TestInternalStreamAuth_BruteForceLockout(t *testing.T) {
const secret = "test-secret"
srv, d := newSecretServer(t, secret)
id := makeApprovedUser(t, d, "dj_fm", "hunter2hunter2")
if err := d.SetUserServiceEnabled(id, "radio", true); err != nil {
t.Fatalf("grant streaming: %v", err)
}
if err := d.SetUserServiceData(id, "radio", "fmradio"); err != nil {
t.Fatalf("set stations: %v", err)
}
// 10 wrong-password attempts exhaust the per-username budget.
for i := 0; i < 10; i++ {
if rr := streamAuth(srv, secret, "dj_fm", "wrong", "fmradio"); rr.Code != http.StatusUnauthorized {
t.Fatalf("attempt %d: got %d, want 401", i, rr.Code)
}
}
// Further attempts are locked out with 429 — even with the CORRECT password.
if rr := streamAuth(srv, secret, "dj_fm", "hunter2hunter2", "fmradio"); rr.Code != http.StatusTooManyRequests {
t.Errorf("after lockout: got %d, want 429", rr.Code)
}
// A different username is unaffected (lockout is per-account).
id2 := makeApprovedUser(t, d, "dj_angel", "hunter2hunter2")
if err := d.SetUserServiceEnabled(id2, "radio", true); err != nil {
t.Fatalf("grant streaming: %v", err)
}
if err := d.SetUserServiceData(id2, "radio", "angelcast"); err != nil {
t.Fatalf("set stations: %v", err)
}
if rr := streamAuth(srv, secret, "dj_angel", "hunter2hunter2", "angelcast"); rr.Code != http.StatusOK {
t.Errorf("other user: got %d, want 200", rr.Code)
}
}
func TestInternalStreamAuth_SuccessNeverLocks(t *testing.T) {
const secret = "test-secret"
srv, d := newSecretServer(t, secret)
id := makeApprovedUser(t, d, "dj_fm", "hunter2hunter2")
if err := d.SetUserServiceEnabled(id, "radio", true); err != nil {
t.Fatalf("grant streaming: %v", err)
}
if err := d.SetUserServiceData(id, "radio", "fmradio"); err != nil {
t.Fatalf("set stations: %v", err)
}
// Many successful (re)connections must never trip the limiter.
for i := 0; i < 25; i++ {
if rr := streamAuth(srv, secret, "dj_fm", "hunter2hunter2", "fmradio"); rr.Code != http.StatusOK {
t.Fatalf("success %d: got %d, want 200", i, rr.Code)
}
}
}
func TestInternalStreamAuth_WildcardAnyMount(t *testing.T) {
const secret = "test-secret"
srv, d := newSecretServer(t, secret)
id := makeApprovedUser(t, d, "star", "hunter2hunter2") // not a misaki admin
if err := d.SetUserServiceEnabled(id, "radio", true); err != nil {
t.Fatalf("grant streaming: %v", err)
}
if err := d.SetUserServiceData(id, "radio", "*"); err != nil {
t.Fatalf("set stations: %v", err)
}
// "*" lets a non-admin push any mount...
rr := streamAuth(srv, secret, "star", "hunter2hunter2", "fmradio")
if rr.Code != http.StatusOK {
t.Errorf("wildcard push: got %d, want %d", rr.Code, http.StatusOK)
}
// ...but the bearer is still required.
rr = streamAuth(srv, "wrong", "star", "hunter2hunter2", "fmradio")
if rr.Code != http.StatusUnauthorized {
t.Errorf("bad bearer: got %d, want %d", rr.Code, http.StatusUnauthorized)
}
}