Skip to content
Merged
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
233 changes: 233 additions & 0 deletions internal/http/router_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1453,3 +1453,236 @@ func TestIndexingStatusEndpoint(t *testing.T) {
t.Errorf("Expected 'active' field in response, got: %v", status)
}
}

// uploadTestAsset is a helper function that creates a page, uploads an asset, and returns the asset URL and auth cookies.
// If needsAuth is true, it will obtain authentication cookies; otherwise it will get CSRF token only (for AuthDisabled mode).
func uploadTestAsset(t *testing.T, router *gin.Engine, w *wiki.Wiki, content string, needsAuth bool) (assetURL string, cookies []*http.Cookie) {
// Create a page
page, err := w.CreatePage("system", nil, "Test Page", "test-page")
if err != nil {
t.Fatalf("Failed to create page: %v", err)
}

// Prepare the file upload
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile("file", "test.txt")
if err != nil {
t.Fatalf("Failed to create form file: %v", err)
}
if _, err := part.Write([]byte(content)); err != nil {
t.Fatalf("Failed to write file: %v", err)
}
if err := writer.Close(); err != nil {
t.Fatalf("Failed to close multipart writer: %v", err)
}

var csrfToken string

if needsAuth {
// Login to get auth cookies
loginBody := `{"identifier": "admin", "password": "admin"}`
loginReq := httptest.NewRequest(http.MethodPost, "/api/auth/login", strings.NewReader(loginBody))
loginReq.Header.Set("Content-Type", "application/json")
loginRec := httptest.NewRecorder()
router.ServeHTTP(loginRec, loginReq)

if loginRec.Code != http.StatusOK {
t.Fatalf("Expected 200 OK on login, got %d", loginRec.Code)
}

cookies = loginRec.Result().Cookies()
csrfToken = loginRec.Header().Get("X-CSRF-Token")
if csrfToken == "" {
for _, c := range cookies {
if c.Name == "leafwiki_csrf" || c.Name == "__Host-leafwiki_csrf" {
csrfToken = c.Value
break
}
}
}
} else {
// Get CSRF token only (for AuthDisabled mode)
configReq := httptest.NewRequest(http.MethodGet, "/api/config", nil)
configRec := httptest.NewRecorder()
router.ServeHTTP(configRec, configReq)

cookies = configRec.Result().Cookies()
csrfToken = configRec.Header().Get("X-CSRF-Token")
if csrfToken == "" {
for _, c := range cookies {
if c.Name == "leafwiki_csrf" || c.Name == "__Host-leafwiki_csrf" {
csrfToken = c.Value
break
}
}
}
}

// Upload the asset
uploadReq := httptest.NewRequest(http.MethodPost, "/api/pages/"+page.ID+"/assets", body)
uploadReq.Header.Set("Content-Type", writer.FormDataContentType())
for _, cookie := range cookies {
uploadReq.AddCookie(cookie)
}
uploadReq.Header.Set("X-CSRF-Token", csrfToken)

uploadRec := httptest.NewRecorder()
router.ServeHTTP(uploadRec, uploadReq)

if uploadRec.Code != http.StatusCreated {
t.Fatalf("Expected 201 Created on upload, got %d - %s", uploadRec.Code, uploadRec.Body.String())
}

var uploadResp map[string]string
if err := json.Unmarshal(uploadRec.Body.Bytes(), &uploadResp); err != nil {
t.Fatalf("Invalid upload JSON: %v", err)
}

assetURL = uploadResp["file"]
if assetURL == "" {
t.Fatal("Expected file URL in upload response")
}

return assetURL, cookies
}

// TestAssetAccessControl tests the access control for static asset routes
func TestAssetAccessControl(t *testing.T) {
t.Run("PrivateMode_UnauthenticatedAccess_Returns401", func(t *testing.T) {
w := createWikiTestInstance(t)
defer w.Close()

// Create router with PublicAccess=false and AuthDisabled=false
router := NewRouter(w, RouterOptions{
PublicAccess: false,
InjectCodeInHeader: "",
AllowInsecure: true,
AccessTokenTimeout: 15 * time.Minute,
RefreshTokenTimeout: 7 * 24 * time.Hour,
HideLinkMetadataSection: false,
AuthDisabled: false,
})

// Upload an asset (with auth)
assetURL, _ := uploadTestAsset(t, router, w, "test content", true)

// Try to access the asset without authentication
assetReq := httptest.NewRequest(http.MethodGet, assetURL, nil)
assetRec := httptest.NewRecorder()
router.ServeHTTP(assetRec, assetReq)

// Should return 401 Unauthorized
if assetRec.Code != http.StatusUnauthorized {
t.Errorf("Expected 401 Unauthorized when accessing asset without auth in private mode, got %d", assetRec.Code)
}
})

t.Run("PrivateMode_AuthenticatedAccess_Returns200", func(t *testing.T) {
w := createWikiTestInstance(t)
defer w.Close()

// Create router with PublicAccess=false and AuthDisabled=false
router := NewRouter(w, RouterOptions{
PublicAccess: false,
InjectCodeInHeader: "",
AllowInsecure: true,
AccessTokenTimeout: 15 * time.Minute,
RefreshTokenTimeout: 7 * 24 * time.Hour,
HideLinkMetadataSection: false,
AuthDisabled: false,
})

// Upload an asset (with auth) and get cookies
assetURL, cookies := uploadTestAsset(t, router, w, "test content", true)

// Access the asset with authentication
assetReq := httptest.NewRequest(http.MethodGet, assetURL, nil)
for _, cookie := range cookies {
assetReq.AddCookie(cookie)
}
assetRec := httptest.NewRecorder()
router.ServeHTTP(assetRec, assetReq)

// Should return 200 OK
if assetRec.Code != http.StatusOK {
t.Errorf("Expected 200 OK when accessing asset with auth in private mode, got %d", assetRec.Code)
}

// Verify content
content := assetRec.Body.String()
if content != "test content" {
t.Errorf("Expected 'test content', got '%s'", content)
}
})

t.Run("PublicAccessMode_UnauthenticatedAccess_Returns200", func(t *testing.T) {
w := createWikiTestInstance(t)
defer w.Close()

// Create router with PublicAccess=true
router := NewRouter(w, RouterOptions{
PublicAccess: true,
InjectCodeInHeader: "",
AllowInsecure: true,
AccessTokenTimeout: 15 * time.Minute,
RefreshTokenTimeout: 7 * 24 * time.Hour,
HideLinkMetadataSection: false,
AuthDisabled: false,
})

// Upload an asset (with auth)
assetURL, _ := uploadTestAsset(t, router, w, "test content public", true)

// Try to access the asset without authentication
assetReq := httptest.NewRequest(http.MethodGet, assetURL, nil)
assetRec := httptest.NewRecorder()
router.ServeHTTP(assetRec, assetReq)

// Should return 200 OK in public mode
if assetRec.Code != http.StatusOK {
t.Errorf("Expected 200 OK when accessing asset without auth in public mode, got %d", assetRec.Code)
}

// Verify content
content := assetRec.Body.String()
if content != "test content public" {
t.Errorf("Expected 'test content public', got '%s'", content)
}
})

t.Run("AuthDisabledMode_UnauthenticatedAccess_Returns200", func(t *testing.T) {
w := createWikiTestInstance(t)
defer w.Close()

// Create router with AuthDisabled=true
router := NewRouter(w, RouterOptions{
PublicAccess: false,
InjectCodeInHeader: "",
AllowInsecure: true,
AccessTokenTimeout: 15 * time.Minute,
RefreshTokenTimeout: 7 * 24 * time.Hour,
HideLinkMetadataSection: false,
AuthDisabled: true,
})

// Upload an asset (no auth needed, but CSRF token still required)
assetURL, _ := uploadTestAsset(t, router, w, "test content no auth", false)

// Try to access the asset without authentication
assetReq := httptest.NewRequest(http.MethodGet, assetURL, nil)
assetRec := httptest.NewRecorder()
router.ServeHTTP(assetRec, assetReq)

// Should return 200 OK when auth is disabled
if assetRec.Code != http.StatusOK {
t.Errorf("Expected 200 OK when accessing asset without auth when AuthDisabled=true, got %d", assetRec.Code)
}

// Verify content
content := assetRec.Body.String()
if content != "test content no auth" {
t.Errorf("Expected 'test content no auth', got '%s'", content)
}
})
}