Files
kanban/backend/modules_test.go
T
egutierrez c9e15513c7 chore: auto-commit (23 archivos)
- app.md
- backend/dist/assets/index-CFDWXN9Z.js
- backend/dist/index.html
- backend/handlers.go
- backend/main.go
- backend/users.go
- e2e/smoke_live.sh
- frontend/src/App.tsx
- frontend/src/api.ts
- frontend/src/components/CardChatPanel.tsx
- ...

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 18:22:44 +02:00

228 lines
6.4 KiB
Go

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")
}
}