216 lines
6.3 KiB
Go
216 lines
6.3 KiB
Go
package handler
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"io"
|
|
"log/slog"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"filefast/backend/internal/config"
|
|
"filefast/backend/internal/model"
|
|
"filefast/backend/internal/service"
|
|
"filefast/backend/internal/store"
|
|
"filefast/backend/internal/ws"
|
|
)
|
|
|
|
type registeredDevice struct {
|
|
ID string `json:"id"`
|
|
AuthToken string `json:"auth_token"`
|
|
}
|
|
|
|
type transferRecord struct {
|
|
ID string `json:"id"`
|
|
}
|
|
|
|
func TestProtectedRoutesRequireDeviceCredentials(t *testing.T) {
|
|
router, _ := newTestRouter()
|
|
|
|
device := registerDevice(t, router, map[string]any{
|
|
"device_id": "alpha",
|
|
"name": "Alpha",
|
|
"type": "desktop",
|
|
})
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/devices/candidates?deviceId="+device.ID, nil)
|
|
resp := httptest.NewRecorder()
|
|
router.ServeHTTP(resp, req)
|
|
|
|
if resp.Code != http.StatusUnauthorized {
|
|
t.Fatalf("expected 401 for missing device credentials, got %d", resp.Code)
|
|
}
|
|
}
|
|
|
|
func TestProtectedRoutesRejectMismatchedDeviceIdentity(t *testing.T) {
|
|
router, _ := newTestRouter()
|
|
|
|
alpha := registerDevice(t, router, map[string]any{
|
|
"device_id": "alpha",
|
|
"name": "Alpha",
|
|
"type": "desktop",
|
|
})
|
|
bravo := registerDevice(t, router, map[string]any{
|
|
"device_id": "bravo",
|
|
"name": "Bravo",
|
|
"type": "desktop",
|
|
})
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/devices/candidates?deviceId="+bravo.ID, nil)
|
|
req.Header.Set("X-Device-ID", alpha.ID)
|
|
req.Header.Set("X-Device-Token", alpha.AuthToken)
|
|
resp := httptest.NewRecorder()
|
|
router.ServeHTTP(resp, req)
|
|
|
|
if resp.Code != http.StatusForbidden {
|
|
t.Fatalf("expected 403 for mismatched device identity, got %d", resp.Code)
|
|
}
|
|
}
|
|
|
|
func TestTransferStatusUpdateRequiresParticipantOwnership(t *testing.T) {
|
|
router, _ := newTestRouter()
|
|
|
|
sender := registerDevice(t, router, map[string]any{
|
|
"device_id": "sender",
|
|
"name": "Sender",
|
|
"type": "desktop",
|
|
})
|
|
receiver := registerDevice(t, router, map[string]any{
|
|
"device_id": "receiver",
|
|
"name": "Receiver",
|
|
"type": "desktop",
|
|
})
|
|
attacker := registerDevice(t, router, map[string]any{
|
|
"device_id": "attacker",
|
|
"name": "Attacker",
|
|
"type": "desktop",
|
|
})
|
|
|
|
transfer := createTransfer(t, router, sender, map[string]any{
|
|
"kind": "text",
|
|
"name": "text-message",
|
|
"content": "hello",
|
|
"sender_device_id": sender.ID,
|
|
"receiver_device_id": receiver.ID,
|
|
})
|
|
|
|
body, err := json.Marshal(map[string]any{
|
|
"final_status": "completed",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("marshal update status body: %v", err)
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodPatch, "/api/transfers/"+transfer.ID+"/status", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("X-Device-ID", attacker.ID)
|
|
req.Header.Set("X-Device-Token", attacker.AuthToken)
|
|
resp := httptest.NewRecorder()
|
|
router.ServeHTTP(resp, req)
|
|
|
|
if resp.Code != http.StatusForbidden {
|
|
t.Fatalf("expected 403 for non-participant transfer update, got %d", resp.Code)
|
|
}
|
|
}
|
|
|
|
func TestProtectedRoutesAcceptDeviceCredentialsFromCookies(t *testing.T) {
|
|
router, _ := newTestRouter()
|
|
|
|
device := registerDevice(t, router, map[string]any{
|
|
"device_id": "cookie-device",
|
|
"name": "Cookie Device",
|
|
"type": "desktop",
|
|
})
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/devices/candidates?deviceId="+device.ID, nil)
|
|
req.AddCookie(&http.Cookie{Name: deviceIDCookieName, Value: device.ID})
|
|
req.AddCookie(&http.Cookie{Name: deviceTokenCookieName, Value: device.AuthToken})
|
|
resp := httptest.NewRecorder()
|
|
router.ServeHTTP(resp, req)
|
|
|
|
if resp.Code != http.StatusOK {
|
|
t.Fatalf("expected cookie-authenticated request to succeed, got %d: %s", resp.Code, resp.Body.String())
|
|
}
|
|
}
|
|
|
|
func newTestRouter() (http.Handler, *store.MemoryStore) {
|
|
memStore := store.NewMemoryStore(model.RuntimeConfig{})
|
|
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
|
|
deviceService := service.NewDeviceService(memStore, nil, nil)
|
|
deps := Dependencies{
|
|
Config: config.Config{},
|
|
Logger: logger,
|
|
Store: memStore,
|
|
DeviceService: deviceService,
|
|
RoomService: service.NewRoomService(memStore, 0),
|
|
TransferService: service.NewTransferService(memStore),
|
|
AdminService: service.NewAdminService(memStore, config.AdminConfig{}, nil, nil),
|
|
Hub: ws.NewHub(logger, deviceService, nil),
|
|
StorageReady: true,
|
|
RedisReady: true,
|
|
}
|
|
|
|
return NewHTTPHandler(deps).Router(), memStore
|
|
}
|
|
|
|
func registerDevice(t *testing.T, router http.Handler, payload map[string]any) registeredDevice {
|
|
t.Helper()
|
|
body, err := json.Marshal(payload)
|
|
if err != nil {
|
|
t.Fatalf("marshal register body: %v", err)
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/devices/register", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp := httptest.NewRecorder()
|
|
router.ServeHTTP(resp, req)
|
|
|
|
if resp.Code != http.StatusOK {
|
|
t.Fatalf("expected register 200, got %d: %s", resp.Code, resp.Body.String())
|
|
}
|
|
|
|
var payloadWrapper struct {
|
|
Data registeredDevice `json:"data"`
|
|
}
|
|
if err := json.Unmarshal(resp.Body.Bytes(), &payloadWrapper); err != nil {
|
|
t.Fatalf("decode register response: %v", err)
|
|
}
|
|
if payloadWrapper.Data.ID == "" || payloadWrapper.Data.AuthToken == "" {
|
|
t.Fatalf("expected device registration to return id and auth token, got %+v", payloadWrapper.Data)
|
|
}
|
|
|
|
return payloadWrapper.Data
|
|
}
|
|
|
|
func createTransfer(t *testing.T, router http.Handler, device registeredDevice, payload map[string]any) transferRecord {
|
|
t.Helper()
|
|
body, err := json.Marshal(payload)
|
|
if err != nil {
|
|
t.Fatalf("marshal create transfer body: %v", err)
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/transfers", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("X-Device-ID", device.ID)
|
|
req.Header.Set("X-Device-Token", device.AuthToken)
|
|
resp := httptest.NewRecorder()
|
|
router.ServeHTTP(resp, req)
|
|
|
|
if resp.Code != http.StatusOK {
|
|
t.Fatalf("expected create transfer 200, got %d: %s", resp.Code, resp.Body.String())
|
|
}
|
|
|
|
var payloadWrapper struct {
|
|
Data transferRecord `json:"data"`
|
|
}
|
|
if err := json.Unmarshal(resp.Body.Bytes(), &payloadWrapper); err != nil {
|
|
t.Fatalf("decode transfer response: %v", err)
|
|
}
|
|
if payloadWrapper.Data.ID == "" {
|
|
t.Fatalf("expected transfer id in response, got %+v", payloadWrapper.Data)
|
|
}
|
|
|
|
return payloadWrapper.Data
|
|
}
|