feat: add private r2 bucket
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/prabhupadasays/spsays-bff/internal/middleware"
|
||||
"github.com/prabhupadasays/spsays-bff/pkg/realtime"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
const testUser = "user-a"
|
||||
|
||||
func testAuthHandler(t *testing.T) *BroadcastingAuthHandler {
|
||||
t.Helper()
|
||||
rt := realtime.NewSoketiClient("app-1", "test-key", "test-secret", "ws.example.com", 443, true, zap.NewNop())
|
||||
if rt == nil {
|
||||
t.Fatal("realtime client is nil")
|
||||
}
|
||||
// A second rule stands in for the chat conversation channels, proving the
|
||||
// rule table is what decides access rather than anything hardcoded.
|
||||
convRule := ChannelRule{
|
||||
Prefix: "presence-conversation.",
|
||||
Authorize: func(_ context.Context, userID, suffix string) (bool, error) {
|
||||
return suffix == "open-to-user-a" && userID == testUser, nil
|
||||
},
|
||||
}
|
||||
return NewBroadcastingAuthHandler(rt, zap.NewNop(), NotificationsRule(), convRule)
|
||||
}
|
||||
|
||||
// serve posts a form-encoded auth request as userID ("" = unauthenticated).
|
||||
func serve(t *testing.T, h *BroadcastingAuthHandler, userID, socketID, channel string) *httptest.ResponseRecorder {
|
||||
t.Helper()
|
||||
form := url.Values{"socket_id": {socketID}, "channel_name": {channel}}
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/broadcasting/auth", strings.NewReader(form.Encode()))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
if userID != "" {
|
||||
req = req.WithContext(middleware.WithUserID(req.Context(), userID))
|
||||
}
|
||||
rec := httptest.NewRecorder()
|
||||
h.Handle(rec, req)
|
||||
return rec
|
||||
}
|
||||
|
||||
func TestBroadcastingAuth_Authorization(t *testing.T) {
|
||||
h := testAuthHandler(t)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
userID string
|
||||
channel string
|
||||
want int
|
||||
}{
|
||||
{"own notifications channel", testUser, "private-notifications." + testUser, http.StatusOK},
|
||||
{"another user's notifications", testUser, "private-notifications.user-b", http.StatusForbidden},
|
||||
{"unauthenticated", "", "private-notifications." + testUser, http.StatusUnauthorized},
|
||||
{"unknown channel family", testUser, "private-secrets.42", http.StatusForbidden},
|
||||
{"legacy public channel is not signable", testUser, "notifications." + testUser, http.StatusForbidden},
|
||||
{"empty suffix", testUser, "private-notifications.", http.StatusForbidden},
|
||||
{"presence channel allowed by rule", testUser, "presence-conversation.open-to-user-a", http.StatusOK},
|
||||
{"presence channel denied by rule", testUser, "presence-conversation.someone-elses", http.StatusForbidden},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
rec := serve(t, h, tc.userID, "1234.5678", tc.channel)
|
||||
if rec.Code != tc.want {
|
||||
t.Errorf("status = %d, want %d (body %s)", rec.Code, tc.want, rec.Body.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// A prefix match must not be mistaken for authorization: "…user-a-evil" starts
|
||||
// with the victim's id but is a different channel.
|
||||
func TestBroadcastingAuth_SuffixIsComparedExactly(t *testing.T) {
|
||||
rec := serve(t, testAuthHandler(t), testUser, "1234.5678", "private-notifications."+testUser+"-evil")
|
||||
if rec.Code != http.StatusForbidden {
|
||||
t.Errorf("status = %d, want 403", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBroadcastingAuth_SignatureShape(t *testing.T) {
|
||||
rec := serve(t, testAuthHandler(t), testUser, "1234.5678", "private-notifications."+testUser)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want 200", rec.Code)
|
||||
}
|
||||
var body struct {
|
||||
Auth string `json:"auth"`
|
||||
ChannelData string `json:"channel_data"`
|
||||
}
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
|
||||
t.Fatalf("decode: %v", err)
|
||||
}
|
||||
if !strings.HasPrefix(body.Auth, "test-key:") {
|
||||
t.Errorf("auth = %q, want a <key>:<signature> pair", body.Auth)
|
||||
}
|
||||
// channel_data belongs to presence channels only; sending it for a private
|
||||
// channel would make Soketi reject the subscription.
|
||||
if body.ChannelData != "" {
|
||||
t.Errorf("channel_data = %q, want empty for a private channel", body.ChannelData)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBroadcastingAuth_PresenceReturnsChannelData(t *testing.T) {
|
||||
rec := serve(t, testAuthHandler(t), testUser, "1234.5678", "presence-conversation.open-to-user-a")
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want 200", rec.Code)
|
||||
}
|
||||
var body struct {
|
||||
ChannelData string `json:"channel_data"`
|
||||
}
|
||||
_ = json.Unmarshal(rec.Body.Bytes(), &body)
|
||||
if !strings.Contains(body.ChannelData, `"user_id":"`+testUser+`"`) {
|
||||
t.Errorf("channel_data = %q, want the caller's user_id", body.ChannelData)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBroadcastingAuth_AcceptsJSONBody(t *testing.T) {
|
||||
body := `{"socket_id":"1234.5678","channel_name":"private-notifications.` + testUser + `"}`
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/broadcasting/auth", strings.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req = req.WithContext(middleware.WithUserID(req.Context(), testUser))
|
||||
rec := httptest.NewRecorder()
|
||||
testAuthHandler(t).Handle(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Errorf("status = %d, want 200 (body %s)", rec.Code, rec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestBroadcastingAuth_MissingParams(t *testing.T) {
|
||||
h := testAuthHandler(t)
|
||||
if rec := serve(t, h, testUser, "", "private-notifications."+testUser); rec.Code != http.StatusBadRequest {
|
||||
t.Errorf("missing socket_id: status = %d, want 400", rec.Code)
|
||||
}
|
||||
if rec := serve(t, h, testUser, "1234.5678", ""); rec.Code != http.StatusBadRequest {
|
||||
t.Errorf("missing channel_name: status = %d, want 400", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// With realtime disabled there is no secret to sign with; the endpoint must
|
||||
// say so rather than hand back an unusable signature.
|
||||
func TestBroadcastingAuth_UnconfiguredRealtime(t *testing.T) {
|
||||
h := NewBroadcastingAuthHandler(nil, zap.NewNop(), NotificationsRule())
|
||||
if rec := serve(t, h, testUser, "1234.5678", "private-notifications."+testUser); rec.Code != http.StatusServiceUnavailable {
|
||||
t.Errorf("status = %d, want 503", rec.Code)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user