// 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 failMode atomic.Int32 // 0 = healthy, 1 = fail every request ) 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(), "fail_mode": failMode.Load() == 1, }) }) // M8: runtime failure control. The smoke test flips // this to 1 to force all sends into the DLQ, then // back to 0 to allow a successful replay. This // avoids restarting the container for the smoke. mux.HandleFunc("/control", func(w http.ResponseWriter, r *http.Request) { v := r.URL.Query().Get("fail") switch v { case "1", "true": failMode.Store(1) logger.Warn("fakefcmd: fail mode ON") case "0", "false": failMode.Store(0) logger.Info("fakefcmd: fail mode OFF") default: http.Error(w, "pass ?fail=0 or ?fail=1", http.StatusBadRequest) return } w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(map[string]any{ "fail_mode": failMode.Load() == 1, }) }) mux.HandleFunc("/v1/projects/", func(w http.ResponseWriter, r *http.Request) { received.Add(1) body, _ := io.ReadAll(r.Body) _ = r.Body.Close() // M8: runtime fail mode wins over fail-rate. if failMode.Load() == 1 { failed.Add(1) logger.Warn("fakefcmd fault injection (runtime)", "path", r.URL.Path) w.WriteHeader(http.StatusServiceUnavailable) _, _ = w.Write([]byte(`{"error":{"status":"UNAVAILABLE","message":"fakefcmd runtime fail"}}`)) return } 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] + "…" }