mirror of
https://github.com/karust/openserp.git
synced 2026-08-13 12:23:29 +08:00
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:
@@ -5,6 +5,7 @@
|
||||
[](https://goreportcard.com/report/github.com/karust/openserp)
|
||||
[](https://pkg.go.dev/github.com/karust/openserp)
|
||||
[](https://github.com/karust/openserp/releases)
|
||||
[](https://github.com/karust/openserp/actions/workflows/ci.yml)
|
||||
|
||||
<!-- [](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._
|
||||
|
||||
54
baidu/search_integration_test.go
Normal file
54
baidu/search_integration_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
54
bing/search_integration_test.go
Normal file
54
bing/search_integration_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
version = "0.6.2"
|
||||
version = "0.6.3"
|
||||
defaultConfigFilename = "config"
|
||||
envPrefix = "OPENSERP"
|
||||
)
|
||||
|
||||
54
duckduckgo/search_integration_test.go
Normal file
54
duckduckgo/search_integration_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
54
google/search_integration_test.go
Normal file
54
google/search_integration_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
53
testutil/ithelper/ithelper.go
Normal file
53
testutil/ithelper/ithelper.go
Normal 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
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user