test: validate Android heartbeat reports through Server handler (#378)
This commit is contained in:
@@ -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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user