258 lines
7.9 KiB
Go
258 lines
7.9 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func validConfig() Config {
|
|
return Config{
|
|
GLPIURL: "https://glpi.internal.example",
|
|
GLPIClientID: "client-id",
|
|
GLPIClientSecret: "real-secret-value",
|
|
GLPIUsername: "svc-agent",
|
|
GLPIPassword: "real-password-value",
|
|
GLPIAllowedStatusIDs: []int64{1},
|
|
WebAllowAnonymous: true,
|
|
Workers: 1,
|
|
QueueSize: 1,
|
|
CategoryConfidence: .9,
|
|
ReplyConfidence: .9,
|
|
KnowledgeMinScore: .8,
|
|
KnowledgeAllowedSources: []string{"internal-kb"},
|
|
KnowledgeAutoReplySources: []string{"internal-kb"},
|
|
CommunicationLanguage: "de-DE",
|
|
CommunicationStyle: "formal",
|
|
OllamaTimeout: time.Minute,
|
|
OllamaNumPredict: 256,
|
|
OllamaKeepAlive: 10 * time.Minute,
|
|
OllamaThink: false,
|
|
OllamaMaxConcurrent: 1,
|
|
}
|
|
}
|
|
|
|
func TestValidateRejectsPlainHTTPByDefault(t *testing.T) {
|
|
c := validConfig()
|
|
c.GLPIURL = "http://glpi.internal.example"
|
|
if err := c.Validate(); err == nil {
|
|
t.Fatal("expected plain HTTP to be rejected")
|
|
}
|
|
c.GLPIAllowInsecureHTTP = true
|
|
if err := c.Validate(); err != nil {
|
|
t.Fatalf("explicit insecure override should validate: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateRejectsPlaceholderSecrets(t *testing.T) {
|
|
c := validConfig()
|
|
c.GLPIClientSecret = "CHANGE_ME"
|
|
if err := c.Validate(); err == nil {
|
|
t.Fatal("expected placeholder secret to be rejected")
|
|
}
|
|
}
|
|
|
|
func TestValidateRequiresAllowedStatus(t *testing.T) {
|
|
c := validConfig()
|
|
c.GLPIAllowedStatusIDs = nil
|
|
if err := c.Validate(); err == nil {
|
|
t.Fatal("expected empty status whitelist to be rejected")
|
|
}
|
|
}
|
|
|
|
func TestValidateRejectsAutoReplySourceOutsideAllowlist(t *testing.T) {
|
|
c := validConfig()
|
|
c.KnowledgeAutoReplySources = []string{"vendor-docs"}
|
|
if err := c.Validate(); err == nil {
|
|
t.Fatal("expected auto-reply source outside allowlist to be rejected")
|
|
}
|
|
}
|
|
|
|
func TestValidateRejectsUnknownCommunicationStyle(t *testing.T) {
|
|
c := validConfig()
|
|
c.CommunicationStyle = "super-friendly"
|
|
if err := c.Validate(); err == nil {
|
|
t.Fatal("expected unsupported communication style to be rejected")
|
|
}
|
|
}
|
|
|
|
func TestValidateRequiresMajorIncidentFilter(t *testing.T) {
|
|
c := validConfig()
|
|
c.ContextEnabled = true
|
|
c.ContextTimeout = 1
|
|
c.MajorIncidentsEnabled = true
|
|
c.GLPIMajorIncidentLimit = 20
|
|
if err := c.Validate(); err == nil {
|
|
t.Fatal("expected major incident filter to be required")
|
|
}
|
|
}
|
|
|
|
func TestValidateUptimeKumaMetricsRequiresAPIKey(t *testing.T) {
|
|
c := validConfig()
|
|
c.ContextEnabled = true
|
|
c.ContextTimeout = 1
|
|
c.UptimeKumaEnabled = true
|
|
c.UptimeKumaURL = "https://uptime.internal.example"
|
|
c.UptimeKumaMode = "metrics"
|
|
c.UptimeKumaMaxIssues = 20
|
|
if err := c.Validate(); err == nil {
|
|
t.Fatal("expected Uptime Kuma API key to be required in metrics mode")
|
|
}
|
|
c.UptimeKumaAPIKey = "test-api-key"
|
|
if err := c.Validate(); err != nil {
|
|
t.Fatalf("expected metrics mode with API key to validate: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateUserDeviceFilterRequiresUserPlaceholder(t *testing.T) {
|
|
c := validConfig()
|
|
c.ContextEnabled = true
|
|
c.ContextTimeout = 1
|
|
c.UserDeviceContextEnabled = true
|
|
c.GLPIUserDevicePaths = []string{"/Assets/Computer"}
|
|
c.GLPIUserDeviceFilterTemplate = "user.id==42"
|
|
c.GLPIUserDeviceLimit = 20
|
|
if err := c.Validate(); err == nil {
|
|
t.Fatal("expected user device filter template to require {{user_id}}")
|
|
}
|
|
}
|
|
|
|
func TestKnowledgeWebEditRequiresAuthentication(t *testing.T) {
|
|
c := validConfig()
|
|
c.KnowledgeWebEditEnabled = true
|
|
c.WebAllowAnonymous = true
|
|
if err := c.Validate(); err == nil {
|
|
t.Fatal("expected anonymous KB editing to be rejected")
|
|
}
|
|
}
|
|
|
|
func TestValidateGLPIKBRequiresAllowedSource(t *testing.T) {
|
|
c := validConfig()
|
|
c.GLPIKBEnabled = true
|
|
c.GLPIKBPath = "auto"
|
|
c.GLPIKBLimit = 100
|
|
c.GLPIKBSyncInterval = 10 * time.Minute
|
|
c.GLPIKBSource = "glpi-kb"
|
|
if err := c.Validate(); err == nil {
|
|
t.Fatal("expected GLPI KB source outside allowlist to be rejected")
|
|
}
|
|
c.KnowledgeAllowedSources = []string{"internal-kb", "glpi-kb"}
|
|
if err := c.Validate(); err != nil {
|
|
t.Fatalf("expected GLPI KB config to validate: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateGLPIKBAutoReplyRequiresCategoryWhitelist(t *testing.T) {
|
|
c := validConfig()
|
|
c.GLPIKBEnabled = true
|
|
c.GLPIKBPath = "auto"
|
|
c.GLPIKBLimit = 100
|
|
c.GLPIKBSyncInterval = 10 * time.Minute
|
|
c.GLPIKBSource = "glpi-kb"
|
|
c.KnowledgeAllowedSources = []string{"internal-kb", "glpi-kb"}
|
|
c.KnowledgeAutoReplySources = []string{"internal-kb", "glpi-kb"}
|
|
c.GLPIKBAutoReply = true
|
|
if err := c.Validate(); err == nil {
|
|
t.Fatal("expected auto reply category whitelist to be required")
|
|
}
|
|
c.GLPIKBAutoReplyCategoryIDs = []int64{3}
|
|
if err := c.Validate(); err != nil {
|
|
t.Fatalf("expected explicit category whitelist to validate: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateRejectsInvalidKnowledgeScoringConfig(t *testing.T) {
|
|
c := validConfig()
|
|
c.KnowledgeSemanticWeight = 1.2
|
|
if err := c.Validate(); err == nil {
|
|
t.Fatal("expected knowledge weight > 1 to be rejected")
|
|
}
|
|
c = validConfig()
|
|
c.KnowledgeChunkWords = 100
|
|
c.KnowledgeChunkOverlapWords = 100
|
|
if err := c.Validate(); err == nil {
|
|
t.Fatal("expected overlap >= chunk size to be rejected")
|
|
}
|
|
}
|
|
|
|
func TestValidateKnowledgeCategoryModeTrimsWhitespace(t *testing.T) {
|
|
c := validConfig()
|
|
c.KnowledgeCategoryMode = " UNSCOPED "
|
|
if err := c.Validate(); err != nil {
|
|
t.Fatalf("expected whitespace/case normalized category mode to validate: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestEnvNormalizedLowerDefaultsBlankOrWhitespace(t *testing.T) {
|
|
t.Setenv("KNOWLEDGE_CATEGORY_MODE", " ")
|
|
if got := envNormalizedLower("KNOWLEDGE_CATEGORY_MODE", "unscoped"); got != "unscoped" {
|
|
t.Fatalf("expected unscoped default, got %q", got)
|
|
}
|
|
t.Setenv("KNOWLEDGE_CATEGORY_MODE", " STRICT ")
|
|
if got := envNormalizedLower("KNOWLEDGE_CATEGORY_MODE", "unscoped"); got != "strict" {
|
|
t.Fatalf("expected strict, got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestKnowledgeIndexSourcesIncludesCategoryOnlySources(t *testing.T) {
|
|
c := validConfig()
|
|
c.KnowledgeAllowedSources = []string{"internal-kb", "glpi-kb"}
|
|
c.KnowledgeCategorySources = []string{"internal-category", "INTERNAL-KB"}
|
|
got := c.KnowledgeIndexSources()
|
|
want := []string{"internal-kb", "glpi-kb", "internal-category"}
|
|
if fmt.Sprint(got) != fmt.Sprint(want) {
|
|
t.Fatalf("index sources=%v want=%v", got, want)
|
|
}
|
|
}
|
|
|
|
func TestValidateAllowsCategoryOnlyGLPIKBSourceWithoutAutoReply(t *testing.T) {
|
|
c := validConfig()
|
|
c.GLPIKBEnabled = true
|
|
c.GLPIKBPath = "auto"
|
|
c.GLPIKBLimit = 100
|
|
c.GLPIKBSyncInterval = 10 * time.Minute
|
|
c.GLPIKBSource = "category-sync"
|
|
c.KnowledgeCategorySources = []string{"category-sync"}
|
|
if err := c.Validate(); err != nil {
|
|
t.Fatalf("category-only GLPI KB source should validate: %v", err)
|
|
}
|
|
c.GLPIKBAutoReply = true
|
|
c.GLPIKBAutoReplyCategoryIDs = []int64{3}
|
|
if err := c.Validate(); err == nil {
|
|
t.Fatal("category-only GLPI KB source must not be allowed for auto-reply")
|
|
}
|
|
}
|
|
|
|
func TestLoadDefaultsCategorySourcesToAllowedSources(t *testing.T) {
|
|
for key, value := range map[string]string{
|
|
"GLPI_URL": "https://glpi.internal.example",
|
|
"GLPI_CLIENT_ID": "client-id",
|
|
"GLPI_CLIENT_SECRET": "real-secret-value",
|
|
"GLPI_USERNAME": "svc-agent",
|
|
"GLPI_PASSWORD": "real-password-value",
|
|
"WEB_ALLOW_ANONYMOUS": "true",
|
|
"KNOWLEDGE_ALLOWED_SOURCES": "internal-kb,vendor-docs",
|
|
} {
|
|
t.Setenv(key, value)
|
|
}
|
|
old, existed := os.LookupEnv("KNOWLEDGE_CATEGORY_SOURCES")
|
|
if err := os.Unsetenv("KNOWLEDGE_CATEGORY_SOURCES"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() {
|
|
if existed {
|
|
_ = os.Setenv("KNOWLEDGE_CATEGORY_SOURCES", old)
|
|
} else {
|
|
_ = os.Unsetenv("KNOWLEDGE_CATEGORY_SOURCES")
|
|
}
|
|
})
|
|
c, err := Load()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if fmt.Sprint(c.KnowledgeCategorySources) != "[internal-kb vendor-docs]" {
|
|
t.Fatalf("category sources=%v", c.KnowledgeCategorySources)
|
|
}
|
|
}
|