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) } }