|
|
@@ -0,0 +1,103 @@
|
|
|
+// Command fakefcmd is a test double for the FCM HTTP v1 API. It
|
|
|
+// accepts POST /v1/projects/.../messages:send and acks 200 with a
|
|
|
+// fake message name. Used by M1 to prove the round-trip without
|
|
|
+// burning FCM credits or requiring a Google service account.
|
|
|
+//
|
|
|
+// In M3 we replace this with a real FCM client in internal/fcm/.
|
|
|
+//
|
|
|
+// Run:
|
|
|
+//
|
|
|
+// fakefcmd --addr :8820
|
|
|
+//
|
|
|
+// The deliverd-fcm worker points at this via env var
|
|
|
+// BA_FAKECMD_URL (or just hardcoded for M1).
|
|
|
+package main
|
|
|
+
|
|
|
+import (
|
|
|
+ "encoding/json"
|
|
|
+ "flag"
|
|
|
+ "fmt"
|
|
|
+ "io"
|
|
|
+ "log/slog"
|
|
|
+ "net/http"
|
|
|
+ "os"
|
|
|
+ "sync/atomic"
|
|
|
+ "time"
|
|
|
+)
|
|
|
+
|
|
|
+func main() {
|
|
|
+ var (
|
|
|
+ addr = flag.String("addr", ":8820", "HTTP listen addr")
|
|
|
+ failRate = flag.Float64("fail-rate", 0, "fraction of requests to fail (0-1) for fault injection")
|
|
|
+ )
|
|
|
+ flag.Parse()
|
|
|
+ logger := slog.New(slog.NewTextHandler(os.Stderr, nil))
|
|
|
+
|
|
|
+ var (
|
|
|
+ received atomic.Uint64
|
|
|
+ failed atomic.Uint64
|
|
|
+ )
|
|
|
+
|
|
|
+ mux := http.NewServeMux()
|
|
|
+ mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
|
|
|
+ w.Header().Set("Content-Type", "application/json")
|
|
|
+ _ = json.NewEncoder(w).Encode(map[string]any{
|
|
|
+ "status": "ok",
|
|
|
+ "service": "fakefcmd",
|
|
|
+ "received": received.Load(),
|
|
|
+ "failed": failed.Load(),
|
|
|
+ })
|
|
|
+ })
|
|
|
+
|
|
|
+ mux.HandleFunc("/v1/projects/", func(w http.ResponseWriter, r *http.Request) {
|
|
|
+ received.Add(1)
|
|
|
+ body, _ := io.ReadAll(r.Body)
|
|
|
+ _ = r.Body.Close()
|
|
|
+ if *failRate > 0 && (float64(received.Load())*1.0/100.0) < *failRate {
|
|
|
+ // crude: 1 in N fails. good enough for fault-injection tests.
|
|
|
+ failed.Add(1)
|
|
|
+ logger.Warn("fakefcmd fault injection", "path", r.URL.Path)
|
|
|
+ w.WriteHeader(http.StatusServiceUnavailable)
|
|
|
+ _, _ = w.Write([]byte(`{"error":{"status":"UNAVAILABLE","message":"fakefcmd fault inject"}}`))
|
|
|
+ return
|
|
|
+ }
|
|
|
+ // Log a compact view (don't print full payload; can be fat).
|
|
|
+ var msg struct {
|
|
|
+ Message struct {
|
|
|
+ Token string `json:"token"`
|
|
|
+ Notification struct{ Title, Body string } `json:"notification"`
|
|
|
+ Data map[string]string `json:"data"`
|
|
|
+ } `json:"message"`
|
|
|
+ }
|
|
|
+ _ = json.Unmarshal(body, &msg)
|
|
|
+ logger.Info("fakefcmd send",
|
|
|
+ "path", r.URL.Path,
|
|
|
+ "token", truncate(msg.Message.Token, 32),
|
|
|
+ "title", truncate(msg.Message.Notification.Title, 80),
|
|
|
+ "data_keys", len(msg.Message.Data),
|
|
|
+ "alert_id", msg.Message.Data["alert_id"],
|
|
|
+ )
|
|
|
+ w.Header().Set("Content-Type", "application/json")
|
|
|
+ _ = json.NewEncoder(w).Encode(map[string]string{
|
|
|
+ "name": fmt.Sprintf("projects/fakefcmd/messages/%d", time.Now().UnixNano()),
|
|
|
+ })
|
|
|
+ })
|
|
|
+
|
|
|
+ logger.Info("fakefcmd listening", "addr", *addr)
|
|
|
+ srv := &http.Server{
|
|
|
+ Addr: *addr,
|
|
|
+ Handler: mux,
|
|
|
+ ReadHeaderTimeout: 5 * time.Second,
|
|
|
+ }
|
|
|
+ if err := srv.ListenAndServe(); err != nil {
|
|
|
+ logger.Error("fakefcmd", "err", err)
|
|
|
+ os.Exit(1)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func truncate(s string, n int) string {
|
|
|
+ if len(s) <= n {
|
|
|
+ return s
|
|
|
+ }
|
|
|
+ return s[:n] + "…"
|
|
|
+}
|