Git Repository
Stackyard
Stackyard scannt lokale Entwicklungsordner und bündelt Projekte, Git-Status, TODOs, Toolchains, Docker-Compose-Setups, Datei- und Launcher-Funktionen sowie Operations-Informationen in einer lokalen WebUI.
HTTPS
https://zanvex.de/git/stackyard.gitinternal/web/web_test.go
Zum Verzeichnispackage web
import (
"context"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
"time"
"stackyard/internal/config"
"stackyard/internal/project"
"stackyard/internal/storage"
)
func TestBuildFileTreeIgnoresConfiguredDirectories(t *testing.T) {
root := t.TempDir()
mustWriteTreeFile(t, filepath.Join(root, "README.md"))
mustWriteTreeFile(t, filepath.Join(root, "cmd", "main.go"))
mustWriteTreeFile(t, filepath.Join(root, "node_modules", "left-pad", "index.js"))
nodes, err := buildFileTree(root, root, map[string]struct{}{"node_modules": {}})
if err != nil {
t.Fatalf("buildFileTree: %v", err)
}
if len(nodes) != 2 {
t.Fatalf("expected 2 visible top-level nodes, got %d", len(nodes))
}
if nodes[0].Name != "cmd/" || nodes[1].Name != "README.md" {
t.Fatalf("unexpected nodes: %+v", nodes)
}
}
func mustWriteTreeFile(t *testing.T, path string) {
t.Helper()
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatalf("mkdir %s: %v", path, err)
}
if err := os.WriteFile(path, []byte("x"), 0o644); err != nil {
t.Fatalf("write %s: %v", path, err)
}
}
func TestRenderMarkdownRendersBasicBlocks(t *testing.T) {
html := renderMarkdown("# Title\n\n- one\n- two\n\n```go\nfmt.Println(1)\n```")
for _, want := range []string{"<h1>Title</h1>", "<ul><li>one</li><li>two</li></ul>", "<pre><code>fmt.Println(1)"} {
if !strings.Contains(html, want) {
t.Fatalf("expected rendered markdown to contain %q, got %q", want, html)
}
}
}
func TestRedirectWithFlashAddsQueryParameters(t *testing.T) {
req := httptest.NewRequest("POST", "/projects/abc/todo", nil)
rec := httptest.NewRecorder()
redirectWithFlash(rec, req, "/projects/abc", "success", "saved ok")
location := rec.Header().Get("Location")
if !strings.Contains(location, "flash_kind=success") || !strings.Contains(location, "flash=saved%20ok") {
t.Fatalf("unexpected redirect location: %q", location)
}
}
func TestBackToRejectsExternalRedirect(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/action", strings.NewReader("return_to=https%3A%2F%2Fexample.com"))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
if got := backTo(req, "/operations"); got != "/operations" {
t.Fatalf("expected safe fallback, got %q", got)
}
}
func TestFilterProjects(t *testing.T) {
projects := []project.Project{
{Name: "alpha", PrimaryType: "go", Ports: []int{8080}},
{Name: "beta", MiseTools: []string{"node 22"}},
}
got := filterProjects(projects, "8080")
if !reflect.DeepEqual(got, []project.Project{projects[0]}) {
t.Fatalf("unexpected filter result for port: %#v", got)
}
got = filterProjects(projects, "node 22")
if !reflect.DeepEqual(got, []project.Project{projects[1]}) {
t.Fatalf("unexpected filter result for mise: %#v", got)
}
}
func TestFilterByStateAndFavorites(t *testing.T) {
projects := []project.Project{
{Name: "alpha", State: project.StateActive},
{Name: "beta", State: project.StatePaused, Favorite: true},
}
got := filterByState(projects, "paused")
if !reflect.DeepEqual(got, []project.Project{projects[1]}) {
t.Fatalf("unexpected state filter result: %#v", got)
}
got = filterFavorites(projects)
if !reflect.DeepEqual(got, []project.Project{projects[1]}) {
t.Fatalf("unexpected favorites filter result: %#v", got)
}
}
func TestEditorFileRoundTrip(t *testing.T) {
root := t.TempDir()
path := filepath.Join(root, "internal", "main.go")
mustWriteTreeFile(t, path)
if err := writeEditorFile(root, "internal/main.go", "package main\n"); err != nil {
t.Fatalf("writeEditorFile: %v", err)
}
got, err := readEditorFile(root, "internal/main.go")
if err != nil {
t.Fatalf("readEditorFile: %v", err)
}
if got != "package main\n" {
t.Fatalf("unexpected editor content: %q", got)
}
}
func TestEditorRejectsPathOutsideProject(t *testing.T) {
root := t.TempDir()
if _, err := resolveEditorFile(root, "../secret.txt"); err == nil {
t.Fatal("expected path traversal to be rejected")
}
}
func TestEditorRejectsBinaryFile(t *testing.T) {
root := t.TempDir()
path := filepath.Join(root, "image.bin")
if err := os.WriteFile(path, []byte{'x', 0, 'y'}, 0o644); err != nil {
t.Fatal(err)
}
if _, err := readEditorFile(root, "image.bin"); err == nil {
t.Fatal("expected binary file to be rejected")
}
}
func TestEditorRejectsProtectedFiles(t *testing.T) {
root := t.TempDir()
for _, name := range []string{".env", "server.key", "go.sum", "package-lock.json"} {
path := filepath.Join(root, name)
if err := os.WriteFile(path, []byte("secret"), 0o644); err != nil {
t.Fatal(err)
}
if err := writeEditorFile(root, name, "changed"); err == nil {
t.Fatalf("expected %s to be protected", name)
}
}
}
func TestCSRFGuardRejectsCrossSitePost(t *testing.T) {
called := false
handler := csrfGuard(http.HandlerFunc(func(http.ResponseWriter, *http.Request) { called = true }))
req := httptest.NewRequest(http.MethodPost, "http://stackyard.local/collections/create", nil)
req.Header.Set("Origin", "https://attacker.example")
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusForbidden || called {
t.Fatalf("expected cross-site POST rejection, status=%d called=%v", rec.Code, called)
}
}
func TestSortProjectsByLastOpened(t *testing.T) {
projects := []project.Project{{Name: "old", LastOpenedAt: time.Unix(1, 0)}, {Name: "new", LastOpenedAt: time.Unix(2, 0)}}
sortProjects(projects, "opened")
if projects[0].Name != "new" {
t.Fatalf("unexpected sort order: %+v", projects)
}
}
func TestFilterWithoutArchived(t *testing.T) {
projects := []project.Project{{Name: "active", State: project.StateActive}, {Name: "old", State: project.StateArchived}}
got := filterWithoutArchived(projects)
if len(got) != 1 || got[0].Name != "active" {
t.Fatalf("unexpected default state filter: %+v", got)
}
}
func TestProjectGitShowsUnavailablePanelForMissingProjectPath(t *testing.T) {
store, err := storage.Open(filepath.Join(t.TempDir(), "stackyard.db"))
if err != nil {
t.Fatalf("open store: %v", err)
}
t.Cleanup(func() { _ = store.Close() })
missingPath := filepath.Join(t.TempDir(), "missing")
current := project.Project{
ID: project.IDFromPath(missingPath),
Name: "missing",
RootName: "DEV",
Path: missingPath,
State: project.StateActive,
Git: project.GitInfo{IsRepo: true},
}
if err := store.UpsertProjects(context.Background(), []project.Project{current}); err != nil {
t.Fatalf("store project: %v", err)
}
server := NewServer(config.Config{}, store)
req := httptest.NewRequest(http.MethodGet, "/projects/"+current.ID+"/git", nil)
rec := httptest.NewRecorder()
server.Handler().ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected status 200, got %d: %s", rec.Code, rec.Body.String())
}
if body := rec.Body.String(); !strings.Contains(body, "Git nicht verfügbar") || !strings.Contains(body, "Projektpfad ist momentan nicht erreichbar") {
t.Fatalf("unexpected response body: %s", body)
}
}
func TestDashboardWarningsPrioritizeMissingPath(t *testing.T) {
current := project.Project{ID: "missing", Name: "Missing", Path: filepath.Join(t.TempDir(), "gone"), Git: project.GitInfo{IsRepo: true, Dirty: true}}
warnings := dashboardWarnings(current)
if len(warnings) != 1 || warnings[0].Level != "critical" || warnings[0].Priority != 0 {
t.Fatalf("unexpected warnings: %+v", warnings)
}
}
func TestProjectPageUsesFullNavigationForDashboardLink(t *testing.T) {
var rendered strings.Builder
if err := projectTemplate.Execute(&rendered, map[string]any{
"Project": project.Project{ID: "project", Name: "Project", State: project.StateActive},
}); err != nil {
t.Fatalf("render project page: %v", err)
}
body := rendered.String()
if strings.Contains(body, `hx-boost="true"`) {
t.Fatal("project page must not boost full-page navigation because page styles differ")
}
if !strings.Contains(body, `<a class="crumb" href="/">← Alle Projekte</a>`) {
t.Fatal("dashboard link is missing")
}
}