[READ-ONLY] a fast, modern browser for the npm registry
at main 195 lines 7.7 kB view raw
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})