main.go 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103
  1. // Command fakefcmd is a test double for the FCM HTTP v1 API. It
  2. // accepts POST /v1/projects/.../messages:send and acks 200 with a
  3. // fake message name. Used by M1 to prove the round-trip without
  4. // burning FCM credits or requiring a Google service account.
  5. //
  6. // In M3 we replace this with a real FCM client in internal/fcm/.
  7. //
  8. // Run:
  9. //
  10. // fakefcmd --addr :8820
  11. //
  12. // The deliverd-fcm worker points at this via env var
  13. // BA_FAKECMD_URL (or just hardcoded for M1).
  14. package main
  15. import (
  16. "encoding/json"
  17. "flag"
  18. "fmt"
  19. "io"
  20. "log/slog"
  21. "net/http"
  22. "os"
  23. "sync/atomic"
  24. "time"
  25. )
  26. func main() {
  27. var (
  28. addr = flag.String("addr", ":8820", "HTTP listen addr")
  29. failRate = flag.Float64("fail-rate", 0, "fraction of requests to fail (0-1) for fault injection")
  30. )
  31. flag.Parse()
  32. logger := slog.New(slog.NewTextHandler(os.Stderr, nil))
  33. var (
  34. received atomic.Uint64
  35. failed atomic.Uint64
  36. )
  37. mux := http.NewServeMux()
  38. mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
  39. w.Header().Set("Content-Type", "application/json")
  40. _ = json.NewEncoder(w).Encode(map[string]any{
  41. "status": "ok",
  42. "service": "fakefcmd",
  43. "received": received.Load(),
  44. "failed": failed.Load(),
  45. })
  46. })
  47. mux.HandleFunc("/v1/projects/", func(w http.ResponseWriter, r *http.Request) {
  48. received.Add(1)
  49. body, _ := io.ReadAll(r.Body)
  50. _ = r.Body.Close()
  51. if *failRate > 0 && (float64(received.Load())*1.0/100.0) < *failRate {
  52. // crude: 1 in N fails. good enough for fault-injection tests.
  53. failed.Add(1)
  54. logger.Warn("fakefcmd fault injection", "path", r.URL.Path)
  55. w.WriteHeader(http.StatusServiceUnavailable)
  56. _, _ = w.Write([]byte(`{"error":{"status":"UNAVAILABLE","message":"fakefcmd fault inject"}}`))
  57. return
  58. }
  59. // Log a compact view (don't print full payload; can be fat).
  60. var msg struct {
  61. Message struct {
  62. Token string `json:"token"`
  63. Notification struct{ Title, Body string } `json:"notification"`
  64. Data map[string]string `json:"data"`
  65. } `json:"message"`
  66. }
  67. _ = json.Unmarshal(body, &msg)
  68. logger.Info("fakefcmd send",
  69. "path", r.URL.Path,
  70. "token", truncate(msg.Message.Token, 32),
  71. "title", truncate(msg.Message.Notification.Title, 80),
  72. "data_keys", len(msg.Message.Data),
  73. "alert_id", msg.Message.Data["alert_id"],
  74. )
  75. w.Header().Set("Content-Type", "application/json")
  76. _ = json.NewEncoder(w).Encode(map[string]string{
  77. "name": fmt.Sprintf("projects/fakefcmd/messages/%d", time.Now().UnixNano()),
  78. })
  79. })
  80. logger.Info("fakefcmd listening", "addr", *addr)
  81. srv := &http.Server{
  82. Addr: *addr,
  83. Handler: mux,
  84. ReadHeaderTimeout: 5 * time.Second,
  85. }
  86. if err := srv.ListenAndServe(); err != nil {
  87. logger.Error("fakefcmd", "err", err)
  88. os.Exit(1)
  89. }
  90. }
  91. func truncate(s string, n int) string {
  92. if len(s) <= n {
  93. return s
  94. }
  95. return s[:n] + "…"
  96. }