package httpapi import ( "context" "crypto/ed25519" "crypto/rand" "crypto/sha256" "encoding/base64" "encoding/hex" "encoding/json" "fmt" "net/http" "net/http/httptest" "os" "strings" "testing" "time" "data-terminal-cloud/internal/audit" "data-terminal-cloud/internal/identity" "data-terminal-cloud/internal/saas" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgxpool" ) type httpFirmwareStore struct{} func (*httpFirmwareStore) PresignUpload(_ context.Context, ref string, _ time.Duration) (string, error) { return "https://objects.invalid/upload/" + ref, nil } func (*httpFirmwareStore) PresignDownload(_ context.Context, ref string, _ time.Duration, filename string) (string, error) { return "https://objects.invalid/download/" + ref + "?filename=" + filename, nil } func (*httpFirmwareStore) VerifyDigest(context.Context, string, string, int64) error { return nil } func (*httpFirmwareStore) Remove(context.Context, string) error { return nil } func TestFirmwareWriteHandlers(t *testing.T) { databaseURL := os.Getenv("TEST_DATABASE_URL") if databaseURL == "" { t.Skip("TEST_DATABASE_URL is not set") } ctx := context.Background() pool, err := pgxpool.New(ctx, databaseURL) if err != nil { t.Fatal(err) } defer pool.Close() service := saas.NewService(pool, audit.NewRepository(), nil, nil) service.ConfigureFirmwareStore(&httpFirmwareStore{}, 5*time.Minute) principal := identity.Principal{Issuer: "http://localhost:58180/realms/voice-local", Subject: "00000000-0000-0000-0000-000000000101"} householdID := "10000000-0000-0000-0000-000000000211" deviceID := "10000000-0000-0000-0000-000000000511" keyID := "firmware-http-" + uuid.NewString() version := "http-" + uuid.NewString() var releaseID, retiredReleaseID, rolloutID string defer func() { if rolloutID != "" { _, _ = pool.Exec(ctx, `DELETE FROM audit_event WHERE resource_id=$1::uuid`, rolloutID) _, _ = pool.Exec(ctx, `DELETE FROM firmware_rollout WHERE public_id=$1::uuid`, rolloutID) } _, _ = pool.Exec(ctx, `DELETE FROM device_command WHERE idempotency_key LIKE 'firmware-rollout:%' AND payload->>'release_id'=$1`, releaseID) if releaseID != "" { _, _ = pool.Exec(ctx, `DELETE FROM audit_event WHERE resource_id=$1::uuid`, releaseID) _, _ = pool.Exec(ctx, `DELETE FROM firmware_release WHERE public_id=$1::uuid`, releaseID) } if retiredReleaseID != "" { _, _ = pool.Exec(ctx, `DELETE FROM audit_event WHERE resource_id=$1::uuid`, retiredReleaseID) _, _ = pool.Exec(ctx, `DELETE FROM firmware_release WHERE public_id=$1::uuid`, retiredReleaseID) } _, _ = pool.Exec(ctx, `DELETE FROM firmware_signing_key WHERE key_id=$1`, keyID) }() request := authenticatedRequest(http.MethodPost, "/api/v1/households/"+householdID+"/firmware-signing-keys", `{}`, principal) request.SetPathValue("id", householdID) recorder := httptest.NewRecorder() RegisterFirmwareSigningKeyHandler(service).ServeHTTP(recorder, request) if recorder.Code != http.StatusBadRequest { t.Fatalf("invalid key status=%d body=%s", recorder.Code, recorder.Body.String()) } publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader) if err != nil { t.Fatal(err) } keyBody, _ := json.Marshal(map[string]any{"key_id": keyID, "public_key_base64": base64.StdEncoding.EncodeToString(publicKey)}) request = authenticatedRequest(http.MethodPost, "/api/v1/households/"+householdID+"/firmware-signing-keys", string(keyBody), principal) request.SetPathValue("id", householdID) recorder = httptest.NewRecorder() RegisterFirmwareSigningKeyHandler(service).ServeHTTP(recorder, request) if recorder.Code != http.StatusCreated { t.Fatalf("register key status=%d body=%s", recorder.Code, recorder.Body.String()) } request = authenticatedRequest(http.MethodGet, "/api/v1/households/"+householdID+"/firmware-signing-keys", "", principal) request.SetPathValue("id", householdID) recorder = httptest.NewRecorder() ListFirmwareSigningKeysHandler(service).ServeHTTP(recorder, request) if recorder.Code != http.StatusOK { t.Fatalf("list signing keys status=%d body=%s", recorder.Code, recorder.Body.String()) } var keysResponse struct { Data struct { Items []saas.FirmwareSigningKey `json:"items"` } `json:"data"` } if err := json.Unmarshal(recorder.Body.Bytes(), &keysResponse); err != nil || len(keysResponse.Data.Items) != 1 || keysResponse.Data.Items[0].KeyID != keyID { t.Fatalf("signing keys response=%+v err=%v", keysResponse, err) } request = authenticatedRequest(http.MethodPost, "/api/v1/households/"+householdID+"/firmware-releases", `{"model":"simulated-terminal","version":"bad","expected_size":1,"expected_digest":"bad","signature":"bad","signing_key_id":"bad"}`, principal) request.SetPathValue("id", householdID) recorder = httptest.NewRecorder() PrepareFirmwareReleaseHandler(service).ServeHTTP(recorder, request) if recorder.Code != http.StatusBadRequest { t.Fatalf("invalid release status=%d body=%s", recorder.Code, recorder.Body.String()) } image := []byte("synthetic firmware through HTTP") digestBytes := sha256.Sum256(image) digest := hex.EncodeToString(digestBytes[:]) manifest := fmt.Sprintf("firmware-manifest-v1\nmodel=%s\nhardware_revision=%s\nversion=%s\nsha256=%s\nsize=%d\n", "simulated-terminal", "sim-rev-a", version, digest, len(image)) signature := base64.StdEncoding.EncodeToString(ed25519.Sign(privateKey, []byte(manifest))) releaseBody, _ := json.Marshal(map[string]any{ "model": "simulated-terminal", "hardware_revision": "sim-rev-a", "version": version, "expected_size": len(image), "expected_digest": digest, "signature": signature, "signing_key_id": keyID, }) request = authenticatedRequest(http.MethodPost, "/api/v1/households/"+householdID+"/firmware-releases", string(releaseBody), principal) request.SetPathValue("id", householdID) recorder = httptest.NewRecorder() PrepareFirmwareReleaseHandler(service).ServeHTTP(recorder, request) if recorder.Code != http.StatusCreated || recorder.Header().Get("Cache-Control") != "no-store" { t.Fatalf("prepare release status=%d headers=%v body=%s", recorder.Code, recorder.Header(), recorder.Body.String()) } var releaseResponse struct { Data saas.FirmwareRelease `json:"data"` } if err := json.Unmarshal(recorder.Body.Bytes(), &releaseResponse); err != nil { t.Fatal(err) } releaseID = releaseResponse.Data.ID revokeBody, _ := json.Marshal(map[string]string{"key_id": keyID}) request = authenticatedRequest(http.MethodPost, "/api/v1/households/"+householdID+"/firmware-signing-keys/revoke", string(revokeBody), principal) request.SetPathValue("id", householdID) recorder = httptest.NewRecorder() RevokeFirmwareSigningKeyHandler(service).ServeHTTP(recorder, request) if recorder.Code != http.StatusConflict { t.Fatalf("revoke uploading key status=%d body=%s", recorder.Code, recorder.Body.String()) } request = authenticatedRequest(http.MethodPost, "/api/v1/firmware-releases/"+releaseID+"/upload-session", "", principal) request.SetPathValue("id", releaseID) recorder = httptest.NewRecorder() RenewFirmwareUploadHandler(service).ServeHTTP(recorder, request) if recorder.Code != http.StatusOK || recorder.Header().Get("Cache-Control") != "no-store" { t.Fatalf("renew upload status=%d headers=%v body=%s", recorder.Code, recorder.Header(), recorder.Body.String()) } request = authenticatedRequest(http.MethodPost, "/api/v1/firmware-releases/"+releaseID+"/finalize", "", principal) request.SetPathValue("id", releaseID) recorder = httptest.NewRecorder() FinalizeFirmwareReleaseHandler(service).ServeHTTP(recorder, request) if recorder.Code != http.StatusOK { t.Fatalf("finalize release status=%d body=%s", recorder.Code, recorder.Body.String()) } request = authenticatedRequest(http.MethodPost, "/api/v1/firmware-releases/"+releaseID+"/pause", "", principal) request.SetPathValue("id", releaseID) recorder = httptest.NewRecorder() PauseFirmwareReleaseHandler(service).ServeHTTP(recorder, request) if recorder.Code != http.StatusOK || !strings.Contains(recorder.Body.String(), `"status":"PAUSED"`) { t.Fatalf("pause release status=%d body=%s", recorder.Code, recorder.Body.String()) } rolloutBody, _ := json.Marshal(map[string]any{"release_id": releaseID, "name": "HTTP固件下发", "target_percentage": 100, "device_ids": []string{deviceID}}) request = authenticatedRequest(http.MethodPost, "/api/v1/households/"+householdID+"/firmware-rollouts", string(rolloutBody), principal) request.SetPathValue("id", householdID) recorder = httptest.NewRecorder() StartFirmwareRolloutHandler(service).ServeHTTP(recorder, request) if recorder.Code != http.StatusConflict { t.Fatalf("start paused release rollout status=%d body=%s", recorder.Code, recorder.Body.String()) } request = authenticatedRequest(http.MethodPost, "/api/v1/firmware-releases/"+releaseID+"/resume", "", principal) request.SetPathValue("id", releaseID) recorder = httptest.NewRecorder() ResumeFirmwareReleaseHandler(service).ServeHTTP(recorder, request) if recorder.Code != http.StatusOK || !strings.Contains(recorder.Body.String(), `"status":"READY"`) { t.Fatalf("resume release status=%d body=%s", recorder.Code, recorder.Body.String()) } request = authenticatedRequest(http.MethodPost, "/api/v1/households/"+householdID+"/firmware-rollouts", string(rolloutBody), principal) request.SetPathValue("id", householdID) recorder = httptest.NewRecorder() StartFirmwareRolloutHandler(service).ServeHTTP(recorder, request) if recorder.Code != http.StatusAccepted { t.Fatalf("start rollout status=%d body=%s", recorder.Code, recorder.Body.String()) } var rolloutResponse struct { Data saas.FirmwareRollout `json:"data"` } if err := json.Unmarshal(recorder.Body.Bytes(), &rolloutResponse); err != nil { t.Fatal(err) } rolloutID = rolloutResponse.Data.ID request = authenticatedRequest(http.MethodGet, "/api/v1/firmware-releases/"+releaseID+"/impact", "", principal) request.SetPathValue("id", releaseID) recorder = httptest.NewRecorder() GetFirmwareReleaseImpactHandler(service).ServeHTTP(recorder, request) if recorder.Code != http.StatusOK { t.Fatalf("get release impact status=%d body=%s", recorder.Code, recorder.Body.String()) } var impactResponse struct { Data saas.FirmwareReleaseImpact `json:"data"` } if err := json.Unmarshal(recorder.Body.Bytes(), &impactResponse); err != nil || impactResponse.Data.ReleaseStatus != "READY" || impactResponse.Data.Rollouts.Running != 1 || impactResponse.Data.Commands.Queued != 1 { t.Fatalf("release impact response=%+v err=%v", impactResponse, err) } request = authenticatedRequest(http.MethodPost, "/api/v1/firmware-releases/"+releaseID+"/retire", "", principal) request.SetPathValue("id", releaseID) recorder = httptest.NewRecorder() RetireFirmwareReleaseHandler(service).ServeHTTP(recorder, request) if recorder.Code != http.StatusConflict || !strings.Contains(recorder.Body.String(), "FIRMWARE_RELEASE_IN_USE") { t.Fatalf("retire running release status=%d body=%s", recorder.Code, recorder.Body.String()) } request = authenticatedRequest(http.MethodPost, "/api/v1/firmware-releases/"+releaseID+"/pause", "", principal) request.SetPathValue("id", releaseID) recorder = httptest.NewRecorder() PauseFirmwareReleaseHandler(service).ServeHTTP(recorder, request) if recorder.Code != http.StatusOK || !strings.Contains(recorder.Body.String(), `"status":"PAUSED"`) { t.Fatalf("pause running release status=%d body=%s", recorder.Code, recorder.Body.String()) } request = authenticatedRequest(http.MethodGet, "/api/v1/firmware-releases/"+releaseID+"/impact", "", principal) request.SetPathValue("id", releaseID) recorder = httptest.NewRecorder() GetFirmwareReleaseImpactHandler(service).ServeHTTP(recorder, request) if recorder.Code != http.StatusOK { t.Fatalf("get paused release impact status=%d body=%s", recorder.Code, recorder.Body.String()) } if err := json.Unmarshal(recorder.Body.Bytes(), &impactResponse); err != nil || impactResponse.Data.ReleaseStatus != "PAUSED" || impactResponse.Data.Rollouts.Paused != 1 || impactResponse.Data.Commands.CancelledBeforeDelivery != 1 { t.Fatalf("paused release impact response=%+v err=%v", impactResponse, err) } rollout, err := service.GetFirmwareRollout(ctx, principal, rolloutID) if err != nil || rollout.Status != "PAUSED" { t.Fatalf("release pause did not pause rollout=%+v err=%v", rollout, err) } request = authenticatedRequest(http.MethodPost, "/api/v1/firmware-releases/"+releaseID+"/resume", "", principal) request.SetPathValue("id", releaseID) recorder = httptest.NewRecorder() ResumeFirmwareReleaseHandler(service).ServeHTTP(recorder, request) if recorder.Code != http.StatusOK { t.Fatalf("resume after rollout pause status=%d body=%s", recorder.Code, recorder.Body.String()) } request = authenticatedRequest(http.MethodPost, "/api/v1/firmware-releases/"+releaseID+"/retire", "", principal) request.SetPathValue("id", releaseID) recorder = httptest.NewRecorder() RetireFirmwareReleaseHandler(service).ServeHTTP(recorder, request) if recorder.Code != http.StatusOK || !strings.Contains(recorder.Body.String(), `"status":"RETIRED"`) { t.Fatalf("retire verified release status=%d body=%s", recorder.Code, recorder.Body.String()) } retireBody, _ := json.Marshal(map[string]any{ "model": "simulated-terminal", "hardware_revision": "sim-rev-a", "version": version + "-retire", "expected_size": len(image), "expected_digest": digest, "signature": signature, "signing_key_id": keyID, }) request = authenticatedRequest(http.MethodPost, "/api/v1/households/"+householdID+"/firmware-releases", string(retireBody), principal) request.SetPathValue("id", householdID) recorder = httptest.NewRecorder() PrepareFirmwareReleaseHandler(service).ServeHTTP(recorder, request) if recorder.Code != http.StatusCreated { t.Fatalf("prepare retiring release status=%d body=%s", recorder.Code, recorder.Body.String()) } if err := json.Unmarshal(recorder.Body.Bytes(), &releaseResponse); err != nil { t.Fatal(err) } retiredReleaseID = releaseResponse.Data.ID request = authenticatedRequest(http.MethodPost, "/api/v1/firmware-releases/"+retiredReleaseID+"/retire", "", principal) request.SetPathValue("id", retiredReleaseID) recorder = httptest.NewRecorder() RetireFirmwareReleaseHandler(service).ServeHTTP(recorder, request) if recorder.Code != http.StatusOK { t.Fatalf("retire release status=%d body=%s", recorder.Code, recorder.Body.String()) } if err := json.Unmarshal(recorder.Body.Bytes(), &releaseResponse); err != nil || releaseResponse.Data.Status != "RETIRED" { t.Fatalf("retired response=%+v err=%v", releaseResponse, err) } request = authenticatedRequest(http.MethodPost, "/api/v1/households/"+householdID+"/firmware-signing-keys/revoke", string(revokeBody), principal) request.SetPathValue("id", householdID) recorder = httptest.NewRecorder() RevokeFirmwareSigningKeyHandler(service).ServeHTTP(recorder, request) if recorder.Code != http.StatusOK { t.Fatalf("revoke signing key status=%d body=%s", recorder.Code, recorder.Body.String()) } var keyResponse struct { Data saas.FirmwareSigningKey `json:"data"` } if err := json.Unmarshal(recorder.Body.Bytes(), &keyResponse); err != nil || keyResponse.Data.Status != "REVOKED" { t.Fatalf("revoked key response=%+v err=%v", keyResponse, err) } request = authenticatedRequest(http.MethodPost, "/api/v1/households/"+householdID+"/firmware-rollouts", string(rolloutBody), principal) request.SetPathValue("id", householdID) recorder = httptest.NewRecorder() StartFirmwareRolloutHandler(service).ServeHTTP(recorder, request) if recorder.Code != http.StatusConflict { t.Fatalf("rollout with revoked key status=%d body=%s", recorder.Code, recorder.Body.String()) } }