package delivery import ( "context" "log/slog" "net" "net/http" "time" "gorm.io/gorm" "sneak.berlin/go/webhooker/internal/database" ) // Exported constants for test access. const ( ExportDeliveryChannelSize = deliveryChannelSize ExportRetryChannelSize = retryChannelSize ExportDefaultFailureThreshold = defaultFailureThreshold ExportDefaultCooldown = defaultCooldown ) // ExportIsBlockedIP exposes isBlockedIP for testing. func ExportIsBlockedIP(ip net.IP) bool { return isBlockedIP(ip) } // ExportBlockedNetworks exposes blockedNetworks. func ExportBlockedNetworks() []*net.IPNet { return blockedNetworks } // ExportIsForwardableHeader exposes isForwardableHeader. func ExportIsForwardableHeader(name string) bool { return isForwardableHeader(name) } // ExportTruncate exposes truncate for testing. func ExportTruncate(s string, maxLen int) string { return truncate(s, maxLen) } // ExportDeliverHTTP delivers via the http target for testing. func (e *Engine) ExportDeliverHTTP( ctx context.Context, webhookDB *gorm.DB, d *database.Delivery, task *Task, ) { e.httpTarget.Deliver(ctx, webhookDB, d, task, e) } // ExportDeliverDatabase delivers via the database target. func (e *Engine) ExportDeliverDatabase( webhookDB *gorm.DB, d *database.Delivery, ) { e.targets[database.TargetTypeDatabase].Deliver( context.Background(), webhookDB, d, &Task{}, e, ) } // ExportDeliverLog delivers via the log target for testing. func (e *Engine) ExportDeliverLog( webhookDB *gorm.DB, d *database.Delivery, ) { e.targets[database.TargetTypeLog].Deliver( context.Background(), webhookDB, d, &Task{}, e, ) } // ExportDeliverSlack delivers via the slack target for // testing. func (e *Engine) ExportDeliverSlack( ctx context.Context, webhookDB *gorm.DB, d *database.Delivery, ) { task := &Task{ DeliveryID: d.ID, TargetID: d.TargetID, AttemptNum: 1, } e.targets[database.TargetTypeSlack].Deliver( ctx, webhookDB, d, task, e, ) } // ExportProcessNewTask exposes processNewTask. func (e *Engine) ExportProcessNewTask( ctx context.Context, task *Task, ) { e.processNewTask(ctx, task) } // ExportProcessRetryTask exposes processRetryTask. func (e *Engine) ExportProcessRetryTask( ctx context.Context, task *Task, ) { e.processRetryTask(ctx, task) } // ExportProcessDelivery exposes processDelivery. func (e *Engine) ExportProcessDelivery( ctx context.Context, webhookDB *gorm.DB, d *database.Delivery, task *Task, ) { e.processDelivery(ctx, webhookDB, d, task) } // ExportGetCircuitBreaker exposes the http target's // getCircuitBreaker. func (e *Engine) ExportGetCircuitBreaker( targetID string, ) *CircuitBreaker { return e.httpTarget.getCircuitBreaker(targetID) } // ExportParseHTTPConfig exposes parseHTTPConfig. func (e *Engine) ExportParseHTTPConfig( configJSON string, ) (*HTTPTargetConfig, error) { return parseHTTPConfig(configJSON) } // ExportParseSlackConfig exposes parseSlackConfig. func (e *Engine) ExportParseSlackConfig( configJSON string, ) (*SlackTargetConfig, error) { return parseSlackConfig(configJSON) } // ExportDoHTTPRequest exposes the http target's // doHTTPRequest. func (e *Engine) ExportDoHTTPRequest( ctx context.Context, cfg *HTTPTargetConfig, event *database.Event, ) (int, string, int64, error) { return e.httpTarget.doHTTPRequest(ctx, cfg, event) } // ExportClientForConfig exposes the http target's // clientForConfig. func (e *Engine) ExportClientForConfig( cfg *HTTPTargetConfig, ) *http.Client { return e.httpTarget.clientForConfig(cfg) } // ExportClient returns the http target's shared HTTP client. func (e *Engine) ExportClient() *http.Client { return e.httpTarget.client } // ExportScheduleRetry exposes ScheduleRetry. func (e *Engine) ExportScheduleRetry( task Task, delay time.Duration, ) { e.ScheduleRetry(task, delay) } // ExportRecoverPendingDeliveries exposes // recoverPendingDeliveries. func (e *Engine) ExportRecoverPendingDeliveries( ctx context.Context, webhookDB *gorm.DB, webhookID string, ) { e.recoverPendingDeliveries( ctx, webhookDB, webhookID, ) } // ExportRecoverWebhookDeliveries exposes // recoverWebhookDeliveries. func (e *Engine) ExportRecoverWebhookDeliveries( ctx context.Context, webhookID string, ) { e.recoverWebhookDeliveries(ctx, webhookID) } // ExportRecoverInFlight exposes recoverInFlight. func (e *Engine) ExportRecoverInFlight( ctx context.Context, ) { e.recoverInFlight(ctx) } // ExportStart exposes start for testing. func (e *Engine) ExportStart(ctx context.Context) { e.start(ctx) } // ExportStop exposes stop for testing. func (e *Engine) ExportStop() { e.stop() } // ExportDeliveryCh returns the delivery channel. func (e *Engine) ExportDeliveryCh() chan Task { return e.deliveryCh } // ExportRetryCh returns the retry channel. func (e *Engine) ExportRetryCh() chan Task { return e.retryCh } // NewTestEngine creates an Engine for unit tests without // database dependencies. func NewTestEngine( log *slog.Logger, client *http.Client, workers int, ) *Engine { e := &Engine{ log: log, deliveryCh: make(chan Task, deliveryChannelSize), retryCh: make(chan Task, retryChannelSize), workers: workers, } e.initTargets(client) return e } // NewTestEngineSmallRetry creates an Engine with a tiny // retry channel buffer for overflow testing. func NewTestEngineSmallRetry( log *slog.Logger, ) *Engine { e := &Engine{ log: log, retryCh: make(chan Task, 1), } e.initTargets(nil) return e } // NewTestEngineWithDB creates an Engine with a real // database and dbManager for integration tests. func NewTestEngineWithDB( db *database.Database, dbMgr *database.WebhookDBManager, log *slog.Logger, client *http.Client, workers int, ) *Engine { e := &Engine{ database: db, dbManager: dbMgr, log: log, deliveryCh: make(chan Task, deliveryChannelSize), retryCh: make(chan Task, retryChannelSize), workers: workers, } e.initTargets(client) return e } // NewTestCircuitBreaker creates a CircuitBreaker with // custom settings for testing. func NewTestCircuitBreaker( threshold int, cooldown time.Duration, ) *CircuitBreaker { return &CircuitBreaker{ state: CircuitClosed, threshold: threshold, cooldown: cooldown, } } // ExportArchivedEvent aliases the archive row type so black-box // tests can construct and read archive rows. type ExportArchivedEvent = archivedEvent // ExportArchiveWriter wraps an archiveWriter so black-box tests // can exercise the per-webhook archive file mechanics. type ExportArchiveWriter struct { w *archiveWriter } // NewExportArchiveWriter builds an archive writer for tests, // optionally overriding the reopen debounce (a non-positive // debounce keeps the production default). func NewExportArchiveWriter( path string, log *slog.Logger, debounce time.Duration, ) *ExportArchiveWriter { w := newArchiveWriter(path, log) if debounce > 0 { w.debounce = debounce } return &ExportArchiveWriter{w: w} } // Write archives a row through the writer. func (e *ExportArchiveWriter) Write( row ExportArchivedEvent, expiry time.Duration, ) error { return e.w.write(row, expiry) } // Open opens the archive file, pruning when expiry is positive. func (e *ExportArchiveWriter) Open(expiry time.Duration) error { return e.w.open(expiry) } // Reopen closes and reopens the archive file. func (e *ExportArchiveWriter) Reopen( expiry time.Duration, ) error { return e.w.reopen(expiry) } // Reopens reports how many times the file has been opened. func (e *ExportArchiveWriter) Reopens() int { return e.w.reopens } // DB returns the writer's current open handle for row // inspection in tests. func (e *ExportArchiveWriter) DB() *gorm.DB { return e.w.db } // ExportParseArchiveExpiry exposes parseArchiveExpiry. func ExportParseArchiveExpiry( configJSON string, ) (time.Duration, error) { return parseArchiveExpiry(configJSON) }