forked from
npmx.dev/npmx.dev
[READ-ONLY]
a fast, modern browser for the npm registry
1import { describe, expect, it } from 'vitest'
2import {
3 validateUsername,
4 validateOrgName,
5 validateScopeTeam,
6 validatePackageName,
7 extractUrls,
8} from '../../../cli/src/npm-client.ts'
9
10describe('validateUsername', () => {
11 it('accepts valid usernames', () => {
12 expect(() => validateUsername('alice')).not.toThrow()
13 expect(() => validateUsername('bob123')).not.toThrow()
14 expect(() => validateUsername('my-user')).not.toThrow()
15 expect(() => validateUsername('user-name-123')).not.toThrow()
16 expect(() => validateUsername('a')).not.toThrow()
17 expect(() => validateUsername('A1')).not.toThrow()
18 })
19
20 it('rejects empty or missing usernames', () => {
21 expect(() => validateUsername('')).toThrow('Invalid username')
22 expect(() => validateUsername(null as unknown as string)).toThrow('Invalid username')
23 expect(() => validateUsername(undefined as unknown as string)).toThrow('Invalid username')
24 })
25
26 it('rejects usernames that are too long', () => {
27 const longName = 'a'.repeat(51)
28 expect(() => validateUsername(longName)).toThrow('Invalid username')
29 })
30
31 it('rejects usernames with invalid characters', () => {
32 expect(() => validateUsername('user;rm -rf')).toThrow('Invalid username')
33 expect(() => validateUsername('user && evil')).toThrow('Invalid username')
34 expect(() => validateUsername('$(whoami)')).toThrow('Invalid username')
35 expect(() => validateUsername('user`id`')).toThrow('Invalid username')
36 expect(() => validateUsername('user|cat')).toThrow('Invalid username')
37 expect(() => validateUsername('user name')).toThrow('Invalid username')
38 expect(() => validateUsername('user.name')).toThrow('Invalid username')
39 expect(() => validateUsername('user_name')).toThrow('Invalid username')
40 expect(() => validateUsername('user@name')).toThrow('Invalid username')
41 })
42
43 it('rejects usernames starting or ending with hyphen', () => {
44 expect(() => validateUsername('-username')).toThrow('Invalid username')
45 expect(() => validateUsername('username-')).toThrow('Invalid username')
46 expect(() => validateUsername('-')).toThrow('Invalid username')
47 })
48})
49
50describe('validateOrgName', () => {
51 it('accepts valid org names', () => {
52 expect(() => validateOrgName('nuxt')).not.toThrow()
53 expect(() => validateOrgName('my-org')).not.toThrow()
54 expect(() => validateOrgName('org123')).not.toThrow()
55 })
56
57 it('rejects empty or missing org names', () => {
58 expect(() => validateOrgName('')).toThrow('Invalid org name')
59 })
60
61 it('rejects org names that are too long', () => {
62 const longName = 'a'.repeat(51)
63 expect(() => validateOrgName(longName)).toThrow('Invalid org name')
64 })
65
66 it('rejects org names with shell injection characters', () => {
67 expect(() => validateOrgName('org;rm -rf /')).toThrow('Invalid org name')
68 expect(() => validateOrgName('org && evil')).toThrow('Invalid org name')
69 expect(() => validateOrgName('$(whoami)')).toThrow('Invalid org name')
70 })
71})
72
73describe('validateScopeTeam', () => {
74 it('accepts valid scope:team format', () => {
75 expect(() => validateScopeTeam('@nuxt:developers')).not.toThrow()
76 expect(() => validateScopeTeam('@my-org:my-team')).not.toThrow()
77 expect(() => validateScopeTeam('@org123:team456')).not.toThrow()
78 expect(() => validateScopeTeam('@a:b')).not.toThrow()
79 })
80
81 it('rejects empty or missing scope:team', () => {
82 expect(() => validateScopeTeam('')).toThrow('Invalid scope:team')
83 expect(() => validateScopeTeam(null as unknown as string)).toThrow('Invalid scope:team')
84 })
85
86 it('rejects scope:team that is too long', () => {
87 const longScopeTeam = '@' + 'a'.repeat(50) + ':' + 'b'.repeat(50)
88 expect(() => validateScopeTeam(longScopeTeam)).toThrow('Invalid scope:team')
89 })
90
91 it('rejects invalid scope:team format', () => {
92 expect(() => validateScopeTeam('nuxt:developers')).toThrow('Invalid scope:team format')
93 expect(() => validateScopeTeam('@nuxt')).toThrow('Invalid scope:team format')
94 expect(() => validateScopeTeam('developers')).toThrow('Invalid scope:team format')
95 expect(() => validateScopeTeam('@:team')).toThrow('Invalid scope:team format')
96 expect(() => validateScopeTeam('@org:')).toThrow('Invalid scope:team format')
97 })
98
99 it('rejects scope:team with shell injection in scope', () => {
100 expect(() => validateScopeTeam('@org;rm:team')).toThrow('Invalid scope:team format')
101 expect(() => validateScopeTeam('@$(whoami):team')).toThrow('Invalid scope:team format')
102 })
103
104 it('rejects scope:team with shell injection in team', () => {
105 expect(() => validateScopeTeam('@org:team;rm')).toThrow('Invalid scope:team format')
106 expect(() => validateScopeTeam('@org:$(whoami)')).toThrow('Invalid scope:team format')
107 })
108
109 it('rejects scope or team starting/ending with hyphen', () => {
110 expect(() => validateScopeTeam('@-org:team')).toThrow('Invalid scope:team format')
111 expect(() => validateScopeTeam('@org-:team')).toThrow('Invalid scope:team format')
112 expect(() => validateScopeTeam('@org:-team')).toThrow('Invalid scope:team format')
113 expect(() => validateScopeTeam('@org:team-')).toThrow('Invalid scope:team format')
114 })
115})
116
117describe('validatePackageName', () => {
118 it('accepts valid package names', () => {
119 expect(() => validatePackageName('my-package')).not.toThrow()
120 expect(() => validatePackageName('@scope/package')).not.toThrow()
121 expect(() => validatePackageName('package123')).not.toThrow()
122 })
123
124 it('rejects package names with shell injection', () => {
125 expect(() => validatePackageName('pkg;rm -rf /')).toThrow('Invalid package name')
126 expect(() => validatePackageName('pkg && evil')).toThrow('Invalid package name')
127 expect(() => validatePackageName('$(whoami)')).toThrow('Invalid package name')
128 })
129
130 it('rejects empty package names', () => {
131 expect(() => validatePackageName('')).toThrow('Invalid package name')
132 })
133})
134
135describe('extractUrls', () => {
136 it('extracts HTTP URLs from text', () => {
137 const text = 'Visit http://example.com for more info'
138 expect(extractUrls(text)).toEqual(['http://example.com'])
139 })
140
141 it('extracts HTTPS URLs from text', () => {
142 const text = 'Visit https://example.com/path for more info'
143 expect(extractUrls(text)).toEqual(['https://example.com/path'])
144 })
145
146 it('extracts multiple URLs from text', () => {
147 const text = 'See https://example.com and http://other.org/page'
148 expect(extractUrls(text)).toEqual(['https://example.com', 'http://other.org/page'])
149 })
150
151 it('strips trailing punctuation from URLs', () => {
152 expect(extractUrls('Go to https://example.com.')).toEqual(['https://example.com'])
153 expect(extractUrls('Go to https://example.com,')).toEqual(['https://example.com'])
154 expect(extractUrls('Go to https://example.com;')).toEqual(['https://example.com'])
155 expect(extractUrls('Go to https://example.com:')).toEqual(['https://example.com'])
156 expect(extractUrls('Go to https://example.com!')).toEqual(['https://example.com'])
157 expect(extractUrls('Go to https://example.com?')).toEqual(['https://example.com'])
158 expect(extractUrls('Go to https://example.com)')).toEqual(['https://example.com'])
159 })
160
161 it('strips multiple trailing punctuation characters', () => {
162 expect(extractUrls('See https://example.com/path).')).toEqual(['https://example.com/path'])
163 })
164
165 it('preserves query strings and fragments', () => {
166 expect(extractUrls('Go to https://example.com/path?q=1&b=2#anchor')).toEqual([
167 'https://example.com/path?q=1&b=2#anchor',
168 ])
169 })
170
171 it('returns empty array when no URLs found', () => {
172 expect(extractUrls('No URLs here')).toEqual([])
173 expect(extractUrls('')).toEqual([])
174 })
175
176 it('deduplicates identical URLs', () => {
177 const text = 'Visit https://example.com and again https://example.com'
178 expect(extractUrls(text)).toEqual(['https://example.com'])
179 })
180
181 it('extracts URLs from npm auth output', () => {
182 const npmOutput =
183 'Authenticate your account at:\nhttps://www.npmjs.com/login?next=/login/cli/abc123'
184 expect(extractUrls(npmOutput)).toEqual(['https://www.npmjs.com/login?next=/login/cli/abc123'])
185 })
186})