diff --git a/jam-ui/src/services/__tests__/attachmentValidation.test.js b/jam-ui/src/services/__tests__/attachmentValidation.test.js new file mode 100644 index 000000000..73f09efd6 --- /dev/null +++ b/jam-ui/src/services/__tests__/attachmentValidation.test.js @@ -0,0 +1,264 @@ +import { + validateFileSize, + validateFileType, + getAttachmentType, + validateFile, + formatFileSize, + MAX_FILE_SIZE, + ALLOWED_EXTENSIONS +} from '../attachmentValidation'; + +describe('attachmentValidation', () => { + describe('validateFileSize', () => { + test('accepts file under limit', () => { + const file = new File(['content'], 'test.pdf', { type: 'application/pdf' }); + Object.defineProperty(file, 'size', { value: 5 * 1024 * 1024 }); // 5 MB + const result = validateFileSize(file); + expect(result).toEqual({ valid: true, error: null }); + }); + + test('rejects file over limit', () => { + const file = new File(['content'], 'large.pdf', { type: 'application/pdf' }); + Object.defineProperty(file, 'size', { value: 15 * 1024 * 1024 }); // 15 MB + const result = validateFileSize(file); + expect(result.valid).toBe(false); + expect(result.error).toContain('10 MB'); + }); + + test('accepts file exactly at limit', () => { + const file = new File(['content'], 'exact.pdf', { type: 'application/pdf' }); + Object.defineProperty(file, 'size', { value: 10 * 1024 * 1024 }); // Exactly 10 MB + const result = validateFileSize(file); + expect(result).toEqual({ valid: true, error: null }); + }); + + test('rejects file exactly over limit by 1 byte', () => { + const file = new File(['content'], 'oversize.pdf', { type: 'application/pdf' }); + Object.defineProperty(file, 'size', { value: 10 * 1024 * 1024 + 1 }); // 10 MB + 1 byte + const result = validateFileSize(file); + expect(result.valid).toBe(false); + expect(result.error).toContain('10 MB'); + }); + + test('accepts custom max size', () => { + const file = new File(['content'], 'test.pdf', { type: 'application/pdf' }); + Object.defineProperty(file, 'size', { value: 8 * 1024 * 1024 }); // 8 MB + const result = validateFileSize(file, 5 * 1024 * 1024); // 5 MB limit + expect(result.valid).toBe(false); + expect(result.error).toContain('5 MB'); + }); + }); + + describe('validateFileType', () => { + test('accepts pdf file', () => { + const file = new File(['content'], 'document.pdf', { type: 'application/pdf' }); + const result = validateFileType(file); + expect(result).toEqual({ valid: true, error: null }); + }); + + test('accepts mp3 file', () => { + const file = new File(['content'], 'music.mp3', { type: 'audio/mpeg' }); + const result = validateFileType(file); + expect(result).toEqual({ valid: true, error: null }); + }); + + test('accepts wav file', () => { + const file = new File(['content'], 'track.wav', { type: 'audio/wav' }); + const result = validateFileType(file); + expect(result).toEqual({ valid: true, error: null }); + }); + + test('accepts png file', () => { + const file = new File(['content'], 'image.png', { type: 'image/png' }); + const result = validateFileType(file); + expect(result).toEqual({ valid: true, error: null }); + }); + + test('rejects exe file', () => { + const file = new File(['content'], 'virus.exe', { type: 'application/x-msdownload' }); + const result = validateFileType(file); + expect(result.valid).toBe(false); + expect(result.error).toContain('not allowed'); + expect(result.error).toContain('.pdf'); + }); + + test('rejects zip file', () => { + const file = new File(['content'], 'archive.zip', { type: 'application/zip' }); + const result = validateFileType(file); + expect(result.valid).toBe(false); + expect(result.error).toContain('not allowed'); + }); + + test('accepts uppercase extension', () => { + const file = new File(['content'], 'DOCUMENT.PDF', { type: 'application/pdf' }); + const result = validateFileType(file); + expect(result).toEqual({ valid: true, error: null }); + }); + + test('accepts mixed case extension', () => { + const file = new File(['content'], 'Document.Pdf', { type: 'application/pdf' }); + const result = validateFileType(file); + expect(result).toEqual({ valid: true, error: null }); + }); + + test('accepts all allowed extensions', () => { + const extensions = ['.pdf', '.xml', '.mxl', '.txt', '.png', '.jpg', '.jpeg', '.gif', '.mp3', '.wav']; + extensions.forEach(ext => { + const file = new File(['content'], `test${ext}`, { type: 'application/octet-stream' }); + const result = validateFileType(file); + expect(result.valid).toBe(true); + }); + }); + }); + + describe('getAttachmentType', () => { + test('identifies mp3 as audio', () => { + expect(getAttachmentType('song.mp3')).toBe('audio'); + }); + + test('identifies wav as audio', () => { + expect(getAttachmentType('track.wav')).toBe('audio'); + }); + + test('identifies flac as audio', () => { + expect(getAttachmentType('audio.flac')).toBe('audio'); + }); + + test('identifies ogg as audio', () => { + expect(getAttachmentType('music.ogg')).toBe('audio'); + }); + + test('identifies pdf as notation', () => { + expect(getAttachmentType('sheet.pdf')).toBe('notation'); + }); + + test('identifies xml as notation', () => { + expect(getAttachmentType('score.xml')).toBe('notation'); + }); + + test('identifies png as notation', () => { + expect(getAttachmentType('image.png')).toBe('notation'); + }); + + test('identifies txt as notation', () => { + expect(getAttachmentType('notes.txt')).toBe('notation'); + }); + + test('handles uppercase extensions', () => { + expect(getAttachmentType('SONG.MP3')).toBe('audio'); + expect(getAttachmentType('SHEET.PDF')).toBe('notation'); + }); + + test('handles multiple dots in filename', () => { + expect(getAttachmentType('my.song.name.mp3')).toBe('audio'); + expect(getAttachmentType('document.v2.pdf')).toBe('notation'); + }); + }); + + describe('validateFile', () => { + test('accepts valid pdf file', () => { + const file = new File(['content'], 'document.pdf', { type: 'application/pdf' }); + Object.defineProperty(file, 'size', { value: 5 * 1024 * 1024 }); + const result = validateFile(file); + expect(result.valid).toBe(true); + expect(result.error).toBeNull(); + expect(result.warnings).toEqual([]); + }); + + test('rejects file over size limit', () => { + const file = new File(['content'], 'large.pdf', { type: 'application/pdf' }); + Object.defineProperty(file, 'size', { value: 15 * 1024 * 1024 }); + const result = validateFile(file); + expect(result.valid).toBe(false); + expect(result.error).toContain('10 MB'); + expect(result.warnings).toEqual([]); + }); + + test('rejects invalid file type', () => { + const file = new File(['content'], 'virus.exe', { type: 'application/x-msdownload' }); + Object.defineProperty(file, 'size', { value: 1024 }); + const result = validateFile(file); + expect(result.valid).toBe(false); + expect(result.error).toContain('not allowed'); + expect(result.warnings).toEqual([]); + }); + + test('warns about mp3 files (not backend supported)', () => { + const file = new File(['content'], 'music.mp3', { type: 'audio/mpeg' }); + Object.defineProperty(file, 'size', { value: 5 * 1024 * 1024 }); + const result = validateFile(file); + expect(result.valid).toBe(true); + expect(result.error).toBeNull(); + expect(result.warnings).toHaveLength(1); + expect(result.warnings[0]).toContain('.mp3'); + expect(result.warnings[0]).toContain('may not be supported'); + }); + + test('does not warn about wav files (backend supported)', () => { + const file = new File(['content'], 'track.wav', { type: 'audio/wav' }); + Object.defineProperty(file, 'size', { value: 5 * 1024 * 1024 }); + const result = validateFile(file); + expect(result.valid).toBe(true); + expect(result.error).toBeNull(); + expect(result.warnings).toEqual([]); + }); + + test('does not warn about pdf files (backend supported)', () => { + const file = new File(['content'], 'document.pdf', { type: 'application/pdf' }); + Object.defineProperty(file, 'size', { value: 5 * 1024 * 1024 }); + const result = validateFile(file); + expect(result.valid).toBe(true); + expect(result.error).toBeNull(); + expect(result.warnings).toEqual([]); + }); + + test('stops at first error (does not check type if size fails)', () => { + const file = new File(['content'], 'large.exe', { type: 'application/x-msdownload' }); + Object.defineProperty(file, 'size', { value: 15 * 1024 * 1024 }); + const result = validateFile(file); + expect(result.valid).toBe(false); + expect(result.error).toContain('10 MB'); // Size error, not type error + }); + }); + + describe('formatFileSize', () => { + test('formats bytes', () => { + expect(formatFileSize(0)).toBe('0 B'); + expect(formatFileSize(500)).toBe('500 B'); + expect(formatFileSize(1023)).toBe('1023 B'); + }); + + test('formats kilobytes', () => { + expect(formatFileSize(1024)).toBe('1.0 KB'); + expect(formatFileSize(2048)).toBe('2.0 KB'); + expect(formatFileSize(1536)).toBe('1.5 KB'); + expect(formatFileSize(10240)).toBe('10.0 KB'); + }); + + test('formats megabytes', () => { + expect(formatFileSize(1024 * 1024)).toBe('1.0 MB'); + expect(formatFileSize(5 * 1024 * 1024)).toBe('5.0 MB'); + expect(formatFileSize(5242880)).toBe('5.0 MB'); + expect(formatFileSize(10 * 1024 * 1024)).toBe('10.0 MB'); + }); + + test('rounds to one decimal place', () => { + expect(formatFileSize(1536)).toBe('1.5 KB'); + expect(formatFileSize(2.5 * 1024 * 1024)).toBe('2.5 MB'); + expect(formatFileSize(3.75 * 1024 * 1024)).toBe('3.8 MB'); + }); + }); + + describe('constants', () => { + test('MAX_FILE_SIZE is 10 MB', () => { + expect(MAX_FILE_SIZE).toBe(10 * 1024 * 1024); + }); + + test('ALLOWED_EXTENSIONS includes all required types', () => { + const requiredExtensions = ['.pdf', '.xml', '.mxl', '.txt', '.png', '.jpg', '.jpeg', '.gif', '.mp3', '.wav']; + requiredExtensions.forEach(ext => { + expect(ALLOWED_EXTENSIONS).toContain(ext); + }); + }); + }); +});