import 'dart:io'; import 'dart:typed_data'; import 'package:path/path.dart' as p; import 'package:test/test.dart'; import '../../../../bin/domain/repositories/photo_repository.dart'; void main() { late Directory tempDir; late PhotoRepository repository; setUp(() async { tempDir = await Directory.systemTemp.createTemp('photo_repo_test_'); repository = PhotoRepository(tempDir.path); }); tearDown(() async { if (await tempDir.exists()) { await tempDir.delete(recursive: true); } }); // ===== Helper methods to create test image files ===== /// Create a minimal valid PNG file (1x1 pixel, IHDR at known position) File _createTestPng( Directory dir, String name, { int width = 100, int height = 200, }) { final buffer = BytesBuilder(); // PNG signature (8 bytes) buffer.addByte(0x89); buffer.addByte(0x50); buffer.addByte(0x4E); buffer.addByte(0x47); buffer.addByte(0x0D); buffer.addByte(0x0A); buffer.addByte(0x1A); buffer.addByte(0x0A); // IHDR chunk length (4 bytes) - 13 bytes of data buffer.addByte(0x00); buffer.addByte(0x00); buffer.addByte(0x00); buffer.addByte(0x0D); // IHDR chunk type buffer.addByte(0x49); buffer.addByte(0x48); buffer.addByte(0x44); buffer.addByte(0x52); // Width (4 bytes, big-endian) buffer.addByte((width >> 24) & 0xFF); buffer.addByte((width >> 16) & 0xFF); buffer.addByte((width >> 8) & 0xFF); buffer.addByte(width & 0xFF); // Height (4 bytes, big-endian) buffer.addByte((height >> 24) & 0xFF); buffer.addByte((height >> 16) & 0xFF); buffer.addByte((height >> 8) & 0xFF); buffer.addByte(height & 0xFF); // Rest of IHDR buffer.addByte(0x08); buffer.addByte(0x02); buffer.addByte(0x00); buffer.addByte(0x00); buffer.addByte(0x00); // CRC (dummy) buffer.addByte(0x00); buffer.addByte(0x00); buffer.addByte(0x00); buffer.addByte(0x00); // IEND buffer.addByte(0x00); buffer.addByte(0x00); buffer.addByte(0x00); buffer.addByte(0x00); buffer.addByte(0x49); buffer.addByte(0x45); buffer.addByte(0x4E); buffer.addByte(0x44); buffer.addByte(0xAE); buffer.addByte(0x42); buffer.addByte(0x60); buffer.addByte(0x82); final file = File(p.join(dir.path, name)); file.writeAsBytesSync(buffer.toBytes()); return file; } /// Create a minimal valid GIF file File _createTestGif( Directory dir, String name, { int width = 150, int height = 250, }) { final buffer = BytesBuilder(); // GIF89a header buffer.addByte(0x47); buffer.addByte(0x49); buffer.addByte(0x46); buffer.addByte(0x38); buffer.addByte(0x39); buffer.addByte(0x61); // Width (little-endian) buffer.addByte(width & 0xFF); buffer.addByte((width >> 8) & 0xFF); // Height (little-endian) buffer.addByte(height & 0xFF); buffer.addByte((height >> 8) & 0xFF); buffer.addByte(0x00); buffer.addByte(0x00); buffer.addByte(0x00); buffer.addByte(0x3B); final file = File(p.join(dir.path, name)); file.writeAsBytesSync(buffer.toBytes()); return file; } /// Create a minimal valid BMP file File _createTestBmp( Directory dir, String name, { int width = 300, int height = 400, }) { final buffer = BytesBuilder(); buffer.addByte(0x42); buffer.addByte(0x4D); final fileSize = 54 + width * height * 3; buffer.addByte(fileSize & 0xFF); buffer.addByte((fileSize >> 8) & 0xFF); buffer.addByte((fileSize >> 16) & 0xFF); buffer.addByte((fileSize >> 24) & 0xFF); // Reserved buffer.addByte(0x00); buffer.addByte(0x00); buffer.addByte(0x00); buffer.addByte(0x00); // Offset to pixel data buffer.addByte(0x36); buffer.addByte(0x00); buffer.addByte(0x00); buffer.addByte(0x00); // DIB header buffer.addByte(0x28); buffer.addByte(0x00); buffer.addByte(0x00); buffer.addByte(0x00); // Width (little-endian) buffer.addByte(width & 0xFF); buffer.addByte((width >> 8) & 0xFF); buffer.addByte((width >> 16) & 0xFF); buffer.addByte((width >> 24) & 0xFF); // Height (little-endian) buffer.addByte(height & 0xFF); buffer.addByte((height >> 8) & 0xFF); buffer.addByte((height >> 16) & 0xFF); buffer.addByte((height >> 24) & 0xFF); // Planes, bits per pixel buffer.addByte(0x01); buffer.addByte(0x00); buffer.addByte(0x18); buffer.addByte(0x00); // Compression (0) buffer.addByte(0x00); buffer.addByte(0x00); buffer.addByte(0x00); buffer.addByte(0x00); // Padding for (var i = 0; i < 24; i++) { buffer.addByte(0x00); } final file = File(p.join(dir.path, name)); file.writeAsBytesSync(buffer.toBytes()); return file; } /// Create a minimal valid WebP VP8 (lossy) file File _createTestWebpVp8( Directory dir, String name, { int width = 120, int height = 80, }) { final buffer = BytesBuilder(); // RIFF buffer.addByte(0x52); buffer.addByte(0x49); buffer.addByte(0x46); buffer.addByte(0x46); // File size (placeholder) buffer.addByte(0x20); buffer.addByte(0x00); buffer.addByte(0x00); buffer.addByte(0x00); // WEBP buffer.addByte(0x57); buffer.addByte(0x45); buffer.addByte(0x42); buffer.addByte(0x50); // VP8 buffer.addByte(0x56); buffer.addByte(0x50); buffer.addByte(0x38); buffer.addByte(0x20); // Chunk size buffer.addByte(0x10); buffer.addByte(0x00); buffer.addByte(0x00); buffer.addByte(0x00); // Frame tag bytes (positions 20-21) buffer.addByte(0x00); buffer.addByte(0x00); // Key check: (data[23] << 8) | data[22] == 0x9D01 (positions 22-23) buffer.addByte(0x01); buffer.addByte(0x9D); // Padding before width/height (positions 24-25) buffer.addByte(0x00); buffer.addByte(0x00); // Width (little-endian) buffer.addByte(width & 0xFF); buffer.addByte((width >> 8) & 0xFF); // Height (little-endian) buffer.addByte(height & 0xFF); buffer.addByte((height >> 8) & 0xFF); final file = File(p.join(dir.path, name)); file.writeAsBytesSync(buffer.toBytes()); return file; } // ===== Tests ===== group('getPhotosByAlbumId', () { test( 'should return empty list when base directory does not exist', () async { final repo = PhotoRepository('/nonexistent/path'); final photos = await repo.getPhotosByAlbumId(123); expect(photos, isEmpty); }, ); test( 'should return empty list when album directory does not exist', () async { final photos = await repository.getPhotosByAlbumId( p.join(tempDir.path, 'nonexistent').hashCode, ); expect(photos, isEmpty); }, ); test('should return photos for a valid album directory', () async { final albumDir = await Directory( p.join(tempDir.path, 'my_album'), ).create(); _createTestPng(albumDir, 'photo1.png', width: 100, height: 200); _createTestGif(albumDir, 'photo2.gif', width: 150, height: 250); final albumId = p.join(tempDir.path, 'my_album').hashCode; final photos = await repository.getPhotosByAlbumId(albumId); expect(photos.length, 2); expect( photos.map((p) => p.fileName), containsAll(['photo1.png', 'photo2.gif']), ); }); test('should not include non-image files', () async { final albumDir = await Directory(p.join(tempDir.path, 'mixed')).create(); _createTestPng(albumDir, 'image.png'); File(p.join(albumDir.path, 'document.txt')).writeAsString('not an image'); File(p.join(albumDir.path, 'readme.md')).writeAsString('# Readme'); final albumId = p.join(tempDir.path, 'mixed').hashCode; final photos = await repository.getPhotosByAlbumId(albumId); expect(photos.length, 1); expect(photos.first.fileName, 'image.png'); }); test('should parse PNG dimensions correctly', () async { final albumDir = await Directory( p.join(tempDir.path, 'png_test'), ).create(); _createTestPng(albumDir, 'test.png', width: 640, height: 480); final albumId = p.join(tempDir.path, 'png_test').hashCode; final photos = await repository.getPhotosByAlbumId(albumId); expect(photos.length, 1); expect(photos.first.width, 640); expect(photos.first.height, 480); expect(photos.first.mimeType, 'image/png'); }); test('should parse GIF dimensions correctly', () async { final albumDir = await Directory( p.join(tempDir.path, 'gif_test'), ).create(); _createTestGif(albumDir, 'test.gif', width: 320, height: 240); final albumId = p.join(tempDir.path, 'gif_test').hashCode; final photos = await repository.getPhotosByAlbumId(albumId); expect(photos.length, 1); expect(photos.first.width, 320); expect(photos.first.height, 240); expect(photos.first.mimeType, 'image/gif'); }); test('should parse BMP dimensions correctly', () async { final albumDir = await Directory( p.join(tempDir.path, 'bmp_test'), ).create(); _createTestBmp(albumDir, 'test.bmp', width: 800, height: 600); final albumId = p.join(tempDir.path, 'bmp_test').hashCode; final photos = await repository.getPhotosByAlbumId(albumId); expect(photos.length, 1); expect(photos.first.width, 800); expect(photos.first.height, 600); expect(photos.first.mimeType, 'image/bmp'); }); test('should handle WebP VP8 dimensions', () async { final albumDir = await Directory( p.join(tempDir.path, 'webp_test'), ).create(); _createTestWebpVp8(albumDir, 'test.webp', width: 1920, height: 1080); final albumId = p.join(tempDir.path, 'webp_test').hashCode; final photos = await repository.getPhotosByAlbumId(albumId); expect(photos.length, 1); expect(photos.first.width, 1920); expect(photos.first.height, 1080); expect(photos.first.mimeType, 'image/webp'); }); test('should return null dimensions for video files', () async { final albumDir = await Directory( p.join(tempDir.path, 'video_test'), ).create(); File( p.join(albumDir.path, 'clip.mp4'), ).writeAsBytesSync(List.filled(100, 0)); final albumId = p.join(tempDir.path, 'video_test').hashCode; final photos = await repository.getPhotosByAlbumId(albumId); expect(photos.length, 1); expect(photos.first.width, isNull); expect(photos.first.height, isNull); expect(photos.first.mimeType, 'video/mp4'); }); test('should include video files in results', () async { final albumDir = await Directory( p.join(tempDir.path, 'video_album'), ).create(); File( p.join(albumDir.path, 'movie.mp4'), ).writeAsBytesSync(List.filled(100, 0)); final albumId = p.join(tempDir.path, 'video_album').hashCode; final photos = await repository.getPhotosByAlbumId(albumId); expect(photos.length, 1); expect(photos.first.fileName, 'movie.mp4'); }); test('should set correct photo metadata', () async { final albumDir = await Directory( p.join(tempDir.path, 'meta_test'), ).create(); _createTestPng(albumDir, 'test.png', width: 100, height: 100); final albumId = p.join(tempDir.path, 'meta_test').hashCode; final photos = await repository.getPhotosByAlbumId(albumId); expect(photos.first.albumId, albumId); expect(photos.first.fileName, 'test.png'); expect(photos.first.fileSize, greaterThan(0)); expect(photos.first.createdAt, isA()); }); }); group('getPhotoById', () { test('should return null when base directory does not exist', () async { final repo = PhotoRepository('/nonexistent/path'); final photo = await repo.getPhotoById(123); expect(photo, isNull); }); test('should return null when photo does not exist', () async { final photo = await repository.getPhotoById(999999); expect(photo, isNull); }); test('should return photo when id matches', () async { final albumDir = await Directory( p.join(tempDir.path, 'find_album'), ).create(); _createTestPng(albumDir, 'unique.png', width: 50, height: 50); final allPhotos = await repository.getPhotosByAlbumId( p.join(tempDir.path, 'find_album').hashCode, ); final targetId = allPhotos.first.id; final photo = await repository.getPhotoById(targetId); expect(photo, isNotNull); expect(photo!.fileName, 'unique.png'); expect(photo.width, 50); expect(photo.height, 50); }); test('should parse dimensions when retrieving by id', () async { final albumDir = await Directory( p.join(tempDir.path, 'dims_album'), ).create(); _createTestBmp(albumDir, 'dims.bmp', width: 1024, height: 768); final allPhotos = await repository.getPhotosByAlbumId( p.join(tempDir.path, 'dims_album').hashCode, ); final targetId = allPhotos.first.id; final photo = await repository.getPhotoById(targetId); expect(photo, isNotNull); expect(photo!.width, 1024); expect(photo.height, 768); }); }); group('mime type detection', () { test('should detect jpg/jpeg as image/jpeg', () async { final albumDir = await Directory( p.join(tempDir.path, 'mime_jpg'), ).create(); File(p.join(albumDir.path, 'test.jpg')).writeAsBytesSync([0xFF, 0xD8]); File(p.join(albumDir.path, 'test2.jpeg')).writeAsBytesSync([0xFF, 0xD8]); final photos = await repository.getPhotosByAlbumId( p.join(tempDir.path, 'mime_jpg').hashCode, ); expect(photos.length, 2); expect(photos.every((p) => p.mimeType == 'image/jpeg'), isTrue); }); test('should detect png as image/png', () async { final albumDir = await Directory( p.join(tempDir.path, 'mime_png'), ).create(); File( p.join(albumDir.path, 'test.png'), ).writeAsBytesSync(List.filled(100, 0)); final photos = await repository.getPhotosByAlbumId( p.join(tempDir.path, 'mime_png').hashCode, ); expect(photos.first.mimeType, 'image/png'); }); test('should detect webp as image/webp', () async { final albumDir = await Directory( p.join(tempDir.path, 'mime_webp'), ).create(); File( p.join(albumDir.path, 'test.webp'), ).writeAsBytesSync(List.filled(100, 0)); final photos = await repository.getPhotosByAlbumId( p.join(tempDir.path, 'mime_webp').hashCode, ); expect(photos.first.mimeType, 'image/webp'); }); test('should detect mp4 as video/mp4', () async { final albumDir = await Directory( p.join(tempDir.path, 'mime_mp4'), ).create(); File( p.join(albumDir.path, 'test.mp4'), ).writeAsBytesSync(List.filled(100, 0)); final photos = await repository.getPhotosByAlbumId( p.join(tempDir.path, 'mime_mp4').hashCode, ); expect(photos.first.mimeType, 'video/mp4'); }); test('should detect avi as video/avi', () async { final albumDir = await Directory( p.join(tempDir.path, 'mime_avi'), ).create(); File( p.join(albumDir.path, 'test.avi'), ).writeAsBytesSync(List.filled(100, 0)); final photos = await repository.getPhotosByAlbumId( p.join(tempDir.path, 'mime_avi').hashCode, ); expect(photos.first.mimeType, 'video/avi'); }); test('should detect mov as video/quicktime', () async { final albumDir = await Directory( p.join(tempDir.path, 'mime_mov'), ).create(); File( p.join(albumDir.path, 'test.mov'), ).writeAsBytesSync(List.filled(100, 0)); final photos = await repository.getPhotosByAlbumId( p.join(tempDir.path, 'mime_mov').hashCode, ); expect(photos.first.mimeType, 'video/quicktime'); }); test('should detect mkv as video/x-matroska', () async { final albumDir = await Directory( p.join(tempDir.path, 'mime_mkv'), ).create(); File( p.join(albumDir.path, 'test.mkv'), ).writeAsBytesSync(List.filled(100, 0)); final photos = await repository.getPhotosByAlbumId( p.join(tempDir.path, 'mime_mkv').hashCode, ); expect(photos.first.mimeType, 'video/x-matroska'); }); }); group('supported file extensions', () { test('should recognize all image extensions', () async { final albumDir = await Directory( p.join(tempDir.path, 'ext_test'), ).create(); final extensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp']; for (final ext in extensions) { File( p.join(albumDir.path, 'file$ext'), ).writeAsBytesSync(List.filled(10, 0)); } final photos = await repository.getPhotosByAlbumId( p.join(tempDir.path, 'ext_test').hashCode, ); expect(photos.length, extensions.length); }); test('should recognize all video extensions', () async { final albumDir = await Directory( p.join(tempDir.path, 'ext_video'), ).create(); final extensions = ['.mp4', '.avi', '.mov', '.mkv']; for (final ext in extensions) { File( p.join(albumDir.path, 'video$ext'), ).writeAsBytesSync(List.filled(10, 0)); } final photos = await repository.getPhotosByAlbumId( p.join(tempDir.path, 'ext_video').hashCode, ); expect(photos.length, extensions.length); }); test('should ignore unsupported extensions', () async { final albumDir = await Directory( p.join(tempDir.path, 'ext_ignore'), ).create(); _createTestPng(albumDir, 'valid.png'); File(p.join(albumDir.path, 'skip.txt')).writeAsString('text'); File(p.join(albumDir.path, 'skip.pdf')).writeAsBytesSync([]); File(p.join(albumDir.path, 'skip.exe')).writeAsBytesSync([]); final photos = await repository.getPhotosByAlbumId( p.join(tempDir.path, 'ext_ignore').hashCode, ); expect(photos.length, 1); expect(photos.first.fileName, 'valid.png'); }); }); }