705 lines
18 KiB
Go
705 lines
18 KiB
Go
package server
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/dal/kaos/internal/config"
|
|
)
|
|
|
|
const testTask1 = `# T01: Prvi task
|
|
|
|
**Agent:** coder
|
|
**Model:** Sonnet
|
|
**Zavisi od:** —
|
|
|
|
---
|
|
|
|
## Opis
|
|
|
|
Opis prvog taska.
|
|
|
|
---
|
|
`
|
|
|
|
const testTask2 = `# T08: HTTP server
|
|
|
|
**Agent:** coder
|
|
**Model:** Sonnet
|
|
**Zavisi od:** T07
|
|
|
|
---
|
|
|
|
## Opis
|
|
|
|
Implementacija HTTP servera.
|
|
|
|
---
|
|
`
|
|
|
|
func setupTestServer(t *testing.T) *Server {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
|
|
tasksDir := filepath.Join(dir, "TASKS")
|
|
for _, folder := range []string{"backlog", "ready", "active", "review", "done", "reports"} {
|
|
os.MkdirAll(filepath.Join(tasksDir, folder), 0755)
|
|
}
|
|
|
|
os.WriteFile(filepath.Join(tasksDir, "done", "T01.md"), []byte(testTask1), 0644)
|
|
os.WriteFile(filepath.Join(tasksDir, "backlog", "T08.md"), []byte(testTask2), 0644)
|
|
|
|
// Docs: create markdown files in project root
|
|
os.WriteFile(filepath.Join(dir, "CLAUDE.md"), []byte("# CLAUDE.md\n\nGlavni fajl.\n\n| Kolona | Opis |\n|--------|------|\n| A | B |\n"), 0644)
|
|
os.WriteFile(filepath.Join(dir, "README.md"), []byte("# README\n\nOpis projekta.\n"), 0644)
|
|
os.MkdirAll(filepath.Join(dir, "agents", "coder"), 0755)
|
|
os.WriteFile(filepath.Join(dir, "agents", "coder", "CLAUDE.md"), []byte("# Coder Agent\n\nPravila kodiranja.\n"), 0644)
|
|
|
|
cfg := &config.Config{
|
|
TasksDir: tasksDir,
|
|
ProjectPath: dir,
|
|
Port: "0",
|
|
}
|
|
|
|
return New(cfg)
|
|
}
|
|
|
|
func TestAPIGetTasks(t *testing.T) {
|
|
srv := setupTestServer(t)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/tasks", 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())
|
|
}
|
|
|
|
var tasks []taskResponse
|
|
if err := json.Unmarshal(w.Body.Bytes(), &tasks); err != nil {
|
|
t.Fatalf("invalid JSON: %v", err)
|
|
}
|
|
|
|
if len(tasks) != 2 {
|
|
t.Fatalf("expected 2 tasks, got %d", len(tasks))
|
|
}
|
|
|
|
// Check that tasks have correct statuses
|
|
statuses := map[string]string{}
|
|
for _, task := range tasks {
|
|
statuses[task.ID] = task.Status
|
|
}
|
|
if statuses["T01"] != "done" {
|
|
t.Errorf("expected T01 status done, got %s", statuses["T01"])
|
|
}
|
|
if statuses["T08"] != "backlog" {
|
|
t.Errorf("expected T08 status backlog, got %s", statuses["T08"])
|
|
}
|
|
}
|
|
|
|
func TestAPIGetTask(t *testing.T) {
|
|
srv := setupTestServer(t)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/task/T01", nil)
|
|
w := httptest.NewRecorder()
|
|
srv.Router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", w.Code)
|
|
}
|
|
|
|
var detail taskDetailResponse
|
|
if err := json.Unmarshal(w.Body.Bytes(), &detail); err != nil {
|
|
t.Fatalf("invalid JSON: %v", err)
|
|
}
|
|
|
|
if detail.ID != "T01" {
|
|
t.Errorf("expected T01, got %s", detail.ID)
|
|
}
|
|
if detail.Content == "" {
|
|
t.Error("expected non-empty content")
|
|
}
|
|
}
|
|
|
|
func TestAPIGetTask_NotFound(t *testing.T) {
|
|
srv := setupTestServer(t)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/task/T99", nil)
|
|
w := httptest.NewRecorder()
|
|
srv.Router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusNotFound {
|
|
t.Fatalf("expected 404, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestAPIMoveTask(t *testing.T) {
|
|
srv := setupTestServer(t)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/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())
|
|
}
|
|
|
|
// Verify file was moved
|
|
if _, err := os.Stat(filepath.Join(srv.Config.TasksDir, "ready", "T08.md")); err != nil {
|
|
t.Error("expected T08.md in ready/")
|
|
}
|
|
if _, err := os.Stat(filepath.Join(srv.Config.TasksDir, "backlog", "T08.md")); !os.IsNotExist(err) {
|
|
t.Error("expected T08.md removed from backlog/")
|
|
}
|
|
}
|
|
|
|
func TestAPIMoveTask_NotFound(t *testing.T) {
|
|
srv := setupTestServer(t)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/task/T99/move?to=ready", nil)
|
|
w := httptest.NewRecorder()
|
|
srv.Router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusNotFound {
|
|
t.Fatalf("expected 404, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestAPIMoveTask_InvalidFolder(t *testing.T) {
|
|
srv := setupTestServer(t)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/task/T08/move?to=invalid", nil)
|
|
w := httptest.NewRecorder()
|
|
srv.Router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected 400, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
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 TestTaskDetailHTML(t *testing.T) {
|
|
srv := setupTestServer(t)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/task/T01", 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, "T01") {
|
|
t.Error("expected T01 in detail HTML")
|
|
}
|
|
if !containsStr(body, "Prvi task") {
|
|
t.Error("expected task title in detail HTML")
|
|
}
|
|
}
|
|
|
|
func TestTaskDetailHTML_NotFound(t *testing.T) {
|
|
srv := setupTestServer(t)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/task/T99", nil)
|
|
w := httptest.NewRecorder()
|
|
srv.Router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusNotFound {
|
|
t.Fatalf("expected 404, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
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 TestReport_Exists(t *testing.T) {
|
|
srv := setupTestServer(t)
|
|
|
|
// Create a report
|
|
reportsDir := filepath.Join(srv.Config.TasksDir, "reports")
|
|
os.MkdirAll(reportsDir, 0755)
|
|
os.WriteFile(filepath.Join(reportsDir, "T01-report.md"), []byte("# T01 Report\nSve ok."), 0644)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/report/T01", nil)
|
|
w := httptest.NewRecorder()
|
|
srv.Router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", w.Code)
|
|
}
|
|
if !containsStr(w.Body.String(), "T01 Report") {
|
|
t.Error("expected report content")
|
|
}
|
|
}
|
|
|
|
func TestReport_NotFound(t *testing.T) {
|
|
srv := setupTestServer(t)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/report/T99", nil)
|
|
w := httptest.NewRecorder()
|
|
srv.Router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusNotFound {
|
|
t.Fatalf("expected 404, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestTaskDetail_HasMoveButtons(t *testing.T) {
|
|
srv := setupTestServer(t)
|
|
|
|
// T08 is in backlog, should have "Premesti u Ready" 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, "Ready") {
|
|
t.Error("expected 'Ready' move button for backlog task")
|
|
}
|
|
}
|
|
|
|
func TestAPIMoveTask_ForbiddenToActive(t *testing.T) {
|
|
srv := setupTestServer(t)
|
|
|
|
// Put T08 in ready first
|
|
os.Rename(
|
|
filepath.Join(srv.Config.TasksDir, "backlog", "T08.md"),
|
|
filepath.Join(srv.Config.TasksDir, "ready", "T08.md"),
|
|
)
|
|
|
|
// Try to move ready → active (agent-only)
|
|
req := httptest.NewRequest(http.MethodPost, "/api/task/T08/move?to=active", nil)
|
|
w := httptest.NewRecorder()
|
|
srv.Router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusForbidden {
|
|
t.Fatalf("expected 403 for ready→active, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestAPIMoveTask_ForbiddenActiveToReview(t *testing.T) {
|
|
srv := setupTestServer(t)
|
|
|
|
// Put T08 in active
|
|
os.Rename(
|
|
filepath.Join(srv.Config.TasksDir, "backlog", "T08.md"),
|
|
filepath.Join(srv.Config.TasksDir, "active", "T08.md"),
|
|
)
|
|
|
|
// Try to move active → review (agent-only)
|
|
req := httptest.NewRequest(http.MethodPost, "/api/task/T08/move?to=review", nil)
|
|
w := httptest.NewRecorder()
|
|
srv.Router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusForbidden {
|
|
t.Fatalf("expected 403 for active→review, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestAPIMoveTask_AllowedBacklogToReady(t *testing.T) {
|
|
srv := setupTestServer(t)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/task/T08/move?to=ready", nil)
|
|
w := httptest.NewRecorder()
|
|
srv.Router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200 for backlog→ready, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestAPIMoveTask_AllowedReviewToDone(t *testing.T) {
|
|
srv := setupTestServer(t)
|
|
|
|
// Put T08 in review
|
|
os.Rename(
|
|
filepath.Join(srv.Config.TasksDir, "backlog", "T08.md"),
|
|
filepath.Join(srv.Config.TasksDir, "review", "T08.md"),
|
|
)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/task/T08/move?to=done", nil)
|
|
w := httptest.NewRecorder()
|
|
srv.Router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200 for review→done, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
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 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 TestDocsList(t *testing.T) {
|
|
srv := setupTestServer(t)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/docs", 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())
|
|
}
|
|
|
|
body := w.Body.String()
|
|
if !containsStr(body, "CLAUDE.md") {
|
|
t.Error("expected CLAUDE.md in docs list")
|
|
}
|
|
if !containsStr(body, "README.md") {
|
|
t.Error("expected README.md in docs list")
|
|
}
|
|
if !containsStr(body, "agents/coder/CLAUDE.md") {
|
|
t.Error("expected agents/coder/CLAUDE.md in docs list")
|
|
}
|
|
}
|
|
|
|
func TestDocsView_CLAUDE(t *testing.T) {
|
|
srv := setupTestServer(t)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/docs/CLAUDE.md", 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())
|
|
}
|
|
|
|
body := w.Body.String()
|
|
if !containsStr(body, "Glavni fajl") {
|
|
t.Error("expected rendered markdown content")
|
|
}
|
|
// Should have table rendered as HTML
|
|
if !containsStr(body, "<table>") || !containsStr(body, "<th>") {
|
|
t.Error("expected HTML table from markdown")
|
|
}
|
|
}
|
|
|
|
func TestDocsView_NestedFile(t *testing.T) {
|
|
srv := setupTestServer(t)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/docs/agents/coder/CLAUDE.md", 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())
|
|
}
|
|
|
|
body := w.Body.String()
|
|
if !containsStr(body, "Coder Agent") {
|
|
t.Error("expected nested file content")
|
|
}
|
|
// Breadcrumbs
|
|
if !containsStr(body, "agents") {
|
|
t.Error("expected breadcrumb for agents")
|
|
}
|
|
}
|
|
|
|
func TestDocsView_PathTraversal(t *testing.T) {
|
|
srv := setupTestServer(t)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/docs/../../etc/passwd", nil)
|
|
w := httptest.NewRecorder()
|
|
srv.Router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusForbidden {
|
|
t.Fatalf("expected 403 for path traversal, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestDocsView_NonMarkdown(t *testing.T) {
|
|
srv := setupTestServer(t)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/docs/main.go", nil)
|
|
w := httptest.NewRecorder()
|
|
srv.Router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusForbidden {
|
|
t.Fatalf("expected 403 for non-.md file, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestDocsView_NotFound(t *testing.T) {
|
|
srv := setupTestServer(t)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/docs/nonexistent.md", nil)
|
|
w := httptest.NewRecorder()
|
|
srv.Router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusNotFound {
|
|
t.Fatalf("expected 404, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestDocsView_HasBreadcrumbs(t *testing.T) {
|
|
srv := setupTestServer(t)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/docs/agents/coder/CLAUDE.md", nil)
|
|
w := httptest.NewRecorder()
|
|
srv.Router.ServeHTTP(w, req)
|
|
|
|
body := w.Body.String()
|
|
if !containsStr(body, "Dokumenti") {
|
|
t.Error("expected 'Dokumenti' in breadcrumbs")
|
|
}
|
|
if !containsStr(body, "coder") {
|
|
t.Error("expected 'coder' in breadcrumbs")
|
|
}
|
|
}
|
|
|
|
func TestRewriteLinksSimple(t *testing.T) {
|
|
input := `<a href="README.md">link</a> and <a href="https://example.com">ext</a>`
|
|
result := rewriteLinksSimple(input, ".")
|
|
if !containsStr(result, `/docs/README.md`) {
|
|
t.Errorf("expected rewritten link, got: %s", result)
|
|
}
|
|
if !containsStr(result, `https://example.com`) {
|
|
t.Error("external link should not be rewritten")
|
|
}
|
|
}
|
|
|
|
func TestRewriteLinksSimple_NestedDir(t *testing.T) {
|
|
input := `<a href="CLAUDE.md">link</a>`
|
|
result := rewriteLinksSimple(input, "agents/coder")
|
|
if !containsStr(result, `/docs/agents/coder/CLAUDE.md`) {
|
|
t.Errorf("expected nested rewritten link, got: %s", result)
|
|
}
|
|
}
|
|
|
|
func containsStr(s, substr string) bool {
|
|
return len(s) >= len(substr) && findStr(s, substr)
|
|
}
|
|
|
|
func findStr(s, substr string) bool {
|
|
for i := 0; i <= len(s)-len(substr); i++ {
|
|
if s[i:i+len(substr)] == substr {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|