mirror of
https://github.com/go-admin-team/go-admin.git
synced 2026-09-22 02:27:57 +00:00
AfterListen promises a hook that the port answers. The bind was moved onto the caller's goroutine to keep that promise, but with ssl enabled there was a second way to fail after the announcement: ServeTLS reads the certificate files itself, on the serving goroutine, so a bad path or an unreadable key surfaced once the hooks had already run. tls.LoadX509KeyPair now runs before anything is announced, and its error is returned from startServing the way a failed bind is. ServeTLS still does the real work - handing it a tls.Listener built here instead would take over the HTTP/2 negotiation it sets up, and quietly drop h2 for every TLS deployment. The cost is one extra read of the certificate at startup. The fatal in the serving goroutine said "listen:". Neither the bind nor the certificate reaches it any more, so it says "serve:". The test covers the certificate path alongside the bind: neither may announce the phase, and neither may seal it. The counter-proof is not clean, and saying so is the point. Removing the check does turn the run red, but through log.Fatal killing the process from the serving goroutine - "fatal serve: open no-such.pem: no such file or directory" - rather than through the assertion. That still demonstrates the defect, because the process could only get there after startServing had returned successfully and the phase had been announced; it cannot be observed from inside the test, because the fatal races the assertion that would report it. Also: the redis-backed queue tests now fail instead of skipping when CI is set and GO_ADMIN_TEST_REDIS_ADDR is not. A workflow that renamed the variable or dropped the service would otherwise stay green while those two tests quietly did nothing - the same shape as the defect they exist to cover. Locally, with no CI in the environment, they still skip.
131 lines
4.4 KiB
Go
131 lines
4.4 KiB
Go
package storage
|
|
|
|
import (
|
|
"errors"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/go-admin-team/go-admin-core/v2/sdk/config"
|
|
corestorage "github.com/go-admin-team/go-admin-core/v2/storage"
|
|
"github.com/go-admin-team/go-admin-core/v2/storage/queue"
|
|
)
|
|
|
|
// redisAddrEnv points these tests at a server. They are skipped without it, so
|
|
// a developer with no redis running still gets a green run - and CI sets it,
|
|
// which is the point: the ordering rule they cover is invisible on the memory
|
|
// backend, and memory is the default. A suite that only ever exercised the
|
|
// default would report success for a queue that silently drops every consumer.
|
|
const redisAddrEnv = "GO_ADMIN_TEST_REDIS_ADDR"
|
|
|
|
func redisAddr(t *testing.T) string {
|
|
t.Helper()
|
|
addr := os.Getenv(redisAddrEnv)
|
|
if addr != "" {
|
|
return addr
|
|
}
|
|
// Skipping locally is the point; skipping in CI is the failure this whole
|
|
// file exists to prevent. A workflow that renamed the variable, or dropped
|
|
// the service, would otherwise go green while these two tests quietly did
|
|
// nothing - which is the same shape as the defect they cover.
|
|
if os.Getenv("CI") != "" {
|
|
t.Fatalf("%s is not set while CI is: the redis-backed queue tests must not skip here", redisAddrEnv)
|
|
}
|
|
t.Skipf("%s is not set; skipping the redis-backed queue tests", redisAddrEnv)
|
|
return ""
|
|
}
|
|
|
|
// newRedisQueue builds the queue the same way setupQueue does - through
|
|
// config.QueueConfig.Setup - so that what is under test is the adapter this
|
|
// repository actually gets, LegacyQueueAdapter and all, rather than a redis
|
|
// client wired up by the test.
|
|
func newRedisQueue(t *testing.T, prefix string) corestorage.AdapterQueue {
|
|
t.Helper()
|
|
previous := config.QueueConfig
|
|
t.Cleanup(func() { config.QueueConfig = previous })
|
|
|
|
config.QueueConfig = &config.Queue{
|
|
Redis: &config.RedisQueue{
|
|
RedisOptions: config.RedisOptions{Addr: redisAddr(t)},
|
|
Group: prefix,
|
|
KeyPrefix: prefix,
|
|
},
|
|
}
|
|
q, err := config.QueueConfig.Setup()
|
|
if err != nil {
|
|
t.Fatalf("queue setup: %v", err)
|
|
}
|
|
t.Cleanup(q.Shutdown)
|
|
return q
|
|
}
|
|
|
|
func message(t *testing.T, stream string) corestorage.Messager {
|
|
t.Helper()
|
|
m := &queue.Message{}
|
|
m.SetStream(stream)
|
|
m.SetValues(map[string]interface{}{"hello": "world"})
|
|
return m
|
|
}
|
|
|
|
// Registered first, then started: the consumer gets the message. This is the
|
|
// order setupQueue and attachQueueConsumers now produce between them.
|
|
func TestRedisQueueDeliversToAConsumerRegisteredBeforeTheStart(t *testing.T) {
|
|
stream := "t-ordered"
|
|
q := newRedisQueue(t, "gotest-ordered")
|
|
|
|
got := make(chan struct{}, 1)
|
|
q.Register(stream, func(corestorage.Messager) error {
|
|
select {
|
|
case got <- struct{}{}:
|
|
default:
|
|
}
|
|
return nil
|
|
})
|
|
go q.Run()
|
|
|
|
// Give Start a moment to reach its read loop before publishing.
|
|
time.Sleep(500 * time.Millisecond)
|
|
if err := q.Append(message(t, stream)); err != nil {
|
|
t.Fatalf("append: %v", err)
|
|
}
|
|
|
|
select {
|
|
case <-got:
|
|
case <-time.After(15 * time.Second):
|
|
t.Fatal("the consumer never received the message")
|
|
}
|
|
}
|
|
|
|
// Started first, then registered: the registration is refused and every
|
|
// publish afterwards fails.
|
|
//
|
|
// Subscribe answers ErrQueueAlreadyStarted, and LegacyQueueAdapter.Register
|
|
// returns nothing, so the caller cannot know - that part is silent. What is not
|
|
// silent is the consequence: no consumer group was created, so Publish refuses
|
|
// the topic with ErrNoHandler on every single request, and go-admin's call
|
|
// sites log that at error level while the login and operation log rows are
|
|
// never written.
|
|
//
|
|
// This is the test the memory backend cannot provide. queue.Memory's Register
|
|
// starts another consumer goroutine whatever the state, so the same code passes
|
|
// there - which is how the defect survived, memory being the default.
|
|
func TestRedisQueueRefusesAConsumerRegisteredAfterTheStart(t *testing.T) {
|
|
stream := "t-late"
|
|
q := newRedisQueue(t, "gotest-late")
|
|
|
|
go q.Run()
|
|
time.Sleep(500 * time.Millisecond)
|
|
|
|
q.Register(stream, func(corestorage.Messager) error { return nil })
|
|
|
|
err := q.Append(message(t, stream))
|
|
if err == nil {
|
|
t.Fatal("a message was accepted for a topic whose registration came after Start; " +
|
|
"if the backend now accepts late registration, the ordering rule in setupQueue can be revisited")
|
|
}
|
|
if !errors.Is(err, corestorage.ErrNoHandler) {
|
|
t.Fatalf("append failed with %v, want %v - the test is meant to pin the "+
|
|
"missing-consumer path, not any error at all", err, corestorage.ErrNoHandler)
|
|
}
|
|
}
|