package main import ( "errors" "os" "path/filepath" "testing" "time" "github.com/fsnotify/fsnotify" ) func TestLoadConfiguration_valid(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "wicket.yaml") os.WriteFile(path, []byte(` paths: github.com/chrisguidry/docketeer: verify: hmac-sha256 secret: "webhook-secret" signature_header: X-Hub-Signature-256 subscribe_secret: "sub-token" `), 0644) cfg, err := LoadConfiguration(path) if err != nil { t.Fatalf("unexpected error: %v", err) } pc, ok := cfg.Paths["github.com/chrisguidry/docketeer"] if !ok { t.Fatal("expected path config for github.com/chrisguidry/docketeer") } if pc.Verify != "hmac-sha256" { t.Errorf("expected hmac-sha256, got %s", pc.Verify) } if pc.Secret != "webhook-secret" { t.Errorf("expected webhook-secret, got %s", pc.Secret) } if pc.SignatureHeader != "X-Hub-Signature-256" { t.Errorf("expected X-Hub-Signature-256, got %s", pc.SignatureHeader) } if pc.SubscribeSecret != "sub-token" { t.Errorf("expected sub-token, got %s", pc.SubscribeSecret) } } func TestLoadConfiguration_missingFile(t *testing.T) { _, err := LoadConfiguration("/nonexistent/wicket.yaml") if err == nil { t.Fatal("expected error for missing file") } } func TestLoadConfiguration_empty(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "wicket.yaml") os.WriteFile(path, []byte(""), 0644) cfg, err := LoadConfiguration(path) if err != nil { t.Fatalf("unexpected error: %v", err) } if cfg.Paths == nil { cfg.Paths = make(map[string]PathConfiguration) } if len(cfg.Paths) != 0 { t.Errorf("expected no paths, got %d", len(cfg.Paths)) } } func TestLoadConfiguration_invalidYAML(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "wicket.yaml") os.WriteFile(path, []byte("not: valid: yaml: [[["), 0644) _, err := LoadConfiguration(path) if err == nil { t.Fatal("expected error for invalid YAML") } } func TestLookupSubscribeSecret_exactMatch(t *testing.T) { cfg := &Configuration{ Paths: map[string]PathConfiguration{ "github.com/chrisguidry/docketeer": {SubscribeSecret: "token-123"}, }, } secret := cfg.LookupSubscribeSecret("github.com/chrisguidry/docketeer") if secret != "token-123" { t.Errorf("expected token-123, got %s", secret) } } func TestLookupSubscribeSecret_inheritsFromParent(t *testing.T) { cfg := &Configuration{ Paths: map[string]PathConfiguration{ "github.com/chrisguidry": {SubscribeSecret: "parent-token"}, }, } secret := cfg.LookupSubscribeSecret("github.com/chrisguidry/docketeer") if secret != "parent-token" { t.Errorf("expected parent-token, got %s", secret) } } func TestLookupSubscribeSecret_noConfig(t *testing.T) { cfg := &Configuration{ Paths: map[string]PathConfiguration{}, } secret := cfg.LookupSubscribeSecret("github.com/chrisguidry/docketeer") if secret != "" { t.Errorf("expected empty string, got %s", secret) } } func TestLookupVerification_inheritsFromParent(t *testing.T) { cfg := &Configuration{ Paths: map[string]PathConfiguration{ "github.com": { Verify: "hmac-sha256", Secret: "webhook-secret", SignatureHeader: "X-Hub-Signature-256", }, }, } pc := cfg.LookupVerification("github.com") if pc == nil { t.Fatal("expected path config for exact match") } pc = cfg.LookupVerification("github.com/org/repo") if pc == nil { t.Fatal("expected verification to inherit from parent") } if pc.Verify != "hmac-sha256" { t.Errorf("expected hmac-sha256, got %s", pc.Verify) } } func TestLookupVerification_childOverridesParent(t *testing.T) { cfg := &Configuration{ Paths: map[string]PathConfiguration{ "github.com": { Verify: "hmac-sha256", Secret: "parent-secret", SignatureHeader: "X-Hub-Signature-256", }, "github.com/org/repo": { Verify: "hmac-sha1", Secret: "child-secret", SignatureHeader: "X-Hub-Signature", }, }, } pc := cfg.LookupVerification("github.com/org/repo") if pc == nil { t.Fatal("expected path config for child") } if pc.Secret != "child-secret" { t.Errorf("expected child-secret, got %s", pc.Secret) } if pc.Verify != "hmac-sha1" { t.Errorf("expected hmac-sha1, got %s", pc.Verify) } } func TestWatchConfiguration_reloadsOnChange(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "wicket.yaml") os.WriteFile(path, []byte(` paths: test/path: subscribe_secret: "original" `), 0644) reloaded := make(chan *Configuration, 1) stop, err := WatchConfiguration(path, func(cfg *Configuration) { reloaded <- cfg }) if err != nil { t.Fatalf("unexpected error: %v", err) } defer stop() os.WriteFile(path, []byte(` paths: test/path: subscribe_secret: "updated" `), 0644) select { case cfg := <-reloaded: secret := cfg.LookupSubscribeSecret("test/path") if secret != "updated" { t.Errorf("expected updated secret, got %s", secret) } case <-time.After(2 * time.Second): t.Fatal("timed out waiting for config reload") } } func TestWatchConfiguration_ignoresInvalidYAML(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "wicket.yaml") os.WriteFile(path, []byte(` paths: test/path: subscribe_secret: "original" `), 0644) reloaded := make(chan *Configuration, 1) stop, err := WatchConfiguration(path, func(cfg *Configuration) { reloaded <- cfg }) if err != nil { t.Fatalf("unexpected error: %v", err) } defer stop() os.WriteFile(path, []byte("not: valid: yaml: [[["), 0644) select { case <-reloaded: t.Fatal("callback should not be called for invalid YAML") case <-time.After(500 * time.Millisecond): } } func TestWatchConfiguration_errorOnMissingFile(t *testing.T) { _, err := WatchConfiguration("/nonexistent/wicket.yaml", func(cfg *Configuration) {}) if err == nil { t.Fatal("expected error watching nonexistent file") } } func TestWatchConfiguration_errorCreatingWatcher(t *testing.T) { original := newWatcher newWatcher = func() (*fsnotify.Watcher, error) { return nil, errors.New("simulated watcher error") } defer func() { newWatcher = original }() _, err := WatchConfiguration("/any/path", func(cfg *Configuration) {}) if err == nil { t.Fatal("expected error when watcher creation fails") } } func TestLoadConfiguration_expandsEnvVars(t *testing.T) { t.Setenv("WICKET_TEST_SECRET", "expanded-secret") dir := t.TempDir() path := filepath.Join(dir, "wicket.yaml") os.WriteFile(path, []byte(` paths: test/path: verify: hmac-sha256 secret: "${WICKET_TEST_SECRET}" `), 0644) cfg, err := LoadConfiguration(path) if err != nil { t.Fatalf("unexpected error: %v", err) } pc := cfg.Paths["test/path"] if pc.Secret != "expanded-secret" { t.Errorf("expected expanded-secret, got %s", pc.Secret) } } func TestLoadConfiguration_unexpandedEnvVar(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "wicket.yaml") os.WriteFile(path, []byte(` paths: test/path: secret: "${WICKET_UNSET_VAR_xyz}" `), 0644) cfg, err := LoadConfiguration(path) if err != nil { t.Fatalf("unexpected error: %v", err) } pc := cfg.Paths["test/path"] if pc.Secret != "" { t.Errorf("expected empty string for unset var, got %s", pc.Secret) } } func TestLookupSubscribeSecret_nilConfig(t *testing.T) { var cfg *Configuration secret := cfg.LookupSubscribeSecret("any/path") if secret != "" { t.Errorf("expected empty string from nil config, got %s", secret) } } func TestLookupVerification_nilConfig(t *testing.T) { var cfg *Configuration pc := cfg.LookupVerification("any/path") if pc != nil { t.Error("expected nil from nil config") } }