Files

304 lines
15 KiB
Go
Raw Permalink Normal View History

2026-08-26 06:41:07 +08:00
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())
}
}