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