aboutsummaryrefslogtreecommitdiffstats
path: root/api/api_test.go
diff options
context:
space:
mode:
authorClaude <noreply@anthropic.com>2026-02-18 06:18:28 +0000
committerClaude <noreply@anthropic.com>2026-02-18 06:18:28 +0000
commit269e44da41f9feed32214bbab6fc16ec88fffd85 (patch)
tree6c6312b8ad3fd9175b2992e3e044fa6257e3ef43 /api/api_test.go
parent8eb86cdc49c3c2f69d8a64f855220ebd68be336c (diff)
downloadneko-claude/improve-test-coverage-iBkwc.tar.gz
neko-claude/improve-test-coverage-iBkwc.tar.bz2
neko-claude/improve-test-coverage-iBkwc.zip
Increase test coverage across lowest-coverage packagesclaude/improve-test-coverage-iBkwc
Major coverage improvements: - safehttp: 46.7% -> 93.3% (SafeDialer, redirect checking, SSRF protection) - api: 81.8% -> 96.4% (HandleImport 0% -> 100%, stream errors, content types) - importer: 85.3% -> 94.7% (ImportFeeds dispatcher, OPML nesting, edge cases) - cmd/neko: 77.1% -> 85.4% (purge, secure-cookies, minutes, allow-local flags) New tests added: - Security regression tests (CSRF token uniqueness, mismatch rejection, auth cookie HttpOnly, security headers, API auth requirements) - Stress tests for concurrent mixed operations and rapid state toggling - SSRF protection tests for SafeDialer hostname resolution and redirect paths https://claude.ai/code/session_01XUBh32rHpbYue1JYXSH64Q
Diffstat (limited to 'api/api_test.go')
-rw-r--r--api/api_test.go229
1 files changed, 229 insertions, 0 deletions
diff --git a/api/api_test.go b/api/api_test.go
index a2c3415..2c77501 100644
--- a/api/api_test.go
+++ b/api/api_test.go
@@ -3,6 +3,7 @@ package api
import (
"bytes"
"encoding/json"
+ "mime/multipart"
"net/http"
"net/http/httptest"
"path/filepath"
@@ -546,3 +547,231 @@ func TestHandleCategorySuccess(t *testing.T) {
t.Errorf("Expected %d, got %d", http.StatusOK, rr.Code)
}
}
+
+func TestHandleImportOPML(t *testing.T) {
+ setupTestDB(t)
+ server := newTestServer()
+
+ opmlContent := `<?xml version="1.0" encoding="UTF-8"?>
+<opml version="2.0">
+ <head><title>test</title></head>
+ <body>
+ <outline type="rss" text="Test Feed" title="Test Feed" xmlUrl="https://example.com/feed" htmlUrl="https://example.com"/>
+ </body>
+</opml>`
+
+ body := &bytes.Buffer{}
+ writer := multipart.NewWriter(body)
+ part, err := writer.CreateFormFile("file", "feeds.opml")
+ if err != nil {
+ t.Fatal(err)
+ }
+ part.Write([]byte(opmlContent))
+ writer.Close()
+
+ req := httptest.NewRequest("POST", "/import", body)
+ req.Header.Set("Content-Type", writer.FormDataContentType())
+ rr := httptest.NewRecorder()
+ server.HandleImport(rr, req)
+
+ if rr.Code != http.StatusOK {
+ t.Errorf("expected %d, got %d: %s", http.StatusOK, rr.Code, rr.Body.String())
+ }
+
+ var resp map[string]string
+ json.NewDecoder(rr.Body).Decode(&resp)
+ if resp["status"] != "ok" {
+ t.Errorf("expected status ok, got %q", resp["status"])
+ }
+
+ // Verify the feed was imported
+ feeds, _ := feed.All()
+ if len(feeds) != 1 {
+ t.Errorf("expected 1 feed after import, got %d", len(feeds))
+ }
+
+ time.Sleep(100 * time.Millisecond) // let goroutine settle
+}
+
+func TestHandleImportText(t *testing.T) {
+ setupTestDB(t)
+ server := newTestServer()
+
+ textContent := "https://example.com/feed1\nhttps://example.com/feed2\n"
+
+ body := &bytes.Buffer{}
+ writer := multipart.NewWriter(body)
+ part, err := writer.CreateFormFile("file", "feeds.txt")
+ if err != nil {
+ t.Fatal(err)
+ }
+ part.Write([]byte(textContent))
+ writer.WriteField("format", "text")
+ writer.Close()
+
+ req := httptest.NewRequest("POST", "/import?format=text", body)
+ req.Header.Set("Content-Type", writer.FormDataContentType())
+ rr := httptest.NewRecorder()
+ server.HandleImport(rr, req)
+
+ if rr.Code != http.StatusOK {
+ t.Errorf("expected %d, got %d: %s", http.StatusOK, rr.Code, rr.Body.String())
+ }
+
+ feeds, _ := feed.All()
+ if len(feeds) != 2 {
+ t.Errorf("expected 2 feeds after text import, got %d", len(feeds))
+ }
+
+ time.Sleep(100 * time.Millisecond)
+}
+
+func TestHandleImportMethodNotAllowed(t *testing.T) {
+ server := newTestServer()
+
+ req := httptest.NewRequest("GET", "/import", nil)
+ rr := httptest.NewRecorder()
+ server.HandleImport(rr, req)
+
+ if rr.Code != http.StatusMethodNotAllowed {
+ t.Errorf("expected %d, got %d", http.StatusMethodNotAllowed, rr.Code)
+ }
+}
+
+func TestHandleImportNoFile(t *testing.T) {
+ setupTestDB(t)
+ server := newTestServer()
+
+ req := httptest.NewRequest("POST", "/import", nil)
+ rr := httptest.NewRecorder()
+ server.HandleImport(rr, req)
+
+ if rr.Code != http.StatusBadRequest {
+ t.Errorf("expected %d, got %d", http.StatusBadRequest, rr.Code)
+ }
+}
+
+func TestHandleImportUnsupportedFormat(t *testing.T) {
+ setupTestDB(t)
+ server := newTestServer()
+
+ body := &bytes.Buffer{}
+ writer := multipart.NewWriter(body)
+ part, _ := writer.CreateFormFile("file", "feeds.csv")
+ part.Write([]byte("some data"))
+ writer.Close()
+
+ req := httptest.NewRequest("POST", "/import?format=csv", body)
+ req.Header.Set("Content-Type", writer.FormDataContentType())
+ rr := httptest.NewRecorder()
+ server.HandleImport(rr, req)
+
+ if rr.Code != http.StatusInternalServerError {
+ t.Errorf("expected %d for unsupported format, got %d", http.StatusInternalServerError, rr.Code)
+ }
+}
+
+func TestHandleImportInvalidOPML(t *testing.T) {
+ setupTestDB(t)
+ server := newTestServer()
+
+ body := &bytes.Buffer{}
+ writer := multipart.NewWriter(body)
+ part, _ := writer.CreateFormFile("file", "bad.opml")
+ part.Write([]byte("not valid xml at all"))
+ writer.Close()
+
+ req := httptest.NewRequest("POST", "/import?format=opml", body)
+ req.Header.Set("Content-Type", writer.FormDataContentType())
+ rr := httptest.NewRecorder()
+ server.HandleImport(rr, req)
+
+ if rr.Code != http.StatusInternalServerError {
+ t.Errorf("expected %d for invalid OPML, got %d", http.StatusInternalServerError, rr.Code)
+ }
+}
+
+func TestHandleStreamErrorOnClosedDB(t *testing.T) {
+ setupTestDB(t)
+ seedData(t)
+ server := newTestServer()
+
+ // Close the DB to force an error
+ models.DB.Close()
+
+ req := httptest.NewRequest("GET", "/stream", nil)
+ rr := httptest.NewRecorder()
+ server.HandleStream(rr, req)
+
+ if rr.Code != http.StatusInternalServerError {
+ t.Errorf("expected %d for closed DB, got %d", http.StatusInternalServerError, rr.Code)
+ }
+}
+
+func TestHandleItemInvalidJSON(t *testing.T) {
+ setupTestDB(t)
+ seedData(t)
+ server := newTestServer()
+
+ req := httptest.NewRequest("PUT", "/item/1", strings.NewReader("not json"))
+ rr := httptest.NewRecorder()
+ server.HandleItem(rr, req)
+
+ if rr.Code != http.StatusBadRequest {
+ t.Errorf("expected %d for invalid JSON, got %d", http.StatusBadRequest, rr.Code)
+ }
+}
+
+func TestHandleExportContentTypes(t *testing.T) {
+ setupTestDB(t)
+ seedData(t)
+ server := newTestServer()
+
+ testCases := []struct {
+ format string
+ contentType string
+ disposition string
+ }{
+ {"text", "text/plain", "neko_export.txt"},
+ {"opml", "application/xml", "neko_export.opml"},
+ {"json", "application/json", "neko_export.json"},
+ {"html", "text/html", "neko_export.html"},
+ }
+
+ for _, tc := range testCases {
+ req := httptest.NewRequest("GET", "/export/"+tc.format, nil)
+ rr := httptest.NewRecorder()
+ server.HandleExport(rr, req)
+
+ if ct := rr.Header().Get("Content-Type"); ct != tc.contentType {
+ t.Errorf("export/%s: expected Content-Type %q, got %q", tc.format, tc.contentType, ct)
+ }
+ if cd := rr.Header().Get("Content-Disposition"); !strings.Contains(cd, tc.disposition) {
+ t.Errorf("export/%s: expected Content-Disposition containing %q, got %q", tc.format, tc.disposition, cd)
+ }
+ }
+}
+
+func TestHandleImportJSON(t *testing.T) {
+ setupTestDB(t)
+ server := newTestServer()
+
+ jsonContent := `{"title":"Article 1","url":"https://example.com/1","description":"desc","read":false,"starred":false,"date":{"$date":"2024-01-01"},"feed":{"url":"https://example.com/feed","title":"Feed 1"}}`
+
+ body := &bytes.Buffer{}
+ writer := multipart.NewWriter(body)
+ part, _ := writer.CreateFormFile("file", "items.json")
+ part.Write([]byte(jsonContent))
+ writer.Close()
+
+ req := httptest.NewRequest("POST", "/import?format=json", body)
+ req.Header.Set("Content-Type", writer.FormDataContentType())
+ rr := httptest.NewRecorder()
+ server.HandleImport(rr, req)
+
+ if rr.Code != http.StatusOK {
+ t.Errorf("expected %d, got %d: %s", http.StatusOK, rr.Code, rr.Body.String())
+ }
+
+ time.Sleep(100 * time.Millisecond)
+}