test: validate Android heartbeat reports through Server handler (#378)

This commit is contained in:
QiuSW
2026-10-10 10:58:35 +08:00
parent a28b869701
commit 9f489fdf62
@@ -0,0 +1,89 @@
package device
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
// Feed reports captured by Android's real HTTP client into the real Server
// handler. This optional cross-build check uses synthetic test data only.
func TestAndroidGeneratedHeartbeatReports(t *testing.T) {
path := os.Getenv("GOAUTO_378_ANDROID_REPORTS")
if path == "" {
t.Skip("set GOAUTO_378_ANDROID_REPORTS to the Android test-generated JSON array")
}
data, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
var reports []json.RawMessage
if err := json.Unmarshal(data, &reports); err != nil {
t.Fatal("Android report fixture is not a JSON array")
}
if len(reports) < 3 {
t.Fatal("expected zero-failure, failed and interrupted reports")
}
db := openTestDatabase(t)
service := newTestService(t, db)
_, registered := registerHeartbeatDevice(t, service)
gin.SetMode(gin.TestMode)
router := gin.New()
router.POST("/heartbeat", Handler{DB: db}.Heartbeat)
var sawZero, sawFailure, sawInterrupted bool
for i, raw := range reports {
t.Run(fmt.Sprintf("android_report_%d", i), func(t *testing.T) {
canonical, status := validateHeartbeatClientReport(raw)
if status != "accepted" || canonical == nil {
t.Fatalf("Android-generated report rejected: classification=%s", status)
}
var report heartbeatClientReport
if err := json.Unmarshal(raw, &report); err != nil {
t.Fatal("report decode failed")
}
sawZero = sawZero || report.FailedRounds == 0
sawFailure = sawFailure || report.FailedRounds > 0
sawInterrupted = sawInterrupted || report.HasInterruptedRound
id := uuid.NewString()
body := fmt.Sprintf(`{"requestId":%q,"currentTaskId":null,"clientReport":%s}`, id, raw)
for replay := 0; replay < 2; replay++ {
req := httptest.NewRequest(http.MethodPost, "/heartbeat", strings.NewReader(body))
req.Header.Set("Authorization", "Bearer "+testDeviceToken)
res := httptest.NewRecorder()
router.ServeHTTP(res, req)
if res.Code != http.StatusOK {
t.Fatalf("heartbeat status=%d", res.Code)
}
var response struct {
Data struct {
AcceptsClientReport bool `json:"acceptsClientReport"`
} `json:"data"`
}
if json.Unmarshal(res.Body.Bytes(), &response) != nil || !response.Data.AcceptsClientReport {
t.Fatal("successful response did not advertise report capability")
}
}
var rows []struct {
DeviceID uint64
ClientReportStatus string
ClientReportJSON *string
}
if err := db.Table("agent_heartbeat_log").Where("request_id = ?", id).Find(&rows).Error; err != nil {
t.Fatal(err)
}
if len(rows) != 1 || rows[0].DeviceID != registered.DeviceID || rows[0].ClientReportStatus != "accepted" || rows[0].ClientReportJSON == nil || *rows[0].ClientReportJSON != *canonical {
t.Fatal("accepted Android report was not stored once with the correct device and canonical payload")
}
})
}
if !sawZero || !sawFailure || !sawInterrupted {
t.Fatal("fixture must cover zero-failure, failed and interrupted reports")
}
}