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>
This commit is contained in:
@@ -0,0 +1,227 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user