package server import ( "encoding/json" "net/http" "net/http/httptest" "os" "path/filepath" "testing" ) func TestDashboardHTML(t *testing.T) { srv := setupTestServer(t) req := httptest.NewRequest(http.MethodGet, "/", nil) w := httptest.NewRecorder() srv.Router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d", w.Code) } body := w.Body.String() if !containsStr(body, "KAOS Dashboard") { t.Error("expected 'KAOS Dashboard' in HTML") } if !containsStr(body, "T01") { t.Error("expected T01 in HTML") } if !containsStr(body, "T08") { t.Error("expected T08 in HTML") } if !containsStr(body, "BACKLOG") { t.Error("expected BACKLOG column in HTML") } if !containsStr(body, "DONE") { t.Error("expected DONE column in HTML") } } func TestHTMLMoveTask(t *testing.T) { srv := setupTestServer(t) req := httptest.NewRequest(http.MethodPost, "/task/T08/move?to=ready", nil) w := httptest.NewRecorder() srv.Router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) } // Should return updated dashboard HTML body := w.Body.String() if !containsStr(body, "KAOS Dashboard") { t.Error("expected dashboard HTML after move") } } func TestDashboardHTML_HasAllColumns(t *testing.T) { srv := setupTestServer(t) req := httptest.NewRequest(http.MethodGet, "/", nil) w := httptest.NewRecorder() srv.Router.ServeHTTP(w, req) body := w.Body.String() for _, col := range []string{"BACKLOG", "READY", "ACTIVE", "REVIEW", "DONE"} { if !containsStr(body, col) { t.Errorf("expected %s column in dashboard", col) } } } func TestDashboardHTML_HasHTMXAttributes(t *testing.T) { srv := setupTestServer(t) req := httptest.NewRequest(http.MethodGet, "/", nil) w := httptest.NewRecorder() srv.Router.ServeHTTP(w, req) body := w.Body.String() if !containsStr(body, "hx-get") { t.Error("expected hx-get attributes in HTML") } if !containsStr(body, "hx-target") { t.Error("expected hx-target attributes in HTML") } } func TestDashboardHTML_TasksInCorrectColumns(t *testing.T) { srv := setupTestServer(t) req := httptest.NewRequest(http.MethodGet, "/", nil) w := httptest.NewRecorder() srv.Router.ServeHTTP(w, req) body := w.Body.String() // T01 should be in done column, T08 in backlog if !containsStr(body, `id="col-done"`) { t.Error("expected col-done in HTML") } if !containsStr(body, `id="col-backlog"`) { t.Error("expected col-backlog in HTML") } } func TestIsMoveAllowed(t *testing.T) { tests := []struct { from, to string allowed bool }{ {"backlog", "ready", true}, {"ready", "backlog", true}, {"review", "done", true}, {"review", "ready", true}, {"done", "review", true}, {"ready", "active", false}, {"active", "review", false}, {"backlog", "done", false}, {"backlog", "active", false}, {"done", "backlog", false}, {"ready", "ready", false}, } for _, tt := range tests { got := isMoveAllowed(tt.from, tt.to) if got != tt.allowed { t.Errorf("isMoveAllowed(%s, %s) = %v, want %v", tt.from, tt.to, got, tt.allowed) } } } func TestNoCacheHeaders(t *testing.T) { srv := setupTestServer(t) // Dynamic route should have no-cache headers req := httptest.NewRequest(http.MethodGet, "/", nil) w := httptest.NewRecorder() srv.Router.ServeHTTP(w, req) cc := w.Header().Get("Cache-Control") if !containsStr(cc, "no-store") { t.Errorf("expected Cache-Control no-store on dashboard, got %q", cc) } // API route should also have no-cache headers req2 := httptest.NewRequest(http.MethodGet, "/api/tasks", nil) w2 := httptest.NewRecorder() srv.Router.ServeHTTP(w2, req2) cc2 := w2.Header().Get("Cache-Control") if !containsStr(cc2, "no-store") { t.Errorf("expected Cache-Control no-store on API, got %q", cc2) } } func TestDashboardReflectsDiskChanges(t *testing.T) { srv := setupTestServer(t) // Initial state: T08 in backlog req := httptest.NewRequest(http.MethodGet, "/api/tasks", nil) w := httptest.NewRecorder() srv.Router.ServeHTTP(w, req) var tasks1 []taskResponse json.Unmarshal(w.Body.Bytes(), &tasks1) found := false for _, task := range tasks1 { if task.ID == "T08" && task.Status == "backlog" { found = true } } if !found { t.Fatal("expected T08 in backlog initially") } // Move file on disk (simulating external change) os.Rename( filepath.Join(srv.Config.TasksDir, "backlog", "T08.md"), filepath.Join(srv.Config.TasksDir, "ready", "T08.md"), ) // Second request should reflect the change without server restart req2 := httptest.NewRequest(http.MethodGet, "/api/tasks", nil) w2 := httptest.NewRecorder() srv.Router.ServeHTTP(w2, req2) var tasks2 []taskResponse json.Unmarshal(w2.Body.Bytes(), &tasks2) found = false for _, task := range tasks2 { if task.ID == "T08" && task.Status == "ready" { found = true } } if !found { t.Fatal("expected T08 in ready after disk move — server did not read fresh state") } } func TestDashboardHTML_HasSortableScript(t *testing.T) { srv := setupTestServer(t) req := httptest.NewRequest(http.MethodGet, "/", nil) w := httptest.NewRecorder() srv.Router.ServeHTTP(w, req) body := w.Body.String() if !containsStr(body, "sortable.min.js") { t.Error("expected sortable.min.js script tag") } if !containsStr(body, "initSortable") { t.Error("expected initSortable function") } } func TestDashboardHTML_HasDataFolderAttributes(t *testing.T) { srv := setupTestServer(t) req := httptest.NewRequest(http.MethodGet, "/", nil) w := httptest.NewRecorder() srv.Router.ServeHTTP(w, req) body := w.Body.String() if !containsStr(body, `data-folder="backlog"`) { t.Error("expected data-folder attribute on column-tasks") } if !containsStr(body, `data-folder="ready"`) { t.Error("expected data-folder=ready attribute") } } func TestTaskDetail_HasMoveButtons(t *testing.T) { srv := setupTestServer(t) // T08 is in backlog, should have "Odobri" button req := httptest.NewRequest(http.MethodGet, "/task/T08", nil) w := httptest.NewRecorder() srv.Router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d", w.Code) } body := w.Body.String() if !containsStr(body, "Odobri") { t.Error("expected 'Odobri' button for backlog task") } } func TestDashboardHTML_HasRunButton(t *testing.T) { srv := setupTestServer(t) // Move T08 to ready to test run button os.Rename( filepath.Join(srv.Config.TasksDir, "backlog", "T08.md"), filepath.Join(srv.Config.TasksDir, "ready", "T08.md"), ) req := httptest.NewRequest(http.MethodGet, "/", nil) w := httptest.NewRecorder() srv.Router.ServeHTTP(w, req) body := w.Body.String() if !containsStr(body, "Pusti") { t.Error("expected 'Pusti' button for ready task") } if !containsStr(body, "btn-run") { t.Error("expected btn-run class") } } func TestDashboardHTML_BlockedButton(t *testing.T) { srv := setupTestServer(t) // T08 in backlog with dep T07 not in done → blocked req := httptest.NewRequest(http.MethodGet, "/", nil) w := httptest.NewRecorder() srv.Router.ServeHTTP(w, req) body := w.Body.String() if !containsStr(body, "Blokiran") { t.Error("expected 'Blokiran' for backlog task with unmet deps") } if !containsStr(body, "btn-blocked") { t.Error("expected btn-blocked class") } } func TestDashboardHTML_DoneReportButton(t *testing.T) { srv := setupTestServer(t) req := httptest.NewRequest(http.MethodGet, "/", nil) w := httptest.NewRecorder() srv.Router.ServeHTTP(w, req) body := w.Body.String() // T01 is in done → should have "Izvestaj" button if !containsStr(body, "btn-report") { t.Error("expected btn-report for done task") } } func TestDashboard_HasPrijavaNav(t *testing.T) { srv := setupTestServer(t) req := httptest.NewRequest(http.MethodGet, "/", nil) w := httptest.NewRecorder() srv.Router.ServeHTTP(w, req) body := w.Body.String() if !containsStr(body, `href="/submit"`) { t.Error("expected Prijava nav link in dashboard") } }