diff options
| author | Claude <noreply@anthropic.com> | 2026-02-18 06:18:28 +0000 |
|---|---|---|
| committer | Claude <noreply@anthropic.com> | 2026-02-18 06:18:28 +0000 |
| commit | 269e44da41f9feed32214bbab6fc16ec88fffd85 (patch) | |
| tree | 6c6312b8ad3fd9175b2992e3e044fa6257e3ef43 /api/api_stress_test.go | |
| parent | 8eb86cdc49c3c2f69d8a64f855220ebd68be336c (diff) | |
| download | neko-269e44da41f9feed32214bbab6fc16ec88fffd85.tar.gz neko-269e44da41f9feed32214bbab6fc16ec88fffd85.tar.bz2 neko-269e44da41f9feed32214bbab6fc16ec88fffd85.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_stress_test.go')
| -rw-r--r-- | api/api_stress_test.go | 134 |
1 files changed, 134 insertions, 0 deletions
diff --git a/api/api_stress_test.go b/api/api_stress_test.go index a846f75..4fcbf5e 100644 --- a/api/api_stress_test.go +++ b/api/api_stress_test.go @@ -194,6 +194,140 @@ func TestStress_LargeDataset(t *testing.T) { } } +func TestStress_ConcurrentMixedOperations(t *testing.T) { + if testing.Short() { + t.Skip("skipping stress test in short mode") + } + + setupTestDB(t) + + // Create multiple feeds with items across categories + categories := []string{"tech", "news", "science", "art"} + for i, cat := range categories { + f := &feed.Feed{ + Url: fmt.Sprintf("http://example.com/mixed/%d", i), + Title: fmt.Sprintf("Mixed Feed %d", i), + Category: cat, + } + f.Create() + for j := 0; j < 25; j++ { + it := &item.Item{ + Title: fmt.Sprintf("Mixed Item %d-%d", i, j), + Url: fmt.Sprintf("http://example.com/mixed/%d/%d", i, j), + Description: fmt.Sprintf("<p>Mixed content %d-%d</p>", i, j), + PublishDate: "2024-01-01 00:00:00", + FeedId: f.Id, + } + _ = it.Create() + } + } + + server := newTestServer() + + const goroutines = 40 + var wg sync.WaitGroup + errors := make(chan error, goroutines*2) + + start := time.Now() + + // Mix of reads, filtered reads, updates, and exports + for i := 0; i < goroutines; i++ { + wg.Add(1) + go func(idx int) { + defer wg.Done() + + var req *http.Request + switch idx % 4 { + case 0: + // Stream with category filter + req = httptest.NewRequest("GET", "/stream?tag="+categories[idx%len(categories)], nil) + case 1: + // Stream with search + req = httptest.NewRequest("GET", "/stream?q=Mixed", nil) + case 2: + // Feed list + req = httptest.NewRequest("GET", "/feed", nil) + case 3: + // Export + req = httptest.NewRequest("GET", "/export/json", nil) + } + + rr := httptest.NewRecorder() + server.ServeHTTP(rr, req) + if rr.Code != http.StatusOK { + errors <- fmt.Errorf("op %d (type %d) got status %d", idx, idx%4, rr.Code) + } + }(i) + } + wg.Wait() + close(errors) + elapsed := time.Since(start) + + for err := range errors { + t.Errorf("concurrent mixed operation error: %v", err) + } + + t.Logf("40 concurrent mixed operations completed in %v", elapsed) + if elapsed > 10*time.Second { + t.Errorf("concurrent mixed operations took too long: %v (threshold: 10s)", elapsed) + } +} + +func TestStress_RapidReadMarkUnmark(t *testing.T) { + if testing.Short() { + t.Skip("skipping stress test in short mode") + } + + setupTestDB(t) + + f := &feed.Feed{Url: "http://example.com/rapid", Title: "Rapid Feed"} + f.Create() + it := &item.Item{ + Title: "Rapid Toggle", + Url: "http://example.com/rapid/1", + FeedId: f.Id, + } + _ = it.Create() + + server := newTestServer() + + // Rapidly toggle read state on the same item + const iterations = 100 + var wg sync.WaitGroup + errors := make(chan error, iterations) + + start := time.Now() + for i := 0; i < iterations; i++ { + wg.Add(1) + go func(idx int) { + defer wg.Done() + body, _ := json.Marshal(item.Item{ + Id: it.Id, + ReadState: idx%2 == 0, + Starred: idx%3 == 0, + }) + req := httptest.NewRequest("PUT", "/item/"+strconv.FormatInt(it.Id, 10), bytes.NewBuffer(body)) + rr := httptest.NewRecorder() + server.ServeHTTP(rr, req) + if rr.Code != http.StatusOK { + errors <- fmt.Errorf("rapid update %d got status %d", idx, rr.Code) + } + }(i) + } + wg.Wait() + close(errors) + elapsed := time.Since(start) + + for err := range errors { + t.Errorf("rapid toggle error: %v", err) + } + + t.Logf("100 concurrent read-state toggles completed in %v", elapsed) + if elapsed > 10*time.Second { + t.Errorf("rapid toggles took too long: %v (threshold: 10s)", elapsed) + } +} + func seedStressData(t *testing.T, count int) { t.Helper() f := &feed.Feed{Url: "http://example.com/bench", Title: "Bench Feed", Category: "tech"} |
