- New Go backend binary (backend + runner) replacing old scraper/ - Rename SCRAPER_API_URL → BACKEND_API_URL in UI env and docker-compose - Rename scraperFetch → backendFetch across all 19 UI server files - Remove SCRAPER_PROXY env var and proxy transport from browser.Config - Add Meilisearch, Valkey, Caddy to docker-compose - Add docs/: api-endpoints.md, request-flow.mermaid.md, data-flow.mermaid.md
142 lines
3.9 KiB
Go
142 lines
3.9 KiB
Go
package browser_test
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/libnovel/backend/internal/browser"
|
|
)
|
|
|
|
func TestDirectClient_GetContent_Success(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Write([]byte("<html>hello</html>"))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
c := browser.NewDirectClient(browser.Config{MaxConcurrent: 2, Timeout: 5 * time.Second})
|
|
body, err := c.GetContent(context.Background(), srv.URL)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if body != "<html>hello</html>" {
|
|
t.Errorf("want <html>hello</html>, got %q", body)
|
|
}
|
|
}
|
|
|
|
func TestDirectClient_GetContent_4xxReturnsError(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
c := browser.NewDirectClient(browser.Config{})
|
|
_, err := c.GetContent(context.Background(), srv.URL)
|
|
if err == nil {
|
|
t.Fatal("expected error for 404")
|
|
}
|
|
}
|
|
|
|
func TestDirectClient_SemaphoreBlocksConcurrency(t *testing.T) {
|
|
const maxConcurrent = 2
|
|
var inflight atomic.Int32
|
|
var peak atomic.Int32
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
n := inflight.Add(1)
|
|
if int(n) > int(peak.Load()) {
|
|
peak.Store(n)
|
|
}
|
|
time.Sleep(20 * time.Millisecond)
|
|
inflight.Add(-1)
|
|
w.Write([]byte("ok"))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
c := browser.NewDirectClient(browser.Config{MaxConcurrent: maxConcurrent, Timeout: 5 * time.Second})
|
|
|
|
var wg sync.WaitGroup
|
|
for i := 0; i < 8; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
c.GetContent(context.Background(), srv.URL)
|
|
}()
|
|
}
|
|
wg.Wait()
|
|
|
|
if int(peak.Load()) > maxConcurrent {
|
|
t.Errorf("concurrent requests exceeded limit: peak=%d, limit=%d", peak.Load(), maxConcurrent)
|
|
}
|
|
}
|
|
|
|
func TestDirectClient_ContextCancel(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
time.Sleep(200 * time.Millisecond)
|
|
w.Write([]byte("ok"))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel() // cancel before making the request
|
|
|
|
c := browser.NewDirectClient(browser.Config{})
|
|
_, err := c.GetContent(ctx, srv.URL)
|
|
if err == nil {
|
|
t.Fatal("expected context cancellation error")
|
|
}
|
|
}
|
|
|
|
// ── StubClient ────────────────────────────────────────────────────────────────
|
|
|
|
func TestStubClient_ReturnsRegisteredPage(t *testing.T) {
|
|
stub := browser.NewStub()
|
|
stub.SetPage("http://example.com/page1", "<html>page1</html>")
|
|
|
|
body, err := stub.GetContent(context.Background(), "http://example.com/page1")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if body != "<html>page1</html>" {
|
|
t.Errorf("want page1 html, got %q", body)
|
|
}
|
|
}
|
|
|
|
func TestStubClient_ReturnsRegisteredError(t *testing.T) {
|
|
stub := browser.NewStub()
|
|
want := errors.New("network failure")
|
|
stub.SetError("http://example.com/bad", want)
|
|
|
|
_, err := stub.GetContent(context.Background(), "http://example.com/bad")
|
|
if err == nil {
|
|
t.Fatal("expected error")
|
|
}
|
|
}
|
|
|
|
func TestStubClient_UnknownURLReturnsError(t *testing.T) {
|
|
stub := browser.NewStub()
|
|
_, err := stub.GetContent(context.Background(), "http://unknown.example.com/")
|
|
if err == nil {
|
|
t.Fatal("expected error for unknown URL")
|
|
}
|
|
}
|
|
|
|
func TestStubClient_CallLog(t *testing.T) {
|
|
stub := browser.NewStub()
|
|
stub.SetPage("http://example.com/a", "a")
|
|
stub.SetPage("http://example.com/b", "b")
|
|
|
|
stub.GetContent(context.Background(), "http://example.com/a")
|
|
stub.GetContent(context.Background(), "http://example.com/b")
|
|
|
|
log := stub.CallLog()
|
|
if len(log) != 2 || log[0] != "http://example.com/a" || log[1] != "http://example.com/b" {
|
|
t.Errorf("unexpected call log: %v", log)
|
|
}
|
|
}
|