Add integration tests for multiple search engines and enhance error handling

- Implement integration tests for Baidu, Bing, DuckDuckGo, Google, and Yandex.
- Refactor test utility functions to avoid core import cycles.
This commit is contained in:
Rustem Kamalov
2026-04-14 02:05:58 +03:00
parent de59d07d47
commit ecacfab4d6
10 changed files with 321 additions and 42 deletions

View File

@@ -5,6 +5,7 @@
[![Go Report Card](https://goreportcard.com/badge/github.com/karust/openserp)](https://goreportcard.com/report/github.com/karust/openserp)
[![Go Reference](https://pkg.go.dev/badge/github/karust/openserp?style=for-the-badge)](https://pkg.go.dev/github.com/karust/openserp)
[![release](https://img.shields.io/github/release/karust/openserp)](https://github.com/karust/openserp/releases)
[![CI](https://github.com/karust/openserp/actions/workflows/ci.yml/badge.svg?branch=tests)](https://github.com/karust/openserp/actions/workflows/ci.yml)
<!-- [![Docker Pulls](https://img.shields.io/docker/pulls/karust/openserp)](https://hub.docker.com/repository/docker/karust/openserp) -->
@@ -15,7 +16,6 @@ A developer-friendly alternative to paid SERP API services!
> 💡 OpenSerp is free and open-source. Only links listed in this repository and on the official website are associated with the project.
## Features
- 🔍 **Multi-engine** - search with dedicated endpoints for each engine
@@ -184,5 +184,4 @@ If you encounter issues or have questions:
- Check existing issues for similar reports
- Review the documentation and example config
###### *"OpenSerp" is the name of this open-source project. Use of the name in a way that implies affiliation, endorsement, or official status is not permitted.*
###### _"OpenSerp" is the name of this open-source project. Use of the name in a way that implies affiliation, endorsement, or official status is not permitted._

View File

@@ -0,0 +1,54 @@
//go:build integration
// +build integration
package baidu
import (
"testing"
"github.com/karust/openserp/core"
"github.com/karust/openserp/testutil"
"github.com/karust/openserp/testutil/ithelper"
)
func TestSearchBaidu(t *testing.T) {
testutil.RequireIntegration(t)
browser := ithelper.CreateBrowser(t)
baid := New(*browser, core.SearchEngineOptions{})
query := core.Query{Text: "golang programming", Limit: 10}
results, err := baid.Search(query)
ithelper.HandleError(t, "baidu web search", err)
if len(results) == 0 {
t.Fatal("returned empty results")
}
if results[0].URL == "" {
t.Fatal("first result URL is empty")
}
if results[0].Title == "" {
t.Fatal("first result title is empty")
}
}
func TestImageSearchBaidu(t *testing.T) {
testutil.RequireIntegration(t)
browser := ithelper.CreateBrowser(t)
baid := New(*browser, core.SearchEngineOptions{})
query := core.Query{Text: "golden retriever puppy", Limit: 10}
results, err := baid.SearchImage(query)
ithelper.HandleError(t, "baidu image search", err)
if len(results) == 0 {
t.Fatal("returned empty image results")
}
if results[0].URL == "" {
t.Fatal("first image result URL is empty")
}
if results[0].Title == "" {
t.Fatal("first image result title is empty")
}
}

View File

@@ -0,0 +1,54 @@
//go:build integration
// +build integration
package bing
import (
"testing"
"github.com/karust/openserp/core"
"github.com/karust/openserp/testutil"
"github.com/karust/openserp/testutil/ithelper"
)
func TestSearchBing(t *testing.T) {
testutil.RequireIntegration(t)
browser := ithelper.CreateBrowser(t)
bing := New(*browser, core.SearchEngineOptions{})
query := core.Query{Text: "golang programming", Limit: 10}
results, err := bing.Search(query)
ithelper.HandleError(t, "bing web search", err)
if len(results) == 0 {
t.Fatal("returned empty results")
}
if results[0].URL == "" {
t.Fatal("first result URL is empty")
}
if results[0].Title == "" {
t.Fatal("first result title is empty")
}
}
func TestImageSearchBing(t *testing.T) {
testutil.RequireIntegration(t)
browser := ithelper.CreateBrowser(t)
bing := New(*browser, core.SearchEngineOptions{})
query := core.Query{Text: "golden retriever puppy", Limit: 10}
results, err := bing.SearchImage(query)
ithelper.HandleError(t, "bing image search", err)
if len(results) == 0 {
t.Fatal("returned empty image results")
}
if results[0].URL == "" {
t.Fatal("first image result URL is empty")
}
if results[0].Title == "" {
t.Fatal("first image result title is empty")
}
}

View File

@@ -14,7 +14,7 @@ import (
)
const (
version = "0.6.2"
version = "0.6.3"
defaultConfigFilename = "config"
envPrefix = "OPENSERP"
)

View File

@@ -0,0 +1,54 @@
//go:build integration
// +build integration
package duckduckgo
import (
"testing"
"github.com/karust/openserp/core"
"github.com/karust/openserp/testutil"
"github.com/karust/openserp/testutil/ithelper"
)
func TestSearchDuckDuckGo(t *testing.T) {
testutil.RequireIntegration(t)
browser := ithelper.CreateBrowser(t)
ddg := New(*browser, core.SearchEngineOptions{})
query := core.Query{Text: "wikipedia", Limit: 10}
results, err := ddg.Search(query)
ithelper.HandleError(t, "duckduckgo web search", err)
if len(results) == 0 {
t.Fatal("returned empty results")
}
if results[0].URL == "" {
t.Fatal("first result URL is empty")
}
if results[0].Title == "" {
t.Fatal("first result title is empty")
}
}
func TestImageSearchDuckDuckGo(t *testing.T) {
testutil.RequireIntegration(t)
browser := ithelper.CreateBrowser(t)
ddg := New(*browser, core.SearchEngineOptions{})
query := core.Query{Text: "golden retriever puppy", Limit: 10}
results, err := ddg.SearchImage(query)
ithelper.HandleError(t, "duckduckgo image search", err)
if len(results) == 0 {
t.Fatal("returned empty image results")
}
if results[0].URL == "" {
t.Fatal("first image result URL is empty")
}
if results[0].Title == "" {
t.Fatal("first image result title is empty")
}
}

View File

@@ -0,0 +1,54 @@
//go:build integration
// +build integration
package google
import (
"testing"
"github.com/karust/openserp/core"
"github.com/karust/openserp/testutil"
"github.com/karust/openserp/testutil/ithelper"
)
func TestSearchGoogle(t *testing.T) {
testutil.RequireIntegration(t)
browser := ithelper.CreateBrowser(t)
gogl := New(*browser, core.SearchEngineOptions{})
query := core.Query{Text: "golang programming", Limit: 10}
results, err := gogl.Search(query)
ithelper.HandleError(t, "google web search", err)
if len(results) == 0 {
t.Fatal("returned empty results")
}
if results[0].URL == "" {
t.Fatal("first result URL is empty")
}
if results[0].Title == "" {
t.Fatal("first result title is empty")
}
}
func TestImageSearchGoogle(t *testing.T) {
testutil.RequireIntegration(t)
browser := ithelper.CreateBrowser(t)
gogl := New(*browser, core.SearchEngineOptions{})
query := core.Query{Text: "golden retriever puppy", Limit: 10}
results, err := gogl.SearchImage(query)
ithelper.HandleError(t, "google image search", err)
if len(results) == 0 {
t.Fatal("returned empty image results")
}
if results[0].URL == "" {
t.Fatal("first image result URL is empty")
}
if results[0].Title == "" {
t.Fatal("first image result title is empty")
}
}

View File

@@ -6,9 +6,8 @@ import (
"net/http"
"os"
"path/filepath"
"reflect"
"testing"
"github.com/karust/openserp/core"
)
// ResponseFromFixture reads an HTML file from the package's testdata/ directory
@@ -39,32 +38,49 @@ func ResponseFromBytes(data []byte) *http.Response {
}
// AssertSequentialRanks verifies that result ranks start at 1 and increase by 1.
func AssertSequentialRanks(t *testing.T, results []core.SearchResult) {
// Accepts any slice of structs with an int Rank field (avoids core import cycle).
func AssertSequentialRanks(t *testing.T, results any) {
t.Helper()
for i, r := range results {
if r.Rank != i+1 {
t.Fatalf("rank sequence broken at index %d: got %d, want %d", i, r.Rank, i+1)
v := reflect.ValueOf(results)
if v.Kind() != reflect.Slice {
t.Fatalf("expected a slice of results, got %T", results)
}
for i := 0; i < v.Len(); i++ {
item := v.Index(i)
if item.Kind() == reflect.Ptr {
item = item.Elem()
}
rank := int(item.FieldByName("Rank").Int())
if rank != i+1 {
t.Fatalf("rank sequence broken at index %d: got %d, want %d", i, rank, i+1)
}
}
}
// AssertFirstResultFilled checks that the first result has non-empty URL, Title, and Description.
func AssertFirstResultFilled(t *testing.T, results []core.SearchResult) {
// Accepts any slice of structs with those string fields (avoids core import cycle).
func AssertFirstResultFilled(t *testing.T, results any) {
t.Helper()
if len(results) == 0 {
v := reflect.ValueOf(results)
if v.Kind() != reflect.Slice {
t.Fatalf("expected a slice of results, got %T", results)
}
if v.Len() == 0 {
t.Fatal("expected at least one result")
}
first := results[0]
if first.URL == "" {
t.Fatal("first result URL is empty")
first := v.Index(0)
if first.Kind() == reflect.Ptr {
first = first.Elem()
}
if first.Title == "" {
t.Fatal("first result title is empty")
}
if first.Description == "" {
t.Fatal("first result description is empty")
for _, field := range []string{"URL", "Title", "Description"} {
val := first.FieldByName(field)
if !val.IsValid() || val.String() == "" {
t.Fatalf("first result %s is empty", field)
}
}
}

View File

@@ -7,6 +7,8 @@ import (
)
const IntegrationEnv = "OPENSERP_INTEGRATION_TESTS"
const IntegrationHeadfulEnv = "OPENSERP_INTEGRATION_HEADFUL"
const IntegrationStrictEnv = "OPENSERP_INTEGRATION_STRICT"
func RequireIntegration(t *testing.T) {
t.Helper()
@@ -15,6 +17,16 @@ func RequireIntegration(t *testing.T) {
}
}
func IntegrationHeadful() bool {
value := strings.TrimSpace(strings.ToLower(os.Getenv(IntegrationHeadfulEnv)))
return value == "1" || value == "true" || value == "yes"
}
func IntegrationStrict() bool {
value := strings.TrimSpace(strings.ToLower(os.Getenv(IntegrationStrictEnv)))
return value == "1" || value == "true" || value == "yes"
}
func RequireEnv(t *testing.T, key string) string {
t.Helper()
value := strings.TrimSpace(os.Getenv(key))

View File

@@ -0,0 +1,53 @@
package ithelper
import (
"testing"
"time"
"github.com/karust/openserp/core"
"github.com/karust/openserp/testutil"
)
// HandleError skips on captcha/timeout (expected in live environments),
// fatals on other errors. In strict mode, captcha/timeout also fatal.
func HandleError(t *testing.T, operation string, err error) {
t.Helper()
if err == nil {
return
}
if err == core.ErrCaptcha {
t.Logf("captcha detected during %s: %v", operation, err)
if testutil.IntegrationStrict() {
t.Fatalf("%s failed (strict mode): %v", operation, err)
}
t.Skipf("skipping flaky live %s due to captcha: %v", operation, err)
}
if err == core.ErrSearchTimeout {
if testutil.IntegrationStrict() {
t.Fatalf("%s failed (strict mode): %v", operation, err)
}
t.Skipf("skipping flaky live %s due to timeout: %v", operation, err)
}
t.Fatalf("%s failed: %v", operation, err)
}
// CreateBrowser creates a browser configured for integration tests.
// Respects OPENSERP_INTEGRATION_HEADFUL for debugging.
func CreateBrowser(t *testing.T) *core.Browser {
t.Helper()
headful := testutil.IntegrationHeadful()
opts := core.BrowserOpts{
IsHeadless: !headful,
IsLeakless: false,
Timeout: time.Second * 15,
LeavePageOpen: headful,
}
b, err := core.NewBrowser(opts)
if err != nil {
t.Fatalf("failed to create test browser: %v", err)
}
return b
}

View File

@@ -5,36 +5,21 @@ package yandex
import (
"testing"
"time"
"github.com/karust/openserp/core"
"github.com/karust/openserp/testutil"
"github.com/karust/openserp/testutil/ithelper"
)
func createTestBrowser(t *testing.T) *core.Browser {
t.Helper()
opts := core.BrowserOpts{IsHeadless: false, IsLeakless: false, Timeout: time.Second * 15, LeavePageOpen: true}
b, err := core.NewBrowser(opts)
if err != nil {
t.Fatalf("failed to create test browser: %v", err)
}
return b
}
func TestSearchYandex(t *testing.T) {
testutil.RequireIntegration(t)
browser := createTestBrowser(t)
browser := ithelper.CreateBrowser(t)
yand := New(*browser, core.SearchEngineOptions{})
query := core.Query{Text: "HEY", Limit: 10}
results, err := yand.Search(query)
if err != nil {
if err == core.ErrSearchTimeout || err == core.ErrCaptcha {
t.Skipf("skipping unstable live yandex search result: %v", err)
}
t.Fatalf("Cannot [SearchYandex]: %s", err)
}
ithelper.HandleError(t, "yandex web search", err)
if len(results) == 0 {
t.Fatalf("[SearchYandex] returned empty result")
@@ -44,14 +29,12 @@ func TestSearchYandex(t *testing.T) {
func TestImageYandex(t *testing.T) {
testutil.RequireIntegration(t)
browser := createTestBrowser(t)
browser := ithelper.CreateBrowser(t)
yand := New(*browser, core.SearchEngineOptions{})
query := core.Query{Text: "furry tiger", Limit: 30}
results, err := yand.SearchImage(query)
if err != nil {
t.Fatalf("Cannot [ImageYandex]: %s", err)
}
ithelper.HandleError(t, "yandex image search", err)
if len(results) == 0 {
t.Fatalf("[ImageYandex] returned empty result")