// 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] + "…" }