main.go 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  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. failMode atomic.Int32 // 0 = healthy, 1 = fail every request
  37. )
  38. mux := http.NewServeMux()
  39. mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
  40. w.Header().Set("Content-Type", "application/json")
  41. _ = json.NewEncoder(w).Encode(map[string]any{
  42. "status": "ok",
  43. "service": "fakefcmd",
  44. "received": received.Load(),
  45. "failed": failed.Load(),
  46. "fail_mode": failMode.Load() == 1,
  47. })
  48. })
  49. // M8: runtime failure control. The smoke test flips
  50. // this to 1 to force all sends into the DLQ, then
  51. // back to 0 to allow a successful replay. This
  52. // avoids restarting the container for the smoke.
  53. mux.HandleFunc("/control", func(w http.ResponseWriter, r *http.Request) {
  54. v := r.URL.Query().Get("fail")
  55. switch v {
  56. case "1", "true":
  57. failMode.Store(1)
  58. logger.Warn("fakefcmd: fail mode ON")
  59. case "0", "false":
  60. failMode.Store(0)
  61. logger.Info("fakefcmd: fail mode OFF")
  62. default:
  63. http.Error(w, "pass ?fail=0 or ?fail=1", http.StatusBadRequest)
  64. return
  65. }
  66. w.Header().Set("Content-Type", "application/json")
  67. _ = json.NewEncoder(w).Encode(map[string]any{
  68. "fail_mode": failMode.Load() == 1,
  69. })
  70. })
  71. mux.HandleFunc("/v1/projects/", func(w http.ResponseWriter, r *http.Request) {
  72. received.Add(1)
  73. body, _ := io.ReadAll(r.Body)
  74. _ = r.Body.Close()
  75. // M8: runtime fail mode wins over fail-rate.
  76. if failMode.Load() == 1 {
  77. failed.Add(1)
  78. logger.Warn("fakefcmd fault injection (runtime)", "path", r.URL.Path)
  79. w.WriteHeader(http.StatusServiceUnavailable)
  80. _, _ = w.Write([]byte(`{"error":{"status":"UNAVAILABLE","message":"fakefcmd runtime fail"}}`))
  81. return
  82. }
  83. if *failRate > 0 && (float64(received.Load())*1.0/100.0) < *failRate {
  84. // crude: 1 in N fails. good enough for fault-injection tests.
  85. failed.Add(1)
  86. logger.Warn("fakefcmd fault injection", "path", r.URL.Path)
  87. w.WriteHeader(http.StatusServiceUnavailable)
  88. _, _ = w.Write([]byte(`{"error":{"status":"UNAVAILABLE","message":"fakefcmd fault inject"}}`))
  89. return
  90. }
  91. // Log a compact view (don't print full payload; can be fat).
  92. var msg struct {
  93. Message struct {
  94. Token string `json:"token"`
  95. Notification struct{ Title, Body string } `json:"notification"`
  96. Data map[string]string `json:"data"`
  97. } `json:"message"`
  98. }
  99. _ = json.Unmarshal(body, &msg)
  100. logger.Info("fakefcmd send",
  101. "path", r.URL.Path,
  102. "token", truncate(msg.Message.Token, 32),
  103. "title", truncate(msg.Message.Notification.Title, 80),
  104. "data_keys", len(msg.Message.Data),
  105. "alert_id", msg.Message.Data["alert_id"],
  106. )
  107. w.Header().Set("Content-Type", "application/json")
  108. _ = json.NewEncoder(w).Encode(map[string]string{
  109. "name": fmt.Sprintf("projects/fakefcmd/messages/%d", time.Now().UnixNano()),
  110. })
  111. })
  112. logger.Info("fakefcmd listening", "addr", *addr)
  113. srv := &http.Server{
  114. Addr: *addr,
  115. Handler: mux,
  116. ReadHeaderTimeout: 5 * time.Second,
  117. }
  118. if err := srv.ListenAndServe(); err != nil {
  119. logger.Error("fakefcmd", "err", err)
  120. os.Exit(1)
  121. }
  122. }
  123. func truncate(s string, n int) string {
  124. if len(s) <= n {
  125. return s
  126. }
  127. return s[:n] + "…"
  128. }