Lege ersten Player-Status-Endpunkt an
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
parent
d87bb2b269
commit
3fd6ed7432
3 changed files with 228 additions and 1 deletions
69
server/backend/internal/httpapi/playerstatus.go
Normal file
69
server/backend/internal/httpapi/playerstatus.go
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
package httpapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type playerStatusRequest struct {
|
||||
ScreenID string `json:"screen_id"`
|
||||
Timestamp string `json:"ts"`
|
||||
Status string `json:"status"`
|
||||
ServerURL string `json:"server_url"`
|
||||
MQTTBroker string `json:"mqtt_broker"`
|
||||
HeartbeatEverySeconds int `json:"heartbeat_every_seconds"`
|
||||
StartedAt string `json:"started_at"`
|
||||
LastHeartbeatAt string `json:"last_heartbeat_at"`
|
||||
}
|
||||
|
||||
func handlePlayerStatus(w http.ResponseWriter, r *http.Request) {
|
||||
var request playerStatusRequest
|
||||
if err := decodeJSON(r, &request); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid_json", "ungueltiger JSON-Body", nil)
|
||||
return
|
||||
}
|
||||
|
||||
if strings.TrimSpace(request.ScreenID) == "" {
|
||||
writeError(w, http.StatusBadRequest, "screen_id_required", "screen_id ist erforderlich", nil)
|
||||
return
|
||||
}
|
||||
|
||||
if strings.TrimSpace(request.Timestamp) == "" {
|
||||
writeError(w, http.StatusBadRequest, "timestamp_required", "ts ist erforderlich", nil)
|
||||
return
|
||||
}
|
||||
|
||||
if strings.TrimSpace(request.Status) == "" {
|
||||
writeError(w, http.StatusBadRequest, "status_required", "status ist erforderlich", nil)
|
||||
return
|
||||
}
|
||||
|
||||
if err := validateOptionalRFC3339(request.Timestamp); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid_timestamp", "ts ist kein gueltiger RFC3339-Zeitstempel", nil)
|
||||
return
|
||||
}
|
||||
|
||||
if err := validateOptionalRFC3339(request.StartedAt); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid_started_at", "started_at ist kein gueltiger RFC3339-Zeitstempel", nil)
|
||||
return
|
||||
}
|
||||
|
||||
if err := validateOptionalRFC3339(request.LastHeartbeatAt); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid_last_heartbeat_at", "last_heartbeat_at ist kein gueltiger RFC3339-Zeitstempel", nil)
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]string{
|
||||
"status": "accepted",
|
||||
})
|
||||
}
|
||||
|
||||
func validateOptionalRFC3339(value string) error {
|
||||
if strings.TrimSpace(value) == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err := time.Parse(time.RFC3339, value)
|
||||
return err
|
||||
}
|
||||
156
server/backend/internal/httpapi/playerstatus_test.go
Normal file
156
server/backend/internal/httpapi/playerstatus_test.go
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
package httpapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHandlePlayerStatusAccepted(t *testing.T) {
|
||||
body := []byte(`{
|
||||
"screen_id": "info01-dev",
|
||||
"ts": "2026-03-22T16:00:00Z",
|
||||
"status": "running",
|
||||
"server_url": "http://127.0.0.1:8080",
|
||||
"mqtt_broker": "tcp://127.0.0.1:1883",
|
||||
"heartbeat_every_seconds": 30,
|
||||
"started_at": "2026-03-22T15:59:30Z",
|
||||
"last_heartbeat_at": "2026-03-22T16:00:00Z"
|
||||
}`)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/player/status", bytes.NewReader(body))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handlePlayerStatus(w, req)
|
||||
|
||||
if got, want := w.Code, http.StatusOK; got != want {
|
||||
t.Fatalf("status = %d, want %d", got, want)
|
||||
}
|
||||
|
||||
var response struct {
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
|
||||
t.Fatalf("Unmarshal() error = %v", err)
|
||||
}
|
||||
|
||||
if got, want := response.Status, "accepted"; got != want {
|
||||
t.Fatalf("response status = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandlePlayerStatusRejectsInvalidJSON(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/player/status", bytes.NewBufferString("{"))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handlePlayerStatus(w, req)
|
||||
|
||||
if got, want := w.Code, http.StatusBadRequest; got != want {
|
||||
t.Fatalf("status = %d, want %d", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandlePlayerStatusRejectsMissingScreenID(t *testing.T) {
|
||||
body := []byte(`{
|
||||
"screen_id": " ",
|
||||
"ts": "2026-03-22T16:00:00Z",
|
||||
"status": "running"
|
||||
}`)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/player/status", bytes.NewReader(body))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handlePlayerStatus(w, req)
|
||||
|
||||
if got, want := w.Code, http.StatusBadRequest; got != want {
|
||||
t.Fatalf("status = %d, want %d", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandlePlayerStatusRejectsMissingTimestamp(t *testing.T) {
|
||||
body := []byte(`{
|
||||
"screen_id": "info01-dev",
|
||||
"status": "running"
|
||||
}`)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/player/status", bytes.NewReader(body))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handlePlayerStatus(w, req)
|
||||
|
||||
if got, want := w.Code, http.StatusBadRequest; got != want {
|
||||
t.Fatalf("status = %d, want %d", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandlePlayerStatusRejectsMissingStatus(t *testing.T) {
|
||||
body := []byte(`{
|
||||
"screen_id": "info01-dev",
|
||||
"ts": "2026-03-22T16:00:00Z"
|
||||
}`)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/player/status", bytes.NewReader(body))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handlePlayerStatus(w, req)
|
||||
|
||||
if got, want := w.Code, http.StatusBadRequest; got != want {
|
||||
t.Fatalf("status = %d, want %d", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandlePlayerStatusRejectsMalformedTimestamps(t *testing.T) {
|
||||
body := []byte(`{
|
||||
"screen_id": "info01-dev",
|
||||
"ts": "not-a-time",
|
||||
"status": "running"
|
||||
}`)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/player/status", bytes.NewReader(body))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handlePlayerStatus(w, req)
|
||||
|
||||
if got, want := w.Code, http.StatusBadRequest; got != want {
|
||||
t.Fatalf("status = %d, want %d", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandlePlayerStatusRejectsMalformedStartedAt(t *testing.T) {
|
||||
body := []byte(`{
|
||||
"screen_id": "info01-dev",
|
||||
"ts": "2026-03-22T16:00:00Z",
|
||||
"status": "running",
|
||||
"started_at": "not-a-time"
|
||||
}`)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/player/status", bytes.NewReader(body))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handlePlayerStatus(w, req)
|
||||
|
||||
if got, want := w.Code, http.StatusBadRequest; got != want {
|
||||
t.Fatalf("status = %d, want %d", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandlePlayerStatusRejectsMalformedLastHeartbeatAt(t *testing.T) {
|
||||
body := []byte(`{
|
||||
"screen_id": "info01-dev",
|
||||
"ts": "2026-03-22T16:00:00Z",
|
||||
"status": "running",
|
||||
"last_heartbeat_at": "not-a-time"
|
||||
}`)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/player/status", bytes.NewReader(body))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handlePlayerStatus(w, req)
|
||||
|
||||
if got, want := w.Code, http.StatusBadRequest; got != want {
|
||||
t.Fatalf("status = %d, want %d", got, want)
|
||||
}
|
||||
}
|
||||
|
|
@ -9,7 +9,7 @@ func NewRouter() http.Handler {
|
|||
|
||||
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, map[string]string{
|
||||
"status": "ok",
|
||||
"status": "ok",
|
||||
"service": "morz-infoboard-backend",
|
||||
})
|
||||
})
|
||||
|
|
@ -26,6 +26,8 @@ func NewRouter() http.Handler {
|
|||
|
||||
mux.HandleFunc("GET /api/v1/meta", handleMeta)
|
||||
|
||||
mux.HandleFunc("POST /api/v1/player/status", handlePlayerStatus)
|
||||
|
||||
mux.HandleFunc("POST /api/v1/tools/message-wall/resolve", handleResolveMessageWall)
|
||||
|
||||
return mux
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue