Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions .github/workflows/backend.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
name: Backend CI

on:
pull_request:
branches:
- main

push:
branches:
- main

jobs:
lint-go:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2

- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: '1.23.4'

- name: Install dependencies
run: go mod download

- name: Lint with golangci-lint
uses: golangci/golangci-lint-action@v6
with:
version: latest
args: --timeout=5m

test-go:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2

- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: '1.23.4'

- name: Install dependencies
run: go mod download

- name: Run tests with coverage
run: |
mkdir -p coverage
go test -v -coverprofile=coverage/coverage.out ./...
go tool cover -func=coverage/coverage.out

- name: Upload coverage report as artifact
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage/coverage.out

37 changes: 37 additions & 0 deletions .github/workflows/frontend.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: Frontend CI

on:
pull_request:
branches:
- main
push:
branches: [main]

jobs:
lint-frontend:
runs-on: ubuntu-latest

defaults:
run:
working-directory: ui/leafwiki-ui

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'

- name: Install dependencies
run: npm ci

# Disable Liniting for now!
# Because we have many linting errors in the codebase which we need to fix and to test.
# f.g. useEffect with missing dependencies - This could be a problem when we don't test it, before we resolve the linting errors.
# - name: Lint with ESLint
# run: npm run lint

- name: Run Prettier check
run: npx prettier --check src/**
14 changes: 14 additions & 0 deletions .golangci-lint
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
run:
timeout: 5m
issues:
exclude-use-default: false
linters:
enable:
- govet
- gofmt
- goimports
- unused
- deadcode
- gocritic
- staticcheck
- structcheck
20 changes: 15 additions & 5 deletions internal/core/assets/assets_service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ func createMultipartFile(filename string, content []byte) (multipart.File, strin
if err != nil {
return nil, "", err
}
part.Write(content)
if _, err := part.Write(content); err != nil {
return nil, "", err
}
writer.Close()

reader := multipart.NewReader(body, writer.Boundary())
Expand All @@ -43,8 +45,12 @@ func TestSaveAndListAsset(t *testing.T) {
page := &tree.PageNode{Slug: "lonely-page", ID: "a7b3"}
// Create index.md page
pagePath := filepath.Join(tmp, "lonely-page")
os.MkdirAll(pagePath, 0755)
os.WriteFile(filepath.Join(pagePath, "index.md"), []byte("# Lonely Page"), 0644)
if err := os.MkdirAll(pagePath, 0755); err != nil {
t.Fatalf("failed to create test directory: %v", err)
}
if err := os.WriteFile(filepath.Join(pagePath, "index.md"), []byte("# Lonely Page"), 0644); err != nil {
t.Fatalf("failed to create test file: %v", err)
}
service := NewAssetService(tmp, tree.NewSlugService())

file, name, err := createMultipartFile("my-image.png", []byte("hello image"))
Expand Down Expand Up @@ -77,8 +83,12 @@ func TestDeletePageAndEnsureAllAssetsAreDeleted(t *testing.T) {
page := &tree.PageNode{Slug: "lonely-page", ID: "a7b3"}
// Create index.md page
pagePath := filepath.Join(tmp, "lonely-page")
os.MkdirAll(pagePath, 0755)
os.WriteFile(filepath.Join(pagePath, "index.md"), []byte("# Lonely Page"), 0644)
if err := os.MkdirAll(pagePath, 0755); err != nil {
t.Fatalf("failed to create test directory: %v", err)
}
if err := os.WriteFile(filepath.Join(pagePath, "index.md"), []byte("# Lonely Page"), 0644); err != nil {
t.Fatalf("failed to create test file: %v", err)
}
service := NewAssetService(tmp, tree.NewSlugService())

file, name, err := createMultipartFile("my-image.png", []byte("hello image"))
Expand Down
4 changes: 3 additions & 1 deletion internal/core/auth/auth_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,9 @@ func (a *AuthService) RefreshToken(refreshToken string) (*AuthToken, error) {

func generateJTI() string {
b := make([]byte, 16)
rand.Read(b)
if _, err := rand.Read(b); err != nil {
return ""
}
return hex.EncodeToString(b)
}

Expand Down
3 changes: 1 addition & 2 deletions internal/core/tree/page_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -246,8 +246,7 @@ func (f *PageStore) MovePage(entry *PageNode, parentEntry *PageNode) error {
currentPath := path.Join(f.storageDir, GeneratePathFromPageNode(entry))

// Check if the entry is a file
src := currentPath
dest := parentPath
var src, dest string
if _, err := os.Stat(currentPath + ".md"); err == nil {
src = currentPath + ".md"
dest = path.Join(parentPath, entry.Slug+".md")
Expand Down
39 changes: 28 additions & 11 deletions internal/core/tree/tree_service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -415,17 +415,29 @@ func TestTreeService_FindPageByRoutePath_Success(t *testing.T) {
_ = service.LoadTree()

// Tree: root → architecture → project-a → specs
service.CreatePage(nil, "Architecture", "architecture")
_, err := service.CreatePage(nil, "Architecture", "architecture")
if err != nil {
t.Fatalf("CreatePage failed: %v", err)
}
arch := service.GetTree().Children[0]

service.CreatePage(&arch.ID, "Project A", "project-a")
_, err = service.CreatePage(&arch.ID, "Project A", "project-a")
if err != nil {
t.Fatalf("CreatePage failed: %v", err)
}
projectA := arch.Children[0]

service.CreatePage(&projectA.ID, "Specs", "specs")
_, err = service.CreatePage(&projectA.ID, "Specs", "specs")
if err != nil {
t.Fatalf("CreatePage failed: %v", err)
}

// Datei anlegen
specPath := filepath.Join(tmpDir, "root", "architecture", "project-a", "specs.md")
os.WriteFile(specPath, []byte("# Project A Specs"), 0644)
err = os.WriteFile(specPath, []byte("# Project A Specs"), 0644)
if err != nil {
t.Fatalf("Failed to write specs file: %v", err)
}

// 🔍 Suche über RoutePath
page, err := service.FindPageByRoutePath(service.GetTree().Children, "architecture/project-a/specs")
Expand All @@ -443,10 +455,11 @@ func TestTreeService_FindPageByRoutePath_NotFound(t *testing.T) {
service := NewTreeService(tmpDir)
_ = service.LoadTree()

service.CreatePage(nil, "Top", "top")
if _, err := service.CreatePage(nil, "Top", "top"); err != nil {
t.Fatalf("CreatePage failed: %v", err)
}

_, err := service.FindPageByRoutePath(service.GetTree().Children, "top/missing")
if err == nil {
if _, err := service.FindPageByRoutePath(service.GetTree().Children, "top/missing"); err == nil {
t.Error("Expected error for non-existent nested path, got nil")
}
}
Expand All @@ -456,11 +469,15 @@ func TestTreeService_FindPageByRoutePath_PartialMatch(t *testing.T) {
service := NewTreeService(tmpDir)
_ = service.LoadTree()

service.CreatePage(nil, "Docs", "docs")
service.CreatePage(nil, "API", "api")
if _, err := service.CreatePage(nil, "Docs", "docs"); err != nil {
t.Fatalf("CreatePage failed: %v", err)
}

if _, err := service.CreatePage(nil, "API", "api"); err != nil {
t.Fatalf("CreatePage failed: %v", err)
}

_, err := service.FindPageByRoutePath(service.GetTree().Children, "docs/should-not-exist")
if err == nil {
if _, err := service.FindPageByRoutePath(service.GetTree().Children, "docs/should-not-exist"); err == nil {
t.Error("Expected error for unmatched subpath")
}
}
Expand Down
22 changes: 15 additions & 7 deletions internal/http/router_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -954,17 +954,21 @@ func TestAssetEndpoints(t *testing.T) {
writer := multipart.NewWriter(body)

part, _ := writer.CreateFormFile("file", "testfile.txt")
part.Write([]byte("Hello, asset!"))
if _, err := part.Write([]byte("Hello, asset!")); err != nil {
t.Fatalf("Failed to write file: %v", err)
}
writer.Close()

req := httptest.NewRequest(http.MethodPost, "/api/pages/"+page.ID+"/assets", body)
req.Header.Set("Content-Type", writer.FormDataContentType())

// Auth
// Get Token for authentication
login := authenticatedRequest(t, router, http.MethodPost, "/api/auth/login", strings.NewReader(`{"identifier": "admin", "password": "admin"}`))
var loginResp map[string]string
json.Unmarshal(login.Body.Bytes(), &loginResp)
token := loginResp["token"]
var loginResp map[string]interface{}
if err := json.Unmarshal(login.Body.Bytes(), &loginResp); err != nil {
t.Fatalf("Invalid login JSON: %v", err)
}
token := loginResp["token"].(string)
req.Header.Set("Authorization", "Bearer "+token)

rec := httptest.NewRecorder()
Expand All @@ -988,7 +992,9 @@ func TestAssetEndpoints(t *testing.T) {
t.Fatalf("Expected 200 OK on listing, got %d", listRec.Code)
}
var listResp map[string][]string
json.Unmarshal(listRec.Body.Bytes(), &listResp)
if err := json.Unmarshal(listRec.Body.Bytes(), &listResp); err != nil {
t.Fatalf("Invalid listing JSON: %v", err)
}
if len(listResp["files"]) != 1 || listResp["files"][0] != "/assets/"+page.ID+"/testfile.txt" {
t.Errorf("Expected file in listing, got: %v", listResp["files"])
}
Expand All @@ -1002,7 +1008,9 @@ func TestAssetEndpoints(t *testing.T) {
// Step 5: Verify asset is gone
listRec2 := authenticatedRequest(t, router, http.MethodGet, "/api/pages/"+page.ID+"/assets", nil)
var listResp2 map[string][]string
json.Unmarshal(listRec2.Body.Bytes(), &listResp2)
if err := json.Unmarshal(listRec2.Body.Bytes(), &listResp2); err != nil {
t.Fatalf("Invalid listing JSON: %v", err)
}
if len(listResp2["files"]) != 0 {
t.Errorf("Expected asset to be deleted, got: %v", listResp2["files"])
}
Expand Down