diff --git a/mobile/lib/models/shared_link/shared_link.model.dart b/mobile/lib/models/shared_link/shared_link.model.dart index 1d3c81f1a1..43858b918f 100644 --- a/mobile/lib/models/shared_link/shared_link.model.dart +++ b/mobile/lib/models/shared_link/shared_link.model.dart @@ -27,7 +27,7 @@ class SharedLink { required this.expiresAt, required this.key, required this.showMetadata, - required this.slug, + this.slug, required this.type, }); diff --git a/mobile/lib/utils/url_helper.dart b/mobile/lib/utils/url_helper.dart index c41e3e82fa..2b29e41296 100644 --- a/mobile/lib/utils/url_helper.dart +++ b/mobile/lib/utils/url_helper.dart @@ -98,5 +98,6 @@ String? punycodeDecodeUrl(String? serverUrl) { /// - If slug is present: 's/${sharedLink.slug}' /// - Otherwise: 'share/${sharedLink.key}' String getShareUrlPath(SharedLink sharedLink) { - return sharedLink.slug != null ? 's/${sharedLink.slug}' : 'share/${sharedLink.key}'; + final slug = sharedLink.slug?.trim(); + return slug?.isNotEmpty == true ? 's/$slug' : 'share/${sharedLink.key}'; } diff --git a/mobile/test/modules/shared_link/shared_link_edit_page_test.dart b/mobile/test/modules/shared_link/shared_link_edit_page_test.dart index 7a0cbc22d3..8e436685b4 100644 --- a/mobile/test/modules/shared_link/shared_link_edit_page_test.dart +++ b/mobile/test/modules/shared_link/shared_link_edit_page_test.dart @@ -80,7 +80,7 @@ void main() { clipboardCapturer = ClipboardCapturer(); setupClipboardMock(clipboardCapturer); - setupDefaultMockResponses(mockSharedLinkService); + setupMockResponse(mockSharedLinkService, 'new-slug'); }); tearDown(() { @@ -90,53 +90,45 @@ void main() { }); group('SharedLinkEditPage Tests', () { - testWidgets('copies URL with slug to clipboard after link creation', (tester) async { + testWidgets('copies correct URL for links with slug', (tester) async { await pumpSharedLinkEditPage(tester, container, albumId: 'album-1'); await createSharedLink(tester); - final copyButton = find.byIcon(Icons.copy); - await tester.tap(copyButton); + await tester.tap(find.byIcon(Icons.copy)); await tester.pumpAndSettle(); expect(clipboardCapturer.text, 'https://demo.immich.app/s/new-slug'); }); - testWidgets('copies URL without slug to clipboard after link creation', (tester) async { - setupMockResponseForLinkWithoutSlug(mockSharedLinkService); + testWidgets('copies correct URL for links without slug', (tester) async { + setupMockResponse(mockSharedLinkService, ''); await pumpSharedLinkEditPage(tester, container, albumId: 'album-1'); await createSharedLink(tester); - final copyButton = find.byIcon(Icons.copy); - await tester.tap(copyButton); + await tester.tap(find.byIcon(Icons.copy)); await tester.pumpAndSettle(); - expect(clipboardCapturer.text, 'https://demo.immich.app/share/new-key'); }); - testWidgets('handles empty external domain by using server endpoint', (tester) async { - setupEmptyExternalDomain(mockServerInfoNotifier); + testWidgets('uses server endpoint when external domain is empty', (tester) async { + setupServerInfo(mockServerInfoNotifier, ''); await pumpSharedLinkEditPage(tester, container, albumId: 'album-1'); await createSharedLink(tester); - final copyButton = find.byIcon(Icons.copy); - await tester.tap(copyButton); + await tester.tap(find.byIcon(Icons.copy)); await tester.pumpAndSettle(); - expect(clipboardCapturer.text, 'https://demo.immich.app/s/new-slug'); }); - testWidgets('uses custom domain when external domain is set', (tester) async { - final mockServerInfoWithCustomDomain = createMockServerInfo(externalDomain: 'https://custom-immich.com'); - mockServerInfoNotifier.state = mockServerInfoWithCustomDomain; + testWidgets('uses custom external domain when set', (tester) async { + setupServerInfo(mockServerInfoNotifier, 'https://custom-immich.com'); await pumpSharedLinkEditPage(tester, container, albumId: 'album-1'); await createSharedLink(tester); - final copyButton = find.byIcon(Icons.copy); - await tester.tap(copyButton); + await tester.tap(find.byIcon(Icons.copy)); await tester.pumpAndSettle(); - expect(clipboardCapturer.text, 'https://custom-immich.com/s/new-slug'); }); }); diff --git a/mobile/test/modules/shared_link/shared_link_item_test.dart b/mobile/test/modules/shared_link/shared_link_item_test.dart index 686ce82f09..2a39b5b0c9 100644 --- a/mobile/test/modules/shared_link/shared_link_item_test.dart +++ b/mobile/test/modules/shared_link/shared_link_item_test.dart @@ -47,7 +47,7 @@ void main() { mockSharedLinksNotifier = MockSharedLinksNotifier(); clipboardCapturer = ClipboardCapturer(); - setupDefaultServerInfo(mockServerInfoNotifier); + setupServerInfo(mockServerInfoNotifier, 'https://example.com'); mockSharedLinksNotifier.state = const AsyncValue.data([]); overrides = [ @@ -68,62 +68,51 @@ void main() { }); group('SharedLinkItem Tests', () { - testWidgets('copies URL with slug to clipboard', (tester) async { + testWidgets('copies URL with slug', (tester) async { await tester.pumpConsumerWidget(Scaffold(body: SharedLinkItem(sharedLinkWithSlug)), overrides: overrides); - final copyButton = find.byIcon(Icons.copy_outlined); - await tester.tap(copyButton); + await tester.tap(find.byIcon(Icons.copy_outlined)); await tester.pumpAndSettle(); - expect(clipboardCapturer.text, 'https://example.com/s/test-slug'); }); - testWidgets('copies URL without slug to clipboard', (tester) async { + testWidgets('copies URL without slug', (tester) async { await tester.pumpConsumerWidget(Scaffold(body: SharedLinkItem(sharedLinkWithoutSlug)), overrides: overrides); - final copyButton = find.byIcon(Icons.copy_outlined); - await tester.tap(copyButton); + await tester.tap(find.byIcon(Icons.copy_outlined)); await tester.pumpAndSettle(); - expect(clipboardCapturer.text, 'https://example.com/share/test-key-2'); }); - testWidgets('handles empty external domain by using server URL', (tester) async { - setupEmptyExternalDomain(mockServerInfoNotifier); + testWidgets('falls back to server URL when external domain is empty', (tester) async { + setupServerInfo(mockServerInfoNotifier, ''); await Store.put(StoreKey.serverEndpoint, 'http://example.com'); await tester.pumpConsumerWidget(Scaffold(body: SharedLinkItem(sharedLinkWithSlug)), overrides: overrides); - final copyButton = find.byIcon(Icons.copy_outlined); - await tester.tap(copyButton); + await tester.tap(find.byIcon(Icons.copy_outlined)); await tester.pumpAndSettle(); - expect(clipboardCapturer.text, 'http://example.com/s/test-slug'); }); - testWidgets('shows snackbar on successful copy', (tester) async { + testWidgets('shows success message on copy', (tester) async { await tester.pumpConsumerWidget(Scaffold(body: SharedLinkItem(sharedLinkWithSlug)), overrides: overrides); - final copyButton = find.byIcon(Icons.copy_outlined); - await tester.tap(copyButton); + await tester.tap(find.byIcon(Icons.copy_outlined)); await tester.pumpAndSettle(); - expect(find.textContaining('copied'), findsOneWidget); }); - testWidgets('works with different shared links', (tester) async { + testWidgets('handles different link types correctly', (tester) async { await tester.pumpConsumerWidget(Scaffold(body: SharedLinkItem(sharedLinkWithSlug)), overrides: overrides); - final copyButton = find.byIcon(Icons.copy_outlined); - await tester.tap(copyButton); + await tester.tap(find.byIcon(Icons.copy_outlined)); await tester.pumpAndSettle(); - expect(clipboardCapturer.text, 'https://example.com/s/test-slug'); await tester.pumpConsumerWidget(Scaffold(body: SharedLinkItem(sharedLinkWithoutSlug)), overrides: overrides); - await tester.tap(copyButton); + await tester.tap(find.byIcon(Icons.copy_outlined)); await tester.pumpAndSettle(); - expect(clipboardCapturer.text, 'https://example.com/share/test-key-2'); }); }); diff --git a/mobile/test/modules/shared_link/shared_link_test_utils.dart b/mobile/test/modules/shared_link/shared_link_test_utils.dart index ba184e1be7..cfcc693941 100644 --- a/mobile/test/modules/shared_link/shared_link_test_utils.dart +++ b/mobile/test/modules/shared_link/shared_link_test_utils.dart @@ -32,12 +32,10 @@ class MockSharedLinksNotifier extends StateNotifier> MockSharedLinksNotifier() : super(const AsyncValue.loading()); } -final mockServerInfoNotifierProvider = Provider((ref) => MockServerInfoNotifier()); -/// Creates a mock ServerInfo with customizable parameters for testing +/// Creates a mock ServerInfo for testing ServerInfo createMockServerInfo({ String externalDomain = 'https://demo.immich.app', - String serverEndpoint = 'https://demo.immich.app', }) { return ServerInfo( serverVersion: const ServerVersion(major: 1, minor: 0, patch: 0), @@ -57,34 +55,6 @@ ServerInfo createMockServerInfo({ ); } -/// Creates a mock album shared link with customizable parameters for testing -SharedLink createAlbumSharedLink({ - String id = '1', - String title = 'Test Album', - String description = 'Test Description', - String key = 'test-key', - String? slug = 'test-slug', - bool allowUpload = true, - bool allowDownload = true, - bool showMetadata = true, - String? password, -}) { - return SharedLink( - id: id, - title: title, - description: description, - key: key, - slug: slug, - expiresAt: DateTime.now().add(const Duration(days: 1)), - allowUpload: allowUpload, - allowDownload: allowDownload, - showMetadata: showMetadata, - thumbAssetId: null, - password: password, - type: SharedLinkSource.album, - ); -} - /// Creates a shared link with slug for testing SharedLink createSharedLinkWithSlug() { return SharedLink( @@ -121,20 +91,13 @@ SharedLink createSharedLinkWithoutSlug() { ); } -/// Sets up default server info with external domain for testing -void setupDefaultServerInfo(MockServerInfoNotifier mockServerInfoNotifier) { - final mockServerInfo = createMockServerInfo(externalDomain: 'https://example.com'); - mockServerInfoNotifier.state = mockServerInfo; +/// Sets up server info with external domain for testing +void setupServerInfo(MockServerInfoNotifier mockServerInfoNotifier, String domain) { + mockServerInfoNotifier.state = createMockServerInfo(externalDomain: domain); } -/// Sets up empty external domain for testing -void setupEmptyExternalDomain(ServerInfoNotifier mockServerInfoNotifier) { - final mockServerInfoWithEmptyDomain = createMockServerInfo(externalDomain: ''); - mockServerInfoNotifier.state = mockServerInfoWithEmptyDomain; -} - -/// Sets up default mock responses for shared link service -void setupDefaultMockResponses(MockSharedLinkService mockSharedLinkService) { +/// Sets up mock response for shared link service +void setupMockResponse(MockSharedLinkService mockSharedLinkService, String slug) { when( () => mockSharedLinkService.createSharedLink( albumId: any(named: 'albumId'), @@ -146,25 +109,20 @@ void setupDefaultMockResponses(MockSharedLinkService mockSharedLinkService) { password: any(named: 'password'), expiresAt: any(named: 'expiresAt'), ), - ).thenAnswer( - (_) async => createAlbumSharedLink(id: 'new-link-id', title: 'New Link', key: 'new-key', slug: 'new-slug'), - ); -} - -/// Sets up mock response for link without slug -void setupMockResponseForLinkWithoutSlug(MockSharedLinkService mockSharedLinkService) { - when( - () => mockSharedLinkService.createSharedLink( - albumId: any(named: 'albumId'), - assetIds: any(named: 'assetIds'), - showMeta: any(named: 'showMeta'), - allowDownload: any(named: 'allowDownload'), - allowUpload: any(named: 'allowUpload'), - description: any(named: 'description'), - password: any(named: 'password'), - expiresAt: any(named: 'expiresAt'), - ), - ).thenAnswer((_) async => createAlbumSharedLink(id: 'new-link-id', title: 'New Link', key: 'new-key', slug: null)); + ).thenAnswer((_) async => SharedLink( + id: 'new-link-id', + title: 'New Link', + description: 'Test Description', + key: 'new-key', + slug: slug.isEmpty ? null : slug, + expiresAt: DateTime.now().add(const Duration(days: 1)), + allowUpload: true, + allowDownload: true, + showMetadata: true, + thumbAssetId: null, + password: null, + type: SharedLinkSource.album, + )); } /// Test utility to capture clipboard operations for verification