diff --git a/projects/ai-customer-service/internal/app/app_test.go b/projects/ai-customer-service/internal/app/app_test.go new file mode 100644 index 00000000..93da7e76 --- /dev/null +++ b/projects/ai-customer-service/internal/app/app_test.go @@ -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") + } +} diff --git a/projects/ai-customer-service/internal/config/config_test.go b/projects/ai-customer-service/internal/config/config_test.go new file mode 100644 index 00000000..50f0093c --- /dev/null +++ b/projects/ai-customer-service/internal/config/config_test.go @@ -0,0 +1,115 @@ +package config + +import "testing" + +func TestGetEnvBool_True(t *testing.T) { + t.Setenv("TEST_BOOL", "true") + got := getEnvBool("TEST_BOOL", false) + if !got { + t.Error("getEnvBool(true) = false, want true") + } +} + +func TestGetEnvBool_TrueCaseInsensitive(t *testing.T) { + t.Setenv("TEST_BOOL", "TRUE") + got := getEnvBool("TEST_BOOL", false) + if !got { + t.Error("getEnvBool(TRUE) = false, want true") + } +} + +func TestGetEnvBool_False(t *testing.T) { + t.Setenv("TEST_BOOL", "false") + got := getEnvBool("TEST_BOOL", true) + if got { + t.Error("getEnvBool(false) = true, want false") + } +} + +func TestGetEnvBool_One(t *testing.T) { + t.Setenv("TEST_BOOL", "1") + got := getEnvBool("TEST_BOOL", false) + if !got { + t.Error("getEnvBool(1) = false, want true") + } +} + +func TestGetEnvBool_Zero(t *testing.T) { + t.Setenv("TEST_BOOL", "0") + got := getEnvBool("TEST_BOOL", true) + if got { + t.Error("getEnvBool(0) = true, want false") + } +} + +func TestGetEnvBool_InvalidValue(t *testing.T) { + t.Setenv("TEST_BOOL", "yes") + got := getEnvBool("TEST_BOOL", true) + if !got { + t.Error("getEnvBool(yes) did not return fallback, got false, want true") + } +} + +func TestGetEnvInt_ValidValue(t *testing.T) { + t.Setenv("TEST_INT", "999") + got := getEnvInt("TEST_INT", 5) + if got != 999 { + t.Errorf("getEnvInt(TEST_INT) = %d, want 999", got) + } +} + +func TestGetEnvInt_InvalidValue(t *testing.T) { + t.Setenv("TEST_INT", "notanumber") + got := getEnvInt("TEST_INT", 42) + if got != 42 { + t.Errorf("getEnvInt(invalid) = %d, want fallback 42", got) + } +} + +func TestGetEnvInt64_ValidValue(t *testing.T) { + t.Setenv("TEST_INT64", "12345678901234") + got := getEnvInt64("TEST_INT64", 0) + if got != 12345678901234 { + t.Errorf("getEnvInt64(TEST_INT64) = %d, want 12345678901234", got) + } +} + +func TestLoadDefaults(t *testing.T) { + t.Setenv("AI_CS_ADDR", "") + cfg, err := Load() + if err != nil { + t.Fatalf("Load() error = %v", err) + } + if cfg.HTTP.Addr != ":8080" { + t.Fatalf("addr = %s, want :8080", cfg.HTTP.Addr) + } + if cfg.HTTP.MaxBodyBytes <= 0 { + t.Fatalf("expected positive max body bytes") + } + if cfg.Webhook.TimestampHeader != "X-CS-Timestamp" { + t.Fatalf("timestamp header = %s", cfg.Webhook.TimestampHeader) + } +} + +func TestLoadOverride(t *testing.T) { + t.Setenv("AI_CS_ADDR", ":18080") + t.Setenv("AI_CS_MAX_BODY_BYTES", "2048") + t.Setenv("AI_CS_WEBHOOK_SECRET", "secret") + t.Setenv("AI_CS_WEBHOOK_MAX_SKEW_SECONDS", "60") + cfg, err := Load() + if err != nil { + t.Fatalf("Load() error = %v", err) + } + if cfg.HTTP.Addr != ":18080" { + t.Fatalf("addr = %s, want :18080", cfg.HTTP.Addr) + } + if cfg.HTTP.MaxBodyBytes != 2048 { + t.Fatalf("max body bytes = %d, want 2048", cfg.HTTP.MaxBodyBytes) + } + if cfg.Webhook.Secret != "secret" { + t.Fatalf("expected webhook secret") + } + if cfg.Webhook.MaxSkewSeconds != 60 { + t.Fatalf("skew = %d, want 60", cfg.Webhook.MaxSkewSeconds) + } +} diff --git a/projects/ai-customer-service/internal/domain/intent/intent_test.go b/projects/ai-customer-service/internal/domain/intent/intent_test.go new file mode 100644 index 00000000..81980431 --- /dev/null +++ b/projects/ai-customer-service/internal/domain/intent/intent_test.go @@ -0,0 +1,70 @@ +package intent + +import "testing" + +func TestResult_Fields(t *testing.T) { + r := Result{ + Intent: IntentQuota, + Confidence: 0.95, + Entities: map[string]string{"key": "value"}, + NeedsHuman: false, + Sensitive: false, + } + if r.Intent != IntentQuota { + t.Errorf("Intent = %q, want %q", r.Intent, IntentQuota) + } + if r.Confidence != 0.95 { + t.Errorf("Confidence = %f, want 0.95", r.Confidence) + } + if r.NeedsHuman { + t.Error("NeedsHuman = true, want false") + } +} + +func TestResult_NeedsHuman(t *testing.T) { + r := Result{NeedsHuman: true} + if !r.NeedsHuman { + t.Error("NeedsHuman = false, want true") + } +} + +func TestResult_Sensitive(t *testing.T) { + r := Result{Sensitive: true} + if !r.Sensitive { + t.Error("Sensitive = false, want true") + } +} + +func TestResult_EntitiesMap(t *testing.T) { + r := Result{ + Intent: IntentGeneral, + Entities: map[string]string{"user": "alice", "action": "refund"}, + } + if len(r.Entities) != 2 { + t.Errorf("len(Entities) = %d, want 2", len(r.Entities)) + } + if r.Entities["user"] != "alice" { + t.Errorf("Entities[user] = %q, want %q", r.Entities["user"], "alice") + } +} + +func TestIntentConstants(t *testing.T) { + intents := []string{IntentQuota, IntentToken, IntentError, IntentHandoff, IntentGeneral, IntentRefund, IntentSecurity} + for _, intent := range intents { + if intent == "" { + t.Errorf("intent constant is empty string") + } + } +} + +func TestIntentQuota(t *testing.T) { + if IntentQuota != "quota" { + t.Errorf("IntentQuota = %q, want %q", IntentQuota, "quota") + } +} + +func TestIntentHandoff(t *testing.T) { + if IntentHandoff != "handoff" { + t.Errorf("IntentHandoff = %q, want %q", IntentHandoff, "handoff") + } +} diff --git a/projects/ai-customer-service/internal/domain/message/message_test.go b/projects/ai-customer-service/internal/domain/message/message_test.go new file mode 100644 index 00000000..6fa669d3 --- /dev/null +++ b/projects/ai-customer-service/internal/domain/message/message_test.go @@ -0,0 +1,76 @@ +package message + +import ( + "testing" + "time" +) + +func TestUnifiedMessage_Fields(t *testing.T) { + now := time.Now() + msg := UnifiedMessage{ + MessageID: "msg_123", + Channel: "widget", + OpenID: "user_456", + UserID: "internal_789", + Content: "hello world", + ContentType: "text/plain", + Timestamp: now, + ReplyTo: "parent_msg", + } + if msg.MessageID != "msg_123" { + t.Errorf("MessageID = %q, want %q", msg.MessageID, "msg_123") + } + if msg.Channel != "widget" { + t.Errorf("Channel = %q, want %q", msg.Channel, "widget") + } + if msg.Content != "hello world" { + t.Errorf("Content = %q, want %q", msg.Content, "hello world") + } + if msg.ReplyTo != "parent_msg" { + t.Errorf("ReplyTo = %q, want %q", msg.ReplyTo, "parent_msg") + } +} + +func TestUnifiedMessage_OptionalFields(t *testing.T) { + msg := UnifiedMessage{ + MessageID: "msg_1", + Channel: "web", + OpenID: "u1", + Content: "hi", + } + if msg.UserID != "" { + t.Errorf("UserID = %q, want empty", msg.UserID) + } + if msg.ContentType != "" { + t.Errorf("ContentType = %q, want empty", msg.ContentType) + } + if msg.ReplyTo != "" { + t.Errorf("ReplyTo = %q, want empty", msg.ReplyTo) + } +} + +func TestUnifiedMessage_Timestamp(t *testing.T) { + now := time.Now() + msg := UnifiedMessage{ + MessageID: "msg_1", + Channel: "widget", + OpenID: "u1", + Content: "test", + Timestamp: now, + } + if msg.Timestamp.IsZero() { + t.Error("Timestamp is zero, want time.Time") + } +} + +func TestUnifiedMessage_EmptyContent(t *testing.T) { + msg := UnifiedMessage{ + MessageID: "msg_1", + Channel: "widget", + OpenID: "u1", + Content: "", + } + if msg.Content != "" { + t.Errorf("Content = %q, want empty", msg.Content) + } +} diff --git a/projects/ai-customer-service/internal/domain/ticketstats/stats_test.go b/projects/ai-customer-service/internal/domain/ticketstats/stats_test.go new file mode 100644 index 00000000..a2def4ac --- /dev/null +++ b/projects/ai-customer-service/internal/domain/ticketstats/stats_test.go @@ -0,0 +1,78 @@ +package ticketstats + +import "testing" + +func TestStats_Fields(t *testing.T) { + stats := Stats{ + Total: 100, + Open: 30, + Resolved: 50, + Closed: 20, + ByChannel: map[string]int{"widget": 60, "web": 40}, + ByPriority: map[string]int{"P1": 10, "P2": 30, "P3": 60}, + HandoffCount: 5, + AvgResolutionTimeMinutes: 42.5, + } + if stats.Total != 100 { + t.Errorf("Total = %d, want 100", stats.Total) + } + if stats.Open != 30 { + t.Errorf("Open = %d, want 30", stats.Open) + } + if stats.Resolved != 50 { + t.Errorf("Resolved = %d, want 50", stats.Resolved) + } + if stats.Closed != 20 { + t.Errorf("Closed = %d, want 20", stats.Closed) + } + if stats.HandoffCount != 5 { + t.Errorf("HandoffCount = %d, want 5", stats.HandoffCount) + } + if stats.AvgResolutionTimeMinutes != 42.5 { + t.Errorf("AvgResolutionTimeMinutes = %f, want 42.5", stats.AvgResolutionTimeMinutes) + } +} + +func TestStats_ByChannel(t *testing.T) { + stats := Stats{ByChannel: map[string]int{"widget": 10, "api": 20}} + if stats.ByChannel["widget"] != 10 { + t.Errorf("ByChannel[widget] = %d, want 10", stats.ByChannel["widget"]) + } + if stats.ByChannel["api"] != 20 { + t.Errorf("ByChannel[api] = %d, want 20", stats.ByChannel["api"]) + } +} + +func TestStats_ByPriority(t *testing.T) { + stats := Stats{ByPriority: map[string]int{"P1": 5, "P2": 15, "P3": 80}} + if stats.ByPriority["P1"] != 5 { + t.Errorf("ByPriority[P1] = %d, want 5", stats.ByPriority["P1"]) + } +} + +func TestStats_ZeroValues(t *testing.T) { + stats := Stats{} + if stats.Total != 0 { + t.Errorf("Total = %d, want 0", stats.Total) + } + if stats.Open != 0 { + t.Errorf("Open = %d, want 0", stats.Open) + } + if stats.AvgResolutionTimeMinutes != 0 { + t.Errorf("AvgResolutionTimeMinutes = %f, want 0", stats.AvgResolutionTimeMinutes) + } + if stats.ByChannel != nil && len(stats.ByChannel) != 0 { + t.Errorf("ByChannel = %v, want nil or empty", stats.ByChannel) + } +} + +func TestStats_NilMaps(t *testing.T) { + stats := Stats{Total: 0} + // ByChannel and ByPriority may be nil (zero value of map) + if stats.ByChannel == nil && len(stats.ByChannel) == 0 { + // nil map is valid + } + if stats.ByPriority == nil && len(stats.ByPriority) == 0 { + // nil map is valid + } +} diff --git a/projects/ai-customer-service/internal/platform/logging/logger_test.go b/projects/ai-customer-service/internal/platform/logging/logger_test.go new file mode 100644 index 00000000..a7feaf8a --- /dev/null +++ b/projects/ai-customer-service/internal/platform/logging/logger_test.go @@ -0,0 +1,42 @@ +package logging + +import ( + "log/slog" + "testing" +) + +func TestNew_ReturnsNonNil(t *testing.T) { + logger := New() + if logger == nil { + t.Fatal("New() returned nil") + } +} + +func TestNew_ReturnsSlogLogger(t *testing.T) { + logger := New() + if logger == nil { + t.Fatal("logger is nil") + } + // Verify it's a *slog.Logger by using it + var _ *slog.Logger = logger +} + +func TestNew_InfoLevel(t *testing.T) { + logger := New() + logger.Info("test info message") +} + +func TestNew_WithAttr(t *testing.T) { + logger := New() + logger.Info("test with attrs", slog.String("key", "value")) +} + +func TestNew_Error(t *testing.T) { + logger := New() + logger.Error("test error message") +} + +func TestNew_Debug(t *testing.T) { + logger := New() + logger.Debug("test debug message") +} diff --git a/projects/ai-customer-service/internal/service/intent/service_test.go b/projects/ai-customer-service/internal/service/intent/service_test.go new file mode 100644 index 00000000..a5d99ec2 --- /dev/null +++ b/projects/ai-customer-service/internal/service/intent/service_test.go @@ -0,0 +1,94 @@ +package intent + +import ( + "context" + "testing" +) + +func TestRecognize(t *testing.T) { + tests := []struct { + name string + input string + intent string + handoff bool + }{ + {name: "quota", input: "我的配额还剩多少", intent: "quota", handoff: false}, + {name: "token", input: "今天 token 消耗是多少", intent: "token", handoff: false}, + {name: "refund", input: "我要申请退款", intent: "refund", handoff: true}, + {name: "security", input: "我的数据可能泄露了", intent: "security", handoff: true}, + {name: "general_default", input: "你好", intent: "general", handoff: false}, + {name: "error_keyword", input: "系统报错啦", intent: "error", handoff: false}, + {name: "handoff_human", input: "转人工客服", intent: "handoff", handoff: true}, + {name: "security_attack", input: "遭到攻击了", intent: "security", handoff: true}, + } + + svc := NewService() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := svc.Recognize(context.Background(), "s1", tt.input, nil) + if err != nil { + t.Fatalf("Recognize() error = %v", err) + } + if got.Intent != tt.intent { + t.Fatalf("intent = %s, want %s", got.Intent, tt.intent) + } + if got.NeedsHuman != tt.handoff { + t.Fatalf("NeedsHuman = %v, want %v", got.NeedsHuman, tt.handoff) + } + }) + } +} + +func TestRecognize_GeneralIntent(t *testing.T) { + svc := NewService() + got, err := svc.Recognize(context.Background(), "s1", "今天天气不错", nil) + if err != nil { + t.Fatalf("Recognize() error = %v", err) + } + if got.Intent != "general" { + t.Errorf("intent = %s, want general", got.Intent) + } + if got.Confidence != 0.65 { + t.Errorf("Confidence = %f, want 0.65", got.Confidence) + } +} + +func TestRecognize_CaseInsensitive(t *testing.T) { + svc := NewService() + got, err := svc.Recognize(context.Background(), "s1", "REFUND", nil) + if err != nil { + t.Fatalf("Recognize() error = %v", err) + } + if got.Intent != "refund" { + t.Errorf("intent = %s, want refund (case insensitive)", got.Intent) + } +} + +func TestRecognize_WhitespaceTrimmed(t *testing.T) { + svc := NewService() + got, err := svc.Recognize(context.Background(), "s1", " 退款 ", nil) + if err != nil { + t.Fatalf("Recognize() error = %v", err) + } + if got.Intent != "refund" { + t.Errorf("intent = %s, want refund (whitespace trimmed)", got.Intent) + } +} + +func TestContainsAny_Found(t *testing.T) { + if !containsAny("hello world", "world") { + t.Error("containsAny(hello world, world) = false, want true") + } +} + +func TestContainsAny_NotFound(t *testing.T) { + if containsAny("hello world", "foo") { + t.Error("containsAny(hello world, foo) = true, want false") + } +} + +func TestContainsAny_EmptyTerms(t *testing.T) { + if containsAny("hello", "world") { + t.Error("containsAny(hello, world) = true, want false") + } +}