registryx-server 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/README.md +203 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +146 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +210 -0
- package/coverage/lcov-report/src/adapters/crates.ts.html +847 -0
- package/coverage/lcov-report/src/adapters/index.html +176 -0
- package/coverage/lcov-report/src/adapters/index.ts.html +97 -0
- package/coverage/lcov-report/src/adapters/maven.ts.html +637 -0
- package/coverage/lcov-report/src/adapters/npm.ts.html +817 -0
- package/coverage/lcov-report/src/adapters/pypi.ts.html +730 -0
- package/coverage/lcov-report/src/cache.ts.html +202 -0
- package/coverage/lcov-report/src/config.ts.html +208 -0
- package/coverage/lcov-report/src/index.html +161 -0
- package/coverage/lcov-report/src/server.ts.html +1339 -0
- package/coverage/lcov-report/src/types.ts.html +373 -0
- package/coverage/lcov-report/src/utils/fetch.ts.html +220 -0
- package/coverage/lcov-report/src/utils/format.ts.html +130 -0
- package/coverage/lcov-report/src/utils/index.html +131 -0
- package/coverage/lcov.info +1686 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1210 -0
- package/eslint.config.mjs +16 -0
- package/package.json +41 -0
- package/src/adapters/crates.ts +254 -0
- package/src/adapters/index.ts +4 -0
- package/src/adapters/maven.ts +184 -0
- package/src/adapters/npm.ts +244 -0
- package/src/adapters/pypi.ts +215 -0
- package/src/cache.ts +39 -0
- package/src/config.ts +41 -0
- package/src/index.ts +25 -0
- package/src/server.ts +418 -0
- package/src/types.ts +96 -0
- package/src/utils/fetch.ts +45 -0
- package/src/utils/format.ts +15 -0
- package/test/adapters.test.ts +575 -0
- package/test/cache.test.ts +47 -0
- package/test/config.test.ts +69 -0
- package/test/fetch.test.ts +51 -0
- package/test/format.test.ts +35 -0
- package/test/server.test.ts +23 -0
- package/tsconfig.json +18 -0
- package/tsup.config.ts +11 -0
- package/vitest.config.ts +19 -0
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { NpmAdapter } from '../src/adapters/npm.js';
|
|
3
|
+
import { PypiAdapter } from '../src/adapters/pypi.js';
|
|
4
|
+
import { MavenAdapter } from '../src/adapters/maven.js';
|
|
5
|
+
import { CratesAdapter } from '../src/adapters/crates.js';
|
|
6
|
+
import { Cache } from '../src/cache.js';
|
|
7
|
+
import type { Config } from '../src/config.js';
|
|
8
|
+
|
|
9
|
+
const config: Config = {
|
|
10
|
+
registries: ['npm', 'pypi', 'maven', 'crates'],
|
|
11
|
+
timeoutMs: 5000,
|
|
12
|
+
cacheTtlMs: 60000,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function mockFetch(data: unknown, ok = true) {
|
|
16
|
+
return vi.fn().mockResolvedValue({
|
|
17
|
+
ok,
|
|
18
|
+
status: ok ? 200 : 404,
|
|
19
|
+
json: () => Promise.resolve(data),
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('NpmAdapter', () => {
|
|
24
|
+
let adapter: NpmAdapter;
|
|
25
|
+
let cache: Cache;
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
cache = new Cache(60000);
|
|
29
|
+
adapter = new NpmAdapter(config, cache);
|
|
30
|
+
vi.unstubAllGlobals();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('search returns packages', async () => {
|
|
34
|
+
vi.stubGlobal(
|
|
35
|
+
'fetch',
|
|
36
|
+
mockFetch({
|
|
37
|
+
objects: [
|
|
38
|
+
{ package: { name: 'express', version: '4.18.0', description: 'Web framework' } },
|
|
39
|
+
],
|
|
40
|
+
})
|
|
41
|
+
);
|
|
42
|
+
const results = await adapter.search('express', 5);
|
|
43
|
+
expect(results).toHaveLength(1);
|
|
44
|
+
expect(results[0].name).toBe('express');
|
|
45
|
+
expect(results[0].registry).toBe('npm');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('getPackage returns metadata', async () => {
|
|
49
|
+
vi.stubGlobal(
|
|
50
|
+
'fetch',
|
|
51
|
+
mockFetch({
|
|
52
|
+
name: 'express',
|
|
53
|
+
'dist-tags': { latest: '4.18.0' },
|
|
54
|
+
description: 'Web framework',
|
|
55
|
+
license: 'MIT',
|
|
56
|
+
homepage: 'https://expressjs.com',
|
|
57
|
+
repository: { url: 'git+https://github.com/expressjs/express.git' },
|
|
58
|
+
keywords: ['web', 'framework'],
|
|
59
|
+
versions: { '4.18.0': {} },
|
|
60
|
+
})
|
|
61
|
+
);
|
|
62
|
+
const pkg = await adapter.getPackage('express');
|
|
63
|
+
expect(pkg.name).toBe('express');
|
|
64
|
+
expect(pkg.license).toBe('MIT');
|
|
65
|
+
expect(pkg.registry).toBe('npm');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('listVersions returns versions', async () => {
|
|
69
|
+
vi.stubGlobal(
|
|
70
|
+
'fetch',
|
|
71
|
+
mockFetch({
|
|
72
|
+
versions: { '1.0.0': {}, '2.0.0-beta.1': {}, '2.0.0': {} },
|
|
73
|
+
time: { '1.0.0': '2020-01-01', '2.0.0-beta.1': '2021-01-01', '2.0.0': '2022-01-01' },
|
|
74
|
+
})
|
|
75
|
+
);
|
|
76
|
+
const versions = await adapter.listVersions('express', 10, true);
|
|
77
|
+
expect(versions.length).toBeGreaterThanOrEqual(1);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('getVersion returns detail', async () => {
|
|
81
|
+
vi.stubGlobal(
|
|
82
|
+
'fetch',
|
|
83
|
+
mockFetch({
|
|
84
|
+
name: 'express',
|
|
85
|
+
version: '4.18.0',
|
|
86
|
+
license: 'MIT',
|
|
87
|
+
dist: { unpackedSize: 204800 },
|
|
88
|
+
dependencies: { accepts: '~1.3.8' },
|
|
89
|
+
})
|
|
90
|
+
);
|
|
91
|
+
const v = await adapter.getVersion('express', '4.18.0');
|
|
92
|
+
expect(v.name).toBe('express');
|
|
93
|
+
expect(v.dependencies.length).toBe(1);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('getDependencies returns runtime deps', async () => {
|
|
97
|
+
vi.stubGlobal(
|
|
98
|
+
'fetch',
|
|
99
|
+
mockFetch({
|
|
100
|
+
dependencies: { lodash: '^4.0.0' },
|
|
101
|
+
devDependencies: { vitest: '^2.0.0' },
|
|
102
|
+
})
|
|
103
|
+
);
|
|
104
|
+
const deps = await adapter.getDependencies('test', '1.0.0', 'runtime');
|
|
105
|
+
expect(deps.length).toBe(1);
|
|
106
|
+
expect(deps[0].type).toBe('runtime');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('getDependencies returns all deps', async () => {
|
|
110
|
+
vi.stubGlobal(
|
|
111
|
+
'fetch',
|
|
112
|
+
mockFetch({
|
|
113
|
+
dependencies: { lodash: '^4.0.0' },
|
|
114
|
+
devDependencies: { vitest: '^2.0.0' },
|
|
115
|
+
})
|
|
116
|
+
);
|
|
117
|
+
const deps = await adapter.getDependencies('test', '1.0.0', 'all');
|
|
118
|
+
expect(deps.length).toBe(2);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('getDownloadStats returns stats', async () => {
|
|
122
|
+
vi.stubGlobal('fetch', mockFetch({ downloads: 1000000 }));
|
|
123
|
+
const stats = await adapter.getDownloadStats('express', 'last-month');
|
|
124
|
+
expect(stats.total).toBe(1000000);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('getReadme returns readme', async () => {
|
|
128
|
+
vi.stubGlobal('fetch', mockFetch({ readme: '# Express' }));
|
|
129
|
+
const readme = await adapter.getReadme('express');
|
|
130
|
+
expect(readme).toBe('# Express');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('getMaintainers returns list', async () => {
|
|
134
|
+
vi.stubGlobal(
|
|
135
|
+
'fetch',
|
|
136
|
+
mockFetch({ maintainers: [{ name: 'dougwilson', email: 'doug@somethingdoug.com' }] })
|
|
137
|
+
);
|
|
138
|
+
const maintainers = await adapter.getMaintainers('express');
|
|
139
|
+
expect(maintainers.length).toBe(1);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('getPackageHealth returns health', async () => {
|
|
143
|
+
vi.stubGlobal(
|
|
144
|
+
'fetch',
|
|
145
|
+
mockFetch({
|
|
146
|
+
name: 'express',
|
|
147
|
+
'dist-tags': { latest: '4.18.0' },
|
|
148
|
+
description: 'Web framework',
|
|
149
|
+
license: 'MIT',
|
|
150
|
+
versions: { '4.18.0': { dependencies: {}, types: 'index.d.ts' } },
|
|
151
|
+
time: { '4.18.0': '2024-01-01' },
|
|
152
|
+
readme: 'x'.repeat(600),
|
|
153
|
+
keywords: [],
|
|
154
|
+
})
|
|
155
|
+
);
|
|
156
|
+
const health = await adapter.getPackageHealth('express');
|
|
157
|
+
expect(health.score).toBeGreaterThan(0);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('getSecurityAdvisories returns empty on error', async () => {
|
|
161
|
+
vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('network')));
|
|
162
|
+
const advisories = await adapter.getSecurityAdvisories('express');
|
|
163
|
+
expect(advisories).toEqual([]);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('getReverseDependencies returns packages', async () => {
|
|
167
|
+
vi.stubGlobal(
|
|
168
|
+
'fetch',
|
|
169
|
+
mockFetch({
|
|
170
|
+
objects: [{ package: { name: 'helmet', version: '7.0.0', description: 'security' } }],
|
|
171
|
+
})
|
|
172
|
+
);
|
|
173
|
+
const results = await adapter.getReverseDependencies('express', 5);
|
|
174
|
+
expect(results.length).toBe(1);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('caches search results', async () => {
|
|
178
|
+
const fetchMock = mockFetch({
|
|
179
|
+
objects: [{ package: { name: 'a', version: '1', description: '' } }],
|
|
180
|
+
});
|
|
181
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
182
|
+
await adapter.search('test', 5);
|
|
183
|
+
await adapter.search('test', 5);
|
|
184
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe('PypiAdapter', () => {
|
|
189
|
+
let adapter: PypiAdapter;
|
|
190
|
+
let cache: Cache;
|
|
191
|
+
|
|
192
|
+
beforeEach(() => {
|
|
193
|
+
cache = new Cache(60000);
|
|
194
|
+
adapter = new PypiAdapter(config, cache);
|
|
195
|
+
vi.unstubAllGlobals();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('search returns packages', async () => {
|
|
199
|
+
vi.stubGlobal(
|
|
200
|
+
'fetch',
|
|
201
|
+
mockFetch({
|
|
202
|
+
info: { name: 'requests', version: '2.31.0', summary: 'HTTP library' },
|
|
203
|
+
})
|
|
204
|
+
);
|
|
205
|
+
const results = await adapter.search('requests', 5);
|
|
206
|
+
expect(results.length).toBe(1);
|
|
207
|
+
expect(results[0].registry).toBe('pypi');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('getPackage returns metadata', async () => {
|
|
211
|
+
vi.stubGlobal(
|
|
212
|
+
'fetch',
|
|
213
|
+
mockFetch({
|
|
214
|
+
info: {
|
|
215
|
+
name: 'requests',
|
|
216
|
+
version: '2.31.0',
|
|
217
|
+
summary: 'HTTP library',
|
|
218
|
+
license: 'Apache 2.0',
|
|
219
|
+
home_page: 'https://requests.readthedocs.io',
|
|
220
|
+
project_urls: { Source: 'https://github.com/psf/requests' },
|
|
221
|
+
keywords: 'http,client',
|
|
222
|
+
},
|
|
223
|
+
})
|
|
224
|
+
);
|
|
225
|
+
const pkg = await adapter.getPackage('requests');
|
|
226
|
+
expect(pkg.name).toBe('requests');
|
|
227
|
+
expect(pkg.registry).toBe('pypi');
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('listVersions filters pre-releases', async () => {
|
|
231
|
+
vi.stubGlobal(
|
|
232
|
+
'fetch',
|
|
233
|
+
mockFetch({
|
|
234
|
+
releases: {
|
|
235
|
+
'2.30.0': [{ upload_time_iso_8601: '2023-01-01' }],
|
|
236
|
+
'2.31.0a1': [{ upload_time_iso_8601: '2023-06-01' }],
|
|
237
|
+
'2.31.0': [{ upload_time_iso_8601: '2023-07-01' }],
|
|
238
|
+
},
|
|
239
|
+
})
|
|
240
|
+
);
|
|
241
|
+
const versions = await adapter.listVersions('requests', 10, true);
|
|
242
|
+
expect(versions.every((v) => !v.prerelease)).toBe(true);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('getVersion returns version detail', async () => {
|
|
246
|
+
vi.stubGlobal(
|
|
247
|
+
'fetch',
|
|
248
|
+
mockFetch({
|
|
249
|
+
info: {
|
|
250
|
+
name: 'requests',
|
|
251
|
+
version: '2.31.0',
|
|
252
|
+
license: 'Apache 2.0',
|
|
253
|
+
requires_dist: ['charset-normalizer', 'idna'],
|
|
254
|
+
},
|
|
255
|
+
urls: [{ upload_time_iso_8601: '2023-07-01', size: 62574 }],
|
|
256
|
+
})
|
|
257
|
+
);
|
|
258
|
+
const v = await adapter.getVersion('requests', '2.31.0');
|
|
259
|
+
expect(v.dependencies.length).toBe(2);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('getReverseDependencies returns empty', async () => {
|
|
263
|
+
const results = await adapter.getReverseDependencies('requests', 5);
|
|
264
|
+
expect(results).toEqual([]);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('getDownloadStats returns stats', async () => {
|
|
268
|
+
vi.stubGlobal(
|
|
269
|
+
'fetch',
|
|
270
|
+
mockFetch({
|
|
271
|
+
info: { name: 'requests', version: '2.31.0', summary: '' },
|
|
272
|
+
})
|
|
273
|
+
);
|
|
274
|
+
const stats = await adapter.getDownloadStats('requests', 'last-month');
|
|
275
|
+
expect(stats.registry).toBe('pypi');
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('getReadme returns description', async () => {
|
|
279
|
+
vi.stubGlobal('fetch', mockFetch({ info: { description: '# Requests\nHTTP library' } }));
|
|
280
|
+
const readme = await adapter.getReadme('requests');
|
|
281
|
+
expect(readme).toContain('Requests');
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('getMaintainers returns authors', async () => {
|
|
285
|
+
vi.stubGlobal(
|
|
286
|
+
'fetch',
|
|
287
|
+
mockFetch({
|
|
288
|
+
info: { author: 'Kenneth Reitz', author_email: 'me@kennethreitz.org' },
|
|
289
|
+
})
|
|
290
|
+
);
|
|
291
|
+
const m = await adapter.getMaintainers('requests');
|
|
292
|
+
expect(m.length).toBe(1);
|
|
293
|
+
expect(m[0].name).toBe('Kenneth Reitz');
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('getPackageHealth returns health', async () => {
|
|
297
|
+
vi.stubGlobal(
|
|
298
|
+
'fetch',
|
|
299
|
+
mockFetch({
|
|
300
|
+
info: {
|
|
301
|
+
name: 'requests',
|
|
302
|
+
version: '2.31.0',
|
|
303
|
+
summary: '',
|
|
304
|
+
license: 'Apache 2.0',
|
|
305
|
+
description: 'x'.repeat(600),
|
|
306
|
+
requires_python: '>=3.7',
|
|
307
|
+
project_urls: { Documentation: 'https://docs.python-requests.org' },
|
|
308
|
+
requires_dist: ['a', 'b'],
|
|
309
|
+
classifiers: ['Typing :: Typed'],
|
|
310
|
+
},
|
|
311
|
+
urls: [{ upload_time_iso_8601: '2023-07-01' }],
|
|
312
|
+
})
|
|
313
|
+
);
|
|
314
|
+
const health = await adapter.getPackageHealth('requests');
|
|
315
|
+
expect(health.score).toBeGreaterThan(0);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('getSecurityAdvisories returns empty on error', async () => {
|
|
319
|
+
vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('fail')));
|
|
320
|
+
const advisories = await adapter.getSecurityAdvisories('requests');
|
|
321
|
+
expect(advisories).toEqual([]);
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
describe('MavenAdapter', () => {
|
|
326
|
+
let adapter: MavenAdapter;
|
|
327
|
+
let cache: Cache;
|
|
328
|
+
|
|
329
|
+
beforeEach(() => {
|
|
330
|
+
cache = new Cache(60000);
|
|
331
|
+
adapter = new MavenAdapter(config, cache);
|
|
332
|
+
vi.unstubAllGlobals();
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it('throws for invalid coordinates', async () => {
|
|
336
|
+
vi.stubGlobal('fetch', mockFetch({}));
|
|
337
|
+
await expect(adapter.getPackage('invalid')).rejects.toThrow('groupId:artifactId');
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it('search returns results', async () => {
|
|
341
|
+
vi.stubGlobal(
|
|
342
|
+
'fetch',
|
|
343
|
+
mockFetch({
|
|
344
|
+
response: { docs: [{ g: 'com.google.guava', a: 'guava', latestVersion: '33.0.0' }] },
|
|
345
|
+
})
|
|
346
|
+
);
|
|
347
|
+
const results = await adapter.search('guava', 5);
|
|
348
|
+
expect(results.length).toBe(1);
|
|
349
|
+
expect(results[0].name).toBe('com.google.guava:guava');
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it('getPackage returns metadata', async () => {
|
|
353
|
+
vi.stubGlobal(
|
|
354
|
+
'fetch',
|
|
355
|
+
mockFetch({
|
|
356
|
+
response: {
|
|
357
|
+
docs: [{ g: 'com.google.guava', a: 'guava', latestVersion: '33.0.0', p: 'jar' }],
|
|
358
|
+
},
|
|
359
|
+
})
|
|
360
|
+
);
|
|
361
|
+
const pkg = await adapter.getPackage('com.google.guava:guava');
|
|
362
|
+
expect(pkg.registry).toBe('maven');
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it('listVersions returns versions', async () => {
|
|
366
|
+
vi.stubGlobal(
|
|
367
|
+
'fetch',
|
|
368
|
+
mockFetch({
|
|
369
|
+
response: {
|
|
370
|
+
docs: [
|
|
371
|
+
{ v: '33.0.0', timestamp: 1700000000000 },
|
|
372
|
+
{ v: '33.0.0-beta', timestamp: 1699000000000 },
|
|
373
|
+
],
|
|
374
|
+
},
|
|
375
|
+
})
|
|
376
|
+
);
|
|
377
|
+
const versions = await adapter.listVersions('com.google.guava:guava', 10, true);
|
|
378
|
+
expect(versions.length).toBeGreaterThanOrEqual(1);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it('getReadme returns message', async () => {
|
|
382
|
+
const readme = await adapter.getReadme('com.google.guava:guava');
|
|
383
|
+
expect(readme).toContain('Maven Central');
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
it('getMaintainers returns empty', async () => {
|
|
387
|
+
const m = await adapter.getMaintainers('com.google.guava:guava');
|
|
388
|
+
expect(m).toEqual([]);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it('getDependencies returns empty', async () => {
|
|
392
|
+
const deps = await adapter.getDependencies('com.google.guava:guava', '33.0.0', 'all');
|
|
393
|
+
expect(deps).toEqual([]);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it('getReverseDependencies returns empty', async () => {
|
|
397
|
+
const results = await adapter.getReverseDependencies('com.google.guava:guava', 5);
|
|
398
|
+
expect(results).toEqual([]);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it('getDownloadStats returns zeroed stats', async () => {
|
|
402
|
+
const stats = await adapter.getDownloadStats('com.google.guava:guava', 'last-month');
|
|
403
|
+
expect(stats.total).toBe(0);
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it('getPackageHealth returns baseline', async () => {
|
|
407
|
+
vi.stubGlobal(
|
|
408
|
+
'fetch',
|
|
409
|
+
mockFetch({
|
|
410
|
+
response: { docs: [{ g: 'com.google.guava', a: 'guava', latestVersion: '33.0.0' }] },
|
|
411
|
+
})
|
|
412
|
+
);
|
|
413
|
+
const health = await adapter.getPackageHealth('com.google.guava:guava');
|
|
414
|
+
expect(health.score).toBe(5);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it('getSecurityAdvisories returns empty on error', async () => {
|
|
418
|
+
vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('fail')));
|
|
419
|
+
const adv = await adapter.getSecurityAdvisories('com.google.guava:guava');
|
|
420
|
+
expect(adv).toEqual([]);
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
describe('CratesAdapter', () => {
|
|
425
|
+
let adapter: CratesAdapter;
|
|
426
|
+
let cache: Cache;
|
|
427
|
+
|
|
428
|
+
beforeEach(() => {
|
|
429
|
+
cache = new Cache(60000);
|
|
430
|
+
adapter = new CratesAdapter(config, cache);
|
|
431
|
+
vi.unstubAllGlobals();
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
it('search returns crates', async () => {
|
|
435
|
+
vi.stubGlobal(
|
|
436
|
+
'fetch',
|
|
437
|
+
mockFetch({
|
|
438
|
+
crates: [
|
|
439
|
+
{ name: 'serde', max_version: '1.0.200', description: 'Serialize', downloads: 100000 },
|
|
440
|
+
],
|
|
441
|
+
})
|
|
442
|
+
);
|
|
443
|
+
const results = await adapter.search('serde', 5);
|
|
444
|
+
expect(results.length).toBe(1);
|
|
445
|
+
expect(results[0].registry).toBe('crates');
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it('getPackage returns metadata', async () => {
|
|
449
|
+
vi.stubGlobal(
|
|
450
|
+
'fetch',
|
|
451
|
+
mockFetch({
|
|
452
|
+
crate: {
|
|
453
|
+
name: 'serde',
|
|
454
|
+
max_version: '1.0.200',
|
|
455
|
+
description: 'Serialize',
|
|
456
|
+
homepage: 'https://serde.rs',
|
|
457
|
+
repository: 'https://github.com/serde-rs/serde',
|
|
458
|
+
keywords: ['serde'],
|
|
459
|
+
},
|
|
460
|
+
})
|
|
461
|
+
);
|
|
462
|
+
const pkg = await adapter.getPackage('serde');
|
|
463
|
+
expect(pkg.name).toBe('serde');
|
|
464
|
+
expect(pkg.registry).toBe('crates');
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it('listVersions returns versions', async () => {
|
|
468
|
+
vi.stubGlobal(
|
|
469
|
+
'fetch',
|
|
470
|
+
mockFetch({
|
|
471
|
+
versions: [
|
|
472
|
+
{ num: '1.0.200', created_at: '2024-01-01' },
|
|
473
|
+
{ num: '1.0.200-rc.1', created_at: '2023-12-01' },
|
|
474
|
+
],
|
|
475
|
+
})
|
|
476
|
+
);
|
|
477
|
+
const versions = await adapter.listVersions('serde', 10, true);
|
|
478
|
+
expect(versions.length).toBeGreaterThanOrEqual(1);
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
it('getVersion returns detail', async () => {
|
|
482
|
+
const fetchMock = vi
|
|
483
|
+
.fn()
|
|
484
|
+
.mockResolvedValueOnce({
|
|
485
|
+
ok: true,
|
|
486
|
+
json: () =>
|
|
487
|
+
Promise.resolve({
|
|
488
|
+
version: {
|
|
489
|
+
crate: 'serde',
|
|
490
|
+
num: '1.0.200',
|
|
491
|
+
created_at: '2024-01-01',
|
|
492
|
+
license: 'MIT/Apache-2.0',
|
|
493
|
+
crate_size: 102400,
|
|
494
|
+
},
|
|
495
|
+
}),
|
|
496
|
+
})
|
|
497
|
+
.mockResolvedValueOnce({
|
|
498
|
+
ok: true,
|
|
499
|
+
json: () =>
|
|
500
|
+
Promise.resolve({
|
|
501
|
+
dependencies: [{ crate_id: 'serde_derive', req: '^1.0', kind: 'normal' }],
|
|
502
|
+
}),
|
|
503
|
+
});
|
|
504
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
505
|
+
const v = await adapter.getVersion('serde', '1.0.200');
|
|
506
|
+
expect(v.name).toBe('serde');
|
|
507
|
+
expect(v.dependencies.length).toBe(1);
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
it('getDependencies returns filtered deps', async () => {
|
|
511
|
+
vi.stubGlobal(
|
|
512
|
+
'fetch',
|
|
513
|
+
mockFetch({
|
|
514
|
+
dependencies: [
|
|
515
|
+
{ crate_id: 'serde_derive', req: '^1.0', kind: 'normal' },
|
|
516
|
+
{ crate_id: 'serde_test', req: '^1.0', kind: 'dev' },
|
|
517
|
+
],
|
|
518
|
+
})
|
|
519
|
+
);
|
|
520
|
+
const deps = await adapter.getDependencies('serde', '1.0.200', 'runtime');
|
|
521
|
+
expect(deps.length).toBe(1);
|
|
522
|
+
expect(deps[0].type).toBe('runtime');
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
it('getReverseDependencies returns dependents', async () => {
|
|
526
|
+
vi.stubGlobal('fetch', mockFetch({ versions: [{ crate: 'serde_json', num: '1.0.0' }] }));
|
|
527
|
+
const results = await adapter.getReverseDependencies('serde', 5);
|
|
528
|
+
expect(results.length).toBe(1);
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
it('getDownloadStats returns downloads', async () => {
|
|
532
|
+
vi.stubGlobal('fetch', mockFetch({ crate: { downloads: 500000 } }));
|
|
533
|
+
const stats = await adapter.getDownloadStats('serde', 'last-month');
|
|
534
|
+
expect(stats.total).toBe(500000);
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
it('getReadme returns content', async () => {
|
|
538
|
+
vi.stubGlobal('fetch', mockFetch({ crate: { readme: '# Serde' } }));
|
|
539
|
+
const readme = await adapter.getReadme('serde');
|
|
540
|
+
expect(readme).toBe('# Serde');
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
it('getMaintainers returns owners', async () => {
|
|
544
|
+
vi.stubGlobal('fetch', mockFetch({ users: [{ login: 'dtolnay', name: 'David Tolnay' }] }));
|
|
545
|
+
const m = await adapter.getMaintainers('serde');
|
|
546
|
+
expect(m.length).toBe(1);
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
it('getPackageHealth returns health', async () => {
|
|
550
|
+
vi.stubGlobal(
|
|
551
|
+
'fetch',
|
|
552
|
+
mockFetch({
|
|
553
|
+
crate: {
|
|
554
|
+
name: 'serde',
|
|
555
|
+
max_version: '1.0.200',
|
|
556
|
+
description: 'Serialize',
|
|
557
|
+
documentation: 'https://docs.rs/serde',
|
|
558
|
+
repository: 'https://github.com/serde-rs/serde',
|
|
559
|
+
homepage: 'https://serde.rs',
|
|
560
|
+
updated_at: '2024-01-01',
|
|
561
|
+
recent_downloads: 5000000,
|
|
562
|
+
keywords: [],
|
|
563
|
+
},
|
|
564
|
+
})
|
|
565
|
+
);
|
|
566
|
+
const health = await adapter.getPackageHealth('serde');
|
|
567
|
+
expect(health.score).toBeGreaterThan(0);
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
it('getSecurityAdvisories returns empty on error', async () => {
|
|
571
|
+
vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('fail')));
|
|
572
|
+
const adv = await adapter.getSecurityAdvisories('serde');
|
|
573
|
+
expect(adv).toEqual([]);
|
|
574
|
+
});
|
|
575
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { Cache } from '../src/cache.js';
|
|
3
|
+
|
|
4
|
+
describe('Cache', () => {
|
|
5
|
+
it('stores and retrieves values', () => {
|
|
6
|
+
const cache = new Cache(60000);
|
|
7
|
+
cache.set('key1', 'value1');
|
|
8
|
+
expect(cache.get<string>('key1')).toBe('value1');
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('returns undefined for missing keys', () => {
|
|
12
|
+
const cache = new Cache(60000);
|
|
13
|
+
expect(cache.get('missing')).toBeUndefined();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('expires entries after TTL', () => {
|
|
17
|
+
const cache = new Cache(100);
|
|
18
|
+
cache.set('key1', 'value1');
|
|
19
|
+
vi.useFakeTimers();
|
|
20
|
+
vi.advanceTimersByTime(200);
|
|
21
|
+
expect(cache.get('key1')).toBeUndefined();
|
|
22
|
+
vi.useRealTimers();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('does not store when TTL is 0', () => {
|
|
26
|
+
const cache = new Cache(0);
|
|
27
|
+
cache.set('key1', 'value1');
|
|
28
|
+
expect(cache.size).toBe(0);
|
|
29
|
+
expect(cache.get('key1')).toBeUndefined();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('clears all entries', () => {
|
|
33
|
+
const cache = new Cache(60000);
|
|
34
|
+
cache.set('a', 1);
|
|
35
|
+
cache.set('b', 2);
|
|
36
|
+
expect(cache.size).toBe(2);
|
|
37
|
+
cache.clear();
|
|
38
|
+
expect(cache.size).toBe(0);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('reports size', () => {
|
|
42
|
+
const cache = new Cache(60000);
|
|
43
|
+
expect(cache.size).toBe(0);
|
|
44
|
+
cache.set('a', 1);
|
|
45
|
+
expect(cache.size).toBe(1);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { loadConfig } from '../src/config.js';
|
|
3
|
+
|
|
4
|
+
describe('loadConfig', () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
vi.unstubAllEnvs();
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('returns default config when no env vars set', () => {
|
|
10
|
+
vi.stubEnv('REGISTRYX_MCP_REGISTRIES', '');
|
|
11
|
+
// Override to test default
|
|
12
|
+
vi.stubEnv('REGISTRYX_MCP_REGISTRIES', 'npm,pypi,maven,crates');
|
|
13
|
+
const config = loadConfig();
|
|
14
|
+
expect(config.registries).toEqual(['npm', 'pypi', 'maven', 'crates']);
|
|
15
|
+
expect(config.timeoutMs).toBe(15000);
|
|
16
|
+
expect(config.cacheTtlMs).toBe(300000);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('parses custom registries', () => {
|
|
20
|
+
vi.stubEnv('REGISTRYX_MCP_REGISTRIES', 'npm,crates');
|
|
21
|
+
const config = loadConfig();
|
|
22
|
+
expect(config.registries).toEqual(['npm', 'crates']);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('filters invalid registries', () => {
|
|
26
|
+
vi.stubEnv('REGISTRYX_MCP_REGISTRIES', 'npm,invalid,pypi');
|
|
27
|
+
const config = loadConfig();
|
|
28
|
+
expect(config.registries).toEqual(['npm', 'pypi']);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('throws if no valid registries', () => {
|
|
32
|
+
vi.stubEnv('REGISTRYX_MCP_REGISTRIES', 'invalid,nope');
|
|
33
|
+
expect(() => loadConfig()).toThrow('No valid registries');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('parses timeout', () => {
|
|
37
|
+
vi.stubEnv('REGISTRYX_MCP_TIMEOUT_MS', '5000');
|
|
38
|
+
const config = loadConfig();
|
|
39
|
+
expect(config.timeoutMs).toBe(5000);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('throws for invalid timeout', () => {
|
|
43
|
+
vi.stubEnv('REGISTRYX_MCP_TIMEOUT_MS', '500');
|
|
44
|
+
expect(() => loadConfig()).toThrow('REGISTRYX_MCP_TIMEOUT_MS');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('parses npm token', () => {
|
|
48
|
+
vi.stubEnv('REGISTRYX_MCP_NPM_TOKEN', 'test-token');
|
|
49
|
+
const config = loadConfig();
|
|
50
|
+
expect(config.npmToken).toBe('test-token');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('returns undefined npmToken when empty', () => {
|
|
54
|
+
vi.stubEnv('REGISTRYX_MCP_NPM_TOKEN', '');
|
|
55
|
+
const config = loadConfig();
|
|
56
|
+
expect(config.npmToken).toBeUndefined();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('parses cache TTL', () => {
|
|
60
|
+
vi.stubEnv('REGISTRYX_MCP_CACHE_TTL_MS', '60000');
|
|
61
|
+
const config = loadConfig();
|
|
62
|
+
expect(config.cacheTtlMs).toBe(60000);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('throws for negative cache TTL', () => {
|
|
66
|
+
vi.stubEnv('REGISTRYX_MCP_CACHE_TTL_MS', '-1');
|
|
67
|
+
expect(() => loadConfig()).toThrow('REGISTRYX_MCP_CACHE_TTL_MS');
|
|
68
|
+
});
|
|
69
|
+
});
|