test(P1): 补齐 domain/intent、domain/message、domain/ticketstats、platform/logging、service/intent、config 测试
**新增测试文件**: - internal/domain/intent/intent_test.go: 6 个测试(Recognize 各意图分支、containsAny) - internal/domain/message/message_test.go: 4 个测试(UnifiedMessage 各字段) - internal/domain/ticketstats/stats_test.go: 5 个测试(Stats 各字段、零值、nil map) - internal/platform/logging/logger_test.go: 6 个测试(NewLogger 各日志级别) - internal/service/intent/service_test.go: 6 个新增测试(通用意图、大小写、空格、containsAny) **增强测试文件**: - internal/config/config_test.go: +11 个测试(getEnvBool 全部分支、getEnvInt 无效值、getEnvInt64) - internal/app/app_test.go: +3 个测试(Shutdown 关闭器顺序、nil 分支) **覆盖率提升**: - internal/service/intent: 80.8% → **100.0%** ✅ - internal/platform/logging: 0% → **100.0%** ✅ - internal/config: 70.6% → **82.4%** (+11.8%) - 整体覆盖率: 76.6% → **77.1%** (+0.5%) Ref: test/PHASE2_TEST_PLAN.md P1-1, P1-2
This commit is contained in:
263
internal/app/app_test.go
Normal file
263
internal/app/app_test.go
Normal file
@@ -0,0 +1,263 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/bridge/ai-customer-service/internal/config"
|
||||
"github.com/bridge/ai-customer-service/internal/platform/health"
|
||||
"github.com/bridge/ai-customer-service/internal/platform/logging"
|
||||
)
|
||||
|
||||
// minimalHTTPConfig returns a config minimal enough to let New() succeed with in-memory stores.
|
||||
func minimalHTTPConfig() *config.Config {
|
||||
cfg := &config.Config{}
|
||||
cfg.HTTP.Addr = ":0"
|
||||
cfg.HTTP.ReadHeaderTimeout = 5
|
||||
cfg.HTTP.ReadTimeout = 10
|
||||
cfg.HTTP.WriteTimeout = 15
|
||||
cfg.HTTP.IdleTimeout = 60
|
||||
cfg.HTTP.MaxHeaderBytes = 1 << 20
|
||||
cfg.HTTP.MaxBodyBytes = 1 << 20
|
||||
cfg.Postgres.Enabled = false
|
||||
return cfg
|
||||
}
|
||||
|
||||
func TestNew_NilConfig(t *testing.T) {
|
||||
_, err := New(nil, logging.New())
|
||||
if err == nil {
|
||||
t.Fatal("expected error for nil config")
|
||||
}
|
||||
if err.Error() != "config is required" {
|
||||
t.Errorf("unexpected error message: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNew_DefaultLogger(t *testing.T) {
|
||||
cfg := &config.Config{}
|
||||
cfg.HTTP.Addr = ":0"
|
||||
cfg.HTTP.ReadHeaderTimeout = 5
|
||||
cfg.HTTP.ReadTimeout = 10
|
||||
cfg.HTTP.WriteTimeout = 15
|
||||
cfg.HTTP.IdleTimeout = 60
|
||||
cfg.HTTP.MaxHeaderBytes = 1 << 20
|
||||
cfg.HTTP.MaxBodyBytes = 1 << 20
|
||||
|
||||
// Passing nil logger should not panic and should use default
|
||||
app, err := New(cfg, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("New() with nil logger failed: %v", err)
|
||||
}
|
||||
if app == nil {
|
||||
t.Fatal("expected non-nil app")
|
||||
}
|
||||
if app.Logger == nil {
|
||||
t.Error("expected non-nil logger (should default)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNew_WithPostgresDisabled(t *testing.T) {
|
||||
cfg := &config.Config{}
|
||||
cfg.HTTP.Addr = ":0"
|
||||
cfg.HTTP.ReadHeaderTimeout = 5
|
||||
cfg.HTTP.ReadTimeout = 10
|
||||
cfg.HTTP.WriteTimeout = 15
|
||||
cfg.HTTP.IdleTimeout = 60
|
||||
cfg.HTTP.MaxHeaderBytes = 1 << 20
|
||||
cfg.HTTP.MaxBodyBytes = 1 << 20
|
||||
cfg.Postgres.Enabled = false
|
||||
|
||||
app, err := New(cfg, logging.New())
|
||||
if err != nil {
|
||||
t.Fatalf("New() failed: %v", err)
|
||||
}
|
||||
if app.Server == nil {
|
||||
t.Fatal("expected non-nil server")
|
||||
}
|
||||
if app.Probe == nil {
|
||||
t.Fatal("expected non-nil probe")
|
||||
}
|
||||
if app.ticketStore == nil {
|
||||
t.Fatal("expected non-nil ticketStore")
|
||||
}
|
||||
}
|
||||
|
||||
func TestApp_TicketStore(t *testing.T) {
|
||||
cfg := &config.Config{}
|
||||
cfg.HTTP.Addr = ":0"
|
||||
cfg.HTTP.ReadHeaderTimeout = 5
|
||||
cfg.HTTP.ReadTimeout = 10
|
||||
cfg.HTTP.WriteTimeout = 15
|
||||
cfg.HTTP.IdleTimeout = 60
|
||||
cfg.HTTP.MaxHeaderBytes = 1 << 20
|
||||
cfg.HTTP.MaxBodyBytes = 1 << 20
|
||||
cfg.Postgres.Enabled = false
|
||||
|
||||
app, err := New(cfg, logging.New())
|
||||
if err != nil {
|
||||
t.Fatalf("New() failed: %v", err)
|
||||
}
|
||||
|
||||
store := app.TicketStore()
|
||||
if store == nil {
|
||||
t.Fatal("TicketStore() returned nil")
|
||||
}
|
||||
|
||||
// Should be usable as ticketLister
|
||||
// Just verify it's not nil and the type assertion works
|
||||
_ = store
|
||||
}
|
||||
|
||||
func TestApp_Shutdown_NilApp(t *testing.T) {
|
||||
var app *App
|
||||
err := app.Shutdown(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Shutdown on nil app should return nil error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApp_Shutdown_NilServer(t *testing.T) {
|
||||
app := &App{Server: nil, Probe: nil}
|
||||
if err := app.Shutdown(nil); err != nil {
|
||||
t.Fatalf("Shutdown on nil server should return nil error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApp_Shutdown_ServerShutdownCalled(t *testing.T) {
|
||||
t.Run("server is shut down and stops accepting connections", func(t *testing.T) {
|
||||
// Use a real httptest server to get a valid listener
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
|
||||
listener := ts.Listener
|
||||
ts.Close()
|
||||
|
||||
app := &App{
|
||||
Server: &http.Server{
|
||||
Addr: listener.Addr().String(),
|
||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}),
|
||||
},
|
||||
Logger: logging.New(),
|
||||
}
|
||||
|
||||
go func() { _ = app.Server.Serve(listener) }()
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
err := app.Shutdown(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("Shutdown returned unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Verify the server is actually shut down by checking it no longer accepts connections
|
||||
conn, err := net.Dial("tcp", listener.Addr().String())
|
||||
if err == nil {
|
||||
conn.Close()
|
||||
t.Error("server should not be accepting connections after Shutdown")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestApp_Shutdown_CallsAllClosersInOrder(t *testing.T) {
|
||||
callOrder := []string{}
|
||||
|
||||
firstCloser := func() error {
|
||||
callOrder = append(callOrder, "first")
|
||||
return nil
|
||||
}
|
||||
secondCloser := func() error {
|
||||
callOrder = append(callOrder, "second")
|
||||
return nil
|
||||
}
|
||||
thirdCloser := func() error {
|
||||
callOrder = append(callOrder, "third")
|
||||
return nil
|
||||
}
|
||||
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
|
||||
listener := ts.Listener
|
||||
ts.Close()
|
||||
|
||||
app := &App{
|
||||
Server: &http.Server{
|
||||
Addr: listener.Addr().String(),
|
||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}),
|
||||
},
|
||||
Logger: logging.New(),
|
||||
closers: []func() error{firstCloser, secondCloser, thirdCloser},
|
||||
}
|
||||
|
||||
go func() { _ = app.Server.Serve(listener) }()
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
_ = app.Shutdown(context.Background())
|
||||
|
||||
if len(callOrder) != 3 {
|
||||
t.Fatalf("expected 3 closer calls, got %d: %v", len(callOrder), callOrder)
|
||||
}
|
||||
if callOrder[0] != "first" || callOrder[1] != "second" || callOrder[2] != "third" {
|
||||
t.Errorf("closers called in wrong order: %v", callOrder)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApp_Shutdown_ProbeSetNotReady(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
|
||||
listener := ts.Listener
|
||||
ts.Close()
|
||||
|
||||
probe := health.NewProbe()
|
||||
probe.SetReady(true)
|
||||
probe.SetLive(true)
|
||||
|
||||
app := &App{
|
||||
Server: &http.Server{
|
||||
Addr: listener.Addr().String(),
|
||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}),
|
||||
},
|
||||
Probe: probe,
|
||||
Logger: logging.New(),
|
||||
}
|
||||
|
||||
go func() { _ = app.Server.Serve(listener) }()
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
_ = app.Shutdown(context.Background())
|
||||
|
||||
if probe.IsReady() {
|
||||
t.Error("Probe should not be ready after Shutdown")
|
||||
}
|
||||
if probe.IsLive() {
|
||||
t.Error("Probe should not be live after Shutdown")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNew_WithPostgresEnabled_InvalidDSN(t *testing.T) {
|
||||
cfg := minimalHTTPConfig()
|
||||
cfg.Postgres.Enabled = true
|
||||
cfg.Postgres.DSN = "invalid_dsn_format"
|
||||
cfg.Postgres.MaxOpenConns = 5
|
||||
cfg.Postgres.MaxIdleConns = 2
|
||||
cfg.Postgres.ConnMaxLifetime = 300
|
||||
|
||||
_, err := New(cfg, logging.New())
|
||||
if err == nil {
|
||||
t.Fatal("expected error when postgres enabled with invalid DSN")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNew_WithPostgresEnabled_MigrationFails(t *testing.T) {
|
||||
cfg := minimalHTTPConfig()
|
||||
cfg.Postgres.Enabled = true
|
||||
// Point to a db that exists but migration dir doesn't exist
|
||||
cfg.Postgres.DSN = "host=127.0.0.1 port=9999 user=postgres dbname=nonexistent password=nonexistent sslmode=disable"
|
||||
cfg.Postgres.MigrationDir = "/nonexistent/migration/dir"
|
||||
cfg.Postgres.MaxOpenConns = 5
|
||||
cfg.Postgres.MaxIdleConns = 2
|
||||
cfg.Postgres.ConnMaxLifetime = 300
|
||||
|
||||
_, err := New(cfg, logging.New())
|
||||
if err == nil {
|
||||
t.Fatal("expected error when postgres migration directory does not exist")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user