diff --git a/internal/bitbucket/server.go b/internal/bitbucket/server.go index c636f34..0c0e05d 100644 --- a/internal/bitbucket/server.go +++ b/internal/bitbucket/server.go @@ -247,6 +247,12 @@ func (a *adapter) Connect(ctx context.Context, deps core.AdapterDeps) error { probeCtx, cancel := context.WithTimeout(ctx, a.selfResolveBudget) self, resolveErr := a.fl.resolveSelf(probeCtx) cancel() + // An empty identity with no error (e.g. whoami returns a blank uuid) + // is as dangerous as a failure: isSelf would never match, silently + // disabling the reply-loop guard. Treat it as fatal, like GitLab does. + if resolveErr == nil && self == "" { + resolveErr = fmt.Errorf("%w: whoami returned an empty identity", ErrMissingConfig) + } if resolveErr != nil { _ = ln.Close() // Serve never runs, so nothing else closes it if ctx.Err() == nil { // a canceled startup is a shutdown, not a failure diff --git a/internal/bitbucket/server_test.go b/internal/bitbucket/server_test.go index 30e1dd2..5144152 100644 --- a/internal/bitbucket/server_test.go +++ b/internal/bitbucket/server_test.go @@ -382,6 +382,27 @@ func TestConnectWhoamiFailureFatal(t *testing.T) { } } +// A whoami that succeeds but returns an empty identity is as fatal as an error: +// an empty self silently disables the reply-loop guard, so the bot must not serve. +func TestConnectWhoamiEmptyIdentityFatal(t *testing.T) { + a := newCloudAdapter(t) + a.fl = &stubFlavor{} // self == "", err == nil + done := make(chan error, 1) + deps := core.AdapterDeps{ + Dispatch: func(context.Context, *core.Message) {}, + Done: func(err error) { done <- err }, + Disconnect: func() error { return a.Disconnect() }, + Logger: a.logger, + } + asserts.NoError(t, a.Connect(context.Background(), deps), "connect returns (probe is async)") + select { + case err := <-done: + asserts.ErrorIs(t, err, ErrMissingConfig, "empty identity reported via Done") + case <-time.After(2 * time.Second): + t.Fatal("Done not called on empty identity") + } +} + // A hanging whoami is bounded by selfResolveBudget, so Connect does not leave the // listener bound forever. func TestConnectWhoamiBudget(t *testing.T) {