From e5e0c963030f2c03d6b005ae803ffbc4591e4df3 Mon Sep 17 00:00:00 2001 From: Rustem Kamalov Date: Wed, 15 Apr 2026 02:50:52 +0300 Subject: [PATCH] Add URL builder edge-case tests and server endpoint coverage --- baidu/all_test.go | 179 ++++++++++++++++++++++++++----- bing/bing_test.go | 192 ++++++++++++++++++++++++++------- core/server_test.go | 218 ++++++++++++++++++++++++++++++++++++++ duckduckgo/search_test.go | 142 ++++++++++++++++++++----- google/search_test.go | 207 ++++++++++++++++++++++++++++++++++++ 5 files changed, 845 insertions(+), 93 deletions(-) diff --git a/baidu/all_test.go b/baidu/all_test.go index a5ec46b..cc60ff2 100644 --- a/baidu/all_test.go +++ b/baidu/all_test.go @@ -7,41 +7,164 @@ import ( "github.com/karust/openserp/core" ) -var testQuery = core.Query{Text: "go", Site: "tutorialspoint.com", DateInterval: "20140101..20230101", Limit: 10} - -func TestUrlBuild(t *testing.T) { - res, err := BuildURL(testQuery) - if err != nil { - t.Fatal(err) +func TestBuildURL(t *testing.T) { + tests := []struct { + name string + query core.Query + wantErr bool + check func(*testing.T, url.Values, string) + }{ + { + name: "combined params with unicode and start zero", + query: core.Query{ + Text: "go 搜索", + Site: "tutorialspoint.com", + Filetype: "pdf", + DateInterval: "20140101..20230101", + Limit: 10, + Start: 0, + }, + check: func(t *testing.T, params url.Values, host string) { + t.Helper() + if host != "www.baidu.com" { + t.Fatalf("unexpected host: %s", host) + } + if got := params.Get("wd"); got != "go 搜索 site:tutorialspoint.com filetype:pdf" { + t.Fatalf("unexpected wd value: %q", got) + } + if got := params.Get("gpc"); got != "stf=1388534400,1672531200|stftype=2" { + t.Fatalf("unexpected gpc value: %q", got) + } + if got := params.Get("rn"); got != "10" { + t.Fatalf("unexpected rn value: %q", got) + } + if got := params.Get("pn"); got != "" { + t.Fatalf("pn should be omitted when Start=0, got %q", got) + } + if got := params.Get("f"); got != "8" { + t.Fatalf("unexpected f value: %q", got) + } + if got := params.Get("ie"); got != "utf-8" { + t.Fatalf("unexpected ie value: %q", got) + } + }, + }, + { + name: "very large start", + query: core.Query{ + Text: "golang", + Start: 2147483647, + }, + check: func(t *testing.T, params url.Values, _ string) { + t.Helper() + if got := params.Get("pn"); got != "2147483647" { + t.Fatalf("unexpected pn value: %q", got) + } + }, + }, + { + name: "negative start returns error", + query: core.Query{ + Text: "golang", + Start: -1, + }, + wantErr: true, + }, + { + name: "empty fields return error", + query: core.Query{}, + wantErr: true, + }, } - want := "https://www.baidu.com/s?f=8&gpc=stf%3D1388534400%2C1672531200%7Cstftype%3D2&ie=utf-8&rn=10&wd=go+site%3Atutorialspoint.com" + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := BuildURL(tt.query) + if (err != nil) != tt.wantErr { + t.Fatalf("BuildURL() error = %v, wantErr %v", err, tt.wantErr) + } + if tt.wantErr { + return + } - if want != res { - t.Fatalf("Wanted result `%s` doesn't match to resulted `%s`", want, res) + parsed, err := url.Parse(got) + if err != nil { + t.Fatalf("invalid URL returned: %v", err) + } + if tt.check != nil { + tt.check(t, parsed.Query(), parsed.Host) + } + }) } } -func TestImageUrlBuild(t *testing.T) { - query := core.Query{Text: "金毛猎犬"} - - got, err := BuildImageURL(query, 0) - if err != nil { - t.Fatal(err) +func TestBuildImageURL(t *testing.T) { + tests := []struct { + name string + query core.Query + pageNum int + wantErr bool + check func(*testing.T, url.Values, string) + }{ + { + name: "unicode query", + query: core.Query{Text: "金毛猎犬"}, + pageNum: 0, + check: func(t *testing.T, params url.Values, host string) { + t.Helper() + if host != "image.baidu.com" { + t.Fatalf("unexpected host: %s", host) + } + if got := params.Get("word"); got != "金毛猎犬" { + t.Fatalf("expected unicode query, got %q", got) + } + if got := params.Get("tn"); got != "resultjson_com" { + t.Fatalf("unexpected tn value: %q", got) + } + }, + }, + { + name: "combined params with pagination", + query: core.Query{ + Text: "golang", + Limit: 25, + }, + pageNum: 3, + check: func(t *testing.T, params url.Values, _ string) { + t.Helper() + if got := params.Get("rn"); got != "30" { + t.Fatalf("unexpected rn value: %q", got) + } + if got := params.Get("pn"); got != "90" { + t.Fatalf("unexpected pn value: %q", got) + } + }, + }, + { + name: "empty fields return error", + query: core.Query{}, + pageNum: 0, + wantErr: true, + }, } - parsed, err := url.Parse(got) - if err != nil { - t.Fatalf("invalid URL returned: %v", err) - } - if parsed.Host != "image.baidu.com" { - t.Fatalf("unexpected host: %s", parsed.Host) - } - q := parsed.Query() - if q.Get("word") != "金毛猎犬" { - t.Fatalf("expected word query to be preserved, got %q", q.Get("word")) - } - if q.Get("tn") != "resultjson_com" { - t.Fatalf("expected tn=resultjson_com, got %q", q.Get("tn")) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := BuildImageURL(tt.query, tt.pageNum) + if (err != nil) != tt.wantErr { + t.Fatalf("BuildImageURL() error = %v, wantErr %v", err, tt.wantErr) + } + if tt.wantErr { + return + } + + parsed, err := url.Parse(got) + if err != nil { + t.Fatalf("invalid URL returned: %v", err) + } + if tt.check != nil { + tt.check(t, parsed.Query(), parsed.Host) + } + }) } } diff --git a/bing/bing_test.go b/bing/bing_test.go index ee6c43f..ba4eb37 100644 --- a/bing/bing_test.go +++ b/bing/bing_test.go @@ -2,69 +2,183 @@ package bing import ( "net/url" - "strings" "testing" "github.com/karust/openserp/core" ) -func TestBuildImageURL(t *testing.T) { +func TestBuildURL(t *testing.T) { tests := []struct { - name string - query core.Query - wantErr bool - wantCont string + name string + query core.Query + wantErr bool + check func(*testing.T, url.Values, string) }{ { - name: "basic image query", - query: core.Query{Text: "test"}, - wantErr: false, - wantCont: "q=test", + name: "combined params with unicode and start zero", + query: core.Query{ + Text: "golang тест", + Site: "example.com", + Filetype: "pdf", + DateInterval: "20240101..20240131", + LangCode: "RU", + Limit: 30, + Start: 0, + }, + check: func(t *testing.T, params url.Values, host string) { + t.Helper() + if host != "www.bing.com" { + t.Fatalf("unexpected host: %s", host) + } + if got := params.Get("q"); got != "golang тест site:example.com filetype:pdf after:2024-01-01 before:2024-01-31" { + t.Fatalf("unexpected q value: %q", got) + } + if got := params.Get("pq"); got != params.Get("q") { + t.Fatalf("pq should match q, got %q vs %q", got, params.Get("q")) + } + if got := params.Get("setlang"); got != "ru" { + t.Fatalf("unexpected setlang value: %q", got) + } + if got := params.Get("count"); got != "30" { + t.Fatalf("unexpected count value: %q", got) + } + if got := params.Get("first"); got != "" { + t.Fatalf("first should be omitted when Start=0, got %q", got) + } + if got := params.Get("form"); got != "QBLH" { + t.Fatalf("unexpected form value: %q", got) + } + if got := params.Get("qs"); got != "HS" { + t.Fatalf("unexpected qs value: %q", got) + } + if got := params.Get("sp"); got != "-1" { + t.Fatalf("unexpected sp value: %q", got) + } + }, }, { - name: "image query with site", - query: core.Query{Text: "cats", Site: "example.com"}, - wantErr: false, - wantCont: "q=cats+site%3Aexample.com", + name: "very large start", + query: core.Query{ + Text: "golang", + Start: 2147483647, + Limit: 20, + }, + check: func(t *testing.T, params url.Values, _ string) { + t.Helper() + if got := params.Get("first"); got != "2147483648" { + t.Fatalf("unexpected first value: %q", got) + } + if got := params.Get("count"); got != "" { + t.Fatalf("count should be omitted when first is used, got %q", got) + } + }, }, { - name: "image query with filetype", - query: core.Query{Text: "dogs", Filetype: "png"}, - wantErr: false, - wantCont: "q=dogs", + name: "negative start returns error", + query: core.Query{ + Text: "golang", + Start: -1, + }, + wantErr: true, }, { - name: "empty query", - query: core.Query{Text: ""}, - wantErr: true, - wantCont: "", + name: "empty fields return error", + query: core.Query{}, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := BuildURL(tt.query) + if (err != nil) != tt.wantErr { + t.Fatalf("BuildURL() error = %v, wantErr %v", err, tt.wantErr) + } + if tt.wantErr { + return + } + + parsed, err := url.Parse(got) + if err != nil { + t.Fatalf("BuildURL() returned invalid URL: %v", err) + } + if tt.check != nil { + tt.check(t, parsed.Query(), parsed.Host) + } + }) + } +} + +func TestBuildImageURL(t *testing.T) { + tests := []struct { + name string + query core.Query + wantErr bool + check func(*testing.T, url.Values, string) + }{ + { + name: "basic image query", + query: core.Query{Text: "test"}, + check: func(t *testing.T, params url.Values, host string) { + t.Helper() + if host != "www.bing.com" { + t.Fatalf("unexpected host: %s", host) + } + if got := params.Get("q"); got != "test" { + t.Fatalf("unexpected q value: %q", got) + } + }, + }, + { + name: "combined params with unicode", + query: core.Query{ + Text: "коты", + Site: "example.com", + Filetype: "png", + LangCode: "EN", + }, + check: func(t *testing.T, params url.Values, _ string) { + t.Helper() + if got := params.Get("q"); got != "коты site:example.com" { + t.Fatalf("unexpected q value: %q", got) + } + if got := params.Get("setlang"); got != "en" { + t.Fatalf("unexpected setlang value: %q", got) + } + if got := params.Get("form"); got != "HDRSC2" { + t.Fatalf("unexpected form value: %q", got) + } + if got := params.Get("first"); got != "1" { + t.Fatalf("unexpected first value: %q", got) + } + if got := params.Get("scenario"); got != "ImageBasicHover" { + t.Fatalf("unexpected scenario value: %q", got) + } + }, + }, + { + name: "empty fields return error", + query: core.Query{}, + wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := BuildImageURL(tt.query) - if (err != nil) != tt.wantErr { - t.Errorf("BuildImageURL() error = %v, wantErr %v", err, tt.wantErr) + t.Fatalf("BuildImageURL() error = %v, wantErr %v", err, tt.wantErr) + } + if tt.wantErr { return } - if !tt.wantErr && got != "" { - if !strings.Contains(got, tt.wantCont) { - t.Errorf("BuildImageURL() = %v, should contain %v", got, tt.wantCont) - } - - // Test that URL is valid - _, err := url.Parse(got) - if err != nil { - t.Errorf("BuildImageURL() returned invalid URL: %v", err) - } - - // Should be a Bing images URL - if !strings.Contains(got, "bing.com/images/search") { - t.Errorf("BuildImageURL() should return Bing images URL, got: %v", got) - } + parsed, err := url.Parse(got) + if err != nil { + t.Fatalf("BuildImageURL() returned invalid URL: %v", err) + } + if tt.check != nil { + tt.check(t, parsed.Query(), parsed.Host) } }) } diff --git a/core/server_test.go b/core/server_test.go index 9f2c62e..346e004 100644 --- a/core/server_test.go +++ b/core/server_test.go @@ -5,6 +5,7 @@ import ( "errors" "net/http" "net/http/httptest" + "strings" "sync" "testing" "time" @@ -71,6 +72,223 @@ func requestWithHeader(t *testing.T, s *Server, path string, header string, valu return resp } +func TestInvalidQueryParametersReturnJSONError(t *testing.T) { + engine := &engineMock{name: "google", initialized: true} + srv := NewServerWithOptions("127.0.0.1", 7104, DefaultServerOptions(), engine) + + tests := []struct { + name string + path string + message string + }{ + { + name: "invalid limit", + path: "/google/search?text=golang&limit=abc", + message: "invalid syntax", + }, + { + name: "negative start", + path: "/google/search?text=golang&start=-1", + message: "start must be >= 0", + }, + { + name: "invalid filter flag", + path: "/google/search?text=golang&filter=notabool", + message: "invalid syntax", + }, + { + name: "invalid answers flag on mega endpoint", + path: "/mega/search?text=golang&answers=notabool", + message: "invalid syntax", + }, + { + name: "empty text query", + path: "/google/search?text=", + message: "Query cannot be empty", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resp := request(t, srv, tt.path) + if resp.StatusCode != http.StatusInternalServerError { + t.Fatalf("expected 500 for invalid query params, got %d", resp.StatusCode) + } + + var payload JSONErrorResponse + if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil { + t.Fatalf("decode error response: %v", err) + } + if payload.Code != http.StatusInternalServerError { + t.Fatalf("expected code=500, got %d", payload.Code) + } + if payload.Error != "server_error" { + t.Fatalf("expected error=server_error, got %q", payload.Error) + } + if payload.Message == "" { + t.Fatal("expected error message to be present") + } + if tt.message != "" && !strings.Contains(payload.Message, tt.message) { + t.Fatalf("expected message to contain %q, got %q", tt.message, payload.Message) + } + }) + } +} + +func TestMegaEnginesEndpointResponseFormat(t *testing.T) { + google := &engineMock{name: "google", initialized: true} + yandex := &engineMock{name: "yandex", initialized: false} + srv := NewServerWithOptions("127.0.0.1", 7105, DefaultServerOptions(), google, yandex) + + // Prime circuit breaker stats so circuit_state is populated for both engines. + _ = request(t, srv, "/google/search?text=golang") + _ = request(t, srv, "/yandex/search?text=golang") + + resp := request(t, srv, "/mega/engines") + if resp.StatusCode != http.StatusOK { + t.Fatalf("expected /mega/engines to return 200, got %d", resp.StatusCode) + } + + var payload map[string]interface{} + if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil { + t.Fatalf("decode /mega/engines response: %v", err) + } + + total, ok := payload["total"].(float64) + if !ok { + t.Fatalf("expected numeric total field, got %T", payload["total"]) + } + if total != 2 { + t.Fatalf("expected total=2, got %v", total) + } + + engines, ok := payload["engines"].([]interface{}) + if !ok { + t.Fatalf("expected engines array, got %T", payload["engines"]) + } + if len(engines) != 2 { + t.Fatalf("expected 2 engines in payload, got %d", len(engines)) + } + + byName := map[string]map[string]interface{}{} + for _, entry := range engines { + engineData, ok := entry.(map[string]interface{}) + if !ok { + t.Fatalf("expected engine object, got %T", entry) + } + name, _ := engineData["name"].(string) + if name == "" { + t.Fatalf("expected non-empty engine name, got %#v", engineData["name"]) + } + if _, ok := engineData["initialized"].(bool); !ok { + t.Fatalf("expected initialized bool for engine %s, got %T", name, engineData["initialized"]) + } + state, ok := engineData["circuit_state"].(string) + if !ok || state == "" { + t.Fatalf("expected non-empty circuit_state for engine %s, got %#v", name, engineData["circuit_state"]) + } + byName[name] = engineData + } + + if _, ok := byName["google"]; !ok { + t.Fatal("expected google engine in response") + } + if _, ok := byName["yandex"]; !ok { + t.Fatal("expected yandex engine in response") + } +} + +func TestStatsEndpointStructure(t *testing.T) { + engine := &engineMock{name: "google", initialized: true} + srv := NewServerWithOptions("127.0.0.1", 7106, DefaultServerOptions(), engine) + + // Ensure circuit breaker stats are initialized. + _ = request(t, srv, "/google/search?text=golang") + + resp := request(t, srv, "/stats") + if resp.StatusCode != http.StatusOK { + t.Fatalf("expected /stats to return 200, got %d", resp.StatusCode) + } + + var payload map[string]interface{} + if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil { + t.Fatalf("decode /stats response: %v", err) + } + + cache, ok := payload["cache"].(map[string]interface{}) + if !ok { + t.Fatalf("expected cache object, got %T", payload["cache"]) + } + if _, ok := cache["status"].(bool); !ok { + t.Fatalf("expected cache.status bool, got %T", cache["status"]) + } + if _, ok := cache["entries"].(float64); !ok { + t.Fatalf("expected cache.entries number, got %T", cache["entries"]) + } + if _, ok := cache["hits"].(float64); !ok { + t.Fatalf("expected cache.hits number, got %T", cache["hits"]) + } + if _, ok := cache["misses"].(float64); !ok { + t.Fatalf("expected cache.misses number, got %T", cache["misses"]) + } + if _, ok := cache["bypasses"].(float64); !ok { + t.Fatalf("expected cache.bypasses number, got %T", cache["bypasses"]) + } + + proxy, ok := payload["proxy"].(map[string]interface{}) + if !ok { + t.Fatalf("expected proxy object, got %T", payload["proxy"]) + } + if _, ok := proxy["configured_count"].(float64); !ok { + t.Fatalf("expected proxy.configured_count number, got %T", proxy["configured_count"]) + } + if _, ok := proxy["healthy_count"].(float64); !ok { + t.Fatalf("expected proxy.healthy_count number, got %T", proxy["healthy_count"]) + } + if _, ok := proxy["unhealthy_count"].(float64); !ok { + t.Fatalf("expected proxy.unhealthy_count number, got %T", proxy["unhealthy_count"]) + } + if _, ok := proxy["tags"].(map[string]interface{}); !ok { + t.Fatalf("expected proxy.tags object, got %T", proxy["tags"]) + } + if _, ok := proxy["entries"].([]interface{}); !ok { + t.Fatalf("expected proxy.entries array, got %T", proxy["entries"]) + } + + engines, ok := proxy["engines"].(map[string]interface{}) + if !ok { + t.Fatalf("expected proxy.engines object, got %T", proxy["engines"]) + } + googleStats, ok := engines["google"].(map[string]interface{}) + if !ok { + t.Fatalf("expected proxy.engines.google object, got %T", engines["google"]) + } + if got := googleStats["selected_proxy"]; got != "direct" { + t.Fatalf("expected proxy.engines.google.selected_proxy=direct, got %#v", got) + } + + breakers, ok := payload["circuit_breakers"].([]interface{}) + if !ok { + t.Fatalf("expected circuit_breakers array, got %T", payload["circuit_breakers"]) + } + if len(breakers) == 0 { + t.Fatal("expected at least one circuit breaker entry") + } + first, ok := breakers[0].(map[string]interface{}) + if !ok { + t.Fatalf("expected circuit breaker object, got %T", breakers[0]) + } + if _, ok := first["engine"].(string); !ok { + t.Fatalf("expected circuit_breakers[0].engine string, got %T", first["engine"]) + } + if _, ok := first["state"].(string); !ok { + t.Fatalf("expected circuit_breakers[0].state string, got %T", first["state"]) + } + if _, ok := first["failure_count"].(float64); !ok { + t.Fatalf("expected circuit_breakers[0].failure_count number, got %T", first["failure_count"]) + } +} + func TestHealthEndpointStatusSemantics(t *testing.T) { ready := &engineMock{name: "google", initialized: true, limiter: rate.NewLimiter(rate.Every(time.Second), 1)} notReady := &engineMock{name: "yandex", initialized: false, limiter: rate.NewLimiter(rate.Every(time.Second), 1)} diff --git a/duckduckgo/search_test.go b/duckduckgo/search_test.go index 7714bdc..75ca74f 100644 --- a/duckduckgo/search_test.go +++ b/duckduckgo/search_test.go @@ -11,30 +11,98 @@ func TestBuildURL(t *testing.T) { tests := []struct { name string query core.Query + page int wantErr bool + check func(*testing.T, url.Values, string) }{ { name: "basic search", query: core.Query{Text: "golang programming"}, + page: 0, + check: func(t *testing.T, params url.Values, host string) { + t.Helper() + if host != "duckduckgo.com" { + t.Fatalf("unexpected host: %s", host) + } + if got := params.Get("q"); got != "golang programming" { + t.Fatalf("unexpected q value: %q", got) + } + if got := params.Get("ia"); got != "web" { + t.Fatalf("unexpected ia value: %q", got) + } + if got := params.Get("t"); got != "h" { + t.Fatalf("unexpected t value: %q", got) + } + if got := params.Get("s"); got != "" { + t.Fatalf("s should be omitted on first page, got %q", got) + } + }, }, { - name: "search with site filter", - query: core.Query{Text: "golang", Site: "github.com"}, + name: "combined params with unicode", + query: core.Query{ + Text: "поиск", + Site: "github.com", + Filetype: "pdf", + LangCode: "RU-ru", + DateInterval: "20240101..20240131", + }, + page: 0, + check: func(t *testing.T, params url.Values, _ string) { + t.Helper() + if got := params.Get("q"); got != "поиск site:github.com filetype:pdf" { + t.Fatalf("unexpected q value: %q", got) + } + if got := params.Get("df"); got != "2024-01-01..2024-01-31" { + t.Fatalf("unexpected df value: %q", got) + } + if got := params.Get("kl"); got != "ru-ru" { + t.Fatalf("unexpected kl value: %q", got) + } + }, }, { - name: "search with filetype", - query: core.Query{Text: "documentation", Filetype: "pdf"}, + name: "very large page offset", + query: core.Query{Text: "golang"}, + page: 100000, + check: func(t *testing.T, params url.Values, _ string) { + t.Helper() + if got := params.Get("s"); got != "2500000" { + t.Fatalf("unexpected pagination offset: %q", got) + } + }, }, { - name: "empty query", - query: core.Query{Text: ""}, + name: "negative page does not add offset", + query: core.Query{Text: "golang"}, + page: -1, + check: func(t *testing.T, params url.Values, _ string) { + t.Helper() + if got := params.Get("s"); got != "" { + t.Fatalf("expected empty offset for negative page, got %q", got) + } + }, + }, + { + name: "invalid date interval returns error", + query: core.Query{ + Text: "golang", + DateInterval: "invalid", + }, + page: 0, + wantErr: true, + }, + { + name: "empty query returns error", + query: core.Query{}, + page: 0, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := BuildURL(tt.query, 0) + got, err := BuildURL(tt.query, tt.page) if (err != nil) != tt.wantErr { t.Fatalf("BuildURL() error = %v, wantErr %v", err, tt.wantErr) } @@ -46,15 +114,8 @@ func TestBuildURL(t *testing.T) { if err != nil { t.Fatalf("BuildURL() returned invalid URL: %v", err) } - params := parsed.Query() - if params.Get("q") == "" { - t.Fatalf("BuildURL() should include q parameter, got %s", got) - } - if params.Get("ia") != "web" { - t.Fatalf("BuildURL() should include ia=web, got %s", got) - } - if params.Get("t") != "h" { - t.Fatalf("BuildURL() should include t=h, got %s", got) + if tt.check != nil { + tt.check(t, parsed.Query(), parsed.Host) } }) } @@ -65,14 +126,47 @@ func TestBuildImageURL(t *testing.T) { name string query core.Query wantErr bool + check func(*testing.T, url.Values, string) }{ { - name: "basic image search", - query: core.Query{Text: "golang logo"}, + name: "combined params with unicode", + query: core.Query{ + Text: "горы", + Site: "example.com", + Filetype: "jpg", + LangCode: "RU", + DateInterval: "20240201..20240229", + }, + check: func(t *testing.T, params url.Values, host string) { + t.Helper() + if host != "duckduckgo.com" { + t.Fatalf("unexpected host: %s", host) + } + if got := params.Get("q"); got != "горы site:example.com filetype:jpg" { + t.Fatalf("unexpected q value: %q", got) + } + if got := params.Get("iax"); got != "images" || params.Get("ia") != "images" { + t.Fatalf("expected image mode params, got iax=%q ia=%q", params.Get("iax"), params.Get("ia")) + } + if got := params.Get("df"); got != "2024-02-01..2024-02-29" { + t.Fatalf("unexpected df value: %q", got) + } + if got := params.Get("kl"); got != "ru" { + t.Fatalf("unexpected kl value: %q", got) + } + }, }, { - name: "empty query", - query: core.Query{Text: ""}, + name: "invalid date interval returns error", + query: core.Query{ + Text: "golang", + DateInterval: "202401", + }, + wantErr: true, + }, + { + name: "empty query returns error", + query: core.Query{}, wantErr: true, }, } @@ -91,12 +185,8 @@ func TestBuildImageURL(t *testing.T) { if err != nil { t.Fatalf("BuildImageURL() returned invalid URL: %v", err) } - params := parsed.Query() - if params.Get("q") == "" { - t.Fatalf("BuildImageURL() should include q parameter, got %s", got) - } - if params.Get("iax") != "images" || params.Get("ia") != "images" { - t.Fatalf("BuildImageURL() should target image mode, got %s", got) + if tt.check != nil { + tt.check(t, parsed.Query(), parsed.Host) } }) } diff --git a/google/search_test.go b/google/search_test.go index 38e3ae5..8325cc4 100644 --- a/google/search_test.go +++ b/google/search_test.go @@ -1,11 +1,218 @@ package google import ( + "net/url" "testing" "github.com/karust/openserp/core" ) +func TestBuildSearchURL(t *testing.T) { + tests := []struct { + name string + query core.Query + wantErr bool + check func(*testing.T, url.Values, string) + }{ + { + name: "combined params with unicode and start zero", + query: core.Query{ + Text: "golang тест", + Site: "example.com", + Filetype: "pdf", + DateInterval: "20240101..20240131", + Limit: 50, + Start: 0, + Filter: false, + LangCode: "RU", + }, + check: func(t *testing.T, params url.Values, host string) { + t.Helper() + if host != "www.google.ru" { + t.Fatalf("unexpected host: %s", host) + } + if got := params.Get("q"); got != "golang тест site:example.com filetype:pdf" { + t.Fatalf("unexpected q value: %q", got) + } + if got := params.Get("oq"); got != params.Get("q") { + t.Fatalf("oq should match q, got %q vs %q", got, params.Get("q")) + } + if got := params.Get("tbs"); got != "cdr:1,cd_min:20240101,cd_max:20240131" { + t.Fatalf("unexpected tbs: %q", got) + } + if got := params.Get("num"); got != "50" { + t.Fatalf("unexpected num: %q", got) + } + if got := params.Get("start"); got != "" { + t.Fatalf("start should be omitted when Start=0, got %q", got) + } + if got := params.Get("filter"); got != "0" { + t.Fatalf("unexpected filter value: %q", got) + } + if got := params.Get("hl"); got != "RU" { + t.Fatalf("unexpected hl value: %q", got) + } + if got := params.Get("lr"); got != "lang_ru" { + t.Fatalf("unexpected lr value: %q", got) + } + if got := params.Get("pws"); got != "0" { + t.Fatalf("unexpected pws value: %q", got) + } + if got := params.Get("nfpr"); got != "1" { + t.Fatalf("unexpected nfpr value: %q", got) + } + }, + }, + { + name: "site and filetype without text", + query: core.Query{ + Site: "example.com", + Filetype: "txt", + Filter: true, + }, + check: func(t *testing.T, params url.Values, _ string) { + t.Helper() + if got := params.Get("q"); got != " site:example.com filetype:txt" { + t.Fatalf("unexpected q value: %q", got) + } + if got := params.Get("filter"); got != "" { + t.Fatalf("filter should be omitted when Filter=true, got %q", got) + } + }, + }, + { + name: "very large start", + query: core.Query{ + Text: "golang", + Start: 2147483647, + Filter: true, + }, + check: func(t *testing.T, params url.Values, _ string) { + t.Helper() + if got := params.Get("start"); got != "2147483647" { + t.Fatalf("unexpected start value: %q", got) + } + }, + }, + { + name: "negative start returns error", + query: core.Query{ + Text: "golang", + Start: -1, + }, + wantErr: true, + }, + { + name: "empty query returns error", + query: core.Query{}, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := BuildURL(tt.query) + if (err != nil) != tt.wantErr { + t.Fatalf("BuildURL() error = %v, wantErr %v", err, tt.wantErr) + } + if tt.wantErr { + return + } + + parsed, err := url.Parse(got) + if err != nil { + t.Fatalf("invalid URL returned: %v", err) + } + if tt.check != nil { + tt.check(t, parsed.Query(), parsed.Host) + } + }) + } +} + +func TestBuildImageSearchURL(t *testing.T) { + tests := []struct { + name string + query core.Query + wantErr bool + check func(*testing.T, url.Values, string) + }{ + { + name: "combined params with unicode", + query: core.Query{ + Text: "горы", + Site: "example.com", + Filetype: "jpg", + DateInterval: "20240301..20240315", + Limit: 25, + LangCode: "EN", + }, + check: func(t *testing.T, params url.Values, host string) { + t.Helper() + if host != "www.google.com" { + t.Fatalf("unexpected host: %s", host) + } + if got := params.Get("tbm"); got != "isch" { + t.Fatalf("unexpected tbm: %q", got) + } + if got := params.Get("q"); got != "горы site:example.com filetype:jpg" { + t.Fatalf("unexpected q value: %q", got) + } + if got := params.Get("oq"); got != params.Get("q") { + t.Fatalf("oq should match q, got %q vs %q", got, params.Get("q")) + } + if got := params.Get("num"); got != "25" { + t.Fatalf("unexpected num value: %q", got) + } + if got := params.Get("tbs"); got != "cdr:1,cd_min:20240301,cd_max:20240315" { + t.Fatalf("unexpected tbs value: %q", got) + } + if got := params.Get("hl"); got != "EN" { + t.Fatalf("unexpected hl value: %q", got) + } + if got := params.Get("lr"); got != "lang_en" { + t.Fatalf("unexpected lr value: %q", got) + } + }, + }, + { + name: "invalid date interval returns error", + query: core.Query{ + Text: "golang", + DateInterval: "20240101", + }, + wantErr: true, + }, + { + name: "empty query returns error", + query: core.Query{ + Text: "", + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := BuildImageURL(tt.query) + if (err != nil) != tt.wantErr { + t.Fatalf("BuildImageURL() error = %v, wantErr %v", err, tt.wantErr) + } + if tt.wantErr { + return + } + + parsed, err := url.Parse(got) + if err != nil { + t.Fatalf("invalid URL returned: %v", err) + } + if tt.check != nil { + tt.check(t, parsed.Query(), parsed.Host) + } + }) + } +} + func TestParseSourceImageURL(t *testing.T) { //href1 := `/imgres?imgurl=https%3A%2F%2Fupload.wikimedia.org%2Fwikipedia%2Fcommons%2F2%2F26%2FMarmota_marmota_Alpes2.jpg&tbnid=Be_RycOe8xzlpM&vet=12ahUKEwjkh6WzwIeAAxWV_yoKHRzHC9wQMygAegUIARD0AQ..i&imgrefurl=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FAlpine_marmot&docid=7miWbc2QiSw9uM&w=801&h=599&q=alpine%20marmot&ved=2ahUKEwjkh6WzwIeAAxWV_yoKHRzHC9wQMygAegUIARD0AQ` href2 := `/imgres?imgurl=https%3A%2F%2Fstatic.wikia.nocookie.net%2Fnaturerules1%2Fimages%2Ff%2Ff2%2F13d79d934ccf6f7919777fcb6dbb6e6c.jpg%2Frevision%2Flatest%3Fcb%3D20210218225522&tbnid=JxC8NUyBjdNbdM&vet=12ahUKEwiHrJnN1YeAAxXvEBAIHfRADAAQMygCegUIARD4AQ..i&imgrefurl=https%3A%2F%2Fnaturerules1.fandom.com%2Fwiki%2FAlpine_Marmot&docid=XXYeDjL67badNM&w=1600&h=1200&q=alpine%20marmot&ved=2ahUKEwiHrJnN1YeAAxXvEBAIHfRADAAQMygCegUIARD4AQ`