Преглед на файлове

M1(3/8): testfakes/fakefcmd

Fake FCM HTTP v1 endpoint for M1 round-trip. 70 lines of Go:
  - POST /v1/projects/.../messages:send acks 200 with a fake
    message name
  - GET /health returns received/failed counts
  - --fail-rate knob for fault injection tests (M10)
  - truncates the token + title in logs to keep them readable

Used by M1's deliverd-fcm to prove the full ingest -> broker ->
router -> deliver -> third-party path without Google.

In M3 this is replaced by internal/fcm which talks to the real
googleapis.com endpoint.
Luis Rosales преди 2 месеца
родител
ревизия
0d668d69b2
променени са 1 файла, в които са добавени 103 реда и са изтрити 0 реда
  1. 103 0
      testfakes/fakefcmd/main.go

+ 103 - 0
testfakes/fakefcmd/main.go

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