package main import ( "context" "encoding/json" "io" "net/http" "net/http/httptest" "os" "strings" "testing" "time" ) // withModuleKey sets KANBAN_MODULE_KEY for the duration of a test and // restores the previous value afterwards. func withModuleKey(t *testing.T, value string) { t.Helper() prev := os.Getenv(moduleKeyEnv) t.Setenv(moduleKeyEnv, value) t.Cleanup(func() { _ = os.Setenv(moduleKeyEnv, prev) }) } func TestCryptoRoundTrip(t *testing.T) { withModuleKey(t, "test-passphrase") plain := []byte(`{"hello":"world"}`) cipherBlob, nonce, err := encryptConfig(plain) if err != nil { t.Fatalf("encrypt: %v", err) } got, err := decryptConfig(cipherBlob, nonce) if err != nil { t.Fatalf("decrypt: %v", err) } if string(got) != string(plain) { t.Fatalf("roundtrip mismatch: got %q want %q", got, plain) } } func TestCryptoMissingKey(t *testing.T) { t.Setenv(moduleKeyEnv, "") if _, _, err := encryptConfig([]byte("x")); err == nil { t.Fatal("expected error when KANBAN_MODULE_KEY unset") } } func TestSaveAndLoadModule(t *testing.T) { withModuleKey(t, "test-passphrase") db := setupTestDB(t) m := &Module{ Name: "jira-test", Kind: "jira", Enabled: true, EventFilter: []string{"card.created", "card.moved"}, Config: JSONValue{ "base_url": "https://example.atlassian.net", "email": "x@y.z", "api_token": "secret-123", }, } if err := db.saveModule(m); err != nil { t.Fatalf("save: %v", err) } if m.ID == "" { t.Fatal("ID not assigned on insert") } got, err := db.getModule(m.ID) if err != nil { t.Fatalf("get: %v", err) } if got.Config["api_token"] != "secret-123" { t.Fatalf("token roundtrip failed: %v", got.Config["api_token"]) } } func TestFilterMatches(t *testing.T) { if !filterMatches([]string{"card.created"}, "card.created") { t.Fatal("exact match") } if !filterMatches([]string{"*"}, "anything") { t.Fatal("wildcard") } if filterMatches([]string{"card.created"}, "card.moved") { t.Fatal("non-match should be false") } } func TestCardOptOutTag(t *testing.T) { c := cardForJira{Tags: []string{"foo", "NoJira", "bar"}} if !c.hasTag("nojira") { t.Fatal("nojira (case-insensitive) not detected") } if c.hasTag("missing") { t.Fatal("missing tag returned true") } } func TestJiraHandler_TransitionMappingMissing(t *testing.T) { withModuleKey(t, "k") db := setupTestDB(t) col, _ := db.CreateColumn("Backlog") card, _ := db.CreateCard(col.ID, "req", "t", "d", "") // Link the card so the create-fallback path is skipped. _ = db.setCardJiraKey(card.ID, "KAN-1") h := &jiraHandler{} _, err := h.transition(context.Background(), db, jiraConfig{BaseURL: "http://x"}, Event{Type: "card.moved", CardID: card.ID}) if err == nil || !strings.Contains(err.Error(), "status_map") { t.Fatalf("expected status_map error, got %v", err) } } func TestJiraHandler_TestConnectionHitsMyself(t *testing.T) { var path string srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { path = r.URL.Path w.WriteHeader(http.StatusOK) _, _ = io.WriteString(w, `{"accountId":"abc"}`) })) defer srv.Close() h := &jiraHandler{} m := Module{Kind: "jira", Config: JSONValue{ "base_url": srv.URL, "email": "x@y.z", "api_token": "tok", }} status, err := h.TestConnection(context.Background(), m) if err != nil { t.Fatalf("TestConnection: %v", err) } if status != 200 { t.Fatalf("status = %d, want 200", status) } if path != "/rest/api/3/myself" { t.Fatalf("path = %q, want /rest/api/3/myself", path) } } func TestJiraHandler_CreateLinksCardKey(t *testing.T) { withModuleKey(t, "test-passphrase") db := setupTestDB(t) user, _ := db.CreateUser("alice", "passw", "Alice") col, _ := db.CreateColumn("Todo") card, _ := db.CreateCard(col.ID, "req", "Buy bread", "desc", user.ID) srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodPost && r.URL.Path == "/rest/api/3/issue" { b, _ := io.ReadAll(r.Body) var p struct { Fields struct { Summary string `json:"summary"` } `json:"fields"` } _ = json.Unmarshal(b, &p) if p.Fields.Summary != "Buy bread" { t.Errorf("summary = %q", p.Fields.Summary) } w.WriteHeader(http.StatusCreated) _, _ = io.WriteString(w, `{"id":"10000","key":"KAN-1"}`) return } w.WriteHeader(http.StatusNotFound) })) defer srv.Close() h := &jiraHandler{} mod := Module{Kind: "jira", Config: JSONValue{ "base_url": srv.URL, "email": "x@y.z", "api_token": "tok", "project_key": "KAN", "status_map": map[string]interface{}{"Todo": "To Do"}, }} status, err := h.Handle(context.Background(), db, mod, Event{Type: "card.created", CardID: card.ID}) if err != nil { t.Fatalf("Handle: %v", err) } if status != http.StatusCreated { t.Fatalf("status = %d, want 201", status) } again, err := db.getCardForJira(card.ID) if err != nil { t.Fatalf("get card: %v", err) } if again.JiraKey != "KAN-1" { t.Fatalf("jira_key = %q, want KAN-1", again.JiraKey) } } func TestDispatcher_Cutoff(t *testing.T) { withModuleKey(t, "k") db := setupTestDB(t) col, _ := db.CreateColumn("Todo") // Create card BEFORE the module so cutoffOK rejects it. card, _ := db.CreateCard(col.ID, "req", "t", "d", "") time.Sleep(20 * time.Millisecond) mod := Module{ID: "m", CreatedAt: nowRFC3339()} if cutoffOK(db, mod, Event{CardID: card.ID}) { t.Fatal("card pre-dating module should be filtered out") } // Once linked, cutoff should allow it. _ = db.setCardJiraKey(card.ID, "KAN-9") if !cutoffOK(db, mod, Event{CardID: card.ID}) { t.Fatal("linked card must pass cutoff even if older") } } func TestIsAdmin(t *testing.T) { db := setupTestDB(t) u, _ := db.CreateUser("egutierrez", "passw", "Egu") // Migration 015 marks egutierrez admin via UPDATE WHERE username, but // that only takes effect when the row already exists. In production // the migration runs against an existing user list; in tests we create // users after migration, so simulate the same outcome explicitly. if _, err := db.conn.Exec(`UPDATE users SET is_admin = 1 WHERE username = ?`, "egutierrez"); err != nil { t.Fatalf("seed admin: %v", err) } ok, err := db.IsAdmin(u.ID) if err != nil { t.Fatalf("IsAdmin: %v", err) } if !ok { t.Fatal("egutierrez must be admin after seed") } other, _ := db.CreateUser("alice", "passw", "Alice") ok, _ = db.IsAdmin(other.ID) if ok { t.Fatal("alice must not be admin by default") } }