domain-search-mcp 1.0.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/.env.example +52 -0
- package/Dockerfile +15 -0
- package/LICENSE +21 -0
- package/README.md +426 -0
- package/SECURITY.md +252 -0
- package/dist/config.d.ts +25 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +117 -0
- package/dist/config.js.map +1 -0
- package/dist/fallbacks/index.d.ts +6 -0
- package/dist/fallbacks/index.d.ts.map +1 -0
- package/dist/fallbacks/index.js +14 -0
- package/dist/fallbacks/index.js.map +1 -0
- package/dist/fallbacks/rdap.d.ts +18 -0
- package/dist/fallbacks/rdap.d.ts.map +1 -0
- package/dist/fallbacks/rdap.js +339 -0
- package/dist/fallbacks/rdap.js.map +1 -0
- package/dist/fallbacks/whois.d.ts +27 -0
- package/dist/fallbacks/whois.d.ts.map +1 -0
- package/dist/fallbacks/whois.js +219 -0
- package/dist/fallbacks/whois.js.map +1 -0
- package/dist/registrars/base.d.ts +89 -0
- package/dist/registrars/base.d.ts.map +1 -0
- package/dist/registrars/base.js +203 -0
- package/dist/registrars/base.js.map +1 -0
- package/dist/registrars/index.d.ts +7 -0
- package/dist/registrars/index.d.ts.map +1 -0
- package/dist/registrars/index.js +15 -0
- package/dist/registrars/index.js.map +1 -0
- package/dist/registrars/namecheap.d.ts +69 -0
- package/dist/registrars/namecheap.d.ts.map +1 -0
- package/dist/registrars/namecheap.js +307 -0
- package/dist/registrars/namecheap.js.map +1 -0
- package/dist/registrars/porkbun.d.ts +63 -0
- package/dist/registrars/porkbun.d.ts.map +1 -0
- package/dist/registrars/porkbun.js +299 -0
- package/dist/registrars/porkbun.js.map +1 -0
- package/dist/server.d.ts +19 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +209 -0
- package/dist/server.js.map +1 -0
- package/dist/services/domain-search.d.ts +40 -0
- package/dist/services/domain-search.d.ts.map +1 -0
- package/dist/services/domain-search.js +438 -0
- package/dist/services/domain-search.js.map +1 -0
- package/dist/services/index.d.ts +5 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +11 -0
- package/dist/services/index.js.map +1 -0
- package/dist/tools/bulk_search.d.ts +72 -0
- package/dist/tools/bulk_search.d.ts.map +1 -0
- package/dist/tools/bulk_search.js +108 -0
- package/dist/tools/bulk_search.js.map +1 -0
- package/dist/tools/check_socials.d.ts +71 -0
- package/dist/tools/check_socials.d.ts.map +1 -0
- package/dist/tools/check_socials.js +357 -0
- package/dist/tools/check_socials.js.map +1 -0
- package/dist/tools/compare_registrars.d.ts +80 -0
- package/dist/tools/compare_registrars.d.ts.map +1 -0
- package/dist/tools/compare_registrars.js +116 -0
- package/dist/tools/compare_registrars.js.map +1 -0
- package/dist/tools/index.d.ts +10 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +31 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/search_domain.d.ts +61 -0
- package/dist/tools/search_domain.d.ts.map +1 -0
- package/dist/tools/search_domain.js +81 -0
- package/dist/tools/search_domain.js.map +1 -0
- package/dist/tools/suggest_domains.d.ts +82 -0
- package/dist/tools/suggest_domains.d.ts.map +1 -0
- package/dist/tools/suggest_domains.js +227 -0
- package/dist/tools/suggest_domains.js.map +1 -0
- package/dist/tools/tld_info.d.ts +56 -0
- package/dist/tools/tld_info.d.ts.map +1 -0
- package/dist/tools/tld_info.js +273 -0
- package/dist/tools/tld_info.js.map +1 -0
- package/dist/types.d.ts +193 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +9 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/cache.d.ts +81 -0
- package/dist/utils/cache.d.ts.map +1 -0
- package/dist/utils/cache.js +192 -0
- package/dist/utils/cache.js.map +1 -0
- package/dist/utils/errors.d.ts +87 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +191 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/index.d.ts +8 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +24 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/logger.d.ts +27 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +132 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/premium-analyzer.d.ts +33 -0
- package/dist/utils/premium-analyzer.d.ts.map +1 -0
- package/dist/utils/premium-analyzer.js +273 -0
- package/dist/utils/premium-analyzer.js.map +1 -0
- package/dist/utils/validators.d.ts +53 -0
- package/dist/utils/validators.d.ts.map +1 -0
- package/dist/utils/validators.js +159 -0
- package/dist/utils/validators.js.map +1 -0
- package/docs/marketing/devto-post.md +135 -0
- package/docs/marketing/hackernews.md +42 -0
- package/docs/marketing/producthunt.md +109 -0
- package/docs/marketing/reddit-post.md +59 -0
- package/docs/marketing/twitter-thread.md +105 -0
- package/examples/bulk-search-50-domains.ts +131 -0
- package/examples/cli-interactive.ts +280 -0
- package/examples/compare-registrars.ts +78 -0
- package/examples/search-single-domain.ts +54 -0
- package/examples/suggest-names.ts +110 -0
- package/glama.json +6 -0
- package/jest.config.js +35 -0
- package/package.json +62 -0
- package/smithery.yaml +36 -0
- package/src/config.ts +121 -0
- package/src/fallbacks/index.ts +6 -0
- package/src/fallbacks/rdap.ts +407 -0
- package/src/fallbacks/whois.ts +250 -0
- package/src/registrars/base.ts +264 -0
- package/src/registrars/index.ts +7 -0
- package/src/registrars/namecheap.ts +378 -0
- package/src/registrars/porkbun.ts +380 -0
- package/src/server.ts +276 -0
- package/src/services/domain-search.ts +567 -0
- package/src/services/index.ts +9 -0
- package/src/tools/bulk_search.ts +142 -0
- package/src/tools/check_socials.ts +467 -0
- package/src/tools/compare_registrars.ts +162 -0
- package/src/tools/index.ts +45 -0
- package/src/tools/search_domain.ts +93 -0
- package/src/tools/suggest_domains.ts +284 -0
- package/src/tools/tld_info.ts +294 -0
- package/src/types.ts +289 -0
- package/src/utils/cache.ts +238 -0
- package/src/utils/errors.ts +262 -0
- package/src/utils/index.ts +8 -0
- package/src/utils/logger.ts +162 -0
- package/src/utils/premium-analyzer.ts +303 -0
- package/src/utils/validators.ts +193 -0
- package/tests/premium-analyzer.test.ts +310 -0
- package/tests/unit/cache.test.ts +123 -0
- package/tests/unit/errors.test.ts +190 -0
- package/tests/unit/tld-info.test.ts +62 -0
- package/tests/unit/tools.test.ts +200 -0
- package/tests/unit/validators.test.ts +146 -0
- package/tsconfig.json +25 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Premium Domain Analyzer.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, it } from '@jest/globals';
|
|
6
|
+
import {
|
|
7
|
+
analyzePremiumReason,
|
|
8
|
+
generatePremiumInsight,
|
|
9
|
+
suggestPremiumAlternatives,
|
|
10
|
+
calculateDomainScore,
|
|
11
|
+
generatePremiumSummary,
|
|
12
|
+
} from '../src/utils/premium-analyzer.js';
|
|
13
|
+
import type { DomainResult } from '../src/types.js';
|
|
14
|
+
|
|
15
|
+
describe('Premium Analyzer', () => {
|
|
16
|
+
describe('analyzePremiumReason', () => {
|
|
17
|
+
it('should detect single character domains', () => {
|
|
18
|
+
const reasons = analyzePremiumReason('x.com');
|
|
19
|
+
expect(reasons).toContain('Single character domain (extremely rare)');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should detect two-character domains', () => {
|
|
23
|
+
const reasons = analyzePremiumReason('ai.io');
|
|
24
|
+
expect(reasons.some(r => r.includes('Two-character'))).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should detect three-character domains', () => {
|
|
28
|
+
const reasons = analyzePremiumReason('app.dev');
|
|
29
|
+
expect(reasons.some(r => r.includes('Three-character'))).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should detect premium keywords', () => {
|
|
33
|
+
const reasons = analyzePremiumReason('cloud.io');
|
|
34
|
+
expect(reasons.some(r => r.includes('Popular keyword'))).toBe(true);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should detect numeric patterns', () => {
|
|
38
|
+
const reasons = analyzePremiumReason('123.com');
|
|
39
|
+
expect(reasons.some(r => r.includes('numeric pattern'))).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should detect repeating characters', () => {
|
|
43
|
+
const reasons = analyzePremiumReason('aaa.io');
|
|
44
|
+
expect(reasons.some(r => r.includes('Repeating character'))).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should detect high-demand TLDs', () => {
|
|
48
|
+
const reasons = analyzePremiumReason('mysite.io');
|
|
49
|
+
expect(reasons.some(r => r.includes('High-demand .io'))).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should return empty for non-premium domains', () => {
|
|
53
|
+
const reasons = analyzePremiumReason('myawesomewebsite.net');
|
|
54
|
+
// Long, non-keyword domain on .net should have no premium reasons
|
|
55
|
+
expect(reasons.length).toBe(0);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('generatePremiumInsight', () => {
|
|
60
|
+
it('should generate insight for premium domain with price', () => {
|
|
61
|
+
const result: DomainResult = {
|
|
62
|
+
domain: 'ai.io',
|
|
63
|
+
available: true,
|
|
64
|
+
premium: true,
|
|
65
|
+
price_first_year: 5000,
|
|
66
|
+
price_renewal: 500,
|
|
67
|
+
currency: 'USD',
|
|
68
|
+
privacy_included: false,
|
|
69
|
+
transfer_price: null,
|
|
70
|
+
registrar: 'porkbun',
|
|
71
|
+
source: 'porkbun_api',
|
|
72
|
+
checked_at: new Date().toISOString(),
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const insight = generatePremiumInsight(result);
|
|
76
|
+
expect(insight).not.toBeNull();
|
|
77
|
+
expect(insight).toContain('ai.io');
|
|
78
|
+
expect(insight).toContain('$5000');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should return null for non-premium domains', () => {
|
|
82
|
+
const result: DomainResult = {
|
|
83
|
+
domain: 'mywebsite.com',
|
|
84
|
+
available: true,
|
|
85
|
+
premium: false,
|
|
86
|
+
price_first_year: 10,
|
|
87
|
+
price_renewal: 12,
|
|
88
|
+
currency: 'USD',
|
|
89
|
+
privacy_included: true,
|
|
90
|
+
transfer_price: null,
|
|
91
|
+
registrar: 'porkbun',
|
|
92
|
+
source: 'porkbun_api',
|
|
93
|
+
checked_at: new Date().toISOString(),
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const insight = generatePremiumInsight(result);
|
|
97
|
+
expect(insight).toBeNull();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should return null for unavailable domains', () => {
|
|
101
|
+
const result: DomainResult = {
|
|
102
|
+
domain: 'google.com',
|
|
103
|
+
available: false,
|
|
104
|
+
premium: true,
|
|
105
|
+
price_first_year: null,
|
|
106
|
+
price_renewal: null,
|
|
107
|
+
currency: 'USD',
|
|
108
|
+
privacy_included: false,
|
|
109
|
+
transfer_price: null,
|
|
110
|
+
registrar: 'rdap',
|
|
111
|
+
source: 'rdap',
|
|
112
|
+
checked_at: new Date().toISOString(),
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const insight = generatePremiumInsight(result);
|
|
116
|
+
expect(insight).toBeNull();
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe('suggestPremiumAlternatives', () => {
|
|
121
|
+
it('should suggest alternatives for premium domain', () => {
|
|
122
|
+
const alternatives = suggestPremiumAlternatives('app.io');
|
|
123
|
+
expect(alternatives.length).toBeGreaterThan(0);
|
|
124
|
+
expect(alternatives.length).toBeLessThanOrEqual(3);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should include prefix variations', () => {
|
|
128
|
+
const alternatives = suggestPremiumAlternatives('cloud.com');
|
|
129
|
+
const hasPrefix = alternatives.some(a =>
|
|
130
|
+
a.startsWith('get') || a.startsWith('try') || a.startsWith('use') ||
|
|
131
|
+
a.startsWith('my') || a.startsWith('the') || a.startsWith('go')
|
|
132
|
+
);
|
|
133
|
+
expect(hasPrefix).toBe(true);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should include suffix variations', () => {
|
|
137
|
+
const alternatives = suggestPremiumAlternatives('tech.io');
|
|
138
|
+
const hasSuffix = alternatives.some(a =>
|
|
139
|
+
a.includes('app') || a.includes('hq') || a.includes('io') ||
|
|
140
|
+
a.includes('now') || a.includes('hub') || a.includes('labs')
|
|
141
|
+
);
|
|
142
|
+
expect(hasSuffix).toBe(true);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe('calculateDomainScore', () => {
|
|
147
|
+
it('should return 0 for unavailable domains', () => {
|
|
148
|
+
const result: DomainResult = {
|
|
149
|
+
domain: 'taken.com',
|
|
150
|
+
available: false,
|
|
151
|
+
premium: false,
|
|
152
|
+
price_first_year: null,
|
|
153
|
+
price_renewal: null,
|
|
154
|
+
currency: 'USD',
|
|
155
|
+
privacy_included: false,
|
|
156
|
+
transfer_price: null,
|
|
157
|
+
registrar: 'rdap',
|
|
158
|
+
source: 'rdap',
|
|
159
|
+
checked_at: new Date().toISOString(),
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
expect(calculateDomainScore(result)).toBe(0);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('should give higher score for domains with privacy', () => {
|
|
166
|
+
const withPrivacy: DomainResult = {
|
|
167
|
+
domain: 'test.com',
|
|
168
|
+
available: true,
|
|
169
|
+
premium: false,
|
|
170
|
+
price_first_year: 10,
|
|
171
|
+
price_renewal: 12,
|
|
172
|
+
currency: 'USD',
|
|
173
|
+
privacy_included: true,
|
|
174
|
+
transfer_price: null,
|
|
175
|
+
registrar: 'porkbun',
|
|
176
|
+
source: 'porkbun_api',
|
|
177
|
+
checked_at: new Date().toISOString(),
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const withoutPrivacy: DomainResult = {
|
|
181
|
+
...withPrivacy,
|
|
182
|
+
privacy_included: false,
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
expect(calculateDomainScore(withPrivacy)).toBeGreaterThan(calculateDomainScore(withoutPrivacy));
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('should penalize premium domains', () => {
|
|
189
|
+
const nonPremium: DomainResult = {
|
|
190
|
+
domain: 'test.com',
|
|
191
|
+
available: true,
|
|
192
|
+
premium: false,
|
|
193
|
+
price_first_year: 10,
|
|
194
|
+
price_renewal: 12,
|
|
195
|
+
currency: 'USD',
|
|
196
|
+
privacy_included: false,
|
|
197
|
+
transfer_price: null,
|
|
198
|
+
registrar: 'porkbun',
|
|
199
|
+
source: 'porkbun_api',
|
|
200
|
+
checked_at: new Date().toISOString(),
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const premium: DomainResult = {
|
|
204
|
+
...nonPremium,
|
|
205
|
+
premium: true,
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
expect(calculateDomainScore(nonPremium)).toBeGreaterThan(calculateDomainScore(premium));
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('should return score between 0 and 10', () => {
|
|
212
|
+
const result: DomainResult = {
|
|
213
|
+
domain: 'mybrand.io',
|
|
214
|
+
available: true,
|
|
215
|
+
premium: false,
|
|
216
|
+
price_first_year: 40,
|
|
217
|
+
price_renewal: 50,
|
|
218
|
+
currency: 'USD',
|
|
219
|
+
privacy_included: true,
|
|
220
|
+
transfer_price: null,
|
|
221
|
+
registrar: 'porkbun',
|
|
222
|
+
source: 'porkbun_api',
|
|
223
|
+
checked_at: new Date().toISOString(),
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const score = calculateDomainScore(result);
|
|
227
|
+
expect(score).toBeGreaterThanOrEqual(0);
|
|
228
|
+
expect(score).toBeLessThanOrEqual(10);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
describe('generatePremiumSummary', () => {
|
|
233
|
+
it('should return empty array when no premium domains', () => {
|
|
234
|
+
const results: DomainResult[] = [
|
|
235
|
+
{
|
|
236
|
+
domain: 'mybrand.com',
|
|
237
|
+
available: true,
|
|
238
|
+
premium: false,
|
|
239
|
+
price_first_year: 10,
|
|
240
|
+
price_renewal: 12,
|
|
241
|
+
currency: 'USD',
|
|
242
|
+
privacy_included: true,
|
|
243
|
+
transfer_price: null,
|
|
244
|
+
registrar: 'porkbun',
|
|
245
|
+
source: 'porkbun_api',
|
|
246
|
+
checked_at: new Date().toISOString(),
|
|
247
|
+
},
|
|
248
|
+
];
|
|
249
|
+
|
|
250
|
+
const summary = generatePremiumSummary(results);
|
|
251
|
+
expect(summary).toEqual([]);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('should generate summary for premium domains', () => {
|
|
255
|
+
const results: DomainResult[] = [
|
|
256
|
+
{
|
|
257
|
+
domain: 'ai.io',
|
|
258
|
+
available: true,
|
|
259
|
+
premium: true,
|
|
260
|
+
price_first_year: 5000,
|
|
261
|
+
price_renewal: 500,
|
|
262
|
+
currency: 'USD',
|
|
263
|
+
privacy_included: false,
|
|
264
|
+
transfer_price: null,
|
|
265
|
+
registrar: 'porkbun',
|
|
266
|
+
source: 'porkbun_api',
|
|
267
|
+
checked_at: new Date().toISOString(),
|
|
268
|
+
},
|
|
269
|
+
];
|
|
270
|
+
|
|
271
|
+
const summary = generatePremiumSummary(results);
|
|
272
|
+
expect(summary.length).toBeGreaterThan(0);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('should suggest alternatives when all available domains are premium', () => {
|
|
276
|
+
const results: DomainResult[] = [
|
|
277
|
+
{
|
|
278
|
+
domain: 'cloud.io',
|
|
279
|
+
available: true,
|
|
280
|
+
premium: true,
|
|
281
|
+
price_first_year: 2000,
|
|
282
|
+
price_renewal: 200,
|
|
283
|
+
currency: 'USD',
|
|
284
|
+
privacy_included: false,
|
|
285
|
+
transfer_price: null,
|
|
286
|
+
registrar: 'porkbun',
|
|
287
|
+
source: 'porkbun_api',
|
|
288
|
+
checked_at: new Date().toISOString(),
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
domain: 'cloud.com',
|
|
292
|
+
available: false,
|
|
293
|
+
premium: false,
|
|
294
|
+
price_first_year: null,
|
|
295
|
+
price_renewal: null,
|
|
296
|
+
currency: 'USD',
|
|
297
|
+
privacy_included: false,
|
|
298
|
+
transfer_price: null,
|
|
299
|
+
registrar: 'rdap',
|
|
300
|
+
source: 'rdap',
|
|
301
|
+
checked_at: new Date().toISOString(),
|
|
302
|
+
},
|
|
303
|
+
];
|
|
304
|
+
|
|
305
|
+
const summary = generatePremiumSummary(results);
|
|
306
|
+
const hasSuggestion = summary.some(s => s.includes('Try:'));
|
|
307
|
+
expect(hasSuggestion).toBe(true);
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit Tests for TTL Cache.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { TtlCache, domainCacheKey, tldCacheKey, getOrCompute } from '../../src/utils/cache';
|
|
6
|
+
|
|
7
|
+
describe('TtlCache', () => {
|
|
8
|
+
let cache: TtlCache<string>;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
cache = new TtlCache<string>(1); // 1 second TTL
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
cache.destroy();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('should store and retrieve values', () => {
|
|
19
|
+
cache.set('key1', 'value1');
|
|
20
|
+
expect(cache.get('key1')).toBe('value1');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should return undefined for missing keys', () => {
|
|
24
|
+
expect(cache.get('nonexistent')).toBeUndefined();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should track cache size', () => {
|
|
28
|
+
expect(cache.size).toBe(0);
|
|
29
|
+
cache.set('key1', 'value1');
|
|
30
|
+
expect(cache.size).toBe(1);
|
|
31
|
+
cache.set('key2', 'value2');
|
|
32
|
+
expect(cache.size).toBe(2);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should delete keys', () => {
|
|
36
|
+
cache.set('key1', 'value1');
|
|
37
|
+
expect(cache.has('key1')).toBe(true);
|
|
38
|
+
cache.delete('key1');
|
|
39
|
+
expect(cache.has('key1')).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should clear all entries', () => {
|
|
43
|
+
cache.set('key1', 'value1');
|
|
44
|
+
cache.set('key2', 'value2');
|
|
45
|
+
cache.clear();
|
|
46
|
+
expect(cache.size).toBe(0);
|
|
47
|
+
expect(cache.get('key1')).toBeUndefined();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should expire entries after TTL', async () => {
|
|
51
|
+
cache.set('key1', 'value1');
|
|
52
|
+
expect(cache.get('key1')).toBe('value1');
|
|
53
|
+
|
|
54
|
+
// Wait for TTL to expire
|
|
55
|
+
await new Promise((resolve) => setTimeout(resolve, 1100));
|
|
56
|
+
|
|
57
|
+
expect(cache.get('key1')).toBeUndefined();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should allow custom TTL per entry', async () => {
|
|
61
|
+
cache.set('key1', 'value1', 100); // 100ms TTL
|
|
62
|
+
expect(cache.get('key1')).toBe('value1');
|
|
63
|
+
|
|
64
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
65
|
+
|
|
66
|
+
expect(cache.get('key1')).toBeUndefined();
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('Cache Key Generators', () => {
|
|
71
|
+
it('should generate domain cache keys', () => {
|
|
72
|
+
expect(domainCacheKey('vibecoding.com', 'porkbun')).toBe(
|
|
73
|
+
'domain:vibecoding.com:porkbun',
|
|
74
|
+
);
|
|
75
|
+
expect(domainCacheKey('TEST.IO', 'rdap')).toBe('domain:test.io:rdap');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should generate TLD cache keys', () => {
|
|
79
|
+
expect(tldCacheKey('com')).toBe('tld:com');
|
|
80
|
+
expect(tldCacheKey('IO')).toBe('tld:io');
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('getOrCompute', () => {
|
|
85
|
+
let cache: TtlCache<string>;
|
|
86
|
+
|
|
87
|
+
beforeEach(() => {
|
|
88
|
+
cache = new TtlCache<string>(60);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
afterEach(() => {
|
|
92
|
+
cache.destroy();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should compute and cache value on first call', async () => {
|
|
96
|
+
let computeCount = 0;
|
|
97
|
+
const compute = async () => {
|
|
98
|
+
computeCount++;
|
|
99
|
+
return 'computed';
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const result = await getOrCompute(cache, 'key1', compute);
|
|
103
|
+
|
|
104
|
+
expect(result.value).toBe('computed');
|
|
105
|
+
expect(result.fromCache).toBe(false);
|
|
106
|
+
expect(computeCount).toBe(1);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should return cached value on subsequent calls', async () => {
|
|
110
|
+
let computeCount = 0;
|
|
111
|
+
const compute = async () => {
|
|
112
|
+
computeCount++;
|
|
113
|
+
return 'computed';
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
await getOrCompute(cache, 'key1', compute);
|
|
117
|
+
const result = await getOrCompute(cache, 'key1', compute);
|
|
118
|
+
|
|
119
|
+
expect(result.value).toBe('computed');
|
|
120
|
+
expect(result.fromCache).toBe(true);
|
|
121
|
+
expect(computeCount).toBe(1); // Compute only called once
|
|
122
|
+
});
|
|
123
|
+
});
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit Tests for Custom Errors.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
DomainSearchError,
|
|
7
|
+
InvalidDomainError,
|
|
8
|
+
UnsupportedTldError,
|
|
9
|
+
RateLimitError,
|
|
10
|
+
RegistrarApiError,
|
|
11
|
+
AuthenticationError,
|
|
12
|
+
NoSourceAvailableError,
|
|
13
|
+
TimeoutError,
|
|
14
|
+
ConfigurationError,
|
|
15
|
+
wrapError,
|
|
16
|
+
} from '../../src/utils/errors';
|
|
17
|
+
|
|
18
|
+
describe('DomainSearchError', () => {
|
|
19
|
+
it('should create error with all properties', () => {
|
|
20
|
+
const error = new DomainSearchError(
|
|
21
|
+
'TEST_CODE',
|
|
22
|
+
'Technical message',
|
|
23
|
+
'User-friendly message',
|
|
24
|
+
{
|
|
25
|
+
retryable: true,
|
|
26
|
+
suggestedAction: 'Try again',
|
|
27
|
+
},
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
expect(error.code).toBe('TEST_CODE');
|
|
31
|
+
expect(error.message).toBe('Technical message');
|
|
32
|
+
expect(error.userMessage).toBe('User-friendly message');
|
|
33
|
+
expect(error.retryable).toBe(true);
|
|
34
|
+
expect(error.suggestedAction).toBe('Try again');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should convert to JSON correctly', () => {
|
|
38
|
+
const error = new DomainSearchError(
|
|
39
|
+
'TEST_CODE',
|
|
40
|
+
'Technical message',
|
|
41
|
+
'User-friendly message',
|
|
42
|
+
{ retryable: false },
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const json = error.toJSON();
|
|
46
|
+
|
|
47
|
+
expect(json).toEqual({
|
|
48
|
+
code: 'TEST_CODE',
|
|
49
|
+
message: 'User-friendly message',
|
|
50
|
+
retryable: false,
|
|
51
|
+
suggestedAction: undefined,
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('InvalidDomainError', () => {
|
|
57
|
+
it('should create with domain and reason', () => {
|
|
58
|
+
const error = new InvalidDomainError('my_domain', 'Contains underscore');
|
|
59
|
+
|
|
60
|
+
expect(error.code).toBe('INVALID_DOMAIN');
|
|
61
|
+
expect(error.message).toContain('my_domain');
|
|
62
|
+
expect(error.message).toContain('Contains underscore');
|
|
63
|
+
expect(error.retryable).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('UnsupportedTldError', () => {
|
|
68
|
+
it('should create with TLD and suggestions', () => {
|
|
69
|
+
const error = new UnsupportedTldError('xyz', ['com', 'io', 'dev']);
|
|
70
|
+
|
|
71
|
+
expect(error.code).toBe('UNSUPPORTED_TLD');
|
|
72
|
+
expect(error.message).toContain('.xyz');
|
|
73
|
+
expect(error.suggestedAction).toContain('com');
|
|
74
|
+
expect(error.retryable).toBe(false);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('RateLimitError', () => {
|
|
79
|
+
it('should create with registrar name', () => {
|
|
80
|
+
const error = new RateLimitError('porkbun');
|
|
81
|
+
|
|
82
|
+
expect(error.code).toBe('RATE_LIMIT');
|
|
83
|
+
expect(error.message).toContain('porkbun');
|
|
84
|
+
expect(error.retryable).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should include retry-after time', () => {
|
|
88
|
+
const error = new RateLimitError('porkbun', 30);
|
|
89
|
+
|
|
90
|
+
expect(error.suggestedAction).toContain('30 seconds');
|
|
91
|
+
expect(error.retryAfter).toBeDefined();
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('RegistrarApiError', () => {
|
|
96
|
+
it('should create with registrar and message', () => {
|
|
97
|
+
const error = new RegistrarApiError('namecheap', 'Connection failed');
|
|
98
|
+
|
|
99
|
+
expect(error.code).toBe('REGISTRAR_API_ERROR');
|
|
100
|
+
expect(error.message).toContain('namecheap');
|
|
101
|
+
expect(error.message).toContain('Connection failed');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should be retryable for server errors', () => {
|
|
105
|
+
const error = new RegistrarApiError('porkbun', 'Server error', 503);
|
|
106
|
+
|
|
107
|
+
expect(error.retryable).toBe(true);
|
|
108
|
+
expect(error.statusCode).toBe(503);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should not be retryable for client errors', () => {
|
|
112
|
+
const error = new RegistrarApiError('porkbun', 'Bad request', 400);
|
|
113
|
+
|
|
114
|
+
expect(error.retryable).toBe(false);
|
|
115
|
+
expect(error.statusCode).toBe(400);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('AuthenticationError', () => {
|
|
120
|
+
it('should create with registrar name', () => {
|
|
121
|
+
const error = new AuthenticationError('porkbun');
|
|
122
|
+
|
|
123
|
+
expect(error.code).toBe('AUTH_ERROR');
|
|
124
|
+
expect(error.message).toContain('porkbun');
|
|
125
|
+
expect(error.suggestedAction).toContain('PORKBUN_API_KEY');
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe('NoSourceAvailableError', () => {
|
|
130
|
+
it('should list tried sources', () => {
|
|
131
|
+
const error = new NoSourceAvailableError('example.com', [
|
|
132
|
+
'porkbun',
|
|
133
|
+
'rdap',
|
|
134
|
+
'whois',
|
|
135
|
+
]);
|
|
136
|
+
|
|
137
|
+
expect(error.code).toBe('NO_SOURCE_AVAILABLE');
|
|
138
|
+
expect(error.message).toContain('porkbun');
|
|
139
|
+
expect(error.message).toContain('rdap');
|
|
140
|
+
expect(error.message).toContain('whois');
|
|
141
|
+
expect(error.retryable).toBe(true);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe('TimeoutError', () => {
|
|
146
|
+
it('should include operation and timeout', () => {
|
|
147
|
+
const error = new TimeoutError('API call', 5000);
|
|
148
|
+
|
|
149
|
+
expect(error.code).toBe('TIMEOUT');
|
|
150
|
+
expect(error.message).toContain('API call');
|
|
151
|
+
expect(error.message).toContain('5000');
|
|
152
|
+
expect(error.retryable).toBe(true);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe('ConfigurationError', () => {
|
|
157
|
+
it('should include missing config and fix', () => {
|
|
158
|
+
const error = new ConfigurationError('API_KEY', 'Add it to .env file');
|
|
159
|
+
|
|
160
|
+
expect(error.code).toBe('CONFIG_ERROR');
|
|
161
|
+
expect(error.message).toContain('API_KEY');
|
|
162
|
+
expect(error.suggestedAction).toBe('Add it to .env file');
|
|
163
|
+
expect(error.retryable).toBe(false);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe('wrapError', () => {
|
|
168
|
+
it('should return DomainSearchError as-is', () => {
|
|
169
|
+
const original = new InvalidDomainError('test', 'reason');
|
|
170
|
+
const wrapped = wrapError(original);
|
|
171
|
+
|
|
172
|
+
expect(wrapped).toBe(original);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('should wrap regular Error', () => {
|
|
176
|
+
const original = new Error('Something failed');
|
|
177
|
+
const wrapped = wrapError(original);
|
|
178
|
+
|
|
179
|
+
expect(wrapped).toBeInstanceOf(DomainSearchError);
|
|
180
|
+
expect(wrapped.code).toBe('UNKNOWN_ERROR');
|
|
181
|
+
expect(wrapped.message).toBe('Something failed');
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('should wrap string errors', () => {
|
|
185
|
+
const wrapped = wrapError('String error');
|
|
186
|
+
|
|
187
|
+
expect(wrapped).toBeInstanceOf(DomainSearchError);
|
|
188
|
+
expect(wrapped.message).toBe('String error');
|
|
189
|
+
});
|
|
190
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit Tests for TLD Info Tool.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { executeTldInfo } from '../../src/tools/tld_info';
|
|
6
|
+
|
|
7
|
+
describe('executeTldInfo', () => {
|
|
8
|
+
it('should return info for common TLDs', async () => {
|
|
9
|
+
const result = await executeTldInfo({ tld: 'com', detailed: false });
|
|
10
|
+
|
|
11
|
+
expect(result.tld).toBe('com');
|
|
12
|
+
expect(result.description).toContain('Commercial');
|
|
13
|
+
expect(result.popularity).toBe('high');
|
|
14
|
+
expect(result.category).toBe('generic');
|
|
15
|
+
expect(result.price_range).toBeDefined();
|
|
16
|
+
expect(result.insights).toBeInstanceOf(Array);
|
|
17
|
+
expect(result.recommendation).toBeDefined();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should return info for tech TLDs', async () => {
|
|
21
|
+
const result = await executeTldInfo({ tld: 'io', detailed: false });
|
|
22
|
+
|
|
23
|
+
expect(result.tld).toBe('io');
|
|
24
|
+
expect(result.description).toContain('tech');
|
|
25
|
+
expect(result.popularity).toBe('high');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should return info for dev TLD with restrictions', async () => {
|
|
29
|
+
const result = await executeTldInfo({ tld: 'dev', detailed: false });
|
|
30
|
+
|
|
31
|
+
expect(result.tld).toBe('dev');
|
|
32
|
+
expect(result.restrictions).toContain('Requires HTTPS (HSTS preloaded)');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should normalize TLD input', async () => {
|
|
36
|
+
const result = await executeTldInfo({ tld: '.COM', detailed: false });
|
|
37
|
+
|
|
38
|
+
expect(result.tld).toBe('com');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should return generic info for unknown TLDs', async () => {
|
|
42
|
+
const result = await executeTldInfo({ tld: 'xyz', detailed: false });
|
|
43
|
+
|
|
44
|
+
expect(result.tld).toBe('xyz');
|
|
45
|
+
expect(result.insights).toBeInstanceOf(Array);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should include price insights for cheap TLDs', async () => {
|
|
49
|
+
const result = await executeTldInfo({ tld: 'xyz', detailed: false });
|
|
50
|
+
|
|
51
|
+
const hasPriceInsight = result.insights.some((i) =>
|
|
52
|
+
i.includes('Budget') || i.includes('$'),
|
|
53
|
+
);
|
|
54
|
+
expect(hasPriceInsight).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should include recommendations', async () => {
|
|
58
|
+
const result = await executeTldInfo({ tld: 'ai', detailed: false });
|
|
59
|
+
|
|
60
|
+
expect(result.recommendation).toContain('AI');
|
|
61
|
+
});
|
|
62
|
+
});
|