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("hello")) })) 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 != "hello" { t.Errorf("want hello, 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", "page1") body, err := stub.GetContent(context.Background(), "http://example.com/page1") if err != nil { t.Fatalf("unexpected error: %v", err) } if body != "page1" { 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) } }