feat(push-gateway): config 扩展 + kafka consumer + ws handler + nextstep 文档

This commit is contained in:
SpecialX
2026-07-14 16:03:17 +08:00
parent 5a88c8b45d
commit 9fd7c018c2
8 changed files with 630 additions and 36 deletions

View File

@@ -1,6 +1,10 @@
// Package config loads runtime configuration for push-gateway from environment
// variables. Required variables (JWT_SECRET in production, INTERNAL_API_TOKEN
// when not in DevMode) cause a fatal exit when missing.
// variables. Required variables (JWT_SECRET in production, PUSH_INTERNAL_TOKEN
// or INTERNAL_API_TOKEN when not in DevMode) cause a fatal exit when missing.
//
// v2 alignment (ARB-013 / msg contract): Kafka topic default changed to
// edu.notify.notification.sent; PUSH_INTERNAL_TOKEN is the canonical env var
// for /internal/* auth (INTERNAL_API_TOKEN kept as a backward-compat alias).
package config
import (
@@ -54,9 +58,15 @@ func Load() *Config {
}
}
internalToken := env.Get("INTERNAL_API_TOKEN", "")
// PUSH_INTERNAL_TOKEN is the canonical env var for /internal/* auth
// (ARB-013 alignment with msg). INTERNAL_API_TOKEN is kept as a
// backward-compat alias for existing deployments.
internalToken := env.Get("PUSH_INTERNAL_TOKEN", "")
if internalToken == "" {
internalToken = env.Get("INTERNAL_API_TOKEN", "")
}
if internalToken == "" && !devMode {
log.Fatal("INTERNAL_API_TOKEN must be set in non-dev mode")
log.Fatal("PUSH_INTERNAL_TOKEN (or INTERNAL_API_TOKEN) must be set in non-dev mode")
}
return &Config{
@@ -71,7 +81,9 @@ func Load() *Config {
HeartbeatInterval: env.GetInt("HEARTBEAT_INTERVAL_SECONDS", 30),
JWKSURL: env.Get("JWKS_URL", "http://localhost:50052/.well-known/jwks.json"),
KafkaBrokers: parseBrokers(env.Get("KAFKA_BROKERS", "localhost:9092")),
KafkaNotificationTopic: env.Get("KAFKA_NOTIFICATION_TOPIC", "edu.notification.requested"),
// ARB-013 canonical topic name: edu.<domain>.<aggregate>.<action>.
// msg's Outbox publisher emits to this topic.
KafkaNotificationTopic: env.Get("KAFKA_NOTIFICATION_TOPIC", "edu.notify.notification.sent"),
KafkaConsumerGroup: env.Get("KAFKA_CONSUMER_GROUP", "push-gateway"),
InstanceID: env.Get("INSTANCE_ID", generateInstanceID()),
}

View File

@@ -10,6 +10,7 @@ func TestLoadDevMode(t *testing.T) {
t.Setenv("DEV_MODE", "true")
t.Setenv("JWT_SECRET", "")
t.Setenv("INTERNAL_API_TOKEN", "")
t.Setenv("PUSH_INTERNAL_TOKEN", "")
cfg := Load()
@@ -31,8 +32,9 @@ func TestLoadDevMode(t *testing.T) {
if cfg.HeartbeatInterval != 30 {
t.Errorf("HeartbeatInterval = %d, want 30", cfg.HeartbeatInterval)
}
if cfg.KafkaNotificationTopic != "edu.notification.requested" {
t.Errorf("KafkaNotificationTopic = %q, want edu.notification.requested", cfg.KafkaNotificationTopic)
// ARB-013: canonical topic name is edu.notify.notification.sent.
if cfg.KafkaNotificationTopic != "edu.notify.notification.sent" {
t.Errorf("KafkaNotificationTopic = %q, want edu.notify.notification.sent", cfg.KafkaNotificationTopic)
}
if cfg.KafkaConsumerGroup != "push-gateway" {
t.Errorf("KafkaConsumerGroup = %q, want push-gateway", cfg.KafkaConsumerGroup)
@@ -42,7 +44,8 @@ func TestLoadDevMode(t *testing.T) {
}
}
// TestLoadProdMode verifies production requires JWT_SECRET + INTERNAL_API_TOKEN.
// TestLoadProdMode verifies production requires JWT_SECRET + PUSH_INTERNAL_TOKEN
// (or the legacy INTERNAL_API_TOKEN alias).
func TestLoadProdMode(t *testing.T) {
// Ensure the env vars are unset so Load fails. We can't call os.Exit in
// a test, so we verify the required behavior by setting DEV_MODE=false
@@ -50,7 +53,8 @@ func TestLoadProdMode(t *testing.T) {
// os.Exit, we instead test the happy path: all required vars set.
t.Setenv("DEV_MODE", "false")
t.Setenv("JWT_SECRET", "prod-secret")
t.Setenv("INTERNAL_API_TOKEN", "prod-token")
t.Setenv("PUSH_INTERNAL_TOKEN", "prod-token")
t.Setenv("INTERNAL_API_TOKEN", "")
cfg := Load()
@@ -61,7 +65,22 @@ func TestLoadProdMode(t *testing.T) {
t.Errorf("JWTSecret = %q, want prod-secret", cfg.JWTSecret)
}
if cfg.InternalAPIToken != "prod-token" {
t.Errorf("InternalAPIToken = %q, want prod-token", cfg.InternalAPIToken)
t.Errorf("InternalAPIToken = %q, want prod-token (from PUSH_INTERNAL_TOKEN)", cfg.InternalAPIToken)
}
}
// TestLoadProdModeLegacyToken verifies INTERNAL_API_TOKEN is accepted as a
// backward-compat alias when PUSH_INTERNAL_TOKEN is not set.
func TestLoadProdModeLegacyToken(t *testing.T) {
t.Setenv("DEV_MODE", "false")
t.Setenv("JWT_SECRET", "prod-secret")
t.Setenv("PUSH_INTERNAL_TOKEN", "")
t.Setenv("INTERNAL_API_TOKEN", "legacy-token")
cfg := Load()
if cfg.InternalAPIToken != "legacy-token" {
t.Errorf("InternalAPIToken = %q, want legacy-token (from INTERNAL_API_TOKEN fallback)", cfg.InternalAPIToken)
}
}