@vltpkg/vsr 0.0.0-26
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/DEPLOY.md +163 -0
- package/LICENSE +119 -0
- package/README.md +314 -0
- package/config.ts +221 -0
- package/drizzle.config.js +40 -0
- package/info/COMPARISONS.md +37 -0
- package/info/CONFIGURATION.md +143 -0
- package/info/CONTRIBUTING.md +32 -0
- package/info/DATABASE_SETUP.md +108 -0
- package/info/GRANULAR_ACCESS_TOKENS.md +160 -0
- package/info/PROJECT_STRUCTURE.md +291 -0
- package/info/ROADMAP.md +27 -0
- package/info/SUPPORT.md +39 -0
- package/info/TESTING.md +301 -0
- package/info/USER_SUPPORT.md +31 -0
- package/package.json +77 -0
- package/scripts/build-assets.js +31 -0
- package/scripts/build-bin.js +62 -0
- package/scripts/prepack.js +27 -0
- package/src/assets/public/images/bg.png +0 -0
- package/src/assets/public/images/clients/logo-bun.png +0 -0
- package/src/assets/public/images/clients/logo-deno.png +0 -0
- package/src/assets/public/images/clients/logo-npm.png +0 -0
- package/src/assets/public/images/clients/logo-pnpm.png +0 -0
- package/src/assets/public/images/clients/logo-vlt.png +0 -0
- package/src/assets/public/images/clients/logo-yarn.png +0 -0
- package/src/assets/public/images/favicon/apple-touch-icon.png +0 -0
- package/src/assets/public/images/favicon/favicon-96x96.png +0 -0
- package/src/assets/public/images/favicon/favicon.ico +0 -0
- package/src/assets/public/images/favicon/favicon.svg +3 -0
- package/src/assets/public/images/favicon/site.webmanifest +21 -0
- package/src/assets/public/images/favicon/web-app-manifest-192x192.png +0 -0
- package/src/assets/public/images/favicon/web-app-manifest-512x512.png +0 -0
- package/src/assets/public/styles/styles.css +231 -0
- package/src/bin/demo/package.json +6 -0
- package/src/bin/demo/vlt.json +1 -0
- package/src/bin/vsr.ts +484 -0
- package/src/db/client.ts +590 -0
- package/src/db/migrations/0000_faulty_ricochet.sql +14 -0
- package/src/db/migrations/0000_initial.sql +29 -0
- package/src/db/migrations/0001_uuid_validation.sql +35 -0
- package/src/db/migrations/0001_wealthy_magdalene.sql +7 -0
- package/src/db/migrations/drop.sql +3 -0
- package/src/db/migrations/meta/0000_snapshot.json +104 -0
- package/src/db/migrations/meta/0001_snapshot.json +155 -0
- package/src/db/migrations/meta/_journal.json +20 -0
- package/src/db/schema.ts +43 -0
- package/src/index.ts +434 -0
- package/src/middleware/config.ts +79 -0
- package/src/middleware/telemetry.ts +43 -0
- package/src/queue/index.ts +97 -0
- package/src/routes/access.ts +852 -0
- package/src/routes/docs.ts +63 -0
- package/src/routes/misc.ts +469 -0
- package/src/routes/packages.ts +2823 -0
- package/src/routes/ping.ts +39 -0
- package/src/routes/search.ts +131 -0
- package/src/routes/static.ts +74 -0
- package/src/routes/tokens.ts +259 -0
- package/src/routes/users.ts +68 -0
- package/src/utils/auth.ts +202 -0
- package/src/utils/cache.ts +587 -0
- package/src/utils/config.ts +50 -0
- package/src/utils/database.ts +69 -0
- package/src/utils/docs.ts +146 -0
- package/src/utils/packages.ts +453 -0
- package/src/utils/response.ts +125 -0
- package/src/utils/routes.ts +64 -0
- package/src/utils/spa.ts +52 -0
- package/src/utils/tracing.ts +52 -0
- package/src/utils/upstream.ts +172 -0
- package/test/access.test.ts +705 -0
- package/test/audit.test.ts +828 -0
- package/test/dashboard.test.ts +693 -0
- package/test/dist-tags.test.ts +678 -0
- package/test/manifest.test.ts +436 -0
- package/test/packument.test.ts +530 -0
- package/test/ping.test.ts +41 -0
- package/test/search.test.ts +472 -0
- package/test/setup.ts +130 -0
- package/test/static.test.ts +646 -0
- package/test/tokens.test.ts +389 -0
- package/test/utils/auth.test.ts +214 -0
- package/test/utils/packages.test.ts +235 -0
- package/test/utils/response.test.ts +184 -0
- package/test/whoami.test.ts +119 -0
- package/tsconfig.json +16 -0
- package/tsconfig.worker.json +3 -0
- package/typedoc.mjs +2 -0
- package/types.ts +598 -0
- package/vitest.config.ts +25 -0
- package/vlt.json.example +56 -0
- package/wrangler.json +65 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
packageSpec,
|
|
4
|
+
validatePackageName,
|
|
5
|
+
validateVersion,
|
|
6
|
+
satisfiesRange,
|
|
7
|
+
sortVersionsDescending,
|
|
8
|
+
getLatestVersion,
|
|
9
|
+
generateTarballFilename,
|
|
10
|
+
parsePackageIdentifier,
|
|
11
|
+
} from '../../src/utils/packages.ts'
|
|
12
|
+
import type { HonoContext } from '../../types.ts'
|
|
13
|
+
|
|
14
|
+
describe('Package Utils', () => {
|
|
15
|
+
describe('packageSpec', () => {
|
|
16
|
+
it('should extract scoped package specification', () => {
|
|
17
|
+
const mockContext = {
|
|
18
|
+
req: {
|
|
19
|
+
param: vi.fn().mockReturnValue({
|
|
20
|
+
scope: '@myorg',
|
|
21
|
+
pkg: 'mypackage',
|
|
22
|
+
}),
|
|
23
|
+
},
|
|
24
|
+
} as unknown as HonoContext
|
|
25
|
+
|
|
26
|
+
const spec = packageSpec(mockContext)
|
|
27
|
+
expect(spec).toEqual({
|
|
28
|
+
name: '@myorg/mypackage',
|
|
29
|
+
scope: '@myorg',
|
|
30
|
+
pkg: 'mypackage',
|
|
31
|
+
})
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('should extract scoped package specification without @ prefix', () => {
|
|
35
|
+
const mockContext = {
|
|
36
|
+
req: {
|
|
37
|
+
param: vi.fn().mockReturnValue({
|
|
38
|
+
scope: 'myorg',
|
|
39
|
+
pkg: 'mypackage',
|
|
40
|
+
}),
|
|
41
|
+
},
|
|
42
|
+
} as unknown as HonoContext
|
|
43
|
+
|
|
44
|
+
const spec = packageSpec(mockContext)
|
|
45
|
+
expect(spec).toEqual({
|
|
46
|
+
name: '@myorg/mypackage',
|
|
47
|
+
scope: 'myorg',
|
|
48
|
+
pkg: 'mypackage',
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('should extract unscoped package specification', () => {
|
|
53
|
+
const mockContext = {
|
|
54
|
+
req: {
|
|
55
|
+
param: vi.fn().mockReturnValue({
|
|
56
|
+
scope: 'lodash',
|
|
57
|
+
}),
|
|
58
|
+
},
|
|
59
|
+
} as unknown as HonoContext
|
|
60
|
+
|
|
61
|
+
const spec = packageSpec(mockContext)
|
|
62
|
+
expect(spec).toEqual({
|
|
63
|
+
name: 'lodash',
|
|
64
|
+
pkg: 'lodash',
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('should return empty object for missing parameters', () => {
|
|
69
|
+
const mockContext = {
|
|
70
|
+
req: {
|
|
71
|
+
param: vi.fn().mockReturnValue({}),
|
|
72
|
+
},
|
|
73
|
+
} as unknown as HonoContext
|
|
74
|
+
|
|
75
|
+
const spec = packageSpec(mockContext)
|
|
76
|
+
expect(spec).toEqual({})
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
describe('generateTarballFilename', () => {
|
|
81
|
+
it('should generate filename for scoped package', () => {
|
|
82
|
+
const filename = generateTarballFilename(
|
|
83
|
+
'@myorg/mypackage',
|
|
84
|
+
'1.0.0',
|
|
85
|
+
)
|
|
86
|
+
expect(filename).toBe('mypackage-1.0.0.tgz')
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('should generate filename for unscoped package', () => {
|
|
90
|
+
const filename = generateTarballFilename('lodash', '4.17.21')
|
|
91
|
+
expect(filename).toBe('lodash-4.17.21.tgz')
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('should handle complex version numbers', () => {
|
|
95
|
+
const filename = generateTarballFilename(
|
|
96
|
+
'test-package',
|
|
97
|
+
'1.0.0-beta.1+build.123',
|
|
98
|
+
)
|
|
99
|
+
expect(filename).toBe('test-package-1.0.0-beta.1+build.123.tgz')
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
describe('validatePackageName', () => {
|
|
104
|
+
it('should validate correct package names', () => {
|
|
105
|
+
const result1 = validatePackageName('lodash')
|
|
106
|
+
expect(result1.valid).toBe(true)
|
|
107
|
+
expect(result1.errors).toEqual([])
|
|
108
|
+
|
|
109
|
+
const result2 = validatePackageName('@myorg/mypackage')
|
|
110
|
+
expect(result2.valid).toBe(true)
|
|
111
|
+
expect(result2.errors).toEqual([])
|
|
112
|
+
|
|
113
|
+
const result3 = validatePackageName('my-package')
|
|
114
|
+
expect(result3.valid).toBe(true)
|
|
115
|
+
expect(result3.errors).toEqual([])
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('should reject invalid package names', () => {
|
|
119
|
+
const result = validatePackageName('')
|
|
120
|
+
expect(result.valid).toBe(false)
|
|
121
|
+
expect(result.errors.length).toBeGreaterThan(0)
|
|
122
|
+
|
|
123
|
+
const result2 = validatePackageName('.invalid')
|
|
124
|
+
expect(result2.valid).toBe(false)
|
|
125
|
+
expect(result2.errors.length).toBeGreaterThan(0)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('should handle package names with warnings', () => {
|
|
129
|
+
const result = validatePackageName('UPPERCASE')
|
|
130
|
+
// This might be valid but with warnings, or invalid - let's be flexible
|
|
131
|
+
expect(typeof result.valid).toBe('boolean')
|
|
132
|
+
expect(Array.isArray(result.errors)).toBe(true)
|
|
133
|
+
})
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
describe('validateVersion', () => {
|
|
137
|
+
it('should validate correct semantic versions', () => {
|
|
138
|
+
expect(validateVersion('1.0.0')).toBe(true)
|
|
139
|
+
expect(validateVersion('0.1.2')).toBe(true)
|
|
140
|
+
expect(validateVersion('1.0.0-alpha.1')).toBe(true)
|
|
141
|
+
expect(validateVersion('1.0.0+build.123')).toBe(true)
|
|
142
|
+
expect(validateVersion('2.1.0-beta.1+exp.sha.5114f85')).toBe(
|
|
143
|
+
true,
|
|
144
|
+
)
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('should reject invalid semantic versions', () => {
|
|
148
|
+
expect(validateVersion('1.0')).toBe(false)
|
|
149
|
+
expect(validateVersion('1.0.0.0')).toBe(false)
|
|
150
|
+
expect(validateVersion('invalid')).toBe(false)
|
|
151
|
+
expect(validateVersion('')).toBe(false)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('should handle edge cases in version validation', () => {
|
|
155
|
+
// Some semver libraries accept v prefix, so let's test what our function actually does
|
|
156
|
+
const vPrefixResult = validateVersion('v1.0.0')
|
|
157
|
+
expect(typeof vPrefixResult).toBe('boolean') // Just check it returns a boolean
|
|
158
|
+
})
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
describe('satisfiesRange', () => {
|
|
162
|
+
it('should check if version satisfies range', () => {
|
|
163
|
+
expect(satisfiesRange('1.0.0', '^1.0.0')).toBe(true)
|
|
164
|
+
expect(satisfiesRange('1.5.0', '^1.0.0')).toBe(true)
|
|
165
|
+
expect(satisfiesRange('2.0.0', '^1.0.0')).toBe(false)
|
|
166
|
+
expect(satisfiesRange('1.0.0', '~1.0.0')).toBe(true)
|
|
167
|
+
expect(satisfiesRange('1.0.5', '~1.0.0')).toBe(true)
|
|
168
|
+
expect(satisfiesRange('1.1.0', '~1.0.0')).toBe(false)
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('should handle invalid ranges gracefully', () => {
|
|
172
|
+
expect(satisfiesRange('1.0.0', 'invalid-range')).toBe(false)
|
|
173
|
+
expect(satisfiesRange('invalid-version', '^1.0.0')).toBe(false)
|
|
174
|
+
})
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
describe('sortVersionsDescending', () => {
|
|
178
|
+
it('should sort versions in descending order', () => {
|
|
179
|
+
const versions = ['1.0.0', '2.0.0', '1.5.0', '1.0.1']
|
|
180
|
+
const sorted = sortVersionsDescending(versions)
|
|
181
|
+
expect(sorted).toEqual(['2.0.0', '1.5.0', '1.0.1', '1.0.0'])
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('should handle empty array', () => {
|
|
185
|
+
const sorted = sortVersionsDescending([])
|
|
186
|
+
expect(sorted).toEqual([])
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it('should handle single version', () => {
|
|
190
|
+
const sorted = sortVersionsDescending(['1.0.0'])
|
|
191
|
+
expect(sorted).toEqual(['1.0.0'])
|
|
192
|
+
})
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
describe('getLatestVersion', () => {
|
|
196
|
+
it('should return latest version from array', () => {
|
|
197
|
+
const versions = ['1.0.0', '2.0.0', '1.5.0', '1.0.1']
|
|
198
|
+
const latest = getLatestVersion(versions)
|
|
199
|
+
expect(latest).toBe('2.0.0')
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
it('should return null for empty array', () => {
|
|
203
|
+
const latest = getLatestVersion([])
|
|
204
|
+
expect(latest).toBeNull()
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it('should handle single version', () => {
|
|
208
|
+
const latest = getLatestVersion(['1.0.0'])
|
|
209
|
+
expect(latest).toBe('1.0.0')
|
|
210
|
+
})
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
describe('parsePackageIdentifier', () => {
|
|
214
|
+
it('should parse unscoped package', () => {
|
|
215
|
+
const result = parsePackageIdentifier('lodash')
|
|
216
|
+
expect(result.name).toBe('lodash')
|
|
217
|
+
expect(result.fullName).toBe('lodash')
|
|
218
|
+
expect(result.scope).toBeUndefined()
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it('should parse scoped package', () => {
|
|
222
|
+
const result = parsePackageIdentifier('@types/node')
|
|
223
|
+
expect(result.name).toBe('node')
|
|
224
|
+
expect(result.fullName).toBe('@types/node')
|
|
225
|
+
expect(result.scope).toBe('@types')
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
it('should handle complex scoped package names', () => {
|
|
229
|
+
const result = parsePackageIdentifier('@myorg/my-package-name')
|
|
230
|
+
expect(result.name).toBe('my-package-name')
|
|
231
|
+
expect(result.fullName).toBe('@myorg/my-package-name')
|
|
232
|
+
expect(result.scope).toBe('@myorg')
|
|
233
|
+
})
|
|
234
|
+
})
|
|
235
|
+
})
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
jsonError,
|
|
4
|
+
jsonSuccess,
|
|
5
|
+
notFound,
|
|
6
|
+
unauthorized,
|
|
7
|
+
forbidden,
|
|
8
|
+
internalServerError,
|
|
9
|
+
} from '../../src/utils/response.ts'
|
|
10
|
+
import type { HonoContext, ApiError } from '../../types.ts'
|
|
11
|
+
|
|
12
|
+
describe('Response Utils', () => {
|
|
13
|
+
// Mock Hono context
|
|
14
|
+
const createMockContext = () => {
|
|
15
|
+
const mockJson = vi.fn()
|
|
16
|
+
return {
|
|
17
|
+
json: mockJson,
|
|
18
|
+
_mockJson: mockJson, // Keep reference for assertions
|
|
19
|
+
} as unknown as HonoContext & { _mockJson: any }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe('jsonError', () => {
|
|
23
|
+
it('should create error response from string message', () => {
|
|
24
|
+
const mockContext = createMockContext()
|
|
25
|
+
|
|
26
|
+
jsonError(mockContext, 'Test error message', 400)
|
|
27
|
+
|
|
28
|
+
expect(mockContext._mockJson).toHaveBeenCalledWith(
|
|
29
|
+
{ error: 'Test error message' },
|
|
30
|
+
400,
|
|
31
|
+
)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('should create error response from ApiError object', () => {
|
|
35
|
+
const mockContext = createMockContext()
|
|
36
|
+
const errorObj: ApiError = {
|
|
37
|
+
error: 'Validation failed',
|
|
38
|
+
details: 'Invalid package name',
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
jsonError(mockContext, errorObj, 422)
|
|
42
|
+
|
|
43
|
+
expect(mockContext._mockJson).toHaveBeenCalledWith(
|
|
44
|
+
errorObj,
|
|
45
|
+
422,
|
|
46
|
+
)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('should use default status code 400 when not provided', () => {
|
|
50
|
+
const mockContext = createMockContext()
|
|
51
|
+
|
|
52
|
+
jsonError(mockContext, 'Default error')
|
|
53
|
+
|
|
54
|
+
expect(mockContext._mockJson).toHaveBeenCalledWith(
|
|
55
|
+
{ error: 'Default error' },
|
|
56
|
+
400,
|
|
57
|
+
)
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
describe('jsonSuccess', () => {
|
|
62
|
+
it('should create success response with data', () => {
|
|
63
|
+
const mockContext = createMockContext()
|
|
64
|
+
const data = { name: 'test-package', version: '1.0.0' }
|
|
65
|
+
|
|
66
|
+
jsonSuccess(mockContext, data, 200)
|
|
67
|
+
|
|
68
|
+
expect(mockContext._mockJson).toHaveBeenCalledWith(data, 200)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('should use default status code 200 when not provided', () => {
|
|
72
|
+
const mockContext = createMockContext()
|
|
73
|
+
const data = { success: true }
|
|
74
|
+
|
|
75
|
+
jsonSuccess(mockContext, data)
|
|
76
|
+
|
|
77
|
+
expect(mockContext._mockJson).toHaveBeenCalledWith(data, 200)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('should handle null data', () => {
|
|
81
|
+
const mockContext = createMockContext()
|
|
82
|
+
|
|
83
|
+
jsonSuccess(mockContext, null)
|
|
84
|
+
|
|
85
|
+
expect(mockContext._mockJson).toHaveBeenCalledWith(null, 200)
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
describe('notFound', () => {
|
|
90
|
+
it('should create 404 response with default message', () => {
|
|
91
|
+
const mockContext = createMockContext()
|
|
92
|
+
|
|
93
|
+
notFound(mockContext)
|
|
94
|
+
|
|
95
|
+
expect(mockContext._mockJson).toHaveBeenCalledWith(
|
|
96
|
+
{ error: 'Not Found' },
|
|
97
|
+
404,
|
|
98
|
+
)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('should create 404 response with custom message', () => {
|
|
102
|
+
const mockContext = createMockContext()
|
|
103
|
+
|
|
104
|
+
notFound(mockContext, 'Package not found')
|
|
105
|
+
|
|
106
|
+
expect(mockContext._mockJson).toHaveBeenCalledWith(
|
|
107
|
+
{ error: 'Package not found' },
|
|
108
|
+
404,
|
|
109
|
+
)
|
|
110
|
+
})
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
describe('unauthorized', () => {
|
|
114
|
+
it('should create 401 response with default message', () => {
|
|
115
|
+
const mockContext = createMockContext()
|
|
116
|
+
|
|
117
|
+
unauthorized(mockContext)
|
|
118
|
+
|
|
119
|
+
expect(mockContext._mockJson).toHaveBeenCalledWith(
|
|
120
|
+
{ error: 'Unauthorized' },
|
|
121
|
+
401,
|
|
122
|
+
)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('should create 401 response with custom message', () => {
|
|
126
|
+
const mockContext = createMockContext()
|
|
127
|
+
|
|
128
|
+
unauthorized(mockContext, 'Invalid token')
|
|
129
|
+
|
|
130
|
+
expect(mockContext._mockJson).toHaveBeenCalledWith(
|
|
131
|
+
{ error: 'Invalid token' },
|
|
132
|
+
401,
|
|
133
|
+
)
|
|
134
|
+
})
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
describe('forbidden', () => {
|
|
138
|
+
it('should create 403 response with default message', () => {
|
|
139
|
+
const mockContext = createMockContext()
|
|
140
|
+
|
|
141
|
+
forbidden(mockContext)
|
|
142
|
+
|
|
143
|
+
expect(mockContext._mockJson).toHaveBeenCalledWith(
|
|
144
|
+
{ error: 'Forbidden' },
|
|
145
|
+
403,
|
|
146
|
+
)
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('should create 403 response with custom message', () => {
|
|
150
|
+
const mockContext = createMockContext()
|
|
151
|
+
|
|
152
|
+
forbidden(mockContext, 'Insufficient permissions')
|
|
153
|
+
|
|
154
|
+
expect(mockContext._mockJson).toHaveBeenCalledWith(
|
|
155
|
+
{ error: 'Insufficient permissions' },
|
|
156
|
+
403,
|
|
157
|
+
)
|
|
158
|
+
})
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
describe('internalServerError', () => {
|
|
162
|
+
it('should create 500 response with default message', () => {
|
|
163
|
+
const mockContext = createMockContext()
|
|
164
|
+
|
|
165
|
+
internalServerError(mockContext)
|
|
166
|
+
|
|
167
|
+
expect(mockContext._mockJson).toHaveBeenCalledWith(
|
|
168
|
+
{ error: 'Internal Server Error' },
|
|
169
|
+
500,
|
|
170
|
+
)
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it('should create 500 response with custom message', () => {
|
|
174
|
+
const mockContext = createMockContext()
|
|
175
|
+
|
|
176
|
+
internalServerError(mockContext, 'Database connection failed')
|
|
177
|
+
|
|
178
|
+
expect(mockContext._mockJson).toHaveBeenCalledWith(
|
|
179
|
+
{ error: 'Database connection failed' },
|
|
180
|
+
500,
|
|
181
|
+
)
|
|
182
|
+
})
|
|
183
|
+
})
|
|
184
|
+
})
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { env } from 'cloudflare:test'
|
|
3
|
+
import { app } from '../src/index.ts'
|
|
4
|
+
|
|
5
|
+
// Default admin token from the database schema (unused in current tests)
|
|
6
|
+
// const ADMIN_TOKEN = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
|
|
7
|
+
// const ADMIN_USERNAME = 'admin'
|
|
8
|
+
|
|
9
|
+
describe('Whoami Endpoint', () => {
|
|
10
|
+
describe('Authentication Behavior', () => {
|
|
11
|
+
it('should require authentication for root whoami endpoint', async () => {
|
|
12
|
+
const res = await app.request('/-/whoami', {}, env)
|
|
13
|
+
// Should return 200 with 'anonymous' when no auth provided
|
|
14
|
+
expect(res.status).toBe(200)
|
|
15
|
+
const data = await res.json()
|
|
16
|
+
expect(data).toHaveProperty('username')
|
|
17
|
+
expect(data.username).toBe('anonymous') // No auth = anonymous
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('should require authentication for upstream whoami endpoints', async () => {
|
|
21
|
+
const res = await app.request('/npm/-/whoami', {}, env)
|
|
22
|
+
expect(res.status).toBe(200)
|
|
23
|
+
const data = await res.json()
|
|
24
|
+
expect(data).toHaveProperty('username')
|
|
25
|
+
expect(data.username).toBe('anonymous') // No auth = anonymous
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('should handle malformed authorization header gracefully', async () => {
|
|
29
|
+
const res = await app.request(
|
|
30
|
+
'/-/whoami',
|
|
31
|
+
{
|
|
32
|
+
headers: {
|
|
33
|
+
Authorization: 'InvalidFormat',
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
env,
|
|
37
|
+
)
|
|
38
|
+
expect(res.status).toBe(200)
|
|
39
|
+
const data = await res.json()
|
|
40
|
+
expect(data.username).toBe('anonymous') // Invalid auth = anonymous
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('should handle invalid token gracefully', async () => {
|
|
44
|
+
const res = await app.request(
|
|
45
|
+
'/-/whoami',
|
|
46
|
+
{
|
|
47
|
+
headers: {
|
|
48
|
+
Authorization: 'Bearer invalid-token-12345',
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
env,
|
|
52
|
+
)
|
|
53
|
+
expect(res.status).toBe(200)
|
|
54
|
+
const data = await res.json()
|
|
55
|
+
expect(data.username).toBe('anonymous') // Invalid token = anonymous
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
describe('Endpoint Availability', () => {
|
|
60
|
+
it('should be available on root registry path', async () => {
|
|
61
|
+
const res = await app.request('/-/whoami', {}, env)
|
|
62
|
+
expect(res.status).toBe(200)
|
|
63
|
+
expect(res.headers.get('content-type')).toContain(
|
|
64
|
+
'application/json',
|
|
65
|
+
)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('should be available on npm upstream path', async () => {
|
|
69
|
+
const res = await app.request('/npm/-/whoami', {}, env)
|
|
70
|
+
expect(res.status).toBe(200)
|
|
71
|
+
expect(res.headers.get('content-type')).toContain(
|
|
72
|
+
'application/json',
|
|
73
|
+
)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('should be available on jsr upstream path', async () => {
|
|
77
|
+
const res = await app.request('/jsr/-/whoami', {}, env)
|
|
78
|
+
expect(res.status).toBe(200)
|
|
79
|
+
expect(res.headers.get('content-type')).toContain(
|
|
80
|
+
'application/json',
|
|
81
|
+
)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('should be available on custom upstream path', async () => {
|
|
85
|
+
const res = await app.request('/custom/-/whoami', {}, env)
|
|
86
|
+
expect(res.status).toBe(200)
|
|
87
|
+
expect(res.headers.get('content-type')).toContain(
|
|
88
|
+
'application/json',
|
|
89
|
+
)
|
|
90
|
+
})
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
describe('Response Format', () => {
|
|
94
|
+
it('should return correct JSON structure', async () => {
|
|
95
|
+
const res = await app.request('/-/whoami', {}, env)
|
|
96
|
+
expect(res.status).toBe(200)
|
|
97
|
+
const data = await res.json()
|
|
98
|
+
expect(data).toHaveProperty('username')
|
|
99
|
+
expect(typeof data.username).toBe('string')
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('should return consistent format across all upstream paths', async () => {
|
|
103
|
+
const paths = [
|
|
104
|
+
'/-/whoami',
|
|
105
|
+
'/npm/-/whoami',
|
|
106
|
+
'/jsr/-/whoami',
|
|
107
|
+
'/custom/-/whoami',
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
for (const path of paths) {
|
|
111
|
+
const res = await app.request(path, {}, env)
|
|
112
|
+
expect(res.status).toBe(200)
|
|
113
|
+
const data = await res.json()
|
|
114
|
+
expect(data).toHaveProperty('username')
|
|
115
|
+
expect(typeof data.username).toBe('string')
|
|
116
|
+
}
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
})
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"target": "es2022",
|
|
5
|
+
"module": "esnext",
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"allowSyntheticDefaultImports": true,
|
|
9
|
+
"resolveJsonModule": true,
|
|
10
|
+
"declaration": false,
|
|
11
|
+
"declarationMap": false,
|
|
12
|
+
"sourceMap": true,
|
|
13
|
+
"noEmit": true
|
|
14
|
+
},
|
|
15
|
+
"include": ["src/**/*", "config.ts", "types.ts"]
|
|
16
|
+
}
|
package/typedoc.mjs
ADDED