@coldiq/mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client.d.ts +8 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +47 -0
- package/dist/client.js.map +1 -0
- package/dist/executor.d.ts +21 -0
- package/dist/executor.d.ts.map +1 -0
- package/dist/executor.js +130 -0
- package/dist/executor.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +49 -0
- package/dist/index.js.map +1 -0
- package/dist/registry.d.ts +49 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +3104 -0
- package/dist/registry.js.map +1 -0
- package/dist/tools/enrich-company.d.ts +22 -0
- package/dist/tools/enrich-company.d.ts.map +1 -0
- package/dist/tools/enrich-company.js +21 -0
- package/dist/tools/enrich-company.js.map +1 -0
- package/dist/tools/enrich-email.d.ts +24 -0
- package/dist/tools/enrich-email.d.ts.map +1 -0
- package/dist/tools/enrich-email.js +19 -0
- package/dist/tools/enrich-email.js.map +1 -0
- package/dist/tools/enrich-emails.d.ts +31 -0
- package/dist/tools/enrich-emails.d.ts.map +1 -0
- package/dist/tools/enrich-emails.js +146 -0
- package/dist/tools/enrich-emails.js.map +1 -0
- package/dist/tools/enrich-person.d.ts +26 -0
- package/dist/tools/enrich-person.d.ts.map +1 -0
- package/dist/tools/enrich-person.js +23 -0
- package/dist/tools/enrich-person.js.map +1 -0
- package/dist/tools/fetch-page-content.d.ts +22 -0
- package/dist/tools/fetch-page-content.d.ts.map +1 -0
- package/dist/tools/fetch-page-content.js +32 -0
- package/dist/tools/fetch-page-content.js.map +1 -0
- package/dist/tools/find-email.d.ts +24 -0
- package/dist/tools/find-email.d.ts.map +1 -0
- package/dist/tools/find-email.js +19 -0
- package/dist/tools/find-email.js.map +1 -0
- package/dist/tools/find-emails.d.ts +31 -0
- package/dist/tools/find-emails.d.ts.map +1 -0
- package/dist/tools/find-emails.js +146 -0
- package/dist/tools/find-emails.js.map +1 -0
- package/dist/tools/find-influencers.d.ts +29 -0
- package/dist/tools/find-influencers.d.ts.map +1 -0
- package/dist/tools/find-influencers.js +30 -0
- package/dist/tools/find-influencers.js.map +1 -0
- package/dist/tools/find-people.d.ts +26 -0
- package/dist/tools/find-people.d.ts.map +1 -0
- package/dist/tools/find-people.js +61 -0
- package/dist/tools/find-people.js.map +1 -0
- package/dist/tools/find-phone.d.ts +24 -0
- package/dist/tools/find-phone.d.ts.map +1 -0
- package/dist/tools/find-phone.js +48 -0
- package/dist/tools/find-phone.js.map +1 -0
- package/dist/tools/find-signals.d.ts +26 -0
- package/dist/tools/find-signals.d.ts.map +1 -0
- package/dist/tools/find-signals.js +82 -0
- package/dist/tools/find-signals.js.map +1 -0
- package/dist/tools/search-ads.d.ts +33 -0
- package/dist/tools/search-ads.d.ts.map +1 -0
- package/dist/tools/search-ads.js +33 -0
- package/dist/tools/search-ads.js.map +1 -0
- package/dist/tools/search-companies.d.ts +42 -0
- package/dist/tools/search-companies.d.ts.map +1 -0
- package/dist/tools/search-companies.js +37 -0
- package/dist/tools/search-companies.js.map +1 -0
- package/dist/tools/search-jobs.d.ts +51 -0
- package/dist/tools/search-jobs.d.ts.map +1 -0
- package/dist/tools/search-jobs.js +64 -0
- package/dist/tools/search-jobs.js.map +1 -0
- package/dist/tools/search-places.d.ts +47 -0
- package/dist/tools/search-places.d.ts.map +1 -0
- package/dist/tools/search-places.js +42 -0
- package/dist/tools/search-places.js.map +1 -0
- package/dist/tools/search-reddit.d.ts +27 -0
- package/dist/tools/search-reddit.d.ts.map +1 -0
- package/dist/tools/search-reddit.js +30 -0
- package/dist/tools/search-reddit.js.map +1 -0
- package/dist/tools/search-seo.d.ts +37 -0
- package/dist/tools/search-seo.d.ts.map +1 -0
- package/dist/tools/search-seo.js +49 -0
- package/dist/tools/search-seo.js.map +1 -0
- package/dist/tools/search-web.d.ts +23 -0
- package/dist/tools/search-web.d.ts.map +1 -0
- package/dist/tools/search-web.js +20 -0
- package/dist/tools/search-web.js.map +1 -0
- package/dist/tools/verify-email.d.ts +20 -0
- package/dist/tools/verify-email.d.ts.map +1 -0
- package/dist/tools/verify-email.js +15 -0
- package/dist/tools/verify-email.js.map +1 -0
- package/package.json +28 -0
- package/src/client.ts +60 -0
- package/src/executor.ts +182 -0
- package/src/index.ts +155 -0
- package/src/registry.ts +3159 -0
- package/src/tools/enrich-company.ts +25 -0
- package/src/tools/enrich-person.ts +27 -0
- package/src/tools/fetch-page-content.ts +36 -0
- package/src/tools/find-email.ts +23 -0
- package/src/tools/find-emails.ts +190 -0
- package/src/tools/find-influencers.ts +34 -0
- package/src/tools/find-people.ts +69 -0
- package/src/tools/find-phone.ts +53 -0
- package/src/tools/find-signals.ts +93 -0
- package/src/tools/search-ads.ts +44 -0
- package/src/tools/search-companies.ts +41 -0
- package/src/tools/search-jobs.ts +73 -0
- package/src/tools/search-places.ts +52 -0
- package/src/tools/search-reddit.ts +34 -0
- package/src/tools/search-seo.ts +59 -0
- package/src/tools/search-web.ts +24 -0
- package/src/tools/verify-email.ts +19 -0
- package/test-ads-live.ts +77 -0
- package/test-company-live.ts +91 -0
- package/test-email-live.ts +171 -0
- package/test-influencers-live.ts +66 -0
- package/test-jobs-live.ts +69 -0
- package/test-linkupapi-live.ts +137 -0
- package/test-phone-live.ts +41 -0
- package/test-places-live.ts +89 -0
- package/test-reddit-live.ts +66 -0
- package/test-search-live.ts +79 -0
- package/test-seo-live.ts +68 -0
- package/test-web-live.ts +67 -0
- package/tests/client.test.ts +90 -0
- package/tests/executor.test.ts +83 -0
- package/tests/gtm/01-icp-to-emails.test.ts +43 -0
- package/tests/gtm/02-icp-bulk-emails.test.ts +38 -0
- package/tests/gtm/03-icp-to-phones.test.ts +39 -0
- package/tests/gtm/04-funding-signal-outreach.test.ts +42 -0
- package/tests/gtm/05-hiring-signal-decisionmakers.test.ts +41 -0
- package/tests/gtm/06-intent-signal-outreach.test.ts +44 -0
- package/tests/gtm/07-places-to-content.test.ts +50 -0
- package/tests/gtm/08-domain-to-account.test.ts +44 -0
- package/tests/gtm/09-linkedin-to-everything.test.ts +41 -0
- package/tests/gtm/10-jobs-vs-signals-routing.test.ts +38 -0
- package/tests/gtm/11-find-vs-enrich-routing.test.ts +39 -0
- package/tests/gtm/12-bogus-domain-graceful.test.ts +42 -0
- package/tests/gtm/13-private-linkedin-graceful.test.ts +44 -0
- package/tests/gtm/14-empty-handoff.test.ts +43 -0
- package/tests/gtm/15-seo-reddit-research.test.ts +38 -0
- package/tests/gtm/README.md +59 -0
- package/tests/gtm/harness.ts +217 -0
- package/tests/gtm/tools-bridge.ts +232 -0
- package/tests/gtm-scenarios.md +32 -0
- package/tests/live/smoke-report.ts +255 -0
- package/tests/live/smoke.test.ts +134 -0
- package/tests/registry-enrich-person.test.ts +447 -0
- package/tests/registry-fetch-page-content.test.ts +90 -0
- package/tests/registry-find-people.test.ts +467 -0
- package/tests/registry-find-signals.test.ts +470 -0
- package/tests/registry-linkupapi.test.ts +331 -0
- package/tests/registry-search-companies.test.ts +188 -0
- package/tests/registry-search-jobs.test.ts +116 -0
- package/tests/registry.test.ts +2210 -0
- package/tests/tools/enrich-company.test.ts +92 -0
- package/tests/tools/enrich-email.test.ts +94 -0
- package/tests/tools/enrich-emails.test.ts +271 -0
- package/tests/tools/enrich-person.test.ts +140 -0
- package/tests/tools/fetch-page-content.test.ts +108 -0
- package/tests/tools/find-influencers.test.ts +91 -0
- package/tests/tools/find-people.test.ts +344 -0
- package/tests/tools/find-phone.test.ts +100 -0
- package/tests/tools/find-signals.test.ts +110 -0
- package/tests/tools/search-ads.test.ts +182 -0
- package/tests/tools/search-companies.test.ts +58 -0
- package/tests/tools/search-jobs.test.ts +210 -0
- package/tests/tools/search-places.test.ts +114 -0
- package/tests/tools/search-reddit.test.ts +125 -0
- package/tests/tools/search-seo.test.ts +183 -0
- package/tests/tools/search-web.test.ts +79 -0
- package/tests/tools/verify-email.test.ts +68 -0
- package/tsconfig.json +17 -0
- package/vitest.config.ts +7 -0
|
@@ -0,0 +1,2210 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { getProviders, getSearchWebProviders, isoCountryToName } from '../src/registry.js'
|
|
3
|
+
import type { Capability } from '../src/registry.js'
|
|
4
|
+
|
|
5
|
+
const ALL_CAPABILITIES: Capability[] = [
|
|
6
|
+
'search_companies',
|
|
7
|
+
'find_people',
|
|
8
|
+
'find_email',
|
|
9
|
+
'verify_email',
|
|
10
|
+
'find_phone',
|
|
11
|
+
'enrich_company',
|
|
12
|
+
'enrich_person',
|
|
13
|
+
'search_web',
|
|
14
|
+
'search_jobs',
|
|
15
|
+
'search_ads',
|
|
16
|
+
'search_places',
|
|
17
|
+
'find_influencers',
|
|
18
|
+
'search_reddit',
|
|
19
|
+
'search_seo',
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
describe('registry', () => {
|
|
23
|
+
it('every capability has at least one provider', () => {
|
|
24
|
+
for (const cap of ALL_CAPABILITIES) {
|
|
25
|
+
const providers = getProviders(cap)
|
|
26
|
+
expect(providers.length).toBeGreaterThan(0)
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('providers are sorted by priority', () => {
|
|
31
|
+
for (const cap of ALL_CAPABILITIES) {
|
|
32
|
+
const providers = getProviders(cap)
|
|
33
|
+
for (let i = 1; i < providers.length; i++) {
|
|
34
|
+
expect(providers[i].priority).toBeGreaterThanOrEqual(providers[i - 1].priority)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('returns a copy — mutating does not affect the registry', () => {
|
|
40
|
+
const first = getProviders('search_companies')
|
|
41
|
+
first.pop()
|
|
42
|
+
const second = getProviders('search_companies')
|
|
43
|
+
expect(second.length).toBeGreaterThan(first.length)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('throws for unknown capability', () => {
|
|
47
|
+
expect(() => getProviders('unknown' as Capability)).toThrow('Unknown capability')
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
describe('search_companies mapParams', () => {
|
|
51
|
+
it('CompanyEnrich maps correctly and folds industries into keywords', () => {
|
|
52
|
+
const providers = getProviders('search_companies')
|
|
53
|
+
const ce = providers.find((p) => p.id === 'companyenrich')!
|
|
54
|
+
const result = ce.mapParams({
|
|
55
|
+
keywords: ['SaaS'],
|
|
56
|
+
industries: ['Software'],
|
|
57
|
+
countries: ['US'],
|
|
58
|
+
min_employees: 10,
|
|
59
|
+
max_employees: 200,
|
|
60
|
+
limit: 25,
|
|
61
|
+
})
|
|
62
|
+
expect(result.body).toEqual({
|
|
63
|
+
keywords: ['SaaS', 'Software'],
|
|
64
|
+
countries: ['US'],
|
|
65
|
+
employees: [{ from: 10, to: 200 }],
|
|
66
|
+
foundedYear: undefined,
|
|
67
|
+
pageSize: 25,
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('Apollo maps correctly, translating ISO country codes to English names', () => {
|
|
72
|
+
const providers = getProviders('search_companies')
|
|
73
|
+
const apollo = providers.find((p) => p.id === 'apollo')!
|
|
74
|
+
const result = apollo.mapParams({
|
|
75
|
+
keywords: ['SaaS'],
|
|
76
|
+
countries: ['US'],
|
|
77
|
+
min_employees: 10,
|
|
78
|
+
max_employees: 200,
|
|
79
|
+
limit: 25,
|
|
80
|
+
})
|
|
81
|
+
expect(result.body).toEqual({
|
|
82
|
+
q_organization_keyword_tags: ['SaaS'],
|
|
83
|
+
organization_locations: ['United States'],
|
|
84
|
+
organization_num_employees_ranges: ['10,200'],
|
|
85
|
+
per_page: 25,
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('Apollo folds industries into keyword tags and translates country codes in org_locations', () => {
|
|
90
|
+
const providers = getProviders('search_companies')
|
|
91
|
+
const apollo = providers.find((p) => p.id === 'apollo')!
|
|
92
|
+
const result = apollo.mapParams({
|
|
93
|
+
keywords: ['healthcare'],
|
|
94
|
+
industries: ['Hospital & Health Care', 'Medical Devices'],
|
|
95
|
+
locations: ['Paris, France'],
|
|
96
|
+
countries: ['FR'],
|
|
97
|
+
min_employees: 50,
|
|
98
|
+
max_employees: 100,
|
|
99
|
+
limit: 25,
|
|
100
|
+
})
|
|
101
|
+
expect(result.body).toEqual({
|
|
102
|
+
q_organization_keyword_tags: ['healthcare', 'Hospital & Health Care', 'Medical Devices'],
|
|
103
|
+
organization_locations: ['Paris, France', 'France'],
|
|
104
|
+
organization_num_employees_ranges: ['50,100'],
|
|
105
|
+
per_page: 25,
|
|
106
|
+
})
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('Apollo omits empty keyword/location arrays', () => {
|
|
110
|
+
const providers = getProviders('search_companies')
|
|
111
|
+
const apollo = providers.find((p) => p.id === 'apollo')!
|
|
112
|
+
const result = apollo.mapParams({ min_employees: 50, max_employees: 100, limit: 25 })
|
|
113
|
+
expect(result.body).toEqual({
|
|
114
|
+
q_organization_keyword_tags: undefined,
|
|
115
|
+
organization_locations: undefined,
|
|
116
|
+
organization_num_employees_ranges: ['50,100'],
|
|
117
|
+
per_page: 25,
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('FullEnrich wraps each filter value in {value}, translates ISO country codes to English names', () => {
|
|
122
|
+
const providers = getProviders('search_companies')
|
|
123
|
+
const fe = providers.find((p) => p.id === 'fullenrich')!
|
|
124
|
+
const result = fe.mapParams({
|
|
125
|
+
keywords: ['SaaS', 'fintech'],
|
|
126
|
+
industries: ['Technology'],
|
|
127
|
+
locations: ['Paris'],
|
|
128
|
+
countries: ['FR'],
|
|
129
|
+
min_employees: 50,
|
|
130
|
+
max_employees: 500,
|
|
131
|
+
min_founded_year: 2010,
|
|
132
|
+
limit: 10,
|
|
133
|
+
})
|
|
134
|
+
expect(result.body).toEqual({
|
|
135
|
+
keywords: [{ value: 'SaaS' }, { value: 'fintech' }],
|
|
136
|
+
industries: [{ value: 'Technology' }],
|
|
137
|
+
headquarters_locations: [{ value: 'Paris' }, { value: 'France' }],
|
|
138
|
+
headcounts: [{ min: 50, max: 500 }],
|
|
139
|
+
founded_years: [{ min: 2010 }],
|
|
140
|
+
limit: 10,
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('FullEnrich hasResult checks companies/data/results array', () => {
|
|
145
|
+
const providers = getProviders('search_companies')
|
|
146
|
+
const fe = providers.find((p) => p.id === 'fullenrich')!
|
|
147
|
+
expect(fe.hasResult({ companies: [{ name: 'ColdIQ' }] })).toBe(true)
|
|
148
|
+
expect(fe.hasResult({ data: [{ name: 'ColdIQ' }] })).toBe(true)
|
|
149
|
+
expect(fe.hasResult({ companies: [] })).toBe(false)
|
|
150
|
+
expect(fe.hasResult({})).toBe(false)
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('SignalBase uses keywords for search and industries for the dedicated industry param', () => {
|
|
154
|
+
const providers = getProviders('search_companies')
|
|
155
|
+
const sb = providers.find((p) => p.id === 'signalbase')!
|
|
156
|
+
expect(sb.method).toBe('GET')
|
|
157
|
+
const result = sb.mapParams({
|
|
158
|
+
keywords: ['SaaS'],
|
|
159
|
+
industries: ['Software'],
|
|
160
|
+
countries: ['FR', 'US'],
|
|
161
|
+
min_employees: 50,
|
|
162
|
+
max_employees: 500,
|
|
163
|
+
limit: 25,
|
|
164
|
+
})
|
|
165
|
+
expect(result.body).toBeUndefined()
|
|
166
|
+
expect(result.queryParams).toEqual({
|
|
167
|
+
search: 'SaaS',
|
|
168
|
+
industry: 'Software',
|
|
169
|
+
countries: 'FR,US',
|
|
170
|
+
employee_count_min: '50',
|
|
171
|
+
employee_count_max: '500',
|
|
172
|
+
limit: '25',
|
|
173
|
+
})
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('BlitzAPI folds industries into keywords (industry filter requires LinkedIn taxonomy)', () => {
|
|
177
|
+
const providers = getProviders('search_companies')
|
|
178
|
+
const blitz = providers.find((p) => p.id === 'blitzapi')!
|
|
179
|
+
const result = blitz.mapParams({
|
|
180
|
+
keywords: ['AI'],
|
|
181
|
+
industries: ['Technology'],
|
|
182
|
+
countries: ['US'],
|
|
183
|
+
min_employees: 50,
|
|
184
|
+
max_employees: 500,
|
|
185
|
+
min_founded_year: 2015,
|
|
186
|
+
limit: 10,
|
|
187
|
+
})
|
|
188
|
+
expect(result.body).toEqual({
|
|
189
|
+
company: {
|
|
190
|
+
keywords: { include: ['AI', 'Technology'] },
|
|
191
|
+
employee_count: { min: 50, max: 500 },
|
|
192
|
+
founded_year: { min: 2015 },
|
|
193
|
+
hq: { country_code: ['US'] },
|
|
194
|
+
},
|
|
195
|
+
max_results: 10,
|
|
196
|
+
})
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
it('BlitzAPI isApplicable rejects empty input but accepts basic narrowing filters', () => {
|
|
200
|
+
const providers = getProviders('search_companies')
|
|
201
|
+
const blitz = providers.find((p) => p.id === 'blitzapi')!
|
|
202
|
+
expect(blitz.isApplicable!({})).toBe(false)
|
|
203
|
+
expect(blitz.isApplicable!({ keywords: ['AI'] })).toBe(true)
|
|
204
|
+
expect(blitz.isApplicable!({ countries: ['US'] })).toBe(true)
|
|
205
|
+
expect(blitz.isApplicable!({ min_employees: 50 })).toBe(true)
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
it('BlitzAPI isApplicable rejects advanced filters it cannot apply', () => {
|
|
209
|
+
const providers = getProviders('search_companies')
|
|
210
|
+
const blitz = providers.find((p) => p.id === 'blitzapi')!
|
|
211
|
+
expect(blitz.isApplicable!({ technologies: ['Salesforce'] })).toBe(false)
|
|
212
|
+
expect(blitz.isApplicable!({ funding_stages: ['series_a'] })).toBe(false)
|
|
213
|
+
expect(blitz.isApplicable!({ min_funding_amount: 1_000_000 })).toBe(false)
|
|
214
|
+
expect(blitz.isApplicable!({ max_funding_amount: 50_000_000 })).toBe(false)
|
|
215
|
+
expect(blitz.isApplicable!({ min_revenue: 500_000 })).toBe(false)
|
|
216
|
+
expect(blitz.isApplicable!({ max_revenue: 10_000_000 })).toBe(false)
|
|
217
|
+
expect(blitz.isApplicable!({ exclude_industries: ['Gaming'] })).toBe(false)
|
|
218
|
+
expect(blitz.isApplicable!({ exclude_countries: ['CN'] })).toBe(false)
|
|
219
|
+
expect(blitz.isApplicable!({ is_hiring: true })).toBe(false)
|
|
220
|
+
// is_hiring=false is not an active filter — still fires on other narrowing filters
|
|
221
|
+
expect(blitz.isApplicable!({ keywords: ['SaaS'], is_hiring: false })).toBe(true)
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
it('BlitzAPI postFilter enforces employee count when present, passes through when missing', () => {
|
|
225
|
+
const providers = getProviders('search_companies')
|
|
226
|
+
const blitz = providers.find((p) => p.id === 'blitzapi')!
|
|
227
|
+
const results = [
|
|
228
|
+
{ name: 'A', employees_on_linkedin: 30 },
|
|
229
|
+
{ name: 'B', employees_on_linkedin: 100 },
|
|
230
|
+
{ name: 'C', employees_on_linkedin: 600 },
|
|
231
|
+
{ name: 'D' }, // no field — passes through (lenient)
|
|
232
|
+
]
|
|
233
|
+
const out = blitz.postFilter!({ results }, { min_employees: 50, max_employees: 500 }) as Record<string, unknown>
|
|
234
|
+
expect((out.results as Array<Record<string, unknown>>).map((o) => o.name)).toEqual(['B', 'D'])
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
it('BlitzAPI postFilter is a no-op when no employee filters are set', () => {
|
|
238
|
+
const providers = getProviders('search_companies')
|
|
239
|
+
const blitz = providers.find((p) => p.id === 'blitzapi')!
|
|
240
|
+
const results = [{ name: 'A', employees_on_linkedin: 30 }]
|
|
241
|
+
const out = blitz.postFilter!({ results }, {}) as Record<string, unknown>
|
|
242
|
+
expect((out.results as unknown[]).length).toBe(1)
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
it('BlitzAPI caps max_results at 50', () => {
|
|
246
|
+
const providers = getProviders('search_companies')
|
|
247
|
+
const blitz = providers.find((p) => p.id === 'blitzapi')!
|
|
248
|
+
const result = blitz.mapParams({ keywords: ['AI'], limit: 100 })
|
|
249
|
+
expect((result.body as { max_results: number }).max_results).toBe(50)
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
it('LimaData search folds keywords+industries into query and maps employees to A-H buckets', () => {
|
|
253
|
+
const providers = getProviders('search_companies')
|
|
254
|
+
const ld = providers.find((p) => p.id === 'limadata')!
|
|
255
|
+
const result = ld.mapParams({
|
|
256
|
+
keywords: ['SaaS'],
|
|
257
|
+
industries: ['Software'],
|
|
258
|
+
min_employees: 51,
|
|
259
|
+
max_employees: 500,
|
|
260
|
+
limit: 25,
|
|
261
|
+
})
|
|
262
|
+
expect(result.body).toEqual({
|
|
263
|
+
query: 'SaaS Software',
|
|
264
|
+
company_size: 'C,D',
|
|
265
|
+
})
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
it('LimaData search uses fallback query when no keywords/industries provided', () => {
|
|
269
|
+
const providers = getProviders('search_companies')
|
|
270
|
+
const ld = providers.find((p) => p.id === 'limadata')!
|
|
271
|
+
const result = ld.mapParams({ min_employees: 51, max_employees: 200 })
|
|
272
|
+
expect((result.body as { query: string }).query).toBe('company')
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
it('PredictLeads maps employee range to comma-separated size enum and uses first location', () => {
|
|
276
|
+
const providers = getProviders('search_companies')
|
|
277
|
+
const pl = providers.find((p) => p.id === 'predictleads')!
|
|
278
|
+
expect(pl.method).toBe('GET')
|
|
279
|
+
const result = pl.mapParams({
|
|
280
|
+
countries: ['US'],
|
|
281
|
+
locations: ['San Francisco'],
|
|
282
|
+
min_employees: 51,
|
|
283
|
+
max_employees: 500,
|
|
284
|
+
limit: 25,
|
|
285
|
+
})
|
|
286
|
+
expect(result.queryParams).toEqual({
|
|
287
|
+
location: 'San Francisco',
|
|
288
|
+
sizes: '51-200,201-500',
|
|
289
|
+
limit: '25',
|
|
290
|
+
})
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
it('PredictLeads isApplicable requires both location and employee range', () => {
|
|
294
|
+
const providers = getProviders('search_companies')
|
|
295
|
+
const pl = providers.find((p) => p.id === 'predictleads')!
|
|
296
|
+
expect(pl.isApplicable!({})).toBe(false)
|
|
297
|
+
expect(pl.isApplicable!({ countries: ['US'] })).toBe(false)
|
|
298
|
+
expect(pl.isApplicable!({ min_employees: 50 })).toBe(false)
|
|
299
|
+
expect(pl.isApplicable!({ countries: ['US'], min_employees: 50 })).toBe(true)
|
|
300
|
+
expect(pl.isApplicable!({ locations: ['SF'], max_employees: 500 })).toBe(true)
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
it('TheirStack maps technologies to company_technology_slug_or and forwards employee/industry/country filters', () => {
|
|
304
|
+
const providers = getProviders('search_companies')
|
|
305
|
+
const ts = providers.find((p) => p.id === 'theirstack')!
|
|
306
|
+
const result = ts.mapParams({
|
|
307
|
+
technologies: ['salesforce', 'hubspot'],
|
|
308
|
+
industries: ['Technology'],
|
|
309
|
+
countries: ['US'],
|
|
310
|
+
min_employees: 50,
|
|
311
|
+
max_employees: 500,
|
|
312
|
+
limit: 10,
|
|
313
|
+
})
|
|
314
|
+
expect(result.body).toEqual({
|
|
315
|
+
company_technology_slug_or: ['salesforce', 'hubspot'],
|
|
316
|
+
industry_or: ['Technology'],
|
|
317
|
+
company_country_code_or: ['US'],
|
|
318
|
+
min_employee_count: 50,
|
|
319
|
+
max_employee_count: 500,
|
|
320
|
+
funding_stage_or: undefined,
|
|
321
|
+
min_funding_usd: undefined,
|
|
322
|
+
max_funding_usd: undefined,
|
|
323
|
+
limit: 10,
|
|
324
|
+
})
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
it('TheirStack maps funding_stage_or and funding amounts', () => {
|
|
328
|
+
const providers = getProviders('search_companies')
|
|
329
|
+
const ts = providers.find((p) => p.id === 'theirstack')!
|
|
330
|
+
const result = ts.mapParams({
|
|
331
|
+
industries: ['SaaS'],
|
|
332
|
+
funding_stages: ['series_a', 'series_b'],
|
|
333
|
+
min_funding_amount: 1_000_000,
|
|
334
|
+
max_funding_amount: 50_000_000,
|
|
335
|
+
limit: 10,
|
|
336
|
+
})
|
|
337
|
+
expect(result.body).toEqual({
|
|
338
|
+
company_technology_slug_or: undefined,
|
|
339
|
+
industry_or: ['SaaS'],
|
|
340
|
+
company_country_code_or: undefined,
|
|
341
|
+
min_employee_count: undefined,
|
|
342
|
+
max_employee_count: undefined,
|
|
343
|
+
funding_stage_or: ['series_a', 'series_b'],
|
|
344
|
+
min_funding_usd: 1_000_000,
|
|
345
|
+
max_funding_usd: 50_000_000,
|
|
346
|
+
limit: 10,
|
|
347
|
+
})
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
it('Sumble folds keywords+industries into filters.query', () => {
|
|
351
|
+
const providers = getProviders('search_companies')
|
|
352
|
+
const sumble = providers.find((p) => p.id === 'sumble')!
|
|
353
|
+
const result = sumble.mapParams({
|
|
354
|
+
keywords: ['SaaS'],
|
|
355
|
+
industries: ['Tech'],
|
|
356
|
+
limit: 10,
|
|
357
|
+
})
|
|
358
|
+
expect(result.body).toEqual({
|
|
359
|
+
filters: { query: 'SaaS Tech' },
|
|
360
|
+
limit: 10,
|
|
361
|
+
})
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
it('Sumble passes technologies into filters.technologies', () => {
|
|
365
|
+
const providers = getProviders('search_companies')
|
|
366
|
+
const sumble = providers.find((p) => p.id === 'sumble')!
|
|
367
|
+
const result = sumble.mapParams({
|
|
368
|
+
technologies: ['Salesforce', 'HubSpot'],
|
|
369
|
+
limit: 5,
|
|
370
|
+
})
|
|
371
|
+
expect(result.body).toEqual({
|
|
372
|
+
filters: { technologies: ['Salesforce', 'HubSpot'] },
|
|
373
|
+
limit: 5,
|
|
374
|
+
})
|
|
375
|
+
})
|
|
376
|
+
|
|
377
|
+
it('Sumble isApplicable requires keywords, industries, or technologies', () => {
|
|
378
|
+
const providers = getProviders('search_companies')
|
|
379
|
+
const sumble = providers.find((p) => p.id === 'sumble')!
|
|
380
|
+
expect(sumble.isApplicable!({})).toBe(false)
|
|
381
|
+
expect(sumble.isApplicable!({ keywords: ['SaaS'] })).toBe(true)
|
|
382
|
+
expect(sumble.isApplicable!({ industries: ['Tech'] })).toBe(true)
|
|
383
|
+
expect(sumble.isApplicable!({ technologies: ['Salesforce'] })).toBe(true)
|
|
384
|
+
expect(sumble.isApplicable!({ countries: ['FR'] })).toBe(false)
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
it('Wiza creates a prospect list with [{v}] filter shape, translates ISO codes, and has async config', () => {
|
|
388
|
+
const providers = getProviders('search_companies')
|
|
389
|
+
const wiza = providers.find((p) => p.id === 'wiza')!
|
|
390
|
+
const result = wiza.mapParams({
|
|
391
|
+
industries: ['SaaS'],
|
|
392
|
+
countries: ['FR'],
|
|
393
|
+
min_founded_year: 2015,
|
|
394
|
+
limit: 25,
|
|
395
|
+
})
|
|
396
|
+
expect(result.body).toEqual({
|
|
397
|
+
list: { name: 'mcp-search-companies', max_profiles: 25 },
|
|
398
|
+
filters: {
|
|
399
|
+
company_industry: [{ v: 'SaaS' }],
|
|
400
|
+
company_location: [{ v: 'France' }],
|
|
401
|
+
year_founded_start: [{ v: '2015' }],
|
|
402
|
+
},
|
|
403
|
+
enrichment_level: 'none',
|
|
404
|
+
})
|
|
405
|
+
expect(wiza.async).toBeDefined()
|
|
406
|
+
expect(wiza.async!.pollEndpoint('123')).toBe('/wiza/lists/123')
|
|
407
|
+
expect(wiza.async!.extractId({ data: { id: 123 } })).toBe('123')
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
it('Wiza isComplete accepts both finished and completed terminal states', () => {
|
|
411
|
+
const providers = getProviders('search_companies')
|
|
412
|
+
const wiza = providers.find((p) => p.id === 'wiza')!
|
|
413
|
+
expect(wiza.async!.isComplete({ data: { status: 'finished' } })).toBe(true)
|
|
414
|
+
expect(wiza.async!.isComplete({ data: { status: 'completed' } })).toBe(true)
|
|
415
|
+
expect(wiza.async!.isComplete({ data: { status: 'failed' } })).toBe(true)
|
|
416
|
+
expect(wiza.async!.isComplete({ data: { status: 'scraping' } })).toBe(false)
|
|
417
|
+
expect(wiza.async!.isComplete({ data: { status: 'queued' } })).toBe(false)
|
|
418
|
+
})
|
|
419
|
+
|
|
420
|
+
it('Wiza extractId throws when no list id is present', () => {
|
|
421
|
+
const providers = getProviders('search_companies')
|
|
422
|
+
const wiza = providers.find((p) => p.id === 'wiza')!
|
|
423
|
+
expect(() => wiza.async!.extractId({})).toThrow('Wiza response has no list id')
|
|
424
|
+
expect(() => wiza.async!.extractId({ data: {} })).toThrow('Wiza response has no list id')
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
it('Wiza hasResult checks data.stats.people > 0', () => {
|
|
428
|
+
const providers = getProviders('search_companies')
|
|
429
|
+
const wiza = providers.find((p) => p.id === 'wiza')!
|
|
430
|
+
expect(wiza.hasResult({ data: { status: 'finished', stats: { people: 10 } } })).toBe(true)
|
|
431
|
+
expect(wiza.hasResult({ data: { status: 'finished', stats: { people: 0 } } })).toBe(false)
|
|
432
|
+
expect(wiza.hasResult({ data: { status: 'finished' } })).toBe(false)
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
it('LimaData prospect filter maps employees to company_headcount filter_type', () => {
|
|
436
|
+
const providers = getProviders('search_companies')
|
|
437
|
+
const ld = providers.find((p) => p.id === 'limadata-prospect-filter')!
|
|
438
|
+
const result = ld.mapParams({ min_employees: 51, max_employees: 500 })
|
|
439
|
+
expect(result.body).toEqual({
|
|
440
|
+
filters: [{ filter_type: 'company_headcount', values: ['C', 'D'] }],
|
|
441
|
+
})
|
|
442
|
+
})
|
|
443
|
+
|
|
444
|
+
it('LimaData prospect filter isApplicable requires a non-trivial employee range', () => {
|
|
445
|
+
const providers = getProviders('search_companies')
|
|
446
|
+
const ld = providers.find((p) => p.id === 'limadata-prospect-filter')!
|
|
447
|
+
expect(ld.isApplicable!({})).toBe(false)
|
|
448
|
+
expect(ld.isApplicable!({ keywords: ['SaaS'] })).toBe(false) // no employee filter
|
|
449
|
+
expect(ld.isApplicable!({ min_employees: 51, max_employees: 500 })).toBe(true)
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
it('LimaData prospect URL passes search_url when provided', () => {
|
|
453
|
+
const providers = getProviders('search_companies')
|
|
454
|
+
const ld = providers.find((p) => p.id === 'limadata-prospect-url')!
|
|
455
|
+
const result = ld.mapParams({ linkedin_search_url: 'https://www.linkedin.com/sales/search/company?...' })
|
|
456
|
+
expect(result.body).toEqual({ search_url: 'https://www.linkedin.com/sales/search/company?...' })
|
|
457
|
+
})
|
|
458
|
+
|
|
459
|
+
it('LimaData prospect URL isApplicable requires linkedin_search_url', () => {
|
|
460
|
+
const providers = getProviders('search_companies')
|
|
461
|
+
const ld = providers.find((p) => p.id === 'limadata-prospect-url')!
|
|
462
|
+
expect(ld.isApplicable!({})).toBe(false)
|
|
463
|
+
expect(ld.isApplicable!({ keywords: ['SaaS'] })).toBe(false)
|
|
464
|
+
expect(ld.isApplicable!({ linkedin_search_url: 'https://...' })).toBe(true)
|
|
465
|
+
})
|
|
466
|
+
|
|
467
|
+
it('search_companies has 18 providers in priority order', () => {
|
|
468
|
+
const providers = getProviders('search_companies')
|
|
469
|
+
expect(providers.map((p) => p.id)).toEqual([
|
|
470
|
+
'companyenrich',
|
|
471
|
+
'fullenrich',
|
|
472
|
+
'pdl',
|
|
473
|
+
'theirstack',
|
|
474
|
+
'wiza',
|
|
475
|
+
'signalbase',
|
|
476
|
+
'blitzapi',
|
|
477
|
+
'apollo',
|
|
478
|
+
'limadata',
|
|
479
|
+
'predictleads',
|
|
480
|
+
'sumble',
|
|
481
|
+
'limadata-prospect-filter',
|
|
482
|
+
'limadata-prospect-url',
|
|
483
|
+
'linkupapi-search',
|
|
484
|
+
'linkupapi-fundraising',
|
|
485
|
+
'linkupapi-hiring',
|
|
486
|
+
'prospeo-search-company',
|
|
487
|
+
'ai-ark-companies',
|
|
488
|
+
])
|
|
489
|
+
})
|
|
490
|
+
|
|
491
|
+
it('PDL uses POST to /pdl/company/search', () => {
|
|
492
|
+
const pdl = getProviders('search_companies').find((p) => p.id === 'pdl')!
|
|
493
|
+
expect(pdl.endpoint).toBe('/pdl/company/search')
|
|
494
|
+
expect(pdl.method).toBe('POST')
|
|
495
|
+
})
|
|
496
|
+
})
|
|
497
|
+
|
|
498
|
+
describe('find_people mapParams', () => {
|
|
499
|
+
it('LeadsFactory keeps distinct roles as separate personas (seniority not forwarded)', () => {
|
|
500
|
+
const providers = getProviders('find_people')
|
|
501
|
+
const lf = providers.find((p) => p.id === 'leadsfactory')!
|
|
502
|
+
const result = lf.mapParams({
|
|
503
|
+
company_domains: ['microsoft.com'],
|
|
504
|
+
job_titles: ['CEO', 'CRO'],
|
|
505
|
+
seniorities: ['C-suite'], // must NOT be forwarded
|
|
506
|
+
limit: 5,
|
|
507
|
+
})
|
|
508
|
+
expect(result.body).toEqual({
|
|
509
|
+
company_domains: ['microsoft.com'],
|
|
510
|
+
search: {
|
|
511
|
+
max_persona_results: 5,
|
|
512
|
+
personas: [{ job_title: 'CEO' }, { job_title: 'CRO' }],
|
|
513
|
+
},
|
|
514
|
+
})
|
|
515
|
+
})
|
|
516
|
+
|
|
517
|
+
it('LeadsFactory defaults to Decision Maker persona when no job titles provided', () => {
|
|
518
|
+
const providers = getProviders('find_people')
|
|
519
|
+
const lf = providers.find((p) => p.id === 'leadsfactory')!
|
|
520
|
+
const result = lf.mapParams({ company_domains: ['coldiq.com'], limit: 3 })
|
|
521
|
+
expect(result.body).toEqual({
|
|
522
|
+
company_domains: ['coldiq.com'],
|
|
523
|
+
search: {
|
|
524
|
+
max_persona_results: 3,
|
|
525
|
+
personas: [{ job_title: 'Decision Maker' }],
|
|
526
|
+
},
|
|
527
|
+
})
|
|
528
|
+
})
|
|
529
|
+
|
|
530
|
+
it('LeadsFactory omits company_domains when LinkedIn URLs are provided', () => {
|
|
531
|
+
const providers = getProviders('find_people')
|
|
532
|
+
const lf = providers.find((p) => p.id === 'leadsfactory')!
|
|
533
|
+
const result = lf.mapParams({
|
|
534
|
+
company_linkedin_urls: ['https://linkedin.com/company/microsoft'],
|
|
535
|
+
company_domains: ['microsoft.com'],
|
|
536
|
+
job_titles: ['CEO'],
|
|
537
|
+
limit: 5,
|
|
538
|
+
})
|
|
539
|
+
expect(result.body).toEqual({
|
|
540
|
+
company_linkedin_urls: ['https://linkedin.com/company/microsoft'],
|
|
541
|
+
search: {
|
|
542
|
+
max_persona_results: 5,
|
|
543
|
+
personas: [{ job_title: 'CEO' }],
|
|
544
|
+
},
|
|
545
|
+
})
|
|
546
|
+
})
|
|
547
|
+
|
|
548
|
+
it('LeadsFactory falls back to company_domains when no LinkedIn URLs', () => {
|
|
549
|
+
const providers = getProviders('find_people')
|
|
550
|
+
const lf = providers.find((p) => p.id === 'leadsfactory')!
|
|
551
|
+
const result = lf.mapParams({
|
|
552
|
+
company_domains: ['coldiq.com'],
|
|
553
|
+
job_titles: ['CEO'],
|
|
554
|
+
limit: 3,
|
|
555
|
+
})
|
|
556
|
+
expect((result.body as Record<string, unknown>).company_domains).toEqual(['coldiq.com'])
|
|
557
|
+
expect((result.body as Record<string, unknown>).company_linkedin_urls).toBeUndefined()
|
|
558
|
+
})
|
|
559
|
+
|
|
560
|
+
it('LeadsFactory has async config', () => {
|
|
561
|
+
const providers = getProviders('find_people')
|
|
562
|
+
const lf = providers.find((p) => p.id === 'leadsfactory')!
|
|
563
|
+
expect(lf.async).toBeDefined()
|
|
564
|
+
expect(lf.async!.pollIntervalMs).toBe(15000)
|
|
565
|
+
expect(lf.async!.timeoutMs).toBe(300_000)
|
|
566
|
+
})
|
|
567
|
+
|
|
568
|
+
it('Apollo maps correctly', () => {
|
|
569
|
+
const providers = getProviders('find_people')
|
|
570
|
+
const apollo = providers.find((p) => p.id === 'apollo')!
|
|
571
|
+
const result = apollo.mapParams({
|
|
572
|
+
company_domains: ['microsoft.com'],
|
|
573
|
+
job_titles: ['CEO'],
|
|
574
|
+
seniorities: ['c_suite'],
|
|
575
|
+
limit: 10,
|
|
576
|
+
})
|
|
577
|
+
expect(result.body).toEqual({
|
|
578
|
+
person_titles: ['CEO'],
|
|
579
|
+
q_organization_domains: ['microsoft.com'],
|
|
580
|
+
person_seniorities: ['c_suite'],
|
|
581
|
+
person_locations: undefined,
|
|
582
|
+
q_keywords: undefined,
|
|
583
|
+
per_page: 10,
|
|
584
|
+
})
|
|
585
|
+
})
|
|
586
|
+
|
|
587
|
+
it('PDL uses POST to /pdl/person/search', () => {
|
|
588
|
+
const pdl = getProviders('find_people').find((p) => p.id === 'pdl')!
|
|
589
|
+
expect(pdl.endpoint).toBe('/pdl/person/search')
|
|
590
|
+
expect(pdl.method).toBe('POST')
|
|
591
|
+
})
|
|
592
|
+
})
|
|
593
|
+
|
|
594
|
+
describe('find_email mapParams', () => {
|
|
595
|
+
it('FindyMail maps name + domain', () => {
|
|
596
|
+
const providers = getProviders('find_email')
|
|
597
|
+
const fm = providers.find((p) => p.id === 'findymail')!
|
|
598
|
+
const result = fm.mapParams({
|
|
599
|
+
first_name: 'Michel',
|
|
600
|
+
last_name: 'Lieben',
|
|
601
|
+
domain: 'coldiq.com',
|
|
602
|
+
})
|
|
603
|
+
expect(result.body).toEqual({ name: 'Michel Lieben', domain: 'coldiq.com' })
|
|
604
|
+
})
|
|
605
|
+
|
|
606
|
+
it('IcyPeas maps correctly', () => {
|
|
607
|
+
const providers = getProviders('find_email')
|
|
608
|
+
const icy = providers.find((p) => p.id === 'icypeas')!
|
|
609
|
+
const result = icy.mapParams({
|
|
610
|
+
first_name: 'Michel',
|
|
611
|
+
last_name: 'Lieben',
|
|
612
|
+
domain: 'coldiq.com',
|
|
613
|
+
})
|
|
614
|
+
expect(result.body).toEqual({
|
|
615
|
+
firstname: 'Michel',
|
|
616
|
+
lastname: 'Lieben',
|
|
617
|
+
domainOrCompany: 'coldiq.com',
|
|
618
|
+
})
|
|
619
|
+
})
|
|
620
|
+
|
|
621
|
+
it('Prospeo maps with linkedin_url when provided', () => {
|
|
622
|
+
const providers = getProviders('find_email')
|
|
623
|
+
const prospeo = providers.find((p) => p.id === 'prospeo')!
|
|
624
|
+
const result = prospeo.mapParams({
|
|
625
|
+
linkedin_url: 'https://linkedin.com/in/michel-lieben',
|
|
626
|
+
})
|
|
627
|
+
expect(result.body).toEqual({
|
|
628
|
+
data: { linkedin_url: 'https://linkedin.com/in/michel-lieben' },
|
|
629
|
+
})
|
|
630
|
+
})
|
|
631
|
+
|
|
632
|
+
it('Prospeo hasResult detects email nested at person.email.email', () => {
|
|
633
|
+
const providers = getProviders('find_email')
|
|
634
|
+
const prospeo = providers.find((p) => p.id === 'prospeo')!
|
|
635
|
+
expect(prospeo.hasResult({ error: false, person: { email: { email: 'michel@coldiq.com' } } })).toBe(true)
|
|
636
|
+
expect(prospeo.hasResult({ error: false, person: { email: { email: null } } })).toBe(false)
|
|
637
|
+
expect(prospeo.hasResult({ error: true })).toBe(false)
|
|
638
|
+
expect(prospeo.hasResult({ email: null })).toBe(false)
|
|
639
|
+
})
|
|
640
|
+
|
|
641
|
+
it('FullEnrich maps to batch-of-1 with contact.emails field', () => {
|
|
642
|
+
const providers = getProviders('find_email')
|
|
643
|
+
const fe = providers.find((p) => p.id === 'fullenrich')!
|
|
644
|
+
const result = fe.mapParams({
|
|
645
|
+
first_name: 'Michel',
|
|
646
|
+
last_name: 'Lieben',
|
|
647
|
+
domain: 'coldiq.com',
|
|
648
|
+
linkedin_url: 'https://linkedin.com/in/michel-lieben',
|
|
649
|
+
})
|
|
650
|
+
expect(result.body).toEqual({
|
|
651
|
+
name: 'mcp-enrich',
|
|
652
|
+
data: [{
|
|
653
|
+
first_name: 'Michel',
|
|
654
|
+
last_name: 'Lieben',
|
|
655
|
+
domain: 'coldiq.com',
|
|
656
|
+
linkedin_url: 'https://linkedin.com/in/michel-lieben',
|
|
657
|
+
enrich_fields: ['contact.emails'],
|
|
658
|
+
}],
|
|
659
|
+
})
|
|
660
|
+
})
|
|
661
|
+
|
|
662
|
+
it('FullEnrich hasResult detects email in data[0].emails array', () => {
|
|
663
|
+
const providers = getProviders('find_email')
|
|
664
|
+
const fe = providers.find((p) => p.id === 'fullenrich')!
|
|
665
|
+
expect(fe.hasResult({ status: 'DONE', data: [{ emails: ['michel@coldiq.com'] }] })).toBe(true)
|
|
666
|
+
expect(fe.hasResult({ status: 'DONE', data: [{ emails: [] }] })).toBe(false)
|
|
667
|
+
expect(fe.hasResult({ status: 'DONE', data: [] })).toBe(false)
|
|
668
|
+
expect(fe.hasResult({ status: 'FAILED', data: [] })).toBe(false)
|
|
669
|
+
})
|
|
670
|
+
|
|
671
|
+
it('FullEnrich has async config with correct poll endpoint', () => {
|
|
672
|
+
const providers = getProviders('find_email')
|
|
673
|
+
const fe = providers.find((p) => p.id === 'fullenrich')!
|
|
674
|
+
expect(fe.async).toBeDefined()
|
|
675
|
+
expect(fe.async!.pollEndpoint('abc-123')).toBe('/fullenrich/contact/enrich/bulk/abc-123')
|
|
676
|
+
expect(fe.async!.isComplete({ status: 'DONE' })).toBe(true)
|
|
677
|
+
expect(fe.async!.isComplete({ status: 'FAILED' })).toBe(true)
|
|
678
|
+
expect(fe.async!.isComplete({ status: 'IN_PROGRESS' })).toBe(false)
|
|
679
|
+
expect(fe.async!.extractId({ enrichment_id: 'abc-123' })).toBe('abc-123')
|
|
680
|
+
})
|
|
681
|
+
|
|
682
|
+
it('FullEnrich is priority 6 and LinkupAPI is priority 8 in the new 8-provider chain', () => {
|
|
683
|
+
const providers = getProviders('find_email')
|
|
684
|
+
const fe = providers.find((p) => p.id === 'fullenrich')!
|
|
685
|
+
const lu = providers.find((p) => p.id === 'linkupapi')!
|
|
686
|
+
expect(fe.priority).toBe(6)
|
|
687
|
+
expect(lu.priority).toBe(8)
|
|
688
|
+
})
|
|
689
|
+
|
|
690
|
+
it('LimaData work-email maps full_name + company_domain', () => {
|
|
691
|
+
const providers = getProviders('find_email')
|
|
692
|
+
const ld = providers.find((p) => p.id === 'limadata-work-email')!
|
|
693
|
+
const result = ld.mapParams({
|
|
694
|
+
first_name: 'Michel',
|
|
695
|
+
last_name: 'Lieben',
|
|
696
|
+
domain: 'coldiq.com',
|
|
697
|
+
})
|
|
698
|
+
expect(result.body).toEqual({ full_name: 'Michel Lieben', company_domain: 'coldiq.com' })
|
|
699
|
+
})
|
|
700
|
+
|
|
701
|
+
it('LimaData work-email hasResult accepts top-level email or nested data.email or emails array', () => {
|
|
702
|
+
const providers = getProviders('find_email')
|
|
703
|
+
const ld = providers.find((p) => p.id === 'limadata-work-email')!
|
|
704
|
+
expect(ld.hasResult({ email: 'michel@coldiq.com' })).toBe(true)
|
|
705
|
+
expect(ld.hasResult({ data: { email: 'michel@coldiq.com' } })).toBe(true)
|
|
706
|
+
expect(ld.hasResult({ emails: ['michel@coldiq.com'] })).toBe(true)
|
|
707
|
+
expect(ld.hasResult({ email: null })).toBe(false)
|
|
708
|
+
expect(ld.hasResult({})).toBe(false)
|
|
709
|
+
})
|
|
710
|
+
|
|
711
|
+
it('BlitzAPI maps person_linkedin_url from linkedin_url input', () => {
|
|
712
|
+
const providers = getProviders('find_email')
|
|
713
|
+
const blitz = providers.find((p) => p.id === 'blitzapi')!
|
|
714
|
+
const result = blitz.mapParams({
|
|
715
|
+
linkedin_url: 'https://linkedin.com/in/michel-lieben',
|
|
716
|
+
})
|
|
717
|
+
expect(result.body).toEqual({ person_linkedin_url: 'https://linkedin.com/in/michel-lieben' })
|
|
718
|
+
})
|
|
719
|
+
|
|
720
|
+
it('BlitzAPI isApplicable requires linkedin_url', () => {
|
|
721
|
+
const providers = getProviders('find_email')
|
|
722
|
+
const blitz = providers.find((p) => p.id === 'blitzapi')!
|
|
723
|
+
expect(blitz.isApplicable!({})).toBe(false)
|
|
724
|
+
expect(blitz.isApplicable!({ first_name: 'Michel', domain: 'coldiq.com' })).toBe(false)
|
|
725
|
+
expect(blitz.isApplicable!({ linkedin_url: 'https://...' })).toBe(true)
|
|
726
|
+
})
|
|
727
|
+
|
|
728
|
+
it('BlitzAPI hasResult checks top-level email', () => {
|
|
729
|
+
const providers = getProviders('find_email')
|
|
730
|
+
const blitz = providers.find((p) => p.id === 'blitzapi')!
|
|
731
|
+
expect(blitz.hasResult({ email: 'michel@coldiq.com' })).toBe(true)
|
|
732
|
+
expect(blitz.hasResult({ email: null })).toBe(false)
|
|
733
|
+
})
|
|
734
|
+
|
|
735
|
+
it('LimaData work-email-linkedin maps linkedin_url passthrough', () => {
|
|
736
|
+
const providers = getProviders('find_email')
|
|
737
|
+
const ld = providers.find((p) => p.id === 'limadata-work-email-linkedin')!
|
|
738
|
+
const result = ld.mapParams({
|
|
739
|
+
linkedin_url: 'https://linkedin.com/in/michel-lieben',
|
|
740
|
+
})
|
|
741
|
+
expect(result.body).toEqual({ linkedin_url: 'https://linkedin.com/in/michel-lieben' })
|
|
742
|
+
})
|
|
743
|
+
|
|
744
|
+
it('LimaData work-email-linkedin isApplicable requires linkedin_url', () => {
|
|
745
|
+
const providers = getProviders('find_email')
|
|
746
|
+
const ld = providers.find((p) => p.id === 'limadata-work-email-linkedin')!
|
|
747
|
+
expect(ld.isApplicable!({})).toBe(false)
|
|
748
|
+
expect(ld.isApplicable!({ first_name: 'Michel', domain: 'coldiq.com' })).toBe(false)
|
|
749
|
+
expect(ld.isApplicable!({ linkedin_url: 'https://...' })).toBe(true)
|
|
750
|
+
})
|
|
751
|
+
|
|
752
|
+
it('LimaData work-email isApplicable requires first_name + last_name + domain', () => {
|
|
753
|
+
const providers = getProviders('find_email')
|
|
754
|
+
const ld = providers.find((p) => p.id === 'limadata-work-email')!
|
|
755
|
+
expect(ld.isApplicable!({})).toBe(false)
|
|
756
|
+
expect(ld.isApplicable!({ first_name: 'Michel' })).toBe(false)
|
|
757
|
+
expect(ld.isApplicable!({ first_name: 'Michel', last_name: 'Lieben' })).toBe(false)
|
|
758
|
+
expect(ld.isApplicable!({ first_name: 'Michel', last_name: 'Lieben', domain: 'coldiq.com' })).toBe(true)
|
|
759
|
+
})
|
|
760
|
+
})
|
|
761
|
+
|
|
762
|
+
describe('verify_email', () => {
|
|
763
|
+
it('providers are ordered findymail → icypeas → instantly → linkupapi-validate', () => {
|
|
764
|
+
expect(getProviders('verify_email').map((p) => p.id)).toEqual(['findymail', 'icypeas', 'instantly', 'linkupapi-validate'])
|
|
765
|
+
})
|
|
766
|
+
|
|
767
|
+
it('FindyMail hasResult accepts verified field', () => {
|
|
768
|
+
const providers = getProviders('verify_email')
|
|
769
|
+
const fm = providers.find((p) => p.id === 'findymail')!
|
|
770
|
+
// Findymail verify returns { email, verified: boolean, provider }
|
|
771
|
+
expect(fm.hasResult({ email: 'michel@coldiq.com', verified: true, provider: 'Google' })).toBe(true)
|
|
772
|
+
expect(fm.hasResult({ email: 'michel@coldiq.com', verified: false, provider: 'Google' })).toBe(true)
|
|
773
|
+
expect(fm.hasResult({})).toBe(false)
|
|
774
|
+
expect(fm.hasResult({ email: 'michel@coldiq.com' })).toBe(false)
|
|
775
|
+
})
|
|
776
|
+
|
|
777
|
+
it('IcyPeas hasResult accepts success field', () => {
|
|
778
|
+
const providers = getProviders('verify_email')
|
|
779
|
+
const icy = providers.find((p) => p.id === 'icypeas')!
|
|
780
|
+
// IcyPeas verify returns { success: boolean, item: { _id, status } }
|
|
781
|
+
expect(icy.hasResult({ success: true, item: { _id: 'abc', status: 'NONE' } })).toBe(true)
|
|
782
|
+
expect(icy.hasResult({ success: false })).toBe(true)
|
|
783
|
+
expect(icy.hasResult({})).toBe(false)
|
|
784
|
+
expect(icy.hasResult({ item: { status: 'NONE' } })).toBe(false)
|
|
785
|
+
})
|
|
786
|
+
|
|
787
|
+
it('Instantly mapParams forwards email', () => {
|
|
788
|
+
const inst = getProviders('verify_email').find((p) => p.id === 'instantly')!
|
|
789
|
+
expect(inst.mapParams({ email: 'michel@coldiq.com' }).body).toEqual({ email: 'michel@coldiq.com' })
|
|
790
|
+
})
|
|
791
|
+
|
|
792
|
+
it('Instantly hasResult accepts known terminal statuses', () => {
|
|
793
|
+
const inst = getProviders('verify_email').find((p) => p.id === 'instantly')!
|
|
794
|
+
expect(inst.hasResult({ email: 'michel@coldiq.com', status: 'valid' })).toBe(true)
|
|
795
|
+
expect(inst.hasResult({ email: 'michel@coldiq.com', status: 'invalid' })).toBe(true)
|
|
796
|
+
expect(inst.hasResult({ email: 'michel@coldiq.com', status: 'unknown' })).toBe(true)
|
|
797
|
+
expect(inst.hasResult({ email: 'michel@coldiq.com', status: 'risky' })).toBe(true)
|
|
798
|
+
expect(inst.hasResult({ email: 'michel@coldiq.com', status: 'catch_all' })).toBe(true)
|
|
799
|
+
expect(inst.hasResult({ email: 'michel@coldiq.com', status: 'disposable' })).toBe(true)
|
|
800
|
+
expect(inst.hasResult({ email: 'michel@coldiq.com', status: 'pending' })).toBe(false)
|
|
801
|
+
expect(inst.hasResult({ email: 'michel@coldiq.com' })).toBe(false)
|
|
802
|
+
expect(inst.hasResult({})).toBe(false)
|
|
803
|
+
})
|
|
804
|
+
|
|
805
|
+
it('Instantly async config extracts email as job ID and polls correctly', () => {
|
|
806
|
+
const inst = getProviders('verify_email').find((p) => p.id === 'instantly')!
|
|
807
|
+
expect(inst.async).toBeDefined()
|
|
808
|
+
expect(inst.async!.extractId({ email: 'michel@coldiq.com', status: 'pending' })).toBe('michel@coldiq.com')
|
|
809
|
+
expect(inst.async!.pollEndpoint('michel@coldiq.com')).toBe('/instantly/email-verification/michel%40coldiq.com')
|
|
810
|
+
expect(inst.async!.isComplete({ status: 'valid' })).toBe(true)
|
|
811
|
+
expect(inst.async!.isComplete({ status: 'invalid' })).toBe(true)
|
|
812
|
+
expect(inst.async!.isComplete({ status: 'pending' })).toBe(false)
|
|
813
|
+
expect(inst.async!.isComplete({})).toBe(false)
|
|
814
|
+
})
|
|
815
|
+
|
|
816
|
+
it('Instantly async config throws when email missing from create response', () => {
|
|
817
|
+
const inst = getProviders('verify_email').find((p) => p.id === 'instantly')!
|
|
818
|
+
expect(() => inst.async!.extractId({})).toThrow('no email field')
|
|
819
|
+
})
|
|
820
|
+
})
|
|
821
|
+
|
|
822
|
+
describe('enrich_company', () => {
|
|
823
|
+
it('providers are ordered companyenrich → pdl → apollo → limadata → prospeo → companyenrich_props → blitzapi → wiza → findymail → icypeas → builtwith → openmart → linkupapi-by-domain → linkupapi-by-url', () => {
|
|
824
|
+
const ids = getProviders('enrich_company').map((p) => p.id)
|
|
825
|
+
expect(ids).toEqual([
|
|
826
|
+
'companyenrich', 'pdl', 'apollo', 'limadata', 'prospeo',
|
|
827
|
+
'companyenrich_props', 'blitzapi', 'wiza', 'findymail', 'icypeas',
|
|
828
|
+
'builtwith', 'openmart', 'linkupapi-by-domain', 'linkupapi-by-url',
|
|
829
|
+
])
|
|
830
|
+
})
|
|
831
|
+
|
|
832
|
+
it('CompanyEnrich uses GET with queryParams and is gated on domain', () => {
|
|
833
|
+
const ce = getProviders('enrich_company').find((p) => p.id === 'companyenrich')!
|
|
834
|
+
expect(ce.method).toBe('GET')
|
|
835
|
+
expect(ce.mapParams({ domain: 'stripe.com' }).queryParams).toEqual({ domain: 'stripe.com' })
|
|
836
|
+
expect(ce.isApplicable!({ domain: 'stripe.com' })).toBe(true)
|
|
837
|
+
expect(ce.isApplicable!({ linkedin_url: 'https://linkedin.com/company/stripe' })).toBe(false)
|
|
838
|
+
expect(ce.isApplicable!({})).toBe(false)
|
|
839
|
+
})
|
|
840
|
+
|
|
841
|
+
it('Apollo uses POST with body and is gated on domain', () => {
|
|
842
|
+
const apollo = getProviders('enrich_company').find((p) => p.id === 'apollo')!
|
|
843
|
+
expect(apollo.method).toBe('POST')
|
|
844
|
+
expect(apollo.mapParams({ domain: 'stripe.com' }).body).toEqual({ domain: 'stripe.com' })
|
|
845
|
+
expect(apollo.isApplicable!({ domain: 'stripe.com' })).toBe(true)
|
|
846
|
+
expect(apollo.isApplicable!({ name: 'Stripe' })).toBe(false)
|
|
847
|
+
})
|
|
848
|
+
|
|
849
|
+
it('PDL uses GET with correct path and maps all identifier types', () => {
|
|
850
|
+
const pdl = getProviders('enrich_company').find((p) => p.id === 'pdl')!
|
|
851
|
+
expect(pdl.endpoint).toBe('/pdl/company/enrich')
|
|
852
|
+
expect(pdl.method).toBe('GET')
|
|
853
|
+
expect(pdl.mapParams({ domain: 'stripe.com' }).queryParams).toEqual({ website: 'stripe.com' })
|
|
854
|
+
expect(pdl.mapParams({ linkedin_url: 'https://linkedin.com/company/stripe' }).queryParams).toEqual({ profile: 'https://linkedin.com/company/stripe' })
|
|
855
|
+
expect(pdl.mapParams({ name: 'Stripe' }).queryParams).toEqual({ name: 'Stripe' })
|
|
856
|
+
expect(pdl.mapParams({ domain: 'stripe.com', linkedin_url: 'https://linkedin.com/company/stripe', name: 'Stripe' }).queryParams).toEqual({
|
|
857
|
+
website: 'stripe.com',
|
|
858
|
+
profile: 'https://linkedin.com/company/stripe',
|
|
859
|
+
name: 'Stripe',
|
|
860
|
+
})
|
|
861
|
+
expect(pdl.isApplicable).toBeUndefined()
|
|
862
|
+
})
|
|
863
|
+
|
|
864
|
+
it('Findymail maps all identifier types and has no gate', () => {
|
|
865
|
+
const fm = getProviders('enrich_company').find((p) => p.id === 'findymail')!
|
|
866
|
+
expect(fm.method).toBe('POST')
|
|
867
|
+
expect(fm.mapParams({ domain: 'stripe.com' }).body).toEqual({ domain: 'stripe.com' })
|
|
868
|
+
expect(fm.mapParams({ linkedin_url: 'https://linkedin.com/company/stripe' }).body).toEqual({ linkedin_url: 'https://linkedin.com/company/stripe' })
|
|
869
|
+
expect(fm.mapParams({ name: 'Stripe' }).body).toEqual({ name: 'Stripe' })
|
|
870
|
+
expect(fm.isApplicable).toBeUndefined()
|
|
871
|
+
})
|
|
872
|
+
|
|
873
|
+
it('Wiza is gated on domain or name and maps accordingly', () => {
|
|
874
|
+
const wiza = getProviders('enrich_company').find((p) => p.id === 'wiza')!
|
|
875
|
+
expect(wiza.isApplicable!({ domain: 'stripe.com' })).toBe(true)
|
|
876
|
+
expect(wiza.isApplicable!({ name: 'Stripe' })).toBe(true)
|
|
877
|
+
expect(wiza.isApplicable!({ linkedin_url: 'https://linkedin.com/company/stripe' })).toBe(false)
|
|
878
|
+
expect(wiza.mapParams({ domain: 'stripe.com' }).body).toEqual({ company_domain: 'stripe.com' })
|
|
879
|
+
expect(wiza.mapParams({ name: 'Stripe' }).body).toEqual({ company_name: 'Stripe' })
|
|
880
|
+
})
|
|
881
|
+
|
|
882
|
+
it('LimaData is gated on domain or linkedin_url', () => {
|
|
883
|
+
const ld = getProviders('enrich_company').find((p) => p.id === 'limadata')!
|
|
884
|
+
expect(ld.isApplicable!({ domain: 'stripe.com' })).toBe(true)
|
|
885
|
+
expect(ld.isApplicable!({ linkedin_url: 'https://linkedin.com/company/stripe' })).toBe(true)
|
|
886
|
+
expect(ld.isApplicable!({ name: 'Stripe' })).toBe(false)
|
|
887
|
+
expect(ld.mapParams({ domain: 'stripe.com' }).body).toEqual({ domain: 'stripe.com' })
|
|
888
|
+
expect(ld.mapParams({ linkedin_url: 'https://linkedin.com/company/stripe' }).body).toEqual({ linkedin_url: 'https://linkedin.com/company/stripe' })
|
|
889
|
+
})
|
|
890
|
+
|
|
891
|
+
it('Prospeo wraps all identifiers under data object and has no gate', () => {
|
|
892
|
+
const pr = getProviders('enrich_company').find((p) => p.id === 'prospeo')!
|
|
893
|
+
expect(pr.isApplicable).toBeUndefined()
|
|
894
|
+
expect(pr.mapParams({ domain: 'stripe.com' }).body).toEqual({ data: { company_website: 'stripe.com' } })
|
|
895
|
+
expect(pr.mapParams({ linkedin_url: 'https://linkedin.com/company/stripe' }).body).toEqual({ data: { company_linkedin_url: 'https://linkedin.com/company/stripe' } })
|
|
896
|
+
expect(pr.mapParams({ name: 'Stripe' }).body).toEqual({ data: { company_name: 'Stripe' } })
|
|
897
|
+
})
|
|
898
|
+
|
|
899
|
+
it('Prospeo hasResult detects success via response field', () => {
|
|
900
|
+
const pr = getProviders('enrich_company').find((p) => p.id === 'prospeo')!
|
|
901
|
+
expect(pr.hasResult({ error: false, response: { name: 'Stripe' } })).toBe(true)
|
|
902
|
+
expect(pr.hasResult({ error: true })).toBe(false)
|
|
903
|
+
expect(pr.hasResult({ response: null })).toBe(false)
|
|
904
|
+
})
|
|
905
|
+
|
|
906
|
+
it('CompanyEnrich POST maps name and linkedinUrl (camelCase)', () => {
|
|
907
|
+
const cp = getProviders('enrich_company').find((p) => p.id === 'companyenrich_props')!
|
|
908
|
+
expect(cp.method).toBe('POST')
|
|
909
|
+
expect(cp.isApplicable!({ name: 'Stripe' })).toBe(true)
|
|
910
|
+
expect(cp.isApplicable!({ linkedin_url: 'https://linkedin.com/company/stripe' })).toBe(true)
|
|
911
|
+
expect(cp.isApplicable!({ domain: 'stripe.com' })).toBe(false)
|
|
912
|
+
expect(cp.mapParams({ name: 'Stripe' }).body).toEqual({ name: 'Stripe' })
|
|
913
|
+
expect(cp.mapParams({ linkedin_url: 'https://linkedin.com/company/stripe' }).body).toEqual({ linkedinUrl: 'https://linkedin.com/company/stripe' })
|
|
914
|
+
})
|
|
915
|
+
|
|
916
|
+
it('BlitzAPI is gated on linkedin_url and passes company_linkedin_url', () => {
|
|
917
|
+
const bz = getProviders('enrich_company').find((p) => p.id === 'blitzapi')!
|
|
918
|
+
expect(bz.isApplicable!({ linkedin_url: 'https://linkedin.com/company/stripe' })).toBe(true)
|
|
919
|
+
expect(bz.isApplicable!({ domain: 'stripe.com' })).toBe(false)
|
|
920
|
+
expect(bz.mapParams({ linkedin_url: 'https://linkedin.com/company/stripe' }).body).toEqual({ company_linkedin_url: 'https://linkedin.com/company/stripe' })
|
|
921
|
+
})
|
|
922
|
+
|
|
923
|
+
it('Icypeas is gated on linkedin_url and passes url', () => {
|
|
924
|
+
const ic = getProviders('enrich_company').find((p) => p.id === 'icypeas')!
|
|
925
|
+
expect(ic.isApplicable!({ linkedin_url: 'https://linkedin.com/company/stripe' })).toBe(true)
|
|
926
|
+
expect(ic.isApplicable!({ domain: 'stripe.com' })).toBe(false)
|
|
927
|
+
expect(ic.mapParams({ linkedin_url: 'https://linkedin.com/company/stripe' }).body).toEqual({ url: 'https://linkedin.com/company/stripe' })
|
|
928
|
+
})
|
|
929
|
+
|
|
930
|
+
it('Wiza hasResult checks data.domain / data.industry / data.description', () => {
|
|
931
|
+
const wiza = getProviders('enrich_company').find((p) => p.id === 'wiza')!
|
|
932
|
+
expect(wiza.hasResult({ data: { domain: 'stripe.com' } })).toBe(true)
|
|
933
|
+
expect(wiza.hasResult({ data: { industry: 'fintech' } })).toBe(true)
|
|
934
|
+
expect(wiza.hasResult({ data: {} })).toBe(false)
|
|
935
|
+
expect(wiza.hasResult({})).toBe(false)
|
|
936
|
+
})
|
|
937
|
+
|
|
938
|
+
it('BuiltWith uses POST, gated on domain, body { domain }', () => {
|
|
939
|
+
const bw = getProviders('enrich_company').find((p) => p.id === 'builtwith')!
|
|
940
|
+
expect(bw.method).toBe('POST')
|
|
941
|
+
expect(bw.endpoint).toBe('/builtwith/domain')
|
|
942
|
+
expect(bw.isApplicable!({ domain: 'coldiq.com' })).toBe(true)
|
|
943
|
+
expect(bw.isApplicable!({ linkedin_url: 'https://linkedin.com/company/coldiq' })).toBe(false)
|
|
944
|
+
expect(bw.isApplicable!({ name: 'ColdIQ' })).toBe(false)
|
|
945
|
+
expect(bw.isApplicable!({})).toBe(false)
|
|
946
|
+
expect(bw.mapParams({ domain: 'coldiq.com' }).body).toEqual({ domain: 'coldiq.com' })
|
|
947
|
+
})
|
|
948
|
+
|
|
949
|
+
it('BuiltWith hasResult requires Results[0].Result.Paths non-empty', () => {
|
|
950
|
+
const bw = getProviders('enrich_company').find((p) => p.id === 'builtwith')!
|
|
951
|
+
expect(bw.hasResult({
|
|
952
|
+
Results: [{ Result: { Lookup: 'coldiq.com', Paths: [{ Technologies: [{ Name: 'Hubspot' }] }] } }],
|
|
953
|
+
})).toBe(true)
|
|
954
|
+
expect(bw.hasResult({ Results: [{ Result: { Lookup: 'coldiq.com', Paths: [] } }] })).toBe(false)
|
|
955
|
+
expect(bw.hasResult({ Results: [{ Result: { Lookup: 'coldiq.com' } }] })).toBe(false)
|
|
956
|
+
expect(bw.hasResult({ Results: [] })).toBe(false)
|
|
957
|
+
expect(bw.hasResult({})).toBe(false)
|
|
958
|
+
})
|
|
959
|
+
|
|
960
|
+
it('Openmart uses POST, gated on domain, body { website, limit: 1 }', () => {
|
|
961
|
+
const om = getProviders('enrich_company').find((p) => p.id === 'openmart')!
|
|
962
|
+
expect(om.method).toBe('POST')
|
|
963
|
+
expect(om.endpoint).toBe('/openmart/enrich_company')
|
|
964
|
+
expect(om.isApplicable!({ domain: 'coldiq.com' })).toBe(true)
|
|
965
|
+
expect(om.isApplicable!({ linkedin_url: 'https://linkedin.com/company/coldiq' })).toBe(false)
|
|
966
|
+
expect(om.isApplicable!({ name: 'ColdIQ' })).toBe(false)
|
|
967
|
+
expect(om.isApplicable!({})).toBe(false)
|
|
968
|
+
expect(om.mapParams({ domain: 'coldiq.com' }).body).toEqual({ website: 'coldiq.com', limit: 1 })
|
|
969
|
+
})
|
|
970
|
+
|
|
971
|
+
it('Openmart hasResult accepts bare array, { data: [...] }, or { results: [...] }', () => {
|
|
972
|
+
const om = getProviders('enrich_company').find((p) => p.id === 'openmart')!
|
|
973
|
+
expect(om.hasResult([{ name: 'ColdIQ' }])).toBe(true)
|
|
974
|
+
expect(om.hasResult({ data: [{ name: 'ColdIQ' }] })).toBe(true)
|
|
975
|
+
expect(om.hasResult({ results: [{ name: 'ColdIQ' }] })).toBe(true)
|
|
976
|
+
expect(om.hasResult([])).toBe(false)
|
|
977
|
+
expect(om.hasResult({ data: [] })).toBe(false)
|
|
978
|
+
expect(om.hasResult({})).toBe(false)
|
|
979
|
+
})
|
|
980
|
+
})
|
|
981
|
+
|
|
982
|
+
describe('find_phone', () => {
|
|
983
|
+
it('providers are ordered findymail → limadata → ai-ark', () => {
|
|
984
|
+
const providers = getProviders('find_phone')
|
|
985
|
+
expect(providers.map((p) => p.id)).toEqual(['findymail', 'limadata', 'ai-ark'])
|
|
986
|
+
})
|
|
987
|
+
|
|
988
|
+
it('Findymail isApplicable requires linkedin_url', () => {
|
|
989
|
+
const fm = getProviders('find_phone').find((p) => p.id === 'findymail')!
|
|
990
|
+
expect(fm.isApplicable!({})).toBe(false)
|
|
991
|
+
expect(fm.isApplicable!({ first_name: 'Michel', last_name: 'Lieben', company_domain: 'coldiq.com' })).toBe(false)
|
|
992
|
+
expect(fm.isApplicable!({ linkedin_url: 'https://linkedin.com/in/michel-lieben' })).toBe(true)
|
|
993
|
+
})
|
|
994
|
+
|
|
995
|
+
it('Findymail mapParams passes linkedin_url', () => {
|
|
996
|
+
const fm = getProviders('find_phone').find((p) => p.id === 'findymail')!
|
|
997
|
+
expect(fm.mapParams({ linkedin_url: 'https://linkedin.com/in/michel-lieben' }).body).toEqual({
|
|
998
|
+
linkedin_url: 'https://linkedin.com/in/michel-lieben',
|
|
999
|
+
})
|
|
1000
|
+
})
|
|
1001
|
+
|
|
1002
|
+
it('Findymail hasResult accepts top-level phone or phones array', () => {
|
|
1003
|
+
const fm = getProviders('find_phone').find((p) => p.id === 'findymail')!
|
|
1004
|
+
expect(fm.hasResult({ phone: '+33612345678' })).toBe(true)
|
|
1005
|
+
expect(fm.hasResult({ phones: ['+33612345678'] })).toBe(true)
|
|
1006
|
+
expect(fm.hasResult({})).toBe(false)
|
|
1007
|
+
expect(fm.hasResult({ phone: null })).toBe(false)
|
|
1008
|
+
expect(fm.hasResult({ phones: [] })).toBe(false)
|
|
1009
|
+
})
|
|
1010
|
+
|
|
1011
|
+
it('LimaData isApplicable accepts linkedin_url or name+company', () => {
|
|
1012
|
+
const ld = getProviders('find_phone').find((p) => p.id === 'limadata')!
|
|
1013
|
+
expect(ld.isApplicable!({})).toBe(false)
|
|
1014
|
+
expect(ld.isApplicable!({ first_name: 'Michel' })).toBe(false)
|
|
1015
|
+
expect(ld.isApplicable!({ first_name: 'Michel', last_name: 'Lieben' })).toBe(false)
|
|
1016
|
+
expect(ld.isApplicable!({ linkedin_url: 'https://linkedin.com/in/michel-lieben' })).toBe(true)
|
|
1017
|
+
expect(ld.isApplicable!({ first_name: 'Michel', last_name: 'Lieben', company_name: 'ColdIQ' })).toBe(true)
|
|
1018
|
+
expect(ld.isApplicable!({ first_name: 'Michel', last_name: 'Lieben', company_domain: 'coldiq.com' })).toBe(true)
|
|
1019
|
+
})
|
|
1020
|
+
|
|
1021
|
+
it('LimaData mapParams forwards linkedin_url path', () => {
|
|
1022
|
+
const ld = getProviders('find_phone').find((p) => p.id === 'limadata')!
|
|
1023
|
+
expect(ld.mapParams({ linkedin_url: 'https://linkedin.com/in/michel-lieben' }).body).toEqual({
|
|
1024
|
+
linkedin_url: 'https://linkedin.com/in/michel-lieben',
|
|
1025
|
+
})
|
|
1026
|
+
})
|
|
1027
|
+
|
|
1028
|
+
it('LimaData mapParams forwards name+company path', () => {
|
|
1029
|
+
const ld = getProviders('find_phone').find((p) => p.id === 'limadata')!
|
|
1030
|
+
expect(ld.mapParams({ first_name: 'Michel', last_name: 'Lieben', company_domain: 'coldiq.com' }).body).toEqual({
|
|
1031
|
+
name: 'Michel Lieben',
|
|
1032
|
+
company_domain: 'coldiq.com',
|
|
1033
|
+
})
|
|
1034
|
+
})
|
|
1035
|
+
|
|
1036
|
+
it('LimaData mapParams forwards company_name when no domain', () => {
|
|
1037
|
+
const ld = getProviders('find_phone').find((p) => p.id === 'limadata')!
|
|
1038
|
+
expect(ld.mapParams({ first_name: 'Michel', last_name: 'Lieben', company_name: 'ColdIQ' }).body).toEqual({
|
|
1039
|
+
name: 'Michel Lieben',
|
|
1040
|
+
company_name: 'ColdIQ',
|
|
1041
|
+
})
|
|
1042
|
+
})
|
|
1043
|
+
|
|
1044
|
+
it('LimaData hasResult checks multiple phone shapes including phone_numbers', () => {
|
|
1045
|
+
const ld = getProviders('find_phone').find((p) => p.id === 'limadata')!
|
|
1046
|
+
expect(ld.hasResult({ phone: '+33612345678' })).toBe(true)
|
|
1047
|
+
expect(ld.hasResult({ phones: ['+33612345678'] })).toBe(true)
|
|
1048
|
+
expect(ld.hasResult({ phone_numbers: [{ number: '+33612345678' }] })).toBe(true)
|
|
1049
|
+
expect(ld.hasResult({ data: { phone: '+33612345678' } })).toBe(true)
|
|
1050
|
+
expect(ld.hasResult({ data: { phones: ['+33612345678'] } })).toBe(true)
|
|
1051
|
+
expect(ld.hasResult({ data: { phone_numbers: [{ number: '+33612345678' }] } })).toBe(true)
|
|
1052
|
+
expect(ld.hasResult({})).toBe(false)
|
|
1053
|
+
expect(ld.hasResult({ phone: '' })).toBe(false)
|
|
1054
|
+
expect(ld.hasResult({ phones: [] })).toBe(false)
|
|
1055
|
+
expect(ld.hasResult({ phone_numbers: [] })).toBe(false)
|
|
1056
|
+
expect(ld.hasResult({ data: {} })).toBe(false)
|
|
1057
|
+
})
|
|
1058
|
+
|
|
1059
|
+
it('AI Ark isApplicable accepts linkedin_url or name+company_domain (not company_name)', () => {
|
|
1060
|
+
const ark = getProviders('find_phone').find((p) => p.id === 'ai-ark')!
|
|
1061
|
+
expect(ark.isApplicable!({})).toBe(false)
|
|
1062
|
+
expect(ark.isApplicable!({ first_name: 'Michel', last_name: 'Lieben', company_name: 'ColdIQ' })).toBe(false)
|
|
1063
|
+
expect(ark.isApplicable!({ linkedin_url: 'https://linkedin.com/in/michel-lieben' })).toBe(true)
|
|
1064
|
+
expect(ark.isApplicable!({ first_name: 'Michel', last_name: 'Lieben', company_domain: 'coldiq.com' })).toBe(true)
|
|
1065
|
+
})
|
|
1066
|
+
|
|
1067
|
+
it('AI Ark mapParams uses linkedin key (not linkedin_url)', () => {
|
|
1068
|
+
const ark = getProviders('find_phone').find((p) => p.id === 'ai-ark')!
|
|
1069
|
+
expect(ark.mapParams({ linkedin_url: 'https://linkedin.com/in/michel-lieben' }).body).toEqual({
|
|
1070
|
+
linkedin: 'https://linkedin.com/in/michel-lieben',
|
|
1071
|
+
})
|
|
1072
|
+
})
|
|
1073
|
+
|
|
1074
|
+
it('AI Ark mapParams maps name+domain path', () => {
|
|
1075
|
+
const ark = getProviders('find_phone').find((p) => p.id === 'ai-ark')!
|
|
1076
|
+
expect(ark.mapParams({ first_name: 'Michel', last_name: 'Lieben', company_domain: 'coldiq.com' }).body).toEqual({
|
|
1077
|
+
name: 'Michel Lieben',
|
|
1078
|
+
domain: 'coldiq.com',
|
|
1079
|
+
})
|
|
1080
|
+
})
|
|
1081
|
+
|
|
1082
|
+
it('AI Ark hasResult requires non-empty inner string arrays in data', () => {
|
|
1083
|
+
const ark = getProviders('find_phone').find((p) => p.id === 'ai-ark')!
|
|
1084
|
+
expect(ark.hasResult({ data: [['+33612345678']] })).toBe(true)
|
|
1085
|
+
expect(ark.hasResult({ data: [['mobile', '+33612345678']] })).toBe(true)
|
|
1086
|
+
expect(ark.hasResult({ data: [] })).toBe(false)
|
|
1087
|
+
expect(ark.hasResult({ data: [[]] })).toBe(false)
|
|
1088
|
+
expect(ark.hasResult({})).toBe(false)
|
|
1089
|
+
expect(ark.hasResult({ data: [[123]] })).toBe(false)
|
|
1090
|
+
})
|
|
1091
|
+
})
|
|
1092
|
+
|
|
1093
|
+
describe('search_web provider ordering', () => {
|
|
1094
|
+
it('default order is serper → limadata → exa → jina', () => {
|
|
1095
|
+
const providers = getSearchWebProviders(false)
|
|
1096
|
+
expect(providers.map((p) => p.id)).toEqual(['serper', 'limadata', 'exa', 'jina'])
|
|
1097
|
+
})
|
|
1098
|
+
|
|
1099
|
+
it('neural mode puts exa first, jina stays at the tail', () => {
|
|
1100
|
+
const providers = getSearchWebProviders(true)
|
|
1101
|
+
expect(providers[0].id).toBe('exa')
|
|
1102
|
+
expect(providers[providers.length - 1].id).toBe('jina')
|
|
1103
|
+
})
|
|
1104
|
+
|
|
1105
|
+
it('Jina uses POST /jina/search, maps query/num_results/country, clamps count to 20', () => {
|
|
1106
|
+
const jina = getProviders('search_web').find((p) => p.id === 'jina')!
|
|
1107
|
+
expect(jina.method).toBe('POST')
|
|
1108
|
+
expect(jina.endpoint).toBe('/jina/search')
|
|
1109
|
+
expect(jina.priority).toBe(4)
|
|
1110
|
+
expect(jina.mapParams({ query: 'B2B SaaS news', num_results: 5, country: 'us' }).body).toEqual({
|
|
1111
|
+
q: 'B2B SaaS news',
|
|
1112
|
+
count: 5,
|
|
1113
|
+
gl: 'us',
|
|
1114
|
+
})
|
|
1115
|
+
// Jina's upstream caps at 20; tool allows up to 100
|
|
1116
|
+
expect((jina.mapParams({ query: 'x', num_results: 100 }).body as { count: number }).count).toBe(20)
|
|
1117
|
+
// Default count when num_results omitted
|
|
1118
|
+
expect((jina.mapParams({ query: 'x' }).body as { count: number }).count).toBe(10)
|
|
1119
|
+
})
|
|
1120
|
+
|
|
1121
|
+
it('Jina hasResult checks data/results array non-empty', () => {
|
|
1122
|
+
const jina = getProviders('search_web').find((p) => p.id === 'jina')!
|
|
1123
|
+
expect(jina.hasResult({ data: [{ title: 'x', url: 'https://x' }] })).toBe(true)
|
|
1124
|
+
expect(jina.hasResult({ results: [{ title: 'x' }] })).toBe(true)
|
|
1125
|
+
expect(jina.hasResult({ data: [] })).toBe(false)
|
|
1126
|
+
expect(jina.hasResult({})).toBe(false)
|
|
1127
|
+
})
|
|
1128
|
+
})
|
|
1129
|
+
|
|
1130
|
+
describe('isoCountryToName', () => {
|
|
1131
|
+
it('converts known alpha-2 codes to English names', () => {
|
|
1132
|
+
expect(isoCountryToName('FR')).toBe('France')
|
|
1133
|
+
expect(isoCountryToName('US')).toBe('United States')
|
|
1134
|
+
expect(isoCountryToName('GB')).toBe('United Kingdom')
|
|
1135
|
+
expect(isoCountryToName('DE')).toBe('Germany')
|
|
1136
|
+
})
|
|
1137
|
+
|
|
1138
|
+
it('passes through unknown alpha-2 codes unchanged', () => {
|
|
1139
|
+
expect(isoCountryToName('XX')).toBe('XX')
|
|
1140
|
+
})
|
|
1141
|
+
|
|
1142
|
+
it('passes through free-text strings unchanged', () => {
|
|
1143
|
+
expect(isoCountryToName('Paris, France')).toBe('Paris, France')
|
|
1144
|
+
expect(isoCountryToName('France')).toBe('France')
|
|
1145
|
+
})
|
|
1146
|
+
|
|
1147
|
+
it('passes through empty string unchanged', () => {
|
|
1148
|
+
expect(isoCountryToName('')).toBe('')
|
|
1149
|
+
})
|
|
1150
|
+
|
|
1151
|
+
it('is case-insensitive for alpha-2 input', () => {
|
|
1152
|
+
expect(isoCountryToName('fr')).toBe('France')
|
|
1153
|
+
expect(isoCountryToName('us')).toBe('United States')
|
|
1154
|
+
})
|
|
1155
|
+
})
|
|
1156
|
+
|
|
1157
|
+
describe('search_companies postFilter and gates', () => {
|
|
1158
|
+
it('Apollo isApplicable: rejects input with min_founded_year', () => {
|
|
1159
|
+
const apollo = getProviders('search_companies').find((p) => p.id === 'apollo')!
|
|
1160
|
+
expect(apollo.isApplicable!({})).toBe(true)
|
|
1161
|
+
expect(apollo.isApplicable!({ keywords: ['SaaS'] })).toBe(true)
|
|
1162
|
+
expect(apollo.isApplicable!({ min_founded_year: 2015 })).toBe(false)
|
|
1163
|
+
expect(apollo.isApplicable!({ max_founded_year: 2020 })).toBe(false)
|
|
1164
|
+
expect(apollo.isApplicable!({ min_founded_year: 2015, max_founded_year: 2020 })).toBe(false)
|
|
1165
|
+
})
|
|
1166
|
+
|
|
1167
|
+
it('TheirStack isApplicable: requires technologies, industries, or funding filters (not keywords alone)', () => {
|
|
1168
|
+
const ts = getProviders('search_companies').find((p) => p.id === 'theirstack')!
|
|
1169
|
+
expect(ts.isApplicable!({})).toBe(false)
|
|
1170
|
+
expect(ts.isApplicable!({ keywords: ['x'] })).toBe(false)
|
|
1171
|
+
expect(ts.isApplicable!({ technologies: ['Salesforce'] })).toBe(true)
|
|
1172
|
+
expect(ts.isApplicable!({ industries: ['y'] })).toBe(true)
|
|
1173
|
+
expect(ts.isApplicable!({ funding_stages: ['seed'] })).toBe(true)
|
|
1174
|
+
expect(ts.isApplicable!({ min_funding_amount: 1_000_000 })).toBe(true)
|
|
1175
|
+
expect(ts.isApplicable!({ max_funding_amount: 50_000_000 })).toBe(true)
|
|
1176
|
+
expect(ts.isApplicable!({ countries: ['FR'] })).toBe(false)
|
|
1177
|
+
})
|
|
1178
|
+
|
|
1179
|
+
it('Wiza isApplicable: fires for min/max_founded_year and funding filters', () => {
|
|
1180
|
+
const wiza = getProviders('search_companies').find((p) => p.id === 'wiza')!
|
|
1181
|
+
expect(wiza.isApplicable!({})).toBe(false)
|
|
1182
|
+
expect(wiza.isApplicable!({ keywords: ['x'] })).toBe(false)
|
|
1183
|
+
expect(wiza.isApplicable!({ min_founded_year: 2015 })).toBe(true)
|
|
1184
|
+
expect(wiza.isApplicable!({ max_founded_year: 2020 })).toBe(true)
|
|
1185
|
+
expect(wiza.isApplicable!({ funding_stages: ['seed'] })).toBe(true)
|
|
1186
|
+
expect(wiza.isApplicable!({ min_funding_amount: 500_000 })).toBe(true)
|
|
1187
|
+
expect(wiza.isApplicable!({ max_funding_amount: 10_000_000 })).toBe(true)
|
|
1188
|
+
})
|
|
1189
|
+
|
|
1190
|
+
it('Apollo postFilter strips accounts key entirely', () => {
|
|
1191
|
+
const apollo = getProviders('search_companies').find((p) => p.id === 'apollo')!
|
|
1192
|
+
const data = { organizations: [{ name: 'ColdIQ' }], accounts: [{ id: 1 }] }
|
|
1193
|
+
const result = apollo.postFilter!(data, {}) as Record<string, unknown>
|
|
1194
|
+
expect(result.accounts).toBeUndefined()
|
|
1195
|
+
expect(Array.isArray(result.organizations)).toBe(true)
|
|
1196
|
+
})
|
|
1197
|
+
|
|
1198
|
+
it('Apollo postFilter drops orgs with estimated_num_employees out of range (present-and-wrong)', () => {
|
|
1199
|
+
const apollo = getProviders('search_companies').find((p) => p.id === 'apollo')!
|
|
1200
|
+
const orgs = [
|
|
1201
|
+
{ name: 'A', estimated_num_employees: 100 },
|
|
1202
|
+
{ name: 'B', estimated_num_employees: 10 },
|
|
1203
|
+
{ name: 'C', estimated_num_employees: 500 },
|
|
1204
|
+
{ name: 'D' }, // no field — passes through (lenient)
|
|
1205
|
+
]
|
|
1206
|
+
const result = apollo.postFilter!({ organizations: orgs }, { min_employees: 50, max_employees: 200 }) as Record<string, unknown>
|
|
1207
|
+
expect(((result.organizations as Array<Record<string, unknown>>).map((o) => o.name))).toEqual(['A', 'D'])
|
|
1208
|
+
})
|
|
1209
|
+
|
|
1210
|
+
it('Apollo postFilter drops orgs whose country is present and wrong (ISO-aware), passes orgs with no country', () => {
|
|
1211
|
+
const apollo = getProviders('search_companies').find((p) => p.id === 'apollo')!
|
|
1212
|
+
const orgs = [
|
|
1213
|
+
{ name: 'A', country: 'France' },
|
|
1214
|
+
{ name: 'B', country: 'france' },
|
|
1215
|
+
{ name: 'C', country: 'Germany' },
|
|
1216
|
+
{ name: 'D' }, // no country — passes through (lenient)
|
|
1217
|
+
]
|
|
1218
|
+
const result = apollo.postFilter!({ organizations: orgs }, { countries: ['FR'] }) as Record<string, unknown>
|
|
1219
|
+
expect(((result.organizations as Array<Record<string, unknown>>).map((o) => o.name))).toEqual(['A', 'B', 'D'])
|
|
1220
|
+
})
|
|
1221
|
+
|
|
1222
|
+
it('Apollo postFilter is a no-op for orgs when no filters are set (only strips accounts)', () => {
|
|
1223
|
+
const apollo = getProviders('search_companies').find((p) => p.id === 'apollo')!
|
|
1224
|
+
const orgs = [{ name: 'A' }, { name: 'B' }]
|
|
1225
|
+
const result = apollo.postFilter!({ organizations: orgs, accounts: [] }, {}) as Record<string, unknown>
|
|
1226
|
+
expect((result.organizations as unknown[]).length).toBe(2)
|
|
1227
|
+
expect(result.accounts).toBeUndefined()
|
|
1228
|
+
})
|
|
1229
|
+
|
|
1230
|
+
it('Apollo hasResult returns false when postFilter empties organizations', () => {
|
|
1231
|
+
const apollo = getProviders('search_companies').find((p) => p.id === 'apollo')!
|
|
1232
|
+
const filtered = apollo.postFilter!({ organizations: [] }, {})
|
|
1233
|
+
expect(apollo.hasResult(filtered)).toBe(false)
|
|
1234
|
+
})
|
|
1235
|
+
|
|
1236
|
+
it('Top-tier providers (companyenrich, fullenrich, pdl, signalbase) have no postFilter', () => {
|
|
1237
|
+
const providers = getProviders('search_companies')
|
|
1238
|
+
for (const id of ['companyenrich', 'fullenrich', 'pdl', 'signalbase']) {
|
|
1239
|
+
const p = providers.find((x) => x.id === id)!
|
|
1240
|
+
expect(p.postFilter, `${id} should not have postFilter`).toBeUndefined()
|
|
1241
|
+
}
|
|
1242
|
+
})
|
|
1243
|
+
|
|
1244
|
+
it('Apollo isApplicable: rejects Phase B filters (technologies, funding, revenue, exclusions, workforce growth, is_hiring)', () => {
|
|
1245
|
+
const apollo = getProviders('search_companies').find((p) => p.id === 'apollo')!
|
|
1246
|
+
expect(apollo.isApplicable!({ keywords: ['SaaS'] })).toBe(true)
|
|
1247
|
+
expect(apollo.isApplicable!({ technologies: ['Salesforce'] })).toBe(false)
|
|
1248
|
+
expect(apollo.isApplicable!({ funding_stages: ['seed'] })).toBe(false)
|
|
1249
|
+
expect(apollo.isApplicable!({ min_funding_amount: 1_000_000 })).toBe(false)
|
|
1250
|
+
expect(apollo.isApplicable!({ max_funding_amount: 50_000_000 })).toBe(false)
|
|
1251
|
+
expect(apollo.isApplicable!({ min_revenue: 500_000 })).toBe(false)
|
|
1252
|
+
expect(apollo.isApplicable!({ max_revenue: 10_000_000 })).toBe(false)
|
|
1253
|
+
expect(apollo.isApplicable!({ exclude_domains: ['competitor.com'] })).toBe(false)
|
|
1254
|
+
expect(apollo.isApplicable!({ exclude_industries: ['Gaming'] })).toBe(false)
|
|
1255
|
+
expect(apollo.isApplicable!({ exclude_countries: ['CN'] })).toBe(false)
|
|
1256
|
+
expect(apollo.isApplicable!({ min_workforce_growth_pct: 10 })).toBe(false)
|
|
1257
|
+
expect(apollo.isApplicable!({ is_hiring: true })).toBe(false)
|
|
1258
|
+
})
|
|
1259
|
+
|
|
1260
|
+
it('SignalBase isApplicable: skips for advanced filters it cannot apply', () => {
|
|
1261
|
+
const signalbase = getProviders('search_companies').find((p) => p.id === 'signalbase')!
|
|
1262
|
+
// Passes for basic filters SignalBase supports
|
|
1263
|
+
expect(signalbase.isApplicable!({})).toBe(true)
|
|
1264
|
+
expect(signalbase.isApplicable!({ keywords: ['SaaS'] })).toBe(true)
|
|
1265
|
+
expect(signalbase.isApplicable!({ countries: ['FR'], min_employees: 50 })).toBe(true)
|
|
1266
|
+
expect(signalbase.isApplicable!({ is_hiring: false })).toBe(true)
|
|
1267
|
+
// Skips for filters SignalBase cannot enforce
|
|
1268
|
+
expect(signalbase.isApplicable!({ technologies: ['Salesforce'] })).toBe(false)
|
|
1269
|
+
expect(signalbase.isApplicable!({ funding_stages: ['series_a'] })).toBe(false)
|
|
1270
|
+
expect(signalbase.isApplicable!({ min_funding_amount: 1_000_000 })).toBe(false)
|
|
1271
|
+
expect(signalbase.isApplicable!({ max_funding_amount: 50_000_000 })).toBe(false)
|
|
1272
|
+
expect(signalbase.isApplicable!({ min_revenue: 500_000 })).toBe(false)
|
|
1273
|
+
expect(signalbase.isApplicable!({ max_revenue: 10_000_000 })).toBe(false)
|
|
1274
|
+
expect(signalbase.isApplicable!({ is_hiring: true })).toBe(false)
|
|
1275
|
+
})
|
|
1276
|
+
})
|
|
1277
|
+
|
|
1278
|
+
describe('search_companies Phase B GTM filters', () => {
|
|
1279
|
+
it('CompanyEnrich maps technologies, fundingAmount, fundingYear, revenue, workforceGrowth, and exclude', () => {
|
|
1280
|
+
const ce = getProviders('search_companies').find((p) => p.id === 'companyenrich')!
|
|
1281
|
+
const result = ce.mapParams({
|
|
1282
|
+
keywords: ['SaaS'],
|
|
1283
|
+
technologies: ['Salesforce', 'HubSpot'],
|
|
1284
|
+
countries: ['US'],
|
|
1285
|
+
min_funding_amount: 1_000_000,
|
|
1286
|
+
max_funding_amount: 50_000_000,
|
|
1287
|
+
min_funding_year: 2020,
|
|
1288
|
+
max_funding_year: 2024,
|
|
1289
|
+
min_revenue: 500_000,
|
|
1290
|
+
max_revenue: 10_000_000,
|
|
1291
|
+
min_workforce_growth_pct: 15,
|
|
1292
|
+
exclude_domains: ['competitor.com'],
|
|
1293
|
+
exclude_industries: ['Gaming'],
|
|
1294
|
+
exclude_countries: ['CN'],
|
|
1295
|
+
limit: 10,
|
|
1296
|
+
})
|
|
1297
|
+
expect(result.body).toEqual({
|
|
1298
|
+
countries: ['US'],
|
|
1299
|
+
keywords: ['SaaS'],
|
|
1300
|
+
technologies: ['Salesforce', 'HubSpot'],
|
|
1301
|
+
employees: undefined,
|
|
1302
|
+
foundedYear: undefined,
|
|
1303
|
+
fundingAmount: { from: 1_000_000, to: 50_000_000 },
|
|
1304
|
+
fundingYear: { from: 2020, to: 2024 },
|
|
1305
|
+
revenue: [{ from: 500_000, to: 10_000_000 }],
|
|
1306
|
+
workforceGrowth: { from: 15 },
|
|
1307
|
+
exclude: { domains: ['competitor.com'], industries: ['Gaming'], countries: ['CN'] },
|
|
1308
|
+
pageSize: 10,
|
|
1309
|
+
})
|
|
1310
|
+
})
|
|
1311
|
+
|
|
1312
|
+
it('CompanyEnrich omits optional Phase B fields when not provided', () => {
|
|
1313
|
+
const ce = getProviders('search_companies').find((p) => p.id === 'companyenrich')!
|
|
1314
|
+
const result = ce.mapParams({ countries: ['US'], limit: 5 })
|
|
1315
|
+
const body = result.body as Record<string, unknown>
|
|
1316
|
+
expect(body.technologies).toBeUndefined()
|
|
1317
|
+
expect(body.fundingAmount).toBeUndefined()
|
|
1318
|
+
expect(body.fundingYear).toBeUndefined()
|
|
1319
|
+
expect(body.revenue).toBeUndefined()
|
|
1320
|
+
expect(body.workforceGrowth).toBeUndefined()
|
|
1321
|
+
expect(body.exclude).toBeUndefined()
|
|
1322
|
+
})
|
|
1323
|
+
|
|
1324
|
+
it('CompanyEnrich exclude object only includes keys that were provided', () => {
|
|
1325
|
+
const ce = getProviders('search_companies').find((p) => p.id === 'companyenrich')!
|
|
1326
|
+
const result = ce.mapParams({ exclude_domains: ['a.com'], limit: 5 })
|
|
1327
|
+
expect((result.body as Record<string, unknown>).exclude).toEqual({ domains: ['a.com'] })
|
|
1328
|
+
})
|
|
1329
|
+
|
|
1330
|
+
it('Wiza mapParams adds funding_stage, funding_min, funding_max in [{v}] shape', () => {
|
|
1331
|
+
const wiza = getProviders('search_companies').find((p) => p.id === 'wiza')!
|
|
1332
|
+
const result = wiza.mapParams({
|
|
1333
|
+
funding_stages: ['seed', 'series_a'],
|
|
1334
|
+
min_funding_amount: 500_000,
|
|
1335
|
+
max_funding_amount: 20_000_000,
|
|
1336
|
+
limit: 25,
|
|
1337
|
+
})
|
|
1338
|
+
expect(result.body).toEqual({
|
|
1339
|
+
list: { name: 'mcp-search-companies', max_profiles: 25 },
|
|
1340
|
+
filters: {
|
|
1341
|
+
funding_stage: [{ v: 'seed' }, { v: 'series_a' }],
|
|
1342
|
+
funding_min: [{ v: '500000' }],
|
|
1343
|
+
funding_max: [{ v: '20000000' }],
|
|
1344
|
+
},
|
|
1345
|
+
enrichment_level: 'none',
|
|
1346
|
+
})
|
|
1347
|
+
})
|
|
1348
|
+
|
|
1349
|
+
it('LimaData passes has_jobs: true when is_hiring is true', () => {
|
|
1350
|
+
const ld = getProviders('search_companies').find((p) => p.id === 'limadata')!
|
|
1351
|
+
const result = ld.mapParams({ keywords: ['SaaS'], is_hiring: true })
|
|
1352
|
+
expect((result.body as Record<string, unknown>).has_jobs).toBe(true)
|
|
1353
|
+
})
|
|
1354
|
+
|
|
1355
|
+
it('LimaData omits has_jobs when is_hiring is false or unset', () => {
|
|
1356
|
+
const ld = getProviders('search_companies').find((p) => p.id === 'limadata')!
|
|
1357
|
+
const falseResult = ld.mapParams({ keywords: ['SaaS'], is_hiring: false })
|
|
1358
|
+
const unsetResult = ld.mapParams({ keywords: ['SaaS'] })
|
|
1359
|
+
expect((falseResult.body as Record<string, unknown>).has_jobs).toBeUndefined()
|
|
1360
|
+
expect((unsetResult.body as Record<string, unknown>).has_jobs).toBeUndefined()
|
|
1361
|
+
})
|
|
1362
|
+
})
|
|
1363
|
+
|
|
1364
|
+
describe('search_jobs', () => {
|
|
1365
|
+
it('providers are ordered career_site_jobs → linkedin_jobs_api → theirstack-jobs', () => {
|
|
1366
|
+
const ids = getProviders('search_jobs').map((p) => p.id)
|
|
1367
|
+
expect(ids).toEqual(['career_site_jobs', 'linkedin_jobs_api', 'theirstack-jobs'])
|
|
1368
|
+
})
|
|
1369
|
+
|
|
1370
|
+
it('Career Site is async POST to /career-site-jobs/search', () => {
|
|
1371
|
+
const cs = getProviders('search_jobs').find((p) => p.id === 'career_site_jobs')!
|
|
1372
|
+
expect(cs.method).toBe('POST')
|
|
1373
|
+
expect(cs.endpoint).toBe('/career-site-jobs/search')
|
|
1374
|
+
expect(cs.async).toBeDefined()
|
|
1375
|
+
expect(cs.async!.pollEndpoint('42')).toBe('/career-site-jobs/search/42')
|
|
1376
|
+
expect(cs.async!.isComplete({ status: 'done' })).toBe(true)
|
|
1377
|
+
expect(cs.async!.isComplete({ status: 'failed' })).toBe(true)
|
|
1378
|
+
expect(cs.async!.isComplete({ status: 'timed_out' })).toBe(true)
|
|
1379
|
+
expect(cs.async!.isComplete({ status: 'processing' })).toBe(false)
|
|
1380
|
+
expect(cs.async!.extractId({ jobId: 99 })).toBe('99')
|
|
1381
|
+
expect(cs.async!.extractId({ jobId: '55' })).toBe('55')
|
|
1382
|
+
expect(() => cs.async!.extractId({ jobId: undefined })).toThrow('no jobId')
|
|
1383
|
+
expect(() => cs.async!.extractId({})).toThrow('no jobId')
|
|
1384
|
+
})
|
|
1385
|
+
|
|
1386
|
+
it('Career Site is skipped when LinkedIn-only filters are set', () => {
|
|
1387
|
+
const cs = getProviders('search_jobs').find((p) => p.id === 'career_site_jobs')!
|
|
1388
|
+
expect(cs.isApplicable!({ title_keywords: ['engineer'] })).toBe(true)
|
|
1389
|
+
expect(cs.isApplicable!({ seniority_levels: ['Director'] })).toBe(false)
|
|
1390
|
+
expect(cs.isApplicable!({ min_employees: 100 })).toBe(false)
|
|
1391
|
+
expect(cs.isApplicable!({ industries: ['Software'] })).toBe(false)
|
|
1392
|
+
expect(cs.isApplicable!({ easy_apply_only: true })).toBe(false)
|
|
1393
|
+
expect(cs.isApplicable!({ easy_apply_only: false })).toBe(true)
|
|
1394
|
+
expect(cs.isApplicable!({ seniority_levels: [] })).toBe(true)
|
|
1395
|
+
})
|
|
1396
|
+
|
|
1397
|
+
it('Career Site mapParams clamps limit to 10–500 and translates field names', () => {
|
|
1398
|
+
const cs = getProviders('search_jobs').find((p) => p.id === 'career_site_jobs')!
|
|
1399
|
+
const out = cs.mapParams({
|
|
1400
|
+
title_keywords: ['sales engineer'],
|
|
1401
|
+
ats_slugs: ['greenhouse'],
|
|
1402
|
+
limit: 9999,
|
|
1403
|
+
time_range: '24h',
|
|
1404
|
+
include_description: true,
|
|
1405
|
+
}).body as Record<string, unknown>
|
|
1406
|
+
expect(out.titleSearch).toEqual(['sales engineer'])
|
|
1407
|
+
expect(out.ats).toEqual(['greenhouse'])
|
|
1408
|
+
expect(out.limit).toBe(500)
|
|
1409
|
+
expect(out.timeRange).toBe('24h')
|
|
1410
|
+
expect(out.descriptionType).toBe('text')
|
|
1411
|
+
expect(out.includeAi).toBe(true)
|
|
1412
|
+
})
|
|
1413
|
+
|
|
1414
|
+
it('Career Site mapParams sets descriptionType undefined when include_description is false', () => {
|
|
1415
|
+
const cs = getProviders('search_jobs').find((p) => p.id === 'career_site_jobs')!
|
|
1416
|
+
const out = cs.mapParams({ include_description: false }).body as Record<string, unknown>
|
|
1417
|
+
expect(out.descriptionType).toBeUndefined()
|
|
1418
|
+
})
|
|
1419
|
+
|
|
1420
|
+
it('Career Site hasResult requires jobs[] non-empty', () => {
|
|
1421
|
+
const cs = getProviders('search_jobs').find((p) => p.id === 'career_site_jobs')!
|
|
1422
|
+
expect(cs.hasResult({ jobs: [{ title: 'SE' }] })).toBe(true)
|
|
1423
|
+
expect(cs.hasResult({ jobs: [] })).toBe(false)
|
|
1424
|
+
expect(cs.hasResult({ status: 'done' })).toBe(false)
|
|
1425
|
+
expect(cs.hasResult({})).toBe(false)
|
|
1426
|
+
})
|
|
1427
|
+
|
|
1428
|
+
it('LinkedIn is async POST to /linkedin-jobs-api/search', () => {
|
|
1429
|
+
const li = getProviders('search_jobs').find((p) => p.id === 'linkedin_jobs_api')!
|
|
1430
|
+
expect(li.method).toBe('POST')
|
|
1431
|
+
expect(li.endpoint).toBe('/linkedin-jobs-api/search')
|
|
1432
|
+
expect(li.async).toBeDefined()
|
|
1433
|
+
expect(li.async!.pollEndpoint('77')).toBe('/linkedin-jobs-api/search/77')
|
|
1434
|
+
expect(li.async!.extractId({ jobId: '55' })).toBe('55')
|
|
1435
|
+
})
|
|
1436
|
+
|
|
1437
|
+
it('LinkedIn is skipped when Career-Site-only filters are set', () => {
|
|
1438
|
+
const li = getProviders('search_jobs').find((p) => p.id === 'linkedin_jobs_api')!
|
|
1439
|
+
expect(li.isApplicable!({ title_keywords: ['engineer'] })).toBe(true)
|
|
1440
|
+
expect(li.isApplicable!({ ats_slugs: ['greenhouse'] })).toBe(false)
|
|
1441
|
+
expect(li.isApplicable!({ company_domains: ['stripe.com'] })).toBe(false)
|
|
1442
|
+
expect(li.isApplicable!({ exclude_ats_slugs: ['lever'] })).toBe(false)
|
|
1443
|
+
expect(li.isApplicable!({ ats_slugs: [] })).toBe(true)
|
|
1444
|
+
})
|
|
1445
|
+
|
|
1446
|
+
it('LinkedIn mapParams uses misspelled locationExclusionSeach and PascalCase EmploymentTypeFilter', () => {
|
|
1447
|
+
const li = getProviders('search_jobs').find((p) => p.id === 'linkedin_jobs_api')!
|
|
1448
|
+
const out = li.mapParams({
|
|
1449
|
+
exclude_locations: ['New York, NY, United States'],
|
|
1450
|
+
employment_types: ['FULL_TIME'],
|
|
1451
|
+
seniority_levels: ['Director'],
|
|
1452
|
+
industries: ['Software Development'],
|
|
1453
|
+
min_employees: 100,
|
|
1454
|
+
max_employees: 5000,
|
|
1455
|
+
limit: 50,
|
|
1456
|
+
}).body as Record<string, unknown>
|
|
1457
|
+
expect(out.locationExclusionSeach).toEqual(['New York, NY, United States'])
|
|
1458
|
+
expect(out.EmploymentTypeFilter).toEqual(['FULL_TIME'])
|
|
1459
|
+
expect(out.seniorityFilter).toEqual(['Director'])
|
|
1460
|
+
expect(out.industryFilter).toEqual(['Software Development'])
|
|
1461
|
+
expect(out.organizationEmployeesGte).toBe(100)
|
|
1462
|
+
expect(out.organizationEmployeesLte).toBe(5000)
|
|
1463
|
+
expect(out.limit).toBe(50)
|
|
1464
|
+
expect(out.excludeATSDuplicate).toBe(true)
|
|
1465
|
+
})
|
|
1466
|
+
|
|
1467
|
+
it('LinkedIn mapParams clamps limit to 10–500 (schema cap, not upstream cap)', () => {
|
|
1468
|
+
const li = getProviders('search_jobs').find((p) => p.id === 'linkedin_jobs_api')!
|
|
1469
|
+
const hi = li.mapParams({ limit: 99999 }).body as Record<string, unknown>
|
|
1470
|
+
const lo = li.mapParams({ limit: 1 }).body as Record<string, unknown>
|
|
1471
|
+
expect(hi.limit).toBe(500)
|
|
1472
|
+
expect(lo.limit).toBe(10)
|
|
1473
|
+
})
|
|
1474
|
+
|
|
1475
|
+
it('LinkedIn hasResult requires jobs[] non-empty', () => {
|
|
1476
|
+
const li = getProviders('search_jobs').find((p) => p.id === 'linkedin_jobs_api')!
|
|
1477
|
+
expect(li.hasResult({ jobs: [{ id: 1 }] })).toBe(true)
|
|
1478
|
+
expect(li.hasResult({ jobs: [] })).toBe(false)
|
|
1479
|
+
expect(li.hasResult({})).toBe(false)
|
|
1480
|
+
})
|
|
1481
|
+
|
|
1482
|
+
it('LinkedIn mapParams only forwards easy_apply_only/exclude_easy_apply when truthy', () => {
|
|
1483
|
+
const li = getProviders('search_jobs').find((p) => p.id === 'linkedin_jobs_api')!
|
|
1484
|
+
const trueOut = li.mapParams({ easy_apply_only: true, exclude_easy_apply: true }).body as Record<string, unknown>
|
|
1485
|
+
expect(trueOut.directApply).toBe(true)
|
|
1486
|
+
expect(trueOut.noDirectApply).toBe(true)
|
|
1487
|
+
const falseOut = li.mapParams({ easy_apply_only: false, exclude_easy_apply: false }).body as Record<string, unknown>
|
|
1488
|
+
expect(falseOut.directApply).toBeUndefined()
|
|
1489
|
+
expect(falseOut.noDirectApply).toBeUndefined()
|
|
1490
|
+
})
|
|
1491
|
+
|
|
1492
|
+
it('contradictory filter set: both isApplicable return false', () => {
|
|
1493
|
+
const cs = getProviders('search_jobs').find((p) => p.id === 'career_site_jobs')!
|
|
1494
|
+
const li = getProviders('search_jobs').find((p) => p.id === 'linkedin_jobs_api')!
|
|
1495
|
+
const contradictory = { ats_slugs: ['greenhouse'], seniority_levels: ['Director'] }
|
|
1496
|
+
expect(cs.isApplicable!(contradictory)).toBe(false)
|
|
1497
|
+
expect(li.isApplicable!(contradictory)).toBe(false)
|
|
1498
|
+
})
|
|
1499
|
+
})
|
|
1500
|
+
|
|
1501
|
+
describe('search_ads', () => {
|
|
1502
|
+
it('providers are ordered google_ads → linkedin_ad_library → meta_ads → twitter_ads → reddit_ads', () => {
|
|
1503
|
+
const ids = getProviders('search_ads').map((p) => p.id)
|
|
1504
|
+
expect(ids).toEqual(['google_ads', 'linkedin_ad_library', 'meta_ads', 'twitter_ads', 'reddit_ads'])
|
|
1505
|
+
})
|
|
1506
|
+
|
|
1507
|
+
it('google_ads is async POST to /google-ads/search', () => {
|
|
1508
|
+
const g = getProviders('search_ads').find((p) => p.id === 'google_ads')!
|
|
1509
|
+
expect(g.method).toBe('POST')
|
|
1510
|
+
expect(g.endpoint).toBe('/google-ads/search')
|
|
1511
|
+
expect(g.async).toBeDefined()
|
|
1512
|
+
expect(g.async!.pollEndpoint('42')).toBe('/google-ads/search/42')
|
|
1513
|
+
expect(g.async!.isComplete({ status: 'done' })).toBe(true)
|
|
1514
|
+
expect(g.async!.isComplete({ status: 'failed' })).toBe(true)
|
|
1515
|
+
expect(g.async!.isComplete({ status: 'timed_out' })).toBe(true)
|
|
1516
|
+
expect(g.async!.isComplete({ status: 'processing' })).toBe(false)
|
|
1517
|
+
expect(g.async!.extractId({ jobId: 99 })).toBe('99')
|
|
1518
|
+
expect(g.async!.extractId({ jobId: '55' })).toBe('55')
|
|
1519
|
+
expect(() => g.async!.extractId({ jobId: undefined })).toThrow('no jobId')
|
|
1520
|
+
expect(() => g.async!.extractId({})).toThrow('no jobId')
|
|
1521
|
+
})
|
|
1522
|
+
|
|
1523
|
+
it('linkedin_ad_library is async POST to /linkedin-ad-library/search', () => {
|
|
1524
|
+
const li = getProviders('search_ads').find((p) => p.id === 'linkedin_ad_library')!
|
|
1525
|
+
expect(li.method).toBe('POST')
|
|
1526
|
+
expect(li.endpoint).toBe('/linkedin-ad-library/search')
|
|
1527
|
+
expect(li.async!.pollEndpoint('7')).toBe('/linkedin-ad-library/search/7')
|
|
1528
|
+
expect(li.async!.isComplete({ status: 'done' })).toBe(true)
|
|
1529
|
+
expect(li.async!.isComplete({ status: 'processing' })).toBe(false)
|
|
1530
|
+
})
|
|
1531
|
+
|
|
1532
|
+
it('meta_ads is async POST to /meta-ads/search', () => {
|
|
1533
|
+
const m = getProviders('search_ads').find((p) => p.id === 'meta_ads')!
|
|
1534
|
+
expect(m.method).toBe('POST')
|
|
1535
|
+
expect(m.endpoint).toBe('/meta-ads/search')
|
|
1536
|
+
expect(m.async!.pollEndpoint('3')).toBe('/meta-ads/search/3')
|
|
1537
|
+
})
|
|
1538
|
+
|
|
1539
|
+
it('twitter_ads is async POST to /twitter-ads-scraper/search', () => {
|
|
1540
|
+
const t = getProviders('search_ads').find((p) => p.id === 'twitter_ads')!
|
|
1541
|
+
expect(t.method).toBe('POST')
|
|
1542
|
+
expect(t.endpoint).toBe('/twitter-ads-scraper/search')
|
|
1543
|
+
expect(t.async!.pollEndpoint('8')).toBe('/twitter-ads-scraper/search/8')
|
|
1544
|
+
})
|
|
1545
|
+
|
|
1546
|
+
it('reddit_ads is async POST to /reddit-ads/scrape', () => {
|
|
1547
|
+
const r = getProviders('search_ads').find((p) => p.id === 'reddit_ads')!
|
|
1548
|
+
expect(r.method).toBe('POST')
|
|
1549
|
+
expect(r.endpoint).toBe('/reddit-ads/scrape')
|
|
1550
|
+
expect(r.async!.pollEndpoint('5')).toBe('/reddit-ads/scrape/5')
|
|
1551
|
+
expect(r.async!.isComplete({ status: 'done' })).toBe(true)
|
|
1552
|
+
expect(r.async!.isComplete({ status: 'failed' })).toBe(true)
|
|
1553
|
+
expect(r.async!.isComplete({ status: 'timed_out' })).toBe(true)
|
|
1554
|
+
expect(r.async!.isComplete({ status: 'processing' })).toBe(false)
|
|
1555
|
+
})
|
|
1556
|
+
|
|
1557
|
+
it('google_ads.isApplicable: true for query/domains/advertiser_ids, false for empty or wrong platform', () => {
|
|
1558
|
+
const g = getProviders('search_ads').find((p) => p.id === 'google_ads')!
|
|
1559
|
+
expect(g.isApplicable!({ query: 'Salesforce' })).toBe(true)
|
|
1560
|
+
expect(g.isApplicable!({ domains: ['salesforce.com'] })).toBe(true)
|
|
1561
|
+
expect(g.isApplicable!({ advertiser_ids: ['AR123'] })).toBe(true)
|
|
1562
|
+
expect(g.isApplicable!({})).toBe(false)
|
|
1563
|
+
expect(g.isApplicable!({ domains: [] })).toBe(false)
|
|
1564
|
+
expect(g.isApplicable!({ query: 'Salesforce', platform: 'meta' })).toBe(false)
|
|
1565
|
+
expect(g.isApplicable!({ query: 'Salesforce', platform: 'google' })).toBe(true)
|
|
1566
|
+
})
|
|
1567
|
+
|
|
1568
|
+
it('linkedin_ad_library.isApplicable: true only when search_urls non-empty', () => {
|
|
1569
|
+
const li = getProviders('search_ads').find((p) => p.id === 'linkedin_ad_library')!
|
|
1570
|
+
expect(li.isApplicable!({ search_urls: ['https://linkedin.com/ad-library/search?accountOwner=salesforce'] })).toBe(true)
|
|
1571
|
+
expect(li.isApplicable!({ search_urls: [] })).toBe(false)
|
|
1572
|
+
expect(li.isApplicable!({ query: 'Salesforce' })).toBe(false)
|
|
1573
|
+
expect(li.isApplicable!({})).toBe(false)
|
|
1574
|
+
expect(li.isApplicable!({ search_urls: ['https://...'], platform: 'google' })).toBe(false)
|
|
1575
|
+
expect(li.isApplicable!({ search_urls: ['https://...'], platform: 'linkedin' })).toBe(true)
|
|
1576
|
+
})
|
|
1577
|
+
|
|
1578
|
+
it('meta_ads.isApplicable: true for bare query, false when domains/advertiser_ids/search_urls set or wrong platform', () => {
|
|
1579
|
+
const m = getProviders('search_ads').find((p) => p.id === 'meta_ads')!
|
|
1580
|
+
expect(m.isApplicable!({ query: 'HubSpot' })).toBe(true)
|
|
1581
|
+
expect(m.isApplicable!({})).toBe(false)
|
|
1582
|
+
expect(m.isApplicable!({ domains: ['hubspot.com'] })).toBe(false)
|
|
1583
|
+
expect(m.isApplicable!({ advertiser_ids: ['AR123'] })).toBe(false)
|
|
1584
|
+
expect(m.isApplicable!({ search_urls: ['https://linkedin.com/...'] })).toBe(false)
|
|
1585
|
+
expect(m.isApplicable!({ query: 'HubSpot', platform: 'twitter' })).toBe(false)
|
|
1586
|
+
expect(m.isApplicable!({ query: 'HubSpot', platform: 'meta' })).toBe(true)
|
|
1587
|
+
})
|
|
1588
|
+
|
|
1589
|
+
it('twitter_ads.isApplicable: same gating as meta_ads', () => {
|
|
1590
|
+
const t = getProviders('search_ads').find((p) => p.id === 'twitter_ads')!
|
|
1591
|
+
expect(t.isApplicable!({ query: 'ColdIQ' })).toBe(true)
|
|
1592
|
+
expect(t.isApplicable!({})).toBe(false)
|
|
1593
|
+
expect(t.isApplicable!({ domains: ['coldiq.com'] })).toBe(false)
|
|
1594
|
+
expect(t.isApplicable!({ advertiser_ids: ['AR123'] })).toBe(false)
|
|
1595
|
+
expect(t.isApplicable!({ search_urls: ['https://...'] })).toBe(false)
|
|
1596
|
+
expect(t.isApplicable!({ query: 'ColdIQ', platform: 'meta' })).toBe(false)
|
|
1597
|
+
expect(t.isApplicable!({ query: 'ColdIQ', platform: 'twitter' })).toBe(true)
|
|
1598
|
+
})
|
|
1599
|
+
|
|
1600
|
+
it('reddit_ads.isApplicable: true for empty/query, false when domains/advertiser_ids/search_urls set', () => {
|
|
1601
|
+
const r = getProviders('search_ads').find((p) => p.id === 'reddit_ads')!
|
|
1602
|
+
expect(r.isApplicable!({})).toBe(true)
|
|
1603
|
+
expect(r.isApplicable!({ query: 'sales automation' })).toBe(true)
|
|
1604
|
+
expect(r.isApplicable!({ industry: 'TECH_B2B' })).toBe(true)
|
|
1605
|
+
expect(r.isApplicable!({ domains: ['coldiq.com'] })).toBe(false)
|
|
1606
|
+
expect(r.isApplicable!({ advertiser_ids: ['AR123'] })).toBe(false)
|
|
1607
|
+
expect(r.isApplicable!({ search_urls: ['https://...'] })).toBe(false)
|
|
1608
|
+
expect(r.isApplicable!({ platform: 'google' })).toBe(false)
|
|
1609
|
+
expect(r.isApplicable!({ platform: 'reddit' })).toBe(true)
|
|
1610
|
+
})
|
|
1611
|
+
|
|
1612
|
+
it('platform pin: platform=linkedin makes only linkedin_ad_library applicable (even without search_urls it fails the URL check)', () => {
|
|
1613
|
+
const providers = getProviders('search_ads')
|
|
1614
|
+
const pinned = { platform: 'linkedin', query: 'Salesforce' }
|
|
1615
|
+
const applicable = providers.filter((p) => p.isApplicable!(pinned))
|
|
1616
|
+
expect(applicable.map((p) => p.id)).toEqual([])
|
|
1617
|
+
// linkedin_ad_library passes the platform check but fails the search_urls check
|
|
1618
|
+
const li = providers.find((p) => p.id === 'linkedin_ad_library')!
|
|
1619
|
+
expect(li.isApplicable!({ platform: 'linkedin', search_urls: ['https://...'] })).toBe(true)
|
|
1620
|
+
})
|
|
1621
|
+
|
|
1622
|
+
it('google_ads.mapParams: query→searchTerms[], country→region, max_results clamped [1,1000]', () => {
|
|
1623
|
+
const g = getProviders('search_ads').find((p) => p.id === 'google_ads')!
|
|
1624
|
+
const out = g.mapParams({ query: 'Salesforce', country: 'US', max_results: 50 }).body as Record<string, unknown>
|
|
1625
|
+
expect(out.searchTerms).toEqual(['Salesforce'])
|
|
1626
|
+
expect(out.region).toBe('US')
|
|
1627
|
+
expect(out.maxAds).toBe(50)
|
|
1628
|
+
|
|
1629
|
+
const withDomains = g.mapParams({ domains: ['salesforce.com'], advertiser_ids: ['AR123'], max_results: 9999 }).body as Record<string, unknown>
|
|
1630
|
+
expect(withDomains.searchTerms).toBeUndefined()
|
|
1631
|
+
expect(withDomains.domains).toEqual(['salesforce.com'])
|
|
1632
|
+
expect(withDomains.advertiserIds).toEqual(['AR123'])
|
|
1633
|
+
expect(withDomains.maxAds).toBe(1000)
|
|
1634
|
+
|
|
1635
|
+
const minClamp = g.mapParams({ query: 'x', max_results: 0 }).body as Record<string, unknown>
|
|
1636
|
+
expect(minClamp.maxAds).toBe(1)
|
|
1637
|
+
})
|
|
1638
|
+
|
|
1639
|
+
it('linkedin_ad_library.mapParams: search_urls→searchUrls, max_results clamped [1,200]', () => {
|
|
1640
|
+
const li = getProviders('search_ads').find((p) => p.id === 'linkedin_ad_library')!
|
|
1641
|
+
const out = li.mapParams({ search_urls: ['https://linkedin.com/...'], max_results: 50 }).body as Record<string, unknown>
|
|
1642
|
+
expect(out.searchUrls).toEqual(['https://linkedin.com/...'])
|
|
1643
|
+
expect(out.maxResults).toBe(50)
|
|
1644
|
+
|
|
1645
|
+
const capped = li.mapParams({ search_urls: ['https://...'], max_results: 9999 }).body as Record<string, unknown>
|
|
1646
|
+
expect(capped.maxResults).toBe(200)
|
|
1647
|
+
})
|
|
1648
|
+
|
|
1649
|
+
it('meta_ads.mapParams: query→search, defaults country=US/adType=ALL, max_results clamped [1,200]', () => {
|
|
1650
|
+
const m = getProviders('search_ads').find((p) => p.id === 'meta_ads')!
|
|
1651
|
+
const out = m.mapParams({ query: 'HubSpot', max_results: 30 }).body as Record<string, unknown>
|
|
1652
|
+
expect(out.search).toBe('HubSpot')
|
|
1653
|
+
expect(out.country).toBe('US')
|
|
1654
|
+
expect(out.adType).toBe('ALL')
|
|
1655
|
+
expect(out.maxItems).toBe(30)
|
|
1656
|
+
|
|
1657
|
+
const withOverrides = m.mapParams({ query: 'x', country: 'FR', ad_type: 'POLITICAL_AND_ISSUE_ADS', max_results: 9999 }).body as Record<string, unknown>
|
|
1658
|
+
expect(withOverrides.country).toBe('FR')
|
|
1659
|
+
expect(withOverrides.adType).toBe('POLITICAL_AND_ISSUE_ADS')
|
|
1660
|
+
expect(withOverrides.maxItems).toBe(200)
|
|
1661
|
+
})
|
|
1662
|
+
|
|
1663
|
+
it('twitter_ads.mapParams: query→searchTerms[], forwards dates, max_results clamped [1,100]', () => {
|
|
1664
|
+
const t = getProviders('search_ads').find((p) => p.id === 'twitter_ads')!
|
|
1665
|
+
const out = t.mapParams({ query: 'ColdIQ', country: 'US', start_date: '2025-01-01', end_date: '2025-12-31', max_results: 20 }).body as Record<string, unknown>
|
|
1666
|
+
expect(out.searchTerms).toEqual(['ColdIQ'])
|
|
1667
|
+
expect(out.country).toBe('US')
|
|
1668
|
+
expect(out.startDate).toBe('2025-01-01')
|
|
1669
|
+
expect(out.endDate).toBe('2025-12-31')
|
|
1670
|
+
expect(out.maxItems).toBe(20)
|
|
1671
|
+
|
|
1672
|
+
const capped = t.mapParams({ query: 'x', max_results: 9999 }).body as Record<string, unknown>
|
|
1673
|
+
expect(capped.maxItems).toBe(100)
|
|
1674
|
+
})
|
|
1675
|
+
|
|
1676
|
+
it('reddit_ads.mapParams: query→keywords, forwards industry/budget_category/post_type/objective_type', () => {
|
|
1677
|
+
const r = getProviders('search_ads').find((p) => p.id === 'reddit_ads')!
|
|
1678
|
+
const out = r.mapParams({
|
|
1679
|
+
query: 'sales automation',
|
|
1680
|
+
industry: 'TECH_B2B',
|
|
1681
|
+
budget_category: 'HIGH',
|
|
1682
|
+
post_type: 'VIDEO',
|
|
1683
|
+
objective_type: 'CONVERSIONS',
|
|
1684
|
+
}).body as Record<string, unknown>
|
|
1685
|
+
expect(out.keywords).toBe('sales automation')
|
|
1686
|
+
expect(out.industry).toBe('TECH_B2B')
|
|
1687
|
+
expect(out.budgetCategory).toBe('HIGH')
|
|
1688
|
+
expect(out.postType).toBe('VIDEO')
|
|
1689
|
+
expect(out.objectiveType).toBe('CONVERSIONS')
|
|
1690
|
+
})
|
|
1691
|
+
|
|
1692
|
+
it('reddit_ads.mapParams: query is optional (scrape-all)', () => {
|
|
1693
|
+
const r = getProviders('search_ads').find((p) => p.id === 'reddit_ads')!
|
|
1694
|
+
const out = r.mapParams({ industry: 'TECH_B2B' }).body as Record<string, unknown>
|
|
1695
|
+
expect(out.keywords).toBeUndefined()
|
|
1696
|
+
expect(out.industry).toBe('TECH_B2B')
|
|
1697
|
+
})
|
|
1698
|
+
|
|
1699
|
+
it('hasResult requires ads[] non-empty for all 5 providers', () => {
|
|
1700
|
+
const providers = getProviders('search_ads')
|
|
1701
|
+
for (const p of providers) {
|
|
1702
|
+
expect(p.hasResult({ ads: [{ id: '1' }] }), `${p.id} should return true`).toBe(true)
|
|
1703
|
+
expect(p.hasResult({ ads: [] }), `${p.id} should return false for empty`).toBe(false)
|
|
1704
|
+
expect(p.hasResult({}), `${p.id} should return false for no ads key`).toBe(false)
|
|
1705
|
+
}
|
|
1706
|
+
})
|
|
1707
|
+
})
|
|
1708
|
+
|
|
1709
|
+
describe('search_places', () => {
|
|
1710
|
+
it('has exactly 2 providers in order [openmart, google_maps]', () => {
|
|
1711
|
+
const providers = getProviders('search_places')
|
|
1712
|
+
expect(providers.map((p) => p.id)).toEqual(['openmart', 'google_maps'])
|
|
1713
|
+
})
|
|
1714
|
+
|
|
1715
|
+
it('openmart is sync POST to /openmart/search (no async block)', () => {
|
|
1716
|
+
const om = getProviders('search_places').find((p) => p.id === 'openmart')!
|
|
1717
|
+
expect(om.method).toBe('POST')
|
|
1718
|
+
expect(om.endpoint).toBe('/openmart/search')
|
|
1719
|
+
expect(om.async).toBeUndefined()
|
|
1720
|
+
})
|
|
1721
|
+
|
|
1722
|
+
it('google_maps is async POST to /google-maps/scraper', () => {
|
|
1723
|
+
const gm = getProviders('search_places').find((p) => p.id === 'google_maps')!
|
|
1724
|
+
expect(gm.method).toBe('POST')
|
|
1725
|
+
expect(gm.endpoint).toBe('/google-maps/scraper')
|
|
1726
|
+
expect(gm.async!.pollEndpoint('123')).toBe('/google-maps/scraper/123')
|
|
1727
|
+
expect(gm.async!.isComplete({ status: 'done' })).toBe(true)
|
|
1728
|
+
expect(gm.async!.isComplete({ status: 'failed' })).toBe(true)
|
|
1729
|
+
expect(gm.async!.isComplete({ status: 'timed_out' })).toBe(true)
|
|
1730
|
+
expect(gm.async!.isComplete({ status: 'processing' })).toBe(false)
|
|
1731
|
+
expect(gm.async!.extractId({ jobId: 42 })).toBe('42')
|
|
1732
|
+
expect(gm.async!.extractId({ jobId: '7' })).toBe('7')
|
|
1733
|
+
expect(() => gm.async!.extractId({})).toThrow('no jobId')
|
|
1734
|
+
expect(() => gm.async!.extractId({ jobId: undefined })).toThrow('no jobId')
|
|
1735
|
+
})
|
|
1736
|
+
|
|
1737
|
+
it('openmart.isApplicable: true for query/tags/city/Openmart-country, false for outside-whitelist country/Google-only keys/wrong provider/empty input', () => {
|
|
1738
|
+
const om = getProviders('search_places').find((p) => p.id === 'openmart')!
|
|
1739
|
+
expect(om.isApplicable!({ query: 'coffee' })).toBe(true)
|
|
1740
|
+
expect(om.isApplicable!({ tags: ['cafe'] })).toBe(true)
|
|
1741
|
+
expect(om.isApplicable!({ city: 'NYC' })).toBe(true)
|
|
1742
|
+
expect(om.isApplicable!({ country: 'US', query: 'x' })).toBe(true)
|
|
1743
|
+
expect(om.isApplicable!({ country: 'us', query: 'x' })).toBe(true)
|
|
1744
|
+
expect(om.isApplicable!({ ownership_type: 'INDEPENDENT' })).toBe(true)
|
|
1745
|
+
expect(om.isApplicable!({ state: 'CA' })).toBe(true)
|
|
1746
|
+
expect(om.isApplicable!({})).toBe(false)
|
|
1747
|
+
expect(om.isApplicable!({ country: 'FR' })).toBe(false)
|
|
1748
|
+
expect(om.isApplicable!({ country: 'fr', query: 'boulangerie' })).toBe(false)
|
|
1749
|
+
expect(om.isApplicable!({ start_urls: ['https://maps.google.com/...'] })).toBe(false)
|
|
1750
|
+
expect(om.isApplicable!({ include_opening_hours: true })).toBe(false)
|
|
1751
|
+
expect(om.isApplicable!({ include_additional_info: true })).toBe(false)
|
|
1752
|
+
expect(om.isApplicable!({ language: 'en' })).toBe(false)
|
|
1753
|
+
expect(om.isApplicable!({ query: 'coffee', provider: 'google_maps' })).toBe(false)
|
|
1754
|
+
expect(om.isApplicable!({ query: 'coffee', provider: 'openmart' })).toBe(true)
|
|
1755
|
+
})
|
|
1756
|
+
|
|
1757
|
+
it('google_maps.isApplicable: true for query/start_urls/city, false for empty/filter-only/wrong provider', () => {
|
|
1758
|
+
const gm = getProviders('search_places').find((p) => p.id === 'google_maps')!
|
|
1759
|
+
expect(gm.isApplicable!({ query: 'dentist' })).toBe(true)
|
|
1760
|
+
expect(gm.isApplicable!({ start_urls: ['https://maps.google.com/...'] })).toBe(true)
|
|
1761
|
+
expect(gm.isApplicable!({ city: 'Paris' })).toBe(true)
|
|
1762
|
+
expect(gm.isApplicable!({})).toBe(false)
|
|
1763
|
+
expect(gm.isApplicable!({ ownership_type: 'CHAIN' })).toBe(false)
|
|
1764
|
+
expect(gm.isApplicable!({ min_overall_rating: 4 })).toBe(false)
|
|
1765
|
+
expect(gm.isApplicable!({ query: 'pizza', provider: 'openmart' })).toBe(false)
|
|
1766
|
+
expect(gm.isApplicable!({ query: 'pizza', provider: 'google_maps' })).toBe(true)
|
|
1767
|
+
})
|
|
1768
|
+
|
|
1769
|
+
it('provider pin: provider=openmart blocks google_maps; provider=google_maps blocks openmart', () => {
|
|
1770
|
+
const providers = getProviders('search_places')
|
|
1771
|
+
const pinOpenmart = { provider: 'openmart', query: 'cafe' }
|
|
1772
|
+
expect(providers.filter((p) => p.isApplicable!(pinOpenmart)).map((p) => p.id)).toEqual(['openmart'])
|
|
1773
|
+
|
|
1774
|
+
const pinGoogle = { provider: 'google_maps', query: 'cafe', city: 'London' }
|
|
1775
|
+
expect(providers.filter((p) => p.isApplicable!(pinGoogle)).map((p) => p.id)).toEqual(['google_maps'])
|
|
1776
|
+
})
|
|
1777
|
+
|
|
1778
|
+
it('openmart.mapParams: normalizes country to uppercase, builds location from bits, clamps limit to [1,500]', () => {
|
|
1779
|
+
const om = getProviders('search_places').find((p) => p.id === 'openmart')!
|
|
1780
|
+
const out = om.mapParams({ query: 'coffee', country: 'us', city: 'New York', state: 'NY', limit: 10 }).body as Record<string, unknown>
|
|
1781
|
+
expect(out.query).toBe('coffee')
|
|
1782
|
+
expect(out.limit).toBe(10)
|
|
1783
|
+
const loc = out.location as Record<string, unknown>
|
|
1784
|
+
expect(loc.country).toBe('US')
|
|
1785
|
+
expect(loc.city).toBe('New York')
|
|
1786
|
+
expect(loc.state).toBe('NY')
|
|
1787
|
+
|
|
1788
|
+
const noLocation = om.mapParams({ query: 'x' }).body as Record<string, unknown>
|
|
1789
|
+
expect(noLocation.location).toBeUndefined()
|
|
1790
|
+
|
|
1791
|
+
const clamped = om.mapParams({ query: 'x', limit: 9999 }).body as Record<string, unknown>
|
|
1792
|
+
expect(clamped.limit).toBe(500)
|
|
1793
|
+
|
|
1794
|
+
const minClamp = om.mapParams({ query: 'x', limit: 0 }).body as Record<string, unknown>
|
|
1795
|
+
expect(minClamp.limit).toBe(1)
|
|
1796
|
+
})
|
|
1797
|
+
|
|
1798
|
+
it('openmart.mapParams: forwards all structured filters', () => {
|
|
1799
|
+
const om = getProviders('search_places').find((p) => p.id === 'openmart')!
|
|
1800
|
+
const out = om.mapParams({
|
|
1801
|
+
country: 'US',
|
|
1802
|
+
state: 'CA',
|
|
1803
|
+
ownership_type: 'INDEPENDENT',
|
|
1804
|
+
min_overall_rating: 4,
|
|
1805
|
+
max_overall_rating: 5,
|
|
1806
|
+
has_website: true,
|
|
1807
|
+
min_price_tier: 1,
|
|
1808
|
+
max_price_tier: 2,
|
|
1809
|
+
include_keywords: ['organic'],
|
|
1810
|
+
exclude_keywords: ['chain'],
|
|
1811
|
+
exclude_root_domains: ['yelp.com'],
|
|
1812
|
+
limit: 5,
|
|
1813
|
+
}).body as Record<string, unknown>
|
|
1814
|
+
expect(out.ownership_type).toBe('INDEPENDENT')
|
|
1815
|
+
expect(out.min_overall_rating).toBe(4)
|
|
1816
|
+
expect(out.max_overall_rating).toBe(5)
|
|
1817
|
+
expect(out.has_website).toBe(true)
|
|
1818
|
+
expect(out.min_price_tier).toBe(1)
|
|
1819
|
+
expect(out.max_price_tier).toBe(2)
|
|
1820
|
+
expect(out.include_keywords).toEqual(['organic'])
|
|
1821
|
+
expect(out.exclude_keywords).toEqual(['chain'])
|
|
1822
|
+
expect(out.exclude_root_domains).toEqual(['yelp.com'])
|
|
1823
|
+
})
|
|
1824
|
+
|
|
1825
|
+
it('google_maps.mapParams: query→searchStringsArray, country lowercase, limit clamped [1,200], start_urls→startUrls[]', () => {
|
|
1826
|
+
const gm = getProviders('search_places').find((p) => p.id === 'google_maps')!
|
|
1827
|
+
const out = gm.mapParams({ query: 'boulangerie', country: 'FR', city: 'Paris', limit: 15 }).body as Record<string, unknown>
|
|
1828
|
+
expect(out.searchStringsArray).toEqual(['boulangerie'])
|
|
1829
|
+
expect(out.countryCode).toBe('fr')
|
|
1830
|
+
expect(out.city).toBe('Paris')
|
|
1831
|
+
expect(out.maxCrawledPlacesPerSearch).toBe(15)
|
|
1832
|
+
|
|
1833
|
+
const urls = gm.mapParams({ start_urls: ['https://maps.google.com/place/foo'] }).body as Record<string, unknown>
|
|
1834
|
+
expect(urls.startUrls).toEqual([{ url: 'https://maps.google.com/place/foo' }])
|
|
1835
|
+
expect(urls.searchStringsArray).toBeUndefined()
|
|
1836
|
+
|
|
1837
|
+
const capped = gm.mapParams({ query: 'x', limit: 9999 }).body as Record<string, unknown>
|
|
1838
|
+
expect(capped.maxCrawledPlacesPerSearch).toBe(200)
|
|
1839
|
+
|
|
1840
|
+
const minClamp = gm.mapParams({ query: 'x', limit: 0 }).body as Record<string, unknown>
|
|
1841
|
+
expect(minClamp.maxCrawledPlacesPerSearch).toBe(1)
|
|
1842
|
+
})
|
|
1843
|
+
|
|
1844
|
+
it('google_maps.mapParams: forwards include_opening_hours and include_additional_info', () => {
|
|
1845
|
+
const gm = getProviders('search_places').find((p) => p.id === 'google_maps')!
|
|
1846
|
+
const out = gm.mapParams({ query: 'pizza', include_opening_hours: true, include_additional_info: true, language: 'fr' }).body as Record<string, unknown>
|
|
1847
|
+
expect(out.includeOpeningHours).toBe(true)
|
|
1848
|
+
expect(out.additionalInfo).toBe(true)
|
|
1849
|
+
expect(out.language).toBe('fr')
|
|
1850
|
+
})
|
|
1851
|
+
|
|
1852
|
+
it('openmart.hasResult: true for non-empty root array or {data:[…]}, false otherwise', () => {
|
|
1853
|
+
const om = getProviders('search_places').find((p) => p.id === 'openmart')!
|
|
1854
|
+
expect(om.hasResult([{ name: 'ColdIQ HQ' }])).toBe(true)
|
|
1855
|
+
expect(om.hasResult({ data: [{ name: 'x' }] })).toBe(true)
|
|
1856
|
+
expect(om.hasResult([])).toBe(false)
|
|
1857
|
+
expect(om.hasResult({ data: [] })).toBe(false)
|
|
1858
|
+
expect(om.hasResult({})).toBe(false)
|
|
1859
|
+
})
|
|
1860
|
+
|
|
1861
|
+
it('google_maps.hasResult: true for non-empty places[], false otherwise', () => {
|
|
1862
|
+
const gm = getProviders('search_places').find((p) => p.id === 'google_maps')!
|
|
1863
|
+
expect(gm.hasResult({ places: [{ name: 'Eiffel Tower' }] })).toBe(true)
|
|
1864
|
+
expect(gm.hasResult({ places: [] })).toBe(false)
|
|
1865
|
+
expect(gm.hasResult({})).toBe(false)
|
|
1866
|
+
})
|
|
1867
|
+
})
|
|
1868
|
+
|
|
1869
|
+
describe('find_influencers', () => {
|
|
1870
|
+
it('providers are ordered influencers_similar → influencers_discovery', () => {
|
|
1871
|
+
expect(getProviders('find_influencers').map((p) => p.id)).toEqual(['influencers_similar', 'influencers_discovery'])
|
|
1872
|
+
})
|
|
1873
|
+
|
|
1874
|
+
it('influencers_similar is sync POST to /influencers-club/discovery/creators/similar', () => {
|
|
1875
|
+
const p = getProviders('find_influencers').find((p) => p.id === 'influencers_similar')!
|
|
1876
|
+
expect(p.method).toBe('POST')
|
|
1877
|
+
expect(p.endpoint).toBe('/influencers-club/discovery/creators/similar')
|
|
1878
|
+
expect(p.async).toBeUndefined()
|
|
1879
|
+
})
|
|
1880
|
+
|
|
1881
|
+
it('influencers_similar.isApplicable: true only when handle is set', () => {
|
|
1882
|
+
const p = getProviders('find_influencers').find((p) => p.id === 'influencers_similar')!
|
|
1883
|
+
expect(p.isApplicable!({ handle: 'cristiano', platform: 'instagram' })).toBe(true)
|
|
1884
|
+
expect(p.isApplicable!({ platform: 'instagram' })).toBe(false)
|
|
1885
|
+
expect(p.isApplicable!({})).toBe(false)
|
|
1886
|
+
})
|
|
1887
|
+
|
|
1888
|
+
it('influencers_similar.mapParams: builds paging and filters correctly', () => {
|
|
1889
|
+
const p = getProviders('find_influencers').find((p) => p.id === 'influencers_similar')!
|
|
1890
|
+
const out = p.mapParams({
|
|
1891
|
+
handle: 'cristiano',
|
|
1892
|
+
platform: 'instagram',
|
|
1893
|
+
limit: 10,
|
|
1894
|
+
page: 2,
|
|
1895
|
+
location: ['United States'],
|
|
1896
|
+
gender: 'male',
|
|
1897
|
+
type: 'creator',
|
|
1898
|
+
}).body as Record<string, unknown>
|
|
1899
|
+
expect(out.handle).toBe('cristiano')
|
|
1900
|
+
expect(out.platform).toBe('instagram')
|
|
1901
|
+
expect(out.paging).toEqual({ limit: 10, page: 2 })
|
|
1902
|
+
expect(out.filters).toEqual({ location: ['United States'], gender: 'male', type: 'creator' })
|
|
1903
|
+
})
|
|
1904
|
+
|
|
1905
|
+
it('influencers_similar.mapParams: defaults paging to limit=25, page=1', () => {
|
|
1906
|
+
const p = getProviders('find_influencers').find((p) => p.id === 'influencers_similar')!
|
|
1907
|
+
const out = p.mapParams({ handle: 'cristiano', platform: 'instagram' }).body as Record<string, unknown>
|
|
1908
|
+
expect(out.paging).toEqual({ limit: 25, page: 1 })
|
|
1909
|
+
})
|
|
1910
|
+
|
|
1911
|
+
it('influencers_similar.hasResult: true for non-empty accounts or creators', () => {
|
|
1912
|
+
const p = getProviders('find_influencers').find((p) => p.id === 'influencers_similar')!
|
|
1913
|
+
expect(p.hasResult({ accounts: [{ id: 1 }] })).toBe(true)
|
|
1914
|
+
expect(p.hasResult({ creators: [{ id: 2 }] })).toBe(true)
|
|
1915
|
+
expect(p.hasResult({ accounts: [] })).toBe(false)
|
|
1916
|
+
expect(p.hasResult({})).toBe(false)
|
|
1917
|
+
})
|
|
1918
|
+
|
|
1919
|
+
it('influencers_discovery is sync POST to /influencers-club/discovery with no gate', () => {
|
|
1920
|
+
const p = getProviders('find_influencers').find((p) => p.id === 'influencers_discovery')!
|
|
1921
|
+
expect(p.method).toBe('POST')
|
|
1922
|
+
expect(p.endpoint).toBe('/influencers-club/discovery')
|
|
1923
|
+
expect(p.isApplicable).toBeUndefined()
|
|
1924
|
+
expect(p.async).toBeUndefined()
|
|
1925
|
+
})
|
|
1926
|
+
|
|
1927
|
+
it('influencers_discovery.mapParams: builds sort and filters correctly', () => {
|
|
1928
|
+
const p = getProviders('find_influencers').find((p) => p.id === 'influencers_discovery')!
|
|
1929
|
+
const out = p.mapParams({
|
|
1930
|
+
platform: 'youtube',
|
|
1931
|
+
limit: 10,
|
|
1932
|
+
sort_by: 'engagement_rate',
|
|
1933
|
+
sort_order: 'asc',
|
|
1934
|
+
ai_search: 'fitness nutrition',
|
|
1935
|
+
location: ['United States'],
|
|
1936
|
+
gender: 'female',
|
|
1937
|
+
type: 'creator',
|
|
1938
|
+
}).body as Record<string, unknown>
|
|
1939
|
+
expect(out.platform).toBe('youtube')
|
|
1940
|
+
expect(out.paging).toEqual({ limit: 10, page: 1 })
|
|
1941
|
+
expect(out.sort).toEqual({ sort_by: 'engagement_rate', sort_order: 'asc' })
|
|
1942
|
+
expect(out.filters).toEqual({ ai_search: 'fitness nutrition', location: ['United States'], gender: 'female', type: 'creator' })
|
|
1943
|
+
})
|
|
1944
|
+
|
|
1945
|
+
it('influencers_discovery.mapParams: sort is undefined when sort_by not provided', () => {
|
|
1946
|
+
const p = getProviders('find_influencers').find((p) => p.id === 'influencers_discovery')!
|
|
1947
|
+
const out = p.mapParams({ platform: 'instagram', limit: 5 }).body as Record<string, unknown>
|
|
1948
|
+
expect(out.sort).toBeUndefined()
|
|
1949
|
+
})
|
|
1950
|
+
|
|
1951
|
+
it('influencers_discovery.mapParams: sort_order defaults to desc when sort_by set but sort_order omitted', () => {
|
|
1952
|
+
const p = getProviders('find_influencers').find((p) => p.id === 'influencers_discovery')!
|
|
1953
|
+
const out = p.mapParams({ platform: 'instagram', sort_by: 'number_of_followers' }).body as Record<string, unknown>
|
|
1954
|
+
expect(out.sort).toEqual({ sort_by: 'number_of_followers', sort_order: 'desc' })
|
|
1955
|
+
})
|
|
1956
|
+
})
|
|
1957
|
+
|
|
1958
|
+
describe('search_reddit', () => {
|
|
1959
|
+
it('has exactly 1 provider: reddit', () => {
|
|
1960
|
+
expect(getProviders('search_reddit').map((p) => p.id)).toEqual(['reddit'])
|
|
1961
|
+
})
|
|
1962
|
+
|
|
1963
|
+
it('reddit is async POST to /reddit/scrape', () => {
|
|
1964
|
+
const r = getProviders('search_reddit').find((p) => p.id === 'reddit')!
|
|
1965
|
+
expect(r.method).toBe('POST')
|
|
1966
|
+
expect(r.endpoint).toBe('/reddit/scrape')
|
|
1967
|
+
expect(r.async!.pollEndpoint('42')).toBe('/reddit/scrape/42')
|
|
1968
|
+
expect(r.async!.isComplete({ status: 'done' })).toBe(true)
|
|
1969
|
+
expect(r.async!.isComplete({ status: 'failed' })).toBe(true)
|
|
1970
|
+
expect(r.async!.isComplete({ status: 'timed_out' })).toBe(true)
|
|
1971
|
+
expect(r.async!.isComplete({ status: 'processing' })).toBe(false)
|
|
1972
|
+
expect(r.async!.extractId({ jobId: 99 })).toBe('99')
|
|
1973
|
+
expect(r.async!.extractId({ jobId: '55' })).toBe('55')
|
|
1974
|
+
expect(() => r.async!.extractId({})).toThrow('no jobId')
|
|
1975
|
+
})
|
|
1976
|
+
|
|
1977
|
+
it('reddit.isApplicable: true only when start_urls is non-empty; query alone is not sufficient', () => {
|
|
1978
|
+
const r = getProviders('search_reddit').find((p) => p.id === 'reddit')!
|
|
1979
|
+
expect(r.isApplicable!({ start_urls: ['https://reddit.com/r/sales/'] })).toBe(true)
|
|
1980
|
+
expect(r.isApplicable!({ query: 'x', start_urls: ['https://reddit.com/r/sales/'] })).toBe(true)
|
|
1981
|
+
expect(r.isApplicable!({ query: 'B2B sales' })).toBe(false)
|
|
1982
|
+
expect(r.isApplicable!({})).toBe(false)
|
|
1983
|
+
expect(r.isApplicable!({ start_urls: [] })).toBe(false)
|
|
1984
|
+
})
|
|
1985
|
+
|
|
1986
|
+
it('reddit.mapParams: maps all fields correctly', () => {
|
|
1987
|
+
const r = getProviders('search_reddit').find((p) => p.id === 'reddit')!
|
|
1988
|
+
const out = r.mapParams({
|
|
1989
|
+
query: 'best CRM',
|
|
1990
|
+
type: 'posts',
|
|
1991
|
+
sort: 'top',
|
|
1992
|
+
time: 'month',
|
|
1993
|
+
limit: 10,
|
|
1994
|
+
include_comments: true,
|
|
1995
|
+
}).body as Record<string, unknown>
|
|
1996
|
+
expect(out.searchQueries).toEqual(['best CRM'])
|
|
1997
|
+
expect(out.type).toBe('posts')
|
|
1998
|
+
expect(out.sort).toBe('top')
|
|
1999
|
+
expect(out.time).toBe('month')
|
|
2000
|
+
expect(out.maxItems).toBe(10)
|
|
2001
|
+
expect(out.includeComments).toBe(true)
|
|
2002
|
+
})
|
|
2003
|
+
|
|
2004
|
+
it('reddit.mapParams: clamps maxItems to [1,200]', () => {
|
|
2005
|
+
const r = getProviders('search_reddit').find((p) => p.id === 'reddit')!
|
|
2006
|
+
const hi = r.mapParams({ query: 'x', limit: 9999 }).body as Record<string, unknown>
|
|
2007
|
+
const lo = r.mapParams({ query: 'x', limit: 0 }).body as Record<string, unknown>
|
|
2008
|
+
expect(hi.maxItems).toBe(200)
|
|
2009
|
+
expect(lo.maxItems).toBe(1)
|
|
2010
|
+
})
|
|
2011
|
+
|
|
2012
|
+
it('reddit.mapParams: searchQueries undefined when query not provided; start_urls strings wrapped as {url} objects', () => {
|
|
2013
|
+
const r = getProviders('search_reddit').find((p) => p.id === 'reddit')!
|
|
2014
|
+
const out = r.mapParams({ start_urls: ['https://reddit.com/r/sales/'] }).body as Record<string, unknown>
|
|
2015
|
+
expect(out.searchQueries).toBeUndefined()
|
|
2016
|
+
expect(out.startUrls).toEqual([{ url: 'https://reddit.com/r/sales/' }])
|
|
2017
|
+
})
|
|
2018
|
+
|
|
2019
|
+
it('reddit.hasResult: true for non-empty items[], false otherwise', () => {
|
|
2020
|
+
const r = getProviders('search_reddit').find((p) => p.id === 'reddit')!
|
|
2021
|
+
expect(r.hasResult({ items: [{ id: '1' }] })).toBe(true)
|
|
2022
|
+
expect(r.hasResult({ items: [] })).toBe(false)
|
|
2023
|
+
expect(r.hasResult({})).toBe(false)
|
|
2024
|
+
})
|
|
2025
|
+
})
|
|
2026
|
+
|
|
2027
|
+
describe('search_seo', () => {
|
|
2028
|
+
it('has 16 providers in priority order', () => {
|
|
2029
|
+
const ids = getProviders('search_seo').map((p) => p.id)
|
|
2030
|
+
expect(ids).toEqual([
|
|
2031
|
+
'kw_search_volume', 'kw_trends',
|
|
2032
|
+
'serp_google', 'serp_bing', 'serp_youtube',
|
|
2033
|
+
'bl_summary', 'bl_backlinks', 'bl_referring',
|
|
2034
|
+
'domain_tech', 'domain_whois',
|
|
2035
|
+
'labs_rank_overview', 'labs_ranked_kw', 'labs_competitors', 'labs_kw_ideas',
|
|
2036
|
+
'page_lighthouse', 'page_content',
|
|
2037
|
+
])
|
|
2038
|
+
})
|
|
2039
|
+
|
|
2040
|
+
it('all providers are sync POST (no async)', () => {
|
|
2041
|
+
for (const p of getProviders('search_seo')) {
|
|
2042
|
+
expect(p.method, `${p.id} should be POST`).toBe('POST')
|
|
2043
|
+
expect(p.async, `${p.id} should have no async`).toBeUndefined()
|
|
2044
|
+
}
|
|
2045
|
+
})
|
|
2046
|
+
|
|
2047
|
+
it('kw_search_volume.isApplicable: true for category=keywords + keywords[]', () => {
|
|
2048
|
+
const p = getProviders('search_seo').find((p) => p.id === 'kw_search_volume')!
|
|
2049
|
+
expect(p.isApplicable!({ category: 'keywords', keywords: ['CRM'] })).toBe(true)
|
|
2050
|
+
expect(p.isApplicable!({ category: 'keywords', keywords: [] })).toBe(false)
|
|
2051
|
+
expect(p.isApplicable!({ category: 'serp', keywords: ['CRM'] })).toBe(false)
|
|
2052
|
+
})
|
|
2053
|
+
|
|
2054
|
+
it('kw_search_volume.mapParams: maps keywords, location_name, language_code', () => {
|
|
2055
|
+
const p = getProviders('search_seo').find((p) => p.id === 'kw_search_volume')!
|
|
2056
|
+
const out = p.mapParams({ category: 'keywords', keywords: ['CRM'], location: 'United States', language: 'en' }).body as Record<string, unknown>
|
|
2057
|
+
expect(out.keywords).toEqual(['CRM'])
|
|
2058
|
+
expect(out.location_name).toBe('United States')
|
|
2059
|
+
expect(out.language_code).toBe('en')
|
|
2060
|
+
})
|
|
2061
|
+
|
|
2062
|
+
it('kw_trends.mapParams: maps date_from, date_to, time_range', () => {
|
|
2063
|
+
const p = getProviders('search_seo').find((p) => p.id === 'kw_trends')!
|
|
2064
|
+
const out = p.mapParams({ category: 'keywords', keywords: ['CRM'], date_from: '2024-01-01', date_to: '2024-12-31', time_range: 'past_30_days' }).body as Record<string, unknown>
|
|
2065
|
+
expect(out.date_from).toBe('2024-01-01')
|
|
2066
|
+
expect(out.date_to).toBe('2024-12-31')
|
|
2067
|
+
expect(out.time_range).toBe('past_30_days')
|
|
2068
|
+
})
|
|
2069
|
+
|
|
2070
|
+
it('serp_google.isApplicable: true for category=serp+keyword+default engine, false for bing/youtube', () => {
|
|
2071
|
+
const p = getProviders('search_seo').find((p) => p.id === 'serp_google')!
|
|
2072
|
+
expect(p.isApplicable!({ category: 'serp', keyword: 'CRM' })).toBe(true)
|
|
2073
|
+
expect(p.isApplicable!({ category: 'serp', keyword: 'CRM', engine: 'google' })).toBe(true)
|
|
2074
|
+
expect(p.isApplicable!({ category: 'serp', keyword: 'CRM', engine: 'bing' })).toBe(false)
|
|
2075
|
+
expect(p.isApplicable!({ category: 'serp', keyword: 'CRM', engine: 'youtube' })).toBe(false)
|
|
2076
|
+
expect(p.isApplicable!({ category: 'serp' })).toBe(false)
|
|
2077
|
+
})
|
|
2078
|
+
|
|
2079
|
+
it('serp_bing.isApplicable: true only for engine=bing', () => {
|
|
2080
|
+
const p = getProviders('search_seo').find((p) => p.id === 'serp_bing')!
|
|
2081
|
+
expect(p.isApplicable!({ category: 'serp', keyword: 'CRM', engine: 'bing' })).toBe(true)
|
|
2082
|
+
expect(p.isApplicable!({ category: 'serp', keyword: 'CRM', engine: 'google' })).toBe(false)
|
|
2083
|
+
expect(p.isApplicable!({ category: 'serp', keyword: 'CRM' })).toBe(false)
|
|
2084
|
+
})
|
|
2085
|
+
|
|
2086
|
+
it('serp_youtube.isApplicable: true only for engine=youtube', () => {
|
|
2087
|
+
const p = getProviders('search_seo').find((p) => p.id === 'serp_youtube')!
|
|
2088
|
+
expect(p.isApplicable!({ category: 'serp', keyword: 'CRM', engine: 'youtube' })).toBe(true)
|
|
2089
|
+
expect(p.isApplicable!({ category: 'serp', keyword: 'CRM', engine: 'google' })).toBe(false)
|
|
2090
|
+
})
|
|
2091
|
+
|
|
2092
|
+
it('serp_google.mapParams: maps keyword, depth, device', () => {
|
|
2093
|
+
const p = getProviders('search_seo').find((p) => p.id === 'serp_google')!
|
|
2094
|
+
const out = p.mapParams({ category: 'serp', keyword: 'best CRM', limit: 5, device: 'mobile' }).body as Record<string, unknown>
|
|
2095
|
+
expect(out.keyword).toBe('best CRM')
|
|
2096
|
+
expect(out.depth).toBe(5)
|
|
2097
|
+
expect(out.device).toBe('mobile')
|
|
2098
|
+
})
|
|
2099
|
+
|
|
2100
|
+
it('serp_youtube.mapParams: uses block_depth instead of depth', () => {
|
|
2101
|
+
const p = getProviders('search_seo').find((p) => p.id === 'serp_youtube')!
|
|
2102
|
+
const out = p.mapParams({ category: 'serp', keyword: 'CRM tutorial', limit: 5, engine: 'youtube' }).body as Record<string, unknown>
|
|
2103
|
+
expect(out.block_depth).toBe(5)
|
|
2104
|
+
expect(out.depth).toBeUndefined()
|
|
2105
|
+
})
|
|
2106
|
+
|
|
2107
|
+
it('bl_summary.isApplicable: true for category=backlinks+target', () => {
|
|
2108
|
+
const p = getProviders('search_seo').find((p) => p.id === 'bl_summary')!
|
|
2109
|
+
expect(p.isApplicable!({ category: 'backlinks', target: 'salesforce.com' })).toBe(true)
|
|
2110
|
+
expect(p.isApplicable!({ category: 'backlinks' })).toBe(false)
|
|
2111
|
+
expect(p.isApplicable!({ category: 'domain', target: 'salesforce.com' })).toBe(false)
|
|
2112
|
+
})
|
|
2113
|
+
|
|
2114
|
+
it('bl_summary.hasResult: checks total_count or rank under tasks wrapper', () => {
|
|
2115
|
+
const p = getProviders('search_seo').find((p) => p.id === 'bl_summary')!
|
|
2116
|
+
expect(p.hasResult({ tasks: [{ result: [{ total_count: 1000 }] }] })).toBe(true)
|
|
2117
|
+
expect(p.hasResult({ tasks: [{ result: [{ rank: 50 }] }] })).toBe(true)
|
|
2118
|
+
expect(p.hasResult({ tasks: [{ result: [{}] }] })).toBe(false)
|
|
2119
|
+
expect(p.hasResult({})).toBe(false)
|
|
2120
|
+
})
|
|
2121
|
+
|
|
2122
|
+
it('bl_backlinks.mapParams: passes target, limit, offset=0', () => {
|
|
2123
|
+
const p = getProviders('search_seo').find((p) => p.id === 'bl_backlinks')!
|
|
2124
|
+
const out = p.mapParams({ category: 'backlinks', target: 'salesforce.com', limit: 20 }).body as Record<string, unknown>
|
|
2125
|
+
expect(out.target).toBe('salesforce.com')
|
|
2126
|
+
expect(out.limit).toBe(20)
|
|
2127
|
+
expect(out.offset).toBe(0)
|
|
2128
|
+
})
|
|
2129
|
+
|
|
2130
|
+
it('domain_tech.isApplicable: true for category=domain+target when action is not whois', () => {
|
|
2131
|
+
const p = getProviders('search_seo').find((p) => p.id === 'domain_tech')!
|
|
2132
|
+
expect(p.isApplicable!({ category: 'domain', target: 'hubspot.com' })).toBe(true)
|
|
2133
|
+
expect(p.isApplicable!({ category: 'domain', target: 'hubspot.com', action: 'technologies' })).toBe(true)
|
|
2134
|
+
expect(p.isApplicable!({ category: 'domain', target: 'hubspot.com', action: 'whois' })).toBe(false)
|
|
2135
|
+
expect(p.isApplicable!({ category: 'domain' })).toBe(false)
|
|
2136
|
+
})
|
|
2137
|
+
|
|
2138
|
+
it('domain_whois.isApplicable: true only for action=whois', () => {
|
|
2139
|
+
const p = getProviders('search_seo').find((p) => p.id === 'domain_whois')!
|
|
2140
|
+
expect(p.isApplicable!({ category: 'domain', target: 'hubspot.com', action: 'whois' })).toBe(true)
|
|
2141
|
+
expect(p.isApplicable!({ category: 'domain', target: 'hubspot.com' })).toBe(false)
|
|
2142
|
+
expect(p.isApplicable!({ category: 'domain', target: 'hubspot.com', action: 'technologies' })).toBe(false)
|
|
2143
|
+
})
|
|
2144
|
+
|
|
2145
|
+
it('domain_whois.mapParams: uses filters array format', () => {
|
|
2146
|
+
const p = getProviders('search_seo').find((p) => p.id === 'domain_whois')!
|
|
2147
|
+
const out = p.mapParams({ category: 'domain', target: 'hubspot.com', action: 'whois', limit: 10 }).body as Record<string, unknown>
|
|
2148
|
+
expect(out.filters).toEqual([['domain', '=', 'hubspot.com']])
|
|
2149
|
+
expect(out.limit).toBe(10)
|
|
2150
|
+
})
|
|
2151
|
+
|
|
2152
|
+
it('labs_rank_overview.isApplicable: true for category=labs+target with no lab_action or rank-overview', () => {
|
|
2153
|
+
const p = getProviders('search_seo').find((p) => p.id === 'labs_rank_overview')!
|
|
2154
|
+
expect(p.isApplicable!({ category: 'labs', target: 'pipedrive.com' })).toBe(true)
|
|
2155
|
+
expect(p.isApplicable!({ category: 'labs', target: 'pipedrive.com', lab_action: 'rank-overview' })).toBe(true)
|
|
2156
|
+
expect(p.isApplicable!({ category: 'labs', target: 'pipedrive.com', lab_action: 'ranked-keywords' })).toBe(false)
|
|
2157
|
+
expect(p.isApplicable!({ category: 'labs' })).toBe(false)
|
|
2158
|
+
})
|
|
2159
|
+
|
|
2160
|
+
it('labs_ranked_kw.isApplicable: true for lab_action=ranked-keywords+target', () => {
|
|
2161
|
+
const p = getProviders('search_seo').find((p) => p.id === 'labs_ranked_kw')!
|
|
2162
|
+
expect(p.isApplicable!({ category: 'labs', target: 'pipedrive.com', lab_action: 'ranked-keywords' })).toBe(true)
|
|
2163
|
+
expect(p.isApplicable!({ category: 'labs', target: 'pipedrive.com' })).toBe(false)
|
|
2164
|
+
expect(p.isApplicable!({ category: 'labs', lab_action: 'ranked-keywords' })).toBe(false)
|
|
2165
|
+
})
|
|
2166
|
+
|
|
2167
|
+
it('labs_competitors.isApplicable: true for lab_action=competitors+target', () => {
|
|
2168
|
+
const p = getProviders('search_seo').find((p) => p.id === 'labs_competitors')!
|
|
2169
|
+
expect(p.isApplicable!({ category: 'labs', target: 'pipedrive.com', lab_action: 'competitors' })).toBe(true)
|
|
2170
|
+
expect(p.isApplicable!({ category: 'labs', target: 'pipedrive.com' })).toBe(false)
|
|
2171
|
+
})
|
|
2172
|
+
|
|
2173
|
+
it('labs_kw_ideas.isApplicable: true only for lab_action=keyword-ideas + keywords[]', () => {
|
|
2174
|
+
const p = getProviders('search_seo').find((p) => p.id === 'labs_kw_ideas')!
|
|
2175
|
+
expect(p.isApplicable!({ category: 'labs', keywords: ['CRM'], lab_action: 'keyword-ideas' })).toBe(true)
|
|
2176
|
+
expect(p.isApplicable!({ category: 'labs', keywords: [], lab_action: 'keyword-ideas' })).toBe(false)
|
|
2177
|
+
expect(p.isApplicable!({ category: 'labs', keywords: ['CRM'] })).toBe(false)
|
|
2178
|
+
})
|
|
2179
|
+
|
|
2180
|
+
it('page_lighthouse.isApplicable: true for category=page+url when page_action is not content', () => {
|
|
2181
|
+
const p = getProviders('search_seo').find((p) => p.id === 'page_lighthouse')!
|
|
2182
|
+
expect(p.isApplicable!({ category: 'page', url: 'https://coldiq.com' })).toBe(true)
|
|
2183
|
+
expect(p.isApplicable!({ category: 'page', url: 'https://coldiq.com', page_action: 'lighthouse' })).toBe(true)
|
|
2184
|
+
expect(p.isApplicable!({ category: 'page', url: 'https://coldiq.com', page_action: 'content' })).toBe(false)
|
|
2185
|
+
expect(p.isApplicable!({ category: 'page' })).toBe(false)
|
|
2186
|
+
})
|
|
2187
|
+
|
|
2188
|
+
it('page_content.isApplicable: true only for page_action=content', () => {
|
|
2189
|
+
const p = getProviders('search_seo').find((p) => p.id === 'page_content')!
|
|
2190
|
+
expect(p.isApplicable!({ category: 'page', url: 'https://coldiq.com', page_action: 'content' })).toBe(true)
|
|
2191
|
+
expect(p.isApplicable!({ category: 'page', url: 'https://coldiq.com' })).toBe(false)
|
|
2192
|
+
})
|
|
2193
|
+
|
|
2194
|
+
it('page_lighthouse.mapParams: maps url, enable_javascript, full_data', () => {
|
|
2195
|
+
const p = getProviders('search_seo').find((p) => p.id === 'page_lighthouse')!
|
|
2196
|
+
const out = p.mapParams({ url: 'https://coldiq.com', enable_javascript: true, full_data: true }).body as Record<string, unknown>
|
|
2197
|
+
expect(out.url).toBe('https://coldiq.com')
|
|
2198
|
+
expect(out.enable_javascript).toBe(true)
|
|
2199
|
+
expect(out.full_data).toBe(true)
|
|
2200
|
+
})
|
|
2201
|
+
|
|
2202
|
+
it('page_content.mapParams: maps url and enable_javascript, no full_data', () => {
|
|
2203
|
+
const p = getProviders('search_seo').find((p) => p.id === 'page_content')!
|
|
2204
|
+
const out = p.mapParams({ url: 'https://coldiq.com', enable_javascript: false }).body as Record<string, unknown>
|
|
2205
|
+
expect(out.url).toBe('https://coldiq.com')
|
|
2206
|
+
expect(out.enable_javascript).toBe(false)
|
|
2207
|
+
expect(out.full_data).toBeUndefined()
|
|
2208
|
+
})
|
|
2209
|
+
})
|
|
2210
|
+
})
|