feat(canary): wire manage routes + extract buildHTTPDeps + integration tests
- cmd/canary/main.go: tokenH.RegisterManageRoutes(api) mounts GET +
DELETE /m/{manage_id} inside the /api subrouter (read-tier rate
limit applies; no Turnstile, no create-tier limits — read-style
endpoints by spec §8.1).
- Extract buildHTTPDeps to keep run() under the funlen ceiling (was
102, now under 100). Pulls out registry build + tokenSvc + verifier
+ healthH + tokenH construction. Returns the four handles run()
needs to wire the router and shutdown path.
Integration tests (internal/event/integration_test.go,
testcontainers-Postgres + miniredis):
- TestIntegration_ManagePageReturnsTokenAndEventsAndSilencedCount:
POST creates a webbug, three triggers from the same CF-Connecting-IP
fire (one notification, two deduped per the Phase 10 dedup gate);
GET /api/m/{manage_id} returns token with TriggerCount=3,
EventsTotal=3, EventsSilencedActive=2, three event records with
exactly one NotifySent and two NotifyDeduped, no next page.
- TestIntegration_ManagePageReturns404OnUnknownManageID
- TestIntegration_ManageDeleteCascadesEvents: delete via DELETE
/api/m/{manage_id}, verify CountByToken == 0 (FK ON DELETE CASCADE)
and the token is gone (token.ErrNotFound).
This commit is contained in:
parent
593433fc03
commit
884215288e
|
|
@ -92,43 +92,11 @@ func run(configPath string) error {
|
|||
logger.Info("redis connected")
|
||||
|
||||
notifySvc, eventSvc := buildEventStack(
|
||||
cfg,
|
||||
logger,
|
||||
eventRepo,
|
||||
tokenRepo,
|
||||
rdb,
|
||||
cfg, logger, eventRepo, tokenRepo, rdb,
|
||||
)
|
||||
|
||||
genRegistry := registry.Build(registry.Config{
|
||||
BaseURL: cfg.Canary.BaseURL,
|
||||
MySQLPublicHost: cfg.MySQL.PublicHost,
|
||||
MySQLPublicPort: cfg.MySQL.PublicPort,
|
||||
})
|
||||
tokenSvc := token.NewService(
|
||||
tokenRepo,
|
||||
registryAdapter{r: genRegistry},
|
||||
token.ServiceConfig{
|
||||
BaseURL: cfg.Canary.BaseURL,
|
||||
ManageURL: cfg.Canary.ManageURL,
|
||||
},
|
||||
tokenSvc, verifier, healthH, tokenH := buildHTTPDeps(
|
||||
cfg, logger, db, rdb, eventRepo, tokenRepo, eventSvc,
|
||||
)
|
||||
|
||||
verifier := turnstile.NewVerifier(
|
||||
turnstile.Config{SecretKey: cfg.Turnstile.SecretKey},
|
||||
rdb.Client,
|
||||
)
|
||||
|
||||
healthH := health.NewHandler(db, rdb)
|
||||
tokenH := token.NewHandler(
|
||||
tokenSvc,
|
||||
&eventRecorderAdapter{svc: eventSvc},
|
||||
&fingerprintRecorderAdapter{
|
||||
repo: eventRepo,
|
||||
window: cfg.Notify.FingerprintWindow,
|
||||
},
|
||||
logger,
|
||||
)
|
||||
|
||||
srv := mountRouter(cfg, logger, rdb, healthH, tokenH, verifier)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
|
@ -152,6 +120,47 @@ func run(configPath string) error {
|
|||
return shutdownErr
|
||||
}
|
||||
|
||||
func buildHTTPDeps(
|
||||
cfg *config.Config,
|
||||
logger *slog.Logger,
|
||||
db *core.Database,
|
||||
rdb *core.Redis,
|
||||
eventRepo *event.Repository,
|
||||
tokenRepo *token.Repository,
|
||||
eventSvc *event.Service,
|
||||
) (*token.Service, *turnstile.Verifier, *health.Handler, *token.Handler) {
|
||||
genRegistry := registry.Build(registry.Config{
|
||||
BaseURL: cfg.Canary.BaseURL,
|
||||
MySQLPublicHost: cfg.MySQL.PublicHost,
|
||||
MySQLPublicPort: cfg.MySQL.PublicPort,
|
||||
})
|
||||
tokenSvc := token.NewService(
|
||||
tokenRepo,
|
||||
registryAdapter{r: genRegistry},
|
||||
token.ServiceConfig{
|
||||
BaseURL: cfg.Canary.BaseURL,
|
||||
ManageURL: cfg.Canary.ManageURL,
|
||||
},
|
||||
)
|
||||
verifier := turnstile.NewVerifier(
|
||||
turnstile.Config{SecretKey: cfg.Turnstile.SecretKey},
|
||||
rdb.Client,
|
||||
)
|
||||
healthH := health.NewHandler(db, rdb)
|
||||
tokenH := token.NewHandler(
|
||||
tokenSvc,
|
||||
&eventRecorderAdapter{svc: eventSvc},
|
||||
&fingerprintRecorderAdapter{
|
||||
repo: eventRepo,
|
||||
window: cfg.Notify.FingerprintWindow,
|
||||
},
|
||||
eventRepo,
|
||||
eventSvc,
|
||||
logger,
|
||||
)
|
||||
return tokenSvc, verifier, healthH, tokenH
|
||||
}
|
||||
|
||||
func buildEventStack(
|
||||
cfg *config.Config,
|
||||
logger *slog.Logger,
|
||||
|
|
@ -301,6 +310,7 @@ func mountRouter(
|
|||
createHour,
|
||||
middleware.TurnstileVerify(verifier),
|
||||
).Post("/tokens", tokenH.CreateToken)
|
||||
tokenH.RegisterManageRoutes(api)
|
||||
})
|
||||
|
||||
return srv
|
||||
|
|
|
|||
|
|
@ -124,6 +124,8 @@ func setupIntgStack(t *testing.T) *intgStack {
|
|||
tokenSvc,
|
||||
intgRecorderAdapter{svc: eventSvc},
|
||||
nil,
|
||||
eventRepo,
|
||||
eventSvc,
|
||||
logger,
|
||||
)
|
||||
|
||||
|
|
@ -134,6 +136,7 @@ func setupIntgStack(t *testing.T) *intgStack {
|
|||
r.Route("/api", func(api chi.Router) {
|
||||
api.Get("/tokens/types", tokenH.GetTypes)
|
||||
api.Post("/tokens", tokenH.CreateToken)
|
||||
tokenH.RegisterManageRoutes(api)
|
||||
})
|
||||
|
||||
return &intgStack{
|
||||
|
|
@ -350,6 +353,154 @@ func TestIntegration_DedupTTLExpiry(t *testing.T) {
|
|||
require.Equal(t, 2, sender.callCount(), "after TTL expiry next trigger notifies again")
|
||||
}
|
||||
|
||||
func TestIntegration_ManagePageReturnsTokenAndEventsAndSilencedCount(t *testing.T) {
|
||||
t.Parallel()
|
||||
st := setupIntgStack(t)
|
||||
|
||||
createBody := `{
|
||||
"type": "webbug",
|
||||
"memo": "manage-flow",
|
||||
"alert_channel": "telegram",
|
||||
"telegram_bot": "111:AAA",
|
||||
"telegram_chat": "12345"
|
||||
}`
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/tokens",
|
||||
strings.NewReader(createBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
st.router.ServeHTTP(w, req)
|
||||
require.Equal(t, http.StatusCreated, w.Code, "body=%s", w.Body.String())
|
||||
|
||||
var createResp struct {
|
||||
Data struct {
|
||||
Token struct {
|
||||
ID string `json:"id"`
|
||||
ManageID string `json:"manage_id"`
|
||||
} `json:"token"`
|
||||
} `json:"data"`
|
||||
}
|
||||
require.NoError(t, json.NewDecoder(w.Body).Decode(&createResp))
|
||||
tokenID := createResp.Data.Token.ID
|
||||
manageID := createResp.Data.Token.ManageID
|
||||
|
||||
for range 3 {
|
||||
trigReq := httptest.NewRequest(http.MethodGet, "/c/"+tokenID, nil)
|
||||
trigReq.Header.Set("CF-Connecting-IP", "203.0.113.50")
|
||||
tw := httptest.NewRecorder()
|
||||
st.router.ServeHTTP(tw, trigReq)
|
||||
require.Equal(t, http.StatusOK, tw.Code)
|
||||
}
|
||||
st.notifySvc.Wait()
|
||||
|
||||
mw := httptest.NewRecorder()
|
||||
st.router.ServeHTTP(mw, httptest.NewRequest(http.MethodGet,
|
||||
"/api/m/"+manageID, nil))
|
||||
require.Equal(t, http.StatusOK, mw.Code, "body=%s", mw.Body.String())
|
||||
|
||||
var manageResp struct {
|
||||
Success bool `json:"success"`
|
||||
Data token.ManageResponse `json:"data"`
|
||||
}
|
||||
require.NoError(t, json.NewDecoder(mw.Body).Decode(&manageResp))
|
||||
require.True(t, manageResp.Success)
|
||||
|
||||
require.Equal(t, tokenID, manageResp.Data.Token.ID)
|
||||
require.Equal(t, "https://canary.example.com/c/"+tokenID,
|
||||
manageResp.Data.Token.TriggerURL)
|
||||
require.Equal(t, int64(3), manageResp.Data.Token.TriggerCount)
|
||||
|
||||
require.Equal(t, int64(3), manageResp.Data.EventsTotal,
|
||||
"all 3 events recorded (one sent + two deduped)")
|
||||
require.Equal(t, int64(2), manageResp.Data.EventsSilencedActive,
|
||||
"two triggers deduped within 15-min window")
|
||||
|
||||
require.Len(t, manageResp.Data.Events, 3, "events page payload")
|
||||
require.False(t, manageResp.Data.Page.HasMore,
|
||||
"3 events fits in a 20-page; no next cursor")
|
||||
|
||||
statuses := []event.NotifyStatus{}
|
||||
for _, e := range manageResp.Data.Events {
|
||||
statuses = append(statuses, e.NotifyStatus)
|
||||
}
|
||||
var sent, deduped int
|
||||
for _, s := range statuses {
|
||||
switch s {
|
||||
case event.NotifySent:
|
||||
sent++
|
||||
case event.NotifyDeduped:
|
||||
deduped++
|
||||
}
|
||||
}
|
||||
require.Equal(t, 1, sent)
|
||||
require.Equal(t, 2, deduped)
|
||||
}
|
||||
|
||||
func TestIntegration_ManagePageReturns404OnUnknownManageID(t *testing.T) {
|
||||
t.Parallel()
|
||||
st := setupIntgStack(t)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
st.router.ServeHTTP(w, httptest.NewRequest(http.MethodGet,
|
||||
"/api/m/00000000-0000-0000-0000-000000000000", nil))
|
||||
require.Equal(t, http.StatusNotFound, w.Code)
|
||||
}
|
||||
|
||||
func TestIntegration_ManageDeleteCascadesEvents(t *testing.T) {
|
||||
t.Parallel()
|
||||
st := setupIntgStack(t)
|
||||
|
||||
createBody := `{
|
||||
"type": "webbug",
|
||||
"memo": "delete-flow",
|
||||
"alert_channel": "telegram",
|
||||
"telegram_bot": "111:AAA",
|
||||
"telegram_chat": "12345"
|
||||
}`
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/tokens",
|
||||
strings.NewReader(createBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
st.router.ServeHTTP(w, req)
|
||||
require.Equal(t, http.StatusCreated, w.Code)
|
||||
|
||||
var createResp struct {
|
||||
Data struct {
|
||||
Token struct {
|
||||
ID string `json:"id"`
|
||||
ManageID string `json:"manage_id"`
|
||||
} `json:"token"`
|
||||
} `json:"data"`
|
||||
}
|
||||
require.NoError(t, json.NewDecoder(w.Body).Decode(&createResp))
|
||||
|
||||
for i := range 2 {
|
||||
trigReq := httptest.NewRequest(http.MethodGet, "/c/"+createResp.Data.Token.ID, nil)
|
||||
ip := "203.0.113." + string(rune('1'+i))
|
||||
trigReq.Header.Set("CF-Connecting-IP", ip)
|
||||
tw := httptest.NewRecorder()
|
||||
st.router.ServeHTTP(tw, trigReq)
|
||||
require.Equal(t, http.StatusOK, tw.Code)
|
||||
}
|
||||
st.notifySvc.Wait()
|
||||
|
||||
count, err := st.eventRepo.CountByToken(context.Background(), createResp.Data.Token.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(2), count)
|
||||
|
||||
dw := httptest.NewRecorder()
|
||||
st.router.ServeHTTP(dw, httptest.NewRequest(http.MethodDelete,
|
||||
"/api/m/"+createResp.Data.Token.ManageID, nil))
|
||||
require.Equal(t, http.StatusNoContent, dw.Code)
|
||||
|
||||
count, err = st.eventRepo.CountByToken(context.Background(), createResp.Data.Token.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(0), count, "FK cascade removes events")
|
||||
|
||||
tok, err := st.tokenRepo.GetByID(context.Background(), createResp.Data.Token.ID)
|
||||
require.Nil(t, tok)
|
||||
require.ErrorIs(t, err, token.ErrNotFound)
|
||||
}
|
||||
|
||||
func TestIntegration_RetentionLoopPrunes(t *testing.T) {
|
||||
t.Parallel()
|
||||
db := sqlx.NewDb(testutil.NewTestDB(t), "pgx")
|
||||
|
|
|
|||
Loading…
Reference in New Issue