(tests): webhook runtime (doc): logger options
This commit is contained in:
@@ -1,5 +1,15 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v1.0.0
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Bot-level middleware blocks now emit a final `UpdateHandledEvent` with `Handled=false`, keeping observer update lifecycles balanced.
|
||||||
|
- `BotOpts`, `tgapi.APIOpts`, and logger utility godoc now document `LOG_FORMAT`, `LogFormat`, and logger formatting options consistently.
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
- Added regression coverage proving bot-level middleware blocks still complete the observer update lifecycle.
|
||||||
|
- Added webhook runtime regression coverage for request enqueue through worker execution of a command handler.
|
||||||
|
|
||||||
## v1.0.0-rc.16
|
## v1.0.0-rc.16
|
||||||
|
|
||||||
### Breaking Changes
|
### Breaking Changes
|
||||||
|
|||||||
+7
-2
@@ -71,7 +71,9 @@ type BotOpts struct {
|
|||||||
// It is zero when the options were not loaded from a versioned file.
|
// It is zero when the options were not loaded from a versioned file.
|
||||||
FileConfigVersion int
|
FileConfigVersion int
|
||||||
|
|
||||||
LogFormat utils.LogFormat
|
// LogFormat selects text or JSON output for bot-managed loggers.
|
||||||
|
LogFormat utils.LogFormat
|
||||||
|
// LogFormatter customizes bot-managed logger writers when supported.
|
||||||
LogFormatter *sneklog.Formatter
|
LogFormatter *sneklog.Formatter
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,7 +94,7 @@ type BotOpts struct {
|
|||||||
// - DROP_RL_OVERFLOW: "true" to drop updates on rate limit overflow
|
// - DROP_RL_OVERFLOW: "true" to drop updates on rate limit overflow
|
||||||
// - STRICT_PAYLOAD_TYPE: "true" to reject callback payloads encoded in a different format
|
// - STRICT_PAYLOAD_TYPE: "true" to reject callback payloads encoded in a different format
|
||||||
// - MAX_WORKERS: maximum number of concurrent update handlers (default: 32)
|
// - MAX_WORKERS: maximum number of concurrent update handlers (default: 32)
|
||||||
// - JSON_LOG:
|
// - LOG_FORMAT: logger output format, "text" or "json" (default: "text")
|
||||||
//
|
//
|
||||||
// Returns a populated BotOpts.
|
// Returns a populated BotOpts.
|
||||||
// NewBot validates required fields and returns ErrTokenRequired when TG_TOKEN is missing.
|
// NewBot validates required fields and returns ErrTokenRequired when TG_TOKEN is missing.
|
||||||
@@ -254,10 +256,13 @@ func (opts *BotOpts) SetMaxWorkers(workers int) *BotOpts {
|
|||||||
return opts
|
return opts
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetLogFormat sets the output format used by bot-managed loggers.
|
||||||
func (opts *BotOpts) SetLogFormat(format utils.LogFormat) *BotOpts {
|
func (opts *BotOpts) SetLogFormat(format utils.LogFormat) *BotOpts {
|
||||||
opts.LogFormat = format
|
opts.LogFormat = format
|
||||||
return opts
|
return opts
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetLogFormatter sets the formatter used by bot-managed logger writers.
|
||||||
func (opts *BotOpts) SetLogFormatter(formatter *sneklog.Formatter) *BotOpts {
|
func (opts *BotOpts) SetLogFormatter(formatter *sneklog.Formatter) *BotOpts {
|
||||||
opts.LogFormatter = formatter
|
opts.LogFormatter = formatter
|
||||||
return opts
|
return opts
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"git.scuroneko.dev/scuroneko/laniakea/tgapi"
|
"git.scuroneko.dev/scuroneko/laniakea/tgapi"
|
||||||
"git.scuroneko.dev/scuroneko/sneklog/v2"
|
"git.scuroneko.dev/scuroneko/sneklog/v2"
|
||||||
@@ -108,6 +109,55 @@ func TestRunWebhookRuntimeExecutesRunners(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRunWebhookRuntimeProcessesEnqueuedUpdate(t *testing.T) {
|
||||||
|
var calls atomic.Int32
|
||||||
|
plugin := NewPlugin[NoData]("demo")
|
||||||
|
plugin.NewCommand(func(ctx *MsgContext, db NoData) error {
|
||||||
|
calls.Add(1)
|
||||||
|
return nil
|
||||||
|
}, "start")
|
||||||
|
|
||||||
|
bot := &Bot[NoData]{
|
||||||
|
logger: sneklog.NewLogger(),
|
||||||
|
webHookLogger: sneklog.NewLogger(),
|
||||||
|
prefixes: []string{"/"},
|
||||||
|
plugins: []Plugin[NoData]{*plugin},
|
||||||
|
updateQueue: make(chan *tgapi.Update, 1),
|
||||||
|
maxWorkers: 1,
|
||||||
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
_ = bot.logger.Close()
|
||||||
|
_ = bot.webHookLogger.Close()
|
||||||
|
})
|
||||||
|
|
||||||
|
err := bot.runWebhookRuntime(context.Background(), func(ctx context.Context) error {
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(`{"update_id":9,"message":{"message_id":1,"date":1,"chat":{"id":1,"type":"private"},"from":{"id":2,"is_bot":false,"first_name":"Test"},"text":"/start"}}`))
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
|
||||||
|
updateHandler(ctx, bot, "").ServeHTTP(rec, req)
|
||||||
|
if rec.Result().StatusCode != http.StatusOK {
|
||||||
|
t.Fatalf("unexpected status: got %d want %d", rec.Result().StatusCode, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
deadline := time.After(time.Second)
|
||||||
|
for calls.Load() == 0 {
|
||||||
|
select {
|
||||||
|
case <-deadline:
|
||||||
|
t.Fatal("webhook runtime did not process enqueued update")
|
||||||
|
default:
|
||||||
|
time.Sleep(time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("runWebhookRuntime returned error: %v", err)
|
||||||
|
}
|
||||||
|
if calls.Load() != 1 {
|
||||||
|
t.Fatalf("expected command handler to run once, got %d", calls.Load())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestWebhookAllowedUpdatesUsesBotUpdateTypesByDefault(t *testing.T) {
|
func TestWebhookAllowedUpdatesUsesBotUpdateTypesByDefault(t *testing.T) {
|
||||||
bot := &Bot[NoData]{
|
bot := &Bot[NoData]{
|
||||||
updateTypes: []tgapi.UpdateType{tgapi.UpdateTypeMessage, tgapi.UpdateTypeCallbackQuery},
|
updateTypes: []tgapi.UpdateType{tgapi.UpdateTypeMessage, tgapi.UpdateTypeCallbackQuery},
|
||||||
|
|||||||
@@ -46,6 +46,14 @@ func (bot *Bot[T]) handle(parentCtx context.Context, u *tgapi.Update) {
|
|||||||
|
|
||||||
for _, middleware := range bot.middlewares {
|
for _, middleware := range bot.middlewares {
|
||||||
if !middleware.Execute(msgCtx, bot.appData) {
|
if !middleware.Execute(msgCtx, bot.appData) {
|
||||||
|
bot.safeEmitEvent(ctx, UpdateHandledEvent{
|
||||||
|
UpdateID: u.UpdateID,
|
||||||
|
UpdateType: u.Type,
|
||||||
|
FromID: msgCtx.FromID,
|
||||||
|
ChatID: msgCtx.ChatID,
|
||||||
|
Duration: time.Since(startTime),
|
||||||
|
Handled: false,
|
||||||
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+42
-1
@@ -10,6 +10,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type recordingObserver struct {
|
type recordingObserver struct {
|
||||||
|
received []UpdateReceivedEvent
|
||||||
started []HandlerStartedEvent
|
started []HandlerStartedEvent
|
||||||
finished []HandlerFinishedEvent
|
finished []HandlerFinishedEvent
|
||||||
errors []ErrorEvent
|
errors []ErrorEvent
|
||||||
@@ -19,7 +20,9 @@ type recordingObserver struct {
|
|||||||
retries []PollingRetryEvent
|
retries []PollingRetryEvent
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*recordingObserver) OnReceiveUpdate(context.Context, UpdateReceivedEvent) {}
|
func (o *recordingObserver) OnReceiveUpdate(_ context.Context, ev UpdateReceivedEvent) {
|
||||||
|
o.received = append(o.received, ev)
|
||||||
|
}
|
||||||
func (o *recordingObserver) OnHandledUpdate(_ context.Context, ev UpdateHandledEvent) {
|
func (o *recordingObserver) OnHandledUpdate(_ context.Context, ev UpdateHandledEvent) {
|
||||||
o.handled = append(o.handled, ev)
|
o.handled = append(o.handled, ev)
|
||||||
}
|
}
|
||||||
@@ -587,6 +590,44 @@ func TestHandleUpdateObserverEmitsUpdateErrors(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHandleObserverCompletesUpdateWhenBotMiddlewareBlocks(t *testing.T) {
|
||||||
|
observer := &recordingObserver{}
|
||||||
|
bot := &Bot[NoData]{
|
||||||
|
logger: sneklog.NewLogger(),
|
||||||
|
observer: observer,
|
||||||
|
middlewares: []Middleware[NoData]{
|
||||||
|
NewMiddleware("block", func(ctx *MsgContext, db NoData) bool {
|
||||||
|
return false
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
bot.handle(context.Background(), &tgapi.Update{
|
||||||
|
UpdateID: 8,
|
||||||
|
Type: tgapi.UpdateTypeMessage,
|
||||||
|
Message: &tgapi.Message{
|
||||||
|
MessageID: 1,
|
||||||
|
Date: 1,
|
||||||
|
Chat: &tgapi.Chat{ID: 42, Type: tgapi.ChatTypePrivate},
|
||||||
|
Text: "/start",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if len(observer.received) != 1 {
|
||||||
|
t.Fatalf("expected one received event, got %d", len(observer.received))
|
||||||
|
}
|
||||||
|
if len(observer.handled) != 1 {
|
||||||
|
t.Fatalf("expected one handled event, got %d", len(observer.handled))
|
||||||
|
}
|
||||||
|
got := observer.handled[0]
|
||||||
|
if got.UpdateID != 8 || got.UpdateType != tgapi.UpdateTypeMessage || got.ChatID != 42 || got.Handled {
|
||||||
|
t.Fatalf("unexpected handled event: %#v", got)
|
||||||
|
}
|
||||||
|
if len(observer.started) != 0 || len(observer.finished) != 0 || len(observer.errors) != 0 {
|
||||||
|
t.Fatalf("middleware block should not emit handler lifecycle or errors: started=%d finished=%d errors=%d", len(observer.started), len(observer.finished), len(observer.errors))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestHandleMessageFallbackRunsAfterCommandMiss(t *testing.T) {
|
func TestHandleMessageFallbackRunsAfterCommandMiss(t *testing.T) {
|
||||||
observer := &recordingObserver{}
|
observer := &recordingObserver{}
|
||||||
called := false
|
called := false
|
||||||
|
|||||||
@@ -64,10 +64,13 @@ func (opts *APIOpts) SetAPIURL(apiURL string) *APIOpts {
|
|||||||
return opts
|
return opts
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetLogFormat sets the output format used by API-managed loggers.
|
||||||
func (opts *APIOpts) SetLogFormat(format utils.LogFormat) *APIOpts {
|
func (opts *APIOpts) SetLogFormat(format utils.LogFormat) *APIOpts {
|
||||||
opts.logFormat = format
|
opts.logFormat = format
|
||||||
return opts
|
return opts
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetLogFormatter sets the formatter used by API-managed logger writers.
|
||||||
func (opts *APIOpts) SetLogFormatter(formatter *sneklog.Formatter) *APIOpts {
|
func (opts *APIOpts) SetLogFormatter(formatter *sneklog.Formatter) *APIOpts {
|
||||||
opts.logFormatter = formatter
|
opts.logFormatter = formatter
|
||||||
return opts
|
return opts
|
||||||
|
|||||||
+5
-2
@@ -6,10 +6,13 @@ import (
|
|||||||
"git.scuroneko.dev/scuroneko/sneklog/v2"
|
"git.scuroneko.dev/scuroneko/sneklog/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// LogFormat selects the writer format used by framework loggers.
|
||||||
type LogFormat string
|
type LogFormat string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
// LogFormatText writes human-readable text logs.
|
||||||
LogFormatText LogFormat = "text"
|
LogFormatText LogFormat = "text"
|
||||||
|
// LogFormatJSON writes structured JSON logs.
|
||||||
LogFormatJSON LogFormat = "json"
|
LogFormatJSON LogFormat = "json"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -22,8 +25,8 @@ func GetLoggerLevel() sneklog.LogLevel {
|
|||||||
return level
|
return level
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateLogger creates a logger with the shared default policy:
|
// CreateLogger creates a logger with stdout output, the provided name, level,
|
||||||
// JSON stdout output, provided prefix, and provided level.
|
// format, and optional formatter.
|
||||||
func CreateLogger(
|
func CreateLogger(
|
||||||
name string, level sneklog.LogLevel,
|
name string, level sneklog.LogLevel,
|
||||||
format LogFormat, formatter *sneklog.Formatter,
|
format LogFormat, formatter *sneklog.Formatter,
|
||||||
|
|||||||
+2
-2
@@ -2,7 +2,7 @@ package utils
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
// VersionString is the module version string.
|
// VersionString is the module version string.
|
||||||
VersionString = "1.0.0-rc.16"
|
VersionString = "1.0.0"
|
||||||
// VersionMajor is the module major version.
|
// VersionMajor is the module major version.
|
||||||
VersionMajor = 1
|
VersionMajor = 1
|
||||||
// VersionMinor is the module minor version.
|
// VersionMinor is the module minor version.
|
||||||
@@ -10,5 +10,5 @@ const (
|
|||||||
// VersionPatch is the module patch version.
|
// VersionPatch is the module patch version.
|
||||||
VersionPatch = 0
|
VersionPatch = 0
|
||||||
// VersionBeta is the prerelease counter for the current version.
|
// VersionBeta is the prerelease counter for the current version.
|
||||||
VersionBeta = 16
|
VersionBeta = 0
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user