forked from
npmx.dev/npmx.dev
[READ-ONLY]
a fast, modern browser for the npm registry
1import { describe, expect, it } from 'vitest'
2import { isValidSpdxLicense, getSpdxLicenseUrl, parseLicenseExpression } from '#shared/utils/spdx'
3
4describe('spdx utilities', () => {
5 describe('isValidSpdxLicense', () => {
6 it('returns true for valid SPDX licenses', () => {
7 expect(isValidSpdxLicense('MIT')).toBe(true)
8 expect(isValidSpdxLicense('Apache-2.0')).toBe(true)
9 expect(isValidSpdxLicense('GPL-3.0-only')).toBe(true)
10 expect(isValidSpdxLicense('BSD-2-Clause')).toBe(true)
11 expect(isValidSpdxLicense('ISC')).toBe(true)
12 })
13
14 it('returns false for invalid licenses', () => {
15 expect(isValidSpdxLicense('CustomLicense')).toBe(false)
16 expect(isValidSpdxLicense('INVALID')).toBe(false)
17 expect(isValidSpdxLicense('')).toBe(false)
18 })
19
20 it('is case-sensitive', () => {
21 expect(isValidSpdxLicense('mit')).toBe(false)
22 expect(isValidSpdxLicense('Mit')).toBe(false)
23 expect(isValidSpdxLicense('MIT')).toBe(true)
24 })
25 })
26
27 describe('getSpdxLicenseUrl', () => {
28 it('returns URL for valid license identifiers', () => {
29 expect(getSpdxLicenseUrl('MIT')).toBe('https://spdx.org/licenses/MIT.html')
30 expect(getSpdxLicenseUrl('ISC')).toBe('https://spdx.org/licenses/ISC.html')
31 expect(getSpdxLicenseUrl('Apache-2.0')).toBe('https://spdx.org/licenses/Apache-2.0.html')
32 expect(getSpdxLicenseUrl('GPL-3.0-only')).toBe('https://spdx.org/licenses/GPL-3.0-only.html')
33 expect(getSpdxLicenseUrl('BSD-2-Clause')).toBe('https://spdx.org/licenses/BSD-2-Clause.html')
34 expect(getSpdxLicenseUrl('GPL-3.0+')).toBe('https://spdx.org/licenses/GPL-3.0+.html')
35 })
36
37 it('returns null for invalid licenses', () => {
38 expect(getSpdxLicenseUrl('CustomLicense')).toBeNull()
39 expect(getSpdxLicenseUrl('INVALID')).toBeNull()
40 expect(getSpdxLicenseUrl('MIT OR Apache-2.0')).toBeNull()
41 })
42
43 it('returns null for undefined or empty', () => {
44 expect(getSpdxLicenseUrl(undefined)).toBeNull()
45 expect(getSpdxLicenseUrl('')).toBeNull()
46 expect(getSpdxLicenseUrl(' ')).toBeNull()
47 })
48
49 it('trims whitespace', () => {
50 expect(getSpdxLicenseUrl(' MIT ')).toBe('https://spdx.org/licenses/MIT.html')
51 })
52 })
53
54 describe('parseLicenseExpression', () => {
55 describe('single licenses', () => {
56 it('parses a single valid license', () => {
57 const tokens = parseLicenseExpression('MIT')
58 expect(tokens).toEqual([
59 { type: 'license', value: 'MIT', url: 'https://spdx.org/licenses/MIT.html' },
60 ])
61 })
62
63 it('parses a single invalid license without URL', () => {
64 const tokens = parseLicenseExpression('CustomLicense')
65 expect(tokens).toEqual([{ type: 'license', value: 'CustomLicense', url: undefined }])
66 })
67 })
68
69 describe('OR expressions', () => {
70 it('parses "MIT OR Apache-2.0"', () => {
71 const tokens = parseLicenseExpression('MIT OR Apache-2.0')
72 expect(tokens).toEqual([
73 { type: 'license', value: 'MIT', url: 'https://spdx.org/licenses/MIT.html' },
74 { type: 'operator', value: 'OR' },
75 {
76 type: 'license',
77 value: 'Apache-2.0',
78 url: 'https://spdx.org/licenses/Apache-2.0.html',
79 },
80 ])
81 })
82
83 it('parses triple OR expression', () => {
84 const tokens = parseLicenseExpression('BSD-2-Clause OR MIT OR Apache-2.0')
85 expect(tokens).toHaveLength(5)
86 expect(tokens.filter(t => t.type === 'license')).toHaveLength(3)
87 expect(tokens.filter(t => t.type === 'operator')).toHaveLength(2)
88 })
89 })
90
91 describe('AND expressions', () => {
92 it('parses "MIT AND Zlib"', () => {
93 const tokens = parseLicenseExpression('MIT AND Zlib')
94 expect(tokens).toEqual([
95 { type: 'license', value: 'MIT', url: 'https://spdx.org/licenses/MIT.html' },
96 { type: 'operator', value: 'AND' },
97 { type: 'license', value: 'Zlib', url: 'https://spdx.org/licenses/Zlib.html' },
98 ])
99 })
100 })
101
102 describe('WITH expressions', () => {
103 it('parses license with exception', () => {
104 const tokens = parseLicenseExpression('GPL-2.0-only WITH Classpath-exception-2.0')
105 expect(tokens).toHaveLength(3)
106 expect(tokens[0]).toEqual({
107 type: 'license',
108 value: 'GPL-2.0-only',
109 url: 'https://spdx.org/licenses/GPL-2.0-only.html',
110 })
111 expect(tokens[1]).toEqual({ type: 'operator', value: 'WITH' })
112 // Exception identifiers are not valid licenses
113 expect(tokens[2]?.type).toBe('license')
114 expect(tokens[2]?.value).toBe('Classpath-exception-2.0')
115 })
116 })
117
118 describe('parenthesized expressions', () => {
119 it('strips parentheses from "(MIT OR Apache-2.0)"', () => {
120 const tokens = parseLicenseExpression('(MIT OR Apache-2.0)')
121 expect(tokens).toHaveLength(3)
122 expect(tokens.map(t => t.value)).toEqual(['MIT', 'OR', 'Apache-2.0'])
123 })
124
125 it('strips parentheses from nested expression', () => {
126 const tokens = parseLicenseExpression('((MIT))')
127 expect(tokens).toHaveLength(1)
128 expect(tokens[0]?.value).toBe('MIT')
129 })
130 })
131
132 describe('mixed valid and invalid', () => {
133 it('handles mix of valid and invalid licenses', () => {
134 const tokens = parseLicenseExpression('MIT OR CustomLicense')
135 expect(tokens).toHaveLength(3)
136 expect(tokens[0]?.url).toBe('https://spdx.org/licenses/MIT.html')
137 expect(tokens[2]?.url).toBeUndefined()
138 })
139 })
140
141 describe('real-world examples', () => {
142 it('handles rc package: (BSD-2-Clause OR MIT OR Apache-2.0)', () => {
143 const tokens = parseLicenseExpression('(BSD-2-Clause OR MIT OR Apache-2.0)')
144 const licenses = tokens.filter(t => t.type === 'license')
145 expect(licenses).toHaveLength(3)
146 expect(licenses.map(l => l.value)).toEqual(['BSD-2-Clause', 'MIT', 'Apache-2.0'])
147 expect(licenses.every(l => l.url)).toBe(true)
148 })
149
150 it('handles jszip package: (MIT OR GPL-3.0-or-later)', () => {
151 const tokens = parseLicenseExpression('(MIT OR GPL-3.0-or-later)')
152 const licenses = tokens.filter(t => t.type === 'license')
153 expect(licenses).toHaveLength(2)
154 expect(licenses.every(l => l.url)).toBe(true)
155 })
156
157 it('handles pako package: (MIT AND Zlib)', () => {
158 const tokens = parseLicenseExpression('(MIT AND Zlib)')
159 expect(tokens).toHaveLength(3)
160 expect(tokens[1]?.value).toBe('AND')
161 })
162
163 it('handles complex expression: Apache-2.0 WITH LLVM-exception', () => {
164 const tokens = parseLicenseExpression('Apache-2.0 WITH LLVM-exception')
165 expect(tokens).toHaveLength(3)
166 expect(tokens[0]?.value).toBe('Apache-2.0')
167 expect(tokens[1]?.value).toBe('WITH')
168 expect(tokens[2]?.value).toBe('LLVM-exception')
169 })
170 })
171
172 describe('edge cases', () => {
173 it('handles empty string', () => {
174 const tokens = parseLicenseExpression('')
175 expect(tokens).toEqual([])
176 })
177
178 it('handles whitespace only', () => {
179 const tokens = parseLicenseExpression(' ')
180 expect(tokens).toEqual([])
181 })
182
183 it('handles extra whitespace between tokens', () => {
184 const tokens = parseLicenseExpression('MIT OR Apache-2.0')
185 expect(tokens).toHaveLength(3)
186 expect(tokens.map(t => t.value)).toEqual(['MIT', 'OR', 'Apache-2.0'])
187 })
188
189 it('handles license IDs with dots and plus signs', () => {
190 const tokens = parseLicenseExpression('GPL-2.0+')
191 expect(tokens[0]?.value).toBe('GPL-2.0+')
192 })
193 })
194 })
195})