Source Code added
This commit is contained in:
parent
800376eafd
commit
9efa9bc6dd
3912 changed files with 754770 additions and 2 deletions
23
mobile/test/modules/utils/async_mutex_test.dart
Normal file
23
mobile/test/modules/utils/async_mutex_test.dart
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/utils/async_mutex.dart';
|
||||
|
||||
void main() {
|
||||
group('Test AsyncMutex grouped', () {
|
||||
test('test ordered execution', () async {
|
||||
AsyncMutex lock = AsyncMutex();
|
||||
List<int> events = [];
|
||||
expect(0, lock.enqueued);
|
||||
unawaited(lock.run(() => Future.delayed(const Duration(milliseconds: 10), () => events.add(1))));
|
||||
expect(1, lock.enqueued);
|
||||
unawaited(lock.run(() => Future.delayed(const Duration(milliseconds: 3), () => events.add(2))));
|
||||
expect(2, lock.enqueued);
|
||||
unawaited(lock.run(() => Future.delayed(const Duration(milliseconds: 1), () => events.add(3))));
|
||||
expect(3, lock.enqueued);
|
||||
await lock.run(() => Future.delayed(const Duration(milliseconds: 10), () => events.add(4)));
|
||||
expect(0, lock.enqueued);
|
||||
expect(events, [1, 2, 3, 4]);
|
||||
});
|
||||
});
|
||||
}
|
||||
58
mobile/test/modules/utils/datetime_helpers_test.dart
Normal file
58
mobile/test/modules/utils/datetime_helpers_test.dart
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/utils/datetime_helpers.dart';
|
||||
|
||||
void main() {
|
||||
group('tryFromSecondsSinceEpoch', () {
|
||||
test('returns null for null input', () {
|
||||
final result = tryFromSecondsSinceEpoch(null);
|
||||
expect(result, isNull);
|
||||
});
|
||||
|
||||
test('returns null for value below minimum allowed range', () {
|
||||
// _minMillisecondsSinceEpoch = -62135596800000
|
||||
final seconds = -62135596800000 ~/ 1000 - 1; // One second before min allowed
|
||||
final result = tryFromSecondsSinceEpoch(seconds);
|
||||
expect(result, isNull);
|
||||
});
|
||||
|
||||
test('returns null for value above maximum allowed range', () {
|
||||
// _maxMillisecondsSinceEpoch = 8640000000000000
|
||||
final seconds = 8640000000000000 ~/ 1000 + 1; // One second after max allowed
|
||||
final result = tryFromSecondsSinceEpoch(seconds);
|
||||
expect(result, isNull);
|
||||
});
|
||||
|
||||
test('returns correct DateTime for minimum allowed value', () {
|
||||
final seconds = -62135596800000 ~/ 1000; // Minimum allowed timestamp
|
||||
final result = tryFromSecondsSinceEpoch(seconds);
|
||||
expect(result, DateTime.fromMillisecondsSinceEpoch(-62135596800000));
|
||||
});
|
||||
|
||||
test('returns correct DateTime for maximum allowed value', () {
|
||||
final seconds = 8640000000000000 ~/ 1000; // Maximum allowed timestamp
|
||||
final result = tryFromSecondsSinceEpoch(seconds);
|
||||
expect(result, DateTime.fromMillisecondsSinceEpoch(8640000000000000));
|
||||
});
|
||||
|
||||
test('returns correct DateTime for negative timestamp', () {
|
||||
final seconds = -1577836800; // Dec 31, 1919 (pre-epoch)
|
||||
final result = tryFromSecondsSinceEpoch(seconds);
|
||||
expect(result, DateTime.fromMillisecondsSinceEpoch(-1577836800 * 1000));
|
||||
});
|
||||
|
||||
test('returns correct DateTime for zero timestamp', () {
|
||||
final seconds = 0; // Jan 1, 1970 (epoch)
|
||||
final result = tryFromSecondsSinceEpoch(seconds);
|
||||
expect(result, DateTime.fromMillisecondsSinceEpoch(0));
|
||||
});
|
||||
|
||||
test('returns correct DateTime for recent timestamp', () {
|
||||
final now = DateTime.now();
|
||||
final seconds = now.millisecondsSinceEpoch ~/ 1000;
|
||||
final result = tryFromSecondsSinceEpoch(seconds);
|
||||
expect(result?.year, now.year);
|
||||
expect(result?.month, now.month);
|
||||
expect(result?.day, now.day);
|
||||
});
|
||||
});
|
||||
}
|
||||
41
mobile/test/modules/utils/debouncer_test.dart
Normal file
41
mobile/test/modules/utils/debouncer_test.dart
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/utils/debounce.dart';
|
||||
|
||||
class _Counter {
|
||||
int _count = 0;
|
||||
_Counter();
|
||||
|
||||
int get count => _count;
|
||||
void increment() => _count = _count + 1;
|
||||
}
|
||||
|
||||
void main() {
|
||||
test('Executes the method after the interval', () async {
|
||||
var counter = _Counter();
|
||||
final debouncer = Debouncer(interval: const Duration(milliseconds: 300));
|
||||
debouncer.run(() => counter.increment());
|
||||
expect(counter.count, 0);
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
expect(counter.count, 1);
|
||||
});
|
||||
|
||||
test('Executes the method immediately if zero interval', () async {
|
||||
var counter = _Counter();
|
||||
final debouncer = Debouncer(interval: const Duration(milliseconds: 0));
|
||||
debouncer.run(() => counter.increment());
|
||||
// Even though it is supposed to be executed immediately, it is added to the async queue and so
|
||||
// we need this delay to make sure the actual debounced method is called
|
||||
await Future.delayed(const Duration(milliseconds: 0));
|
||||
expect(counter.count, 1);
|
||||
});
|
||||
|
||||
test('Delayes method execution after all the calls are completed', () async {
|
||||
var counter = _Counter();
|
||||
final debouncer = Debouncer(interval: const Duration(milliseconds: 100));
|
||||
debouncer.run(() => counter.increment());
|
||||
debouncer.run(() => counter.increment());
|
||||
debouncer.run(() => counter.increment());
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
expect(counter.count, 1);
|
||||
});
|
||||
}
|
||||
50
mobile/test/modules/utils/diff_test.dart
Normal file
50
mobile/test/modules/utils/diff_test.dart
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/utils/diff.dart';
|
||||
|
||||
void main() {
|
||||
final List<int> listA = [1, 2, 3, 4, 6];
|
||||
final List<int> listB = [1, 3, 5, 7];
|
||||
|
||||
group('Test grouped', () {
|
||||
test('test partial overlap', () async {
|
||||
final List<int> onlyInA = [];
|
||||
final List<int> onlyInB = [];
|
||||
final List<int> inBoth = [];
|
||||
final changes = await diffSortedLists(
|
||||
listA,
|
||||
listB,
|
||||
compare: (int a, int b) => a.compareTo(b),
|
||||
both: (int a, int b) {
|
||||
inBoth.add(b);
|
||||
return false;
|
||||
},
|
||||
onlyFirst: (int a) => onlyInA.add(a),
|
||||
onlySecond: (int b) => onlyInB.add(b),
|
||||
);
|
||||
expect(changes, true);
|
||||
expect(onlyInA, [2, 4, 6]);
|
||||
expect(onlyInB, [5, 7]);
|
||||
expect(inBoth, [1, 3]);
|
||||
});
|
||||
test('test partial overlap sync', () {
|
||||
final List<int> onlyInA = [];
|
||||
final List<int> onlyInB = [];
|
||||
final List<int> inBoth = [];
|
||||
final changes = diffSortedListsSync(
|
||||
listA,
|
||||
listB,
|
||||
compare: (int a, int b) => a.compareTo(b),
|
||||
both: (int a, int b) {
|
||||
inBoth.add(b);
|
||||
return false;
|
||||
},
|
||||
onlyFirst: (int a) => onlyInA.add(a),
|
||||
onlySecond: (int b) => onlyInB.add(b),
|
||||
);
|
||||
expect(changes, true);
|
||||
expect(onlyInA, [2, 4, 6]);
|
||||
expect(onlyInB, [5, 7]);
|
||||
expect(inBoth, [1, 3]);
|
||||
});
|
||||
});
|
||||
}
|
||||
131
mobile/test/modules/utils/migration_test.dart
Normal file
131
mobile/test/modules/utils/migration_test.dart
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
import 'package:drift/drift.dart' hide isNull;
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/services/store.service.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart';
|
||||
import 'package:immich_mobile/utils/migration.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
import '../../infrastructure/repository.mock.dart';
|
||||
|
||||
void main() {
|
||||
late Drift db;
|
||||
late SyncStreamRepository mockSyncStreamRepository;
|
||||
|
||||
setUpAll(() async {
|
||||
db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true));
|
||||
await StoreService.init(storeRepository: DriftStoreRepository(db));
|
||||
mockSyncStreamRepository = MockSyncStreamRepository();
|
||||
when(() => mockSyncStreamRepository.reset()).thenAnswer((_) async => {});
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await Store.clear();
|
||||
});
|
||||
|
||||
group('handleBetaMigration Tests', () {
|
||||
group("version < 15", () {
|
||||
test('already on new timeline', () async {
|
||||
await Store.put(StoreKey.betaTimeline, true);
|
||||
|
||||
await handleBetaMigration(14, false, mockSyncStreamRepository);
|
||||
|
||||
expect(Store.tryGet(StoreKey.betaTimeline), true);
|
||||
expect(Store.tryGet(StoreKey.needBetaMigration), false);
|
||||
});
|
||||
|
||||
test('already on old timeline', () async {
|
||||
await Store.put(StoreKey.betaTimeline, false);
|
||||
|
||||
await handleBetaMigration(14, false, mockSyncStreamRepository);
|
||||
|
||||
expect(Store.tryGet(StoreKey.needBetaMigration), true);
|
||||
});
|
||||
|
||||
test('fresh install', () async {
|
||||
await Store.delete(StoreKey.betaTimeline);
|
||||
await handleBetaMigration(14, true, mockSyncStreamRepository);
|
||||
|
||||
expect(Store.tryGet(StoreKey.betaTimeline), true);
|
||||
expect(Store.tryGet(StoreKey.needBetaMigration), false);
|
||||
});
|
||||
});
|
||||
|
||||
group("version == 15", () {
|
||||
test('already on new timeline', () async {
|
||||
await Store.put(StoreKey.betaTimeline, true);
|
||||
|
||||
await handleBetaMigration(15, false, mockSyncStreamRepository);
|
||||
|
||||
expect(Store.tryGet(StoreKey.betaTimeline), true);
|
||||
expect(Store.tryGet(StoreKey.needBetaMigration), false);
|
||||
});
|
||||
|
||||
test('already on old timeline', () async {
|
||||
await Store.put(StoreKey.betaTimeline, false);
|
||||
|
||||
await handleBetaMigration(15, false, mockSyncStreamRepository);
|
||||
|
||||
expect(Store.tryGet(StoreKey.needBetaMigration), true);
|
||||
});
|
||||
|
||||
test('fresh install', () async {
|
||||
await Store.delete(StoreKey.betaTimeline);
|
||||
await handleBetaMigration(15, true, mockSyncStreamRepository);
|
||||
|
||||
expect(Store.tryGet(StoreKey.betaTimeline), true);
|
||||
expect(Store.tryGet(StoreKey.needBetaMigration), false);
|
||||
});
|
||||
});
|
||||
|
||||
group("version > 15", () {
|
||||
test('already on new timeline', () async {
|
||||
await Store.put(StoreKey.betaTimeline, true);
|
||||
|
||||
await handleBetaMigration(16, false, mockSyncStreamRepository);
|
||||
|
||||
expect(Store.tryGet(StoreKey.betaTimeline), true);
|
||||
expect(Store.tryGet(StoreKey.needBetaMigration), false);
|
||||
});
|
||||
|
||||
test('already on old timeline', () async {
|
||||
await Store.put(StoreKey.betaTimeline, false);
|
||||
|
||||
await handleBetaMigration(16, false, mockSyncStreamRepository);
|
||||
|
||||
expect(Store.tryGet(StoreKey.betaTimeline), false);
|
||||
expect(Store.tryGet(StoreKey.needBetaMigration), false);
|
||||
});
|
||||
|
||||
test('fresh install', () async {
|
||||
await Store.delete(StoreKey.betaTimeline);
|
||||
await handleBetaMigration(16, true, mockSyncStreamRepository);
|
||||
|
||||
expect(Store.tryGet(StoreKey.betaTimeline), true);
|
||||
expect(Store.tryGet(StoreKey.needBetaMigration), false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
group('sync reset tests', () {
|
||||
test('version < 16', () async {
|
||||
await Store.put(StoreKey.shouldResetSync, false);
|
||||
|
||||
await handleBetaMigration(15, false, mockSyncStreamRepository);
|
||||
|
||||
expect(Store.tryGet(StoreKey.shouldResetSync), true);
|
||||
});
|
||||
|
||||
test('version >= 16', () async {
|
||||
await Store.put(StoreKey.shouldResetSync, false);
|
||||
|
||||
await handleBetaMigration(16, false, mockSyncStreamRepository);
|
||||
|
||||
expect(Store.tryGet(StoreKey.shouldResetSync), false);
|
||||
});
|
||||
});
|
||||
}
|
||||
61
mobile/test/modules/utils/openapi_patching_test.dart
Normal file
61
mobile/test/modules/utils/openapi_patching_test.dart
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
import 'package:immich_mobile/utils/openapi_patching.dart';
|
||||
|
||||
void main() {
|
||||
group('Test OpenApi Patching', () {
|
||||
test('upgradeDto', () {
|
||||
dynamic value;
|
||||
String targetType;
|
||||
|
||||
targetType = 'UserPreferencesResponseDto';
|
||||
value = jsonDecode("""
|
||||
{
|
||||
"download": {
|
||||
"archiveSize": 4294967296,
|
||||
"includeEmbeddedVideos": false
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
upgradeDto(value, targetType);
|
||||
expect(value['tags'], TagsResponse().toJson());
|
||||
expect(value['download']['includeEmbeddedVideos'], false);
|
||||
});
|
||||
|
||||
test('addDefault', () {
|
||||
dynamic value = jsonDecode("""
|
||||
{
|
||||
"download": {
|
||||
"archiveSize": 4294967296,
|
||||
"includeEmbeddedVideos": false
|
||||
}
|
||||
}
|
||||
""");
|
||||
String keys = 'download.unknownKey';
|
||||
dynamic defaultValue = 69420;
|
||||
|
||||
addDefault(value, keys, defaultValue);
|
||||
expect(value['download']['unknownKey'], 69420);
|
||||
|
||||
keys = 'alpha.beta';
|
||||
defaultValue = 'gamma';
|
||||
addDefault(value, keys, defaultValue);
|
||||
expect(value['alpha']['beta'], 'gamma');
|
||||
});
|
||||
|
||||
test('addDefault with null', () {
|
||||
dynamic value = jsonDecode("""
|
||||
{
|
||||
"download": {
|
||||
"archiveSize": 4294967296,
|
||||
"includeEmbeddedVideos": false
|
||||
}
|
||||
}
|
||||
""");
|
||||
expect(value['download']['unknownKey'], isNull);
|
||||
});
|
||||
});
|
||||
}
|
||||
46
mobile/test/modules/utils/throttler_test.dart
Normal file
46
mobile/test/modules/utils/throttler_test.dart
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/utils/throttle.dart';
|
||||
import 'package:immich_mobile/utils/debug_print.dart';
|
||||
|
||||
class _Counter {
|
||||
int _count = 0;
|
||||
_Counter();
|
||||
|
||||
int get count => _count;
|
||||
void increment() {
|
||||
dPrint(() => "Counter inside increment: $count");
|
||||
_count = _count + 1;
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
test('Executes the method immediately if no calls received previously', () async {
|
||||
var counter = _Counter();
|
||||
final throttler = Throttler(interval: const Duration(milliseconds: 300));
|
||||
throttler.run(() => counter.increment());
|
||||
expect(counter.count, 1);
|
||||
});
|
||||
|
||||
test('Does not execute calls before throttle interval', () async {
|
||||
var counter = _Counter();
|
||||
final throttler = Throttler(interval: const Duration(milliseconds: 100));
|
||||
throttler.run(() => counter.increment());
|
||||
throttler.run(() => counter.increment());
|
||||
throttler.run(() => counter.increment());
|
||||
throttler.run(() => counter.increment());
|
||||
throttler.run(() => counter.increment());
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
expect(counter.count, 1);
|
||||
});
|
||||
|
||||
test('Executes the method if received in intervals', () async {
|
||||
var counter = _Counter();
|
||||
final throttler = Throttler(interval: const Duration(milliseconds: 100));
|
||||
for (final _ in Iterable<int>.generate(10)) {
|
||||
throttler.run(() => counter.increment());
|
||||
await Future.delayed(const Duration(milliseconds: 50));
|
||||
}
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
expect(counter.count, 5);
|
||||
});
|
||||
}
|
||||
63
mobile/test/modules/utils/thumbnail_utils_test.dart
Normal file
63
mobile/test/modules/utils/thumbnail_utils_test.dart
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/domain/models/exif.model.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/utils/thumbnail_utils.dart';
|
||||
|
||||
void main() {
|
||||
final dateTime = DateTime(2025, 04, 25, 12, 13, 14);
|
||||
final dateTimeString = DateFormat.yMMMMd().format(dateTime);
|
||||
|
||||
test('returns description if it has one', () {
|
||||
final result = getAltText(const ExifInfo(description: 'description'), dateTime, AssetType.image, []);
|
||||
expect(result, 'description');
|
||||
});
|
||||
|
||||
test('returns image alt text with date if no location', () {
|
||||
final (template, args) = getAltTextTemplate(const ExifInfo(), dateTime, AssetType.image, []);
|
||||
expect(template, "image_alt_text_date");
|
||||
expect(args["isVideo"], "false");
|
||||
expect(args["date"], dateTimeString);
|
||||
});
|
||||
|
||||
test('returns image alt text with date and place', () {
|
||||
final (template, args) = getAltTextTemplate(
|
||||
const ExifInfo(city: 'city', country: 'country'),
|
||||
dateTime,
|
||||
AssetType.video,
|
||||
[],
|
||||
);
|
||||
expect(template, "image_alt_text_date_place");
|
||||
expect(args["isVideo"], "true");
|
||||
expect(args["date"], dateTimeString);
|
||||
expect(args["city"], "city");
|
||||
expect(args["country"], "country");
|
||||
});
|
||||
|
||||
test('returns image alt text with date and some people', () {
|
||||
final (template, args) = getAltTextTemplate(const ExifInfo(), dateTime, AssetType.image, ["Alice", "Bob"]);
|
||||
expect(template, "image_alt_text_date_2_people");
|
||||
expect(args["isVideo"], "false");
|
||||
expect(args["date"], dateTimeString);
|
||||
expect(args["person1"], "Alice");
|
||||
expect(args["person2"], "Bob");
|
||||
});
|
||||
|
||||
test('returns image alt text with date and location and many people', () {
|
||||
final (template, args) = getAltTextTemplate(
|
||||
const ExifInfo(city: "city", country: 'country'),
|
||||
dateTime,
|
||||
AssetType.video,
|
||||
["Alice", "Bob", "Carol", "David", "Eve"],
|
||||
);
|
||||
expect(template, "image_alt_text_date_place_4_or_more_people");
|
||||
expect(args["isVideo"], "true");
|
||||
expect(args["date"], dateTimeString);
|
||||
expect(args["city"], "city");
|
||||
expect(args["country"], "country");
|
||||
expect(args["person1"], "Alice");
|
||||
expect(args["person2"], "Bob");
|
||||
expect(args["person3"], "Carol");
|
||||
expect(args["additionalCount"], "2");
|
||||
});
|
||||
}
|
||||
136
mobile/test/modules/utils/url_helper_test.dart
Normal file
136
mobile/test/modules/utils/url_helper_test.dart
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/utils/url_helper.dart';
|
||||
|
||||
void main() {
|
||||
group('punycodeEncodeUrl', () {
|
||||
test('should return empty string for invalid URL', () {
|
||||
expect(punycodeEncodeUrl('not a url'), equals(''));
|
||||
});
|
||||
|
||||
test('should handle empty input', () {
|
||||
expect(punycodeEncodeUrl(''), equals(''));
|
||||
});
|
||||
|
||||
test('should return ASCII-only URL unchanged', () {
|
||||
const url = 'https://example.com';
|
||||
expect(punycodeEncodeUrl(url), equals(url));
|
||||
});
|
||||
|
||||
test('should encode single-segment Unicode host', () {
|
||||
const url = 'https://bücher';
|
||||
const expected = 'https://xn--bcher-kva';
|
||||
expect(punycodeEncodeUrl(url), equals(expected));
|
||||
});
|
||||
|
||||
test('should encode multi-segment Unicode host', () {
|
||||
const url = 'https://bücher.de';
|
||||
const expected = 'https://xn--bcher-kva.de';
|
||||
expect(punycodeEncodeUrl(url), equals(expected));
|
||||
});
|
||||
|
||||
test('should encode multi-segment Unicode host with multiple non-ASCII segments', () {
|
||||
const url = 'https://bücher.münchen';
|
||||
const expected = 'https://xn--bcher-kva.xn--mnchen-3ya';
|
||||
expect(punycodeEncodeUrl(url), equals(expected));
|
||||
});
|
||||
|
||||
test('should handle URL with port', () {
|
||||
const url = 'https://bücher.de:8080';
|
||||
const expected = 'https://xn--bcher-kva.de:8080';
|
||||
expect(punycodeEncodeUrl(url), equals(expected));
|
||||
});
|
||||
|
||||
test('should handle URL with path', () {
|
||||
const url = 'https://bücher.de/path/to/resource';
|
||||
const expected = 'https://xn--bcher-kva.de/path/to/resource';
|
||||
expect(punycodeEncodeUrl(url), equals(expected));
|
||||
});
|
||||
|
||||
test('should handle URL with port and path', () {
|
||||
const url = 'https://bücher.de:3000/path';
|
||||
const expected = 'https://xn--bcher-kva.de:3000/path';
|
||||
expect(punycodeEncodeUrl(url), equals(expected));
|
||||
});
|
||||
|
||||
test('should not encode ASCII segment in multi-segment host', () {
|
||||
const url = 'https://shop.bücher.de';
|
||||
const expected = 'https://shop.xn--bcher-kva.de';
|
||||
expect(punycodeEncodeUrl(url), equals(expected));
|
||||
});
|
||||
|
||||
test('should handle host with hyphen in Unicode segment', () {
|
||||
const url = 'https://bü-cher.de';
|
||||
const expected = 'https://xn--b-cher-3ya.de';
|
||||
expect(punycodeEncodeUrl(url), equals(expected));
|
||||
});
|
||||
|
||||
test('should handle host with numbers in Unicode segment', () {
|
||||
const url = 'https://bücher123.de';
|
||||
const expected = 'https://xn--bcher123-65a.de';
|
||||
expect(punycodeEncodeUrl(url), equals(expected));
|
||||
});
|
||||
|
||||
test('should encode the domain of the original issue poster :)', () {
|
||||
const url = 'https://фото.большойчлен.рф/';
|
||||
const expected = 'https://xn--n1aalg.xn--90ailhbncb6fh7b.xn--p1ai/';
|
||||
expect(punycodeEncodeUrl(url), expected);
|
||||
});
|
||||
});
|
||||
|
||||
group('punycodeDecodeUrl', () {
|
||||
test('should return null for null input', () {
|
||||
expect(punycodeDecodeUrl(null), isNull);
|
||||
});
|
||||
|
||||
test('should return null for an invalid URL', () {
|
||||
// "not a url" should fail to parse.
|
||||
expect(punycodeDecodeUrl('not a url'), isNull);
|
||||
});
|
||||
|
||||
test('should return null for a URL with empty host', () {
|
||||
// "https://" is a valid scheme but with no host.
|
||||
expect(punycodeDecodeUrl('https://'), isNull);
|
||||
});
|
||||
|
||||
test('should return ASCII-only URL unchanged', () {
|
||||
const url = 'https://example.com';
|
||||
expect(punycodeDecodeUrl(url), equals(url));
|
||||
});
|
||||
|
||||
test('should decode a single-segment Punycode domain', () {
|
||||
const input = 'https://xn--bcher-kva.de';
|
||||
const expected = 'https://bücher.de';
|
||||
expect(punycodeDecodeUrl(input), equals(expected));
|
||||
});
|
||||
|
||||
test('should decode a multi-segment Punycode domain', () {
|
||||
const input = 'https://shop.xn--bcher-kva.de';
|
||||
const expected = 'https://shop.bücher.de';
|
||||
expect(punycodeDecodeUrl(input), equals(expected));
|
||||
});
|
||||
|
||||
test('should decode URL with port', () {
|
||||
const input = 'https://xn--bcher-kva.de:8080';
|
||||
const expected = 'https://bücher.de:8080';
|
||||
expect(punycodeDecodeUrl(input), equals(expected));
|
||||
});
|
||||
|
||||
test('should decode domains with uppercase punycode prefix correctly', () {
|
||||
const input = 'https://XN--BCHER-KVA.de';
|
||||
const expected = 'https://bücher.de';
|
||||
expect(punycodeDecodeUrl(input), equals(expected));
|
||||
});
|
||||
|
||||
test('should handle mixed segments with no punycode in some parts', () {
|
||||
const input = 'https://news.xn--bcher-kva.de';
|
||||
const expected = 'https://news.bücher.de';
|
||||
expect(punycodeDecodeUrl(input), equals(expected));
|
||||
});
|
||||
|
||||
test('should decode the domain of the original issue poster :)', () {
|
||||
const url = 'https://xn--n1aalg.xn--90ailhbncb6fh7b.xn--p1ai/';
|
||||
const expected = 'https://фото.большойчлен.рф/';
|
||||
expect(punycodeDecodeUrl(url), expected);
|
||||
});
|
||||
});
|
||||
}
|
||||
32
mobile/test/modules/utils/version_compatibility_test.dart
Normal file
32
mobile/test/modules/utils/version_compatibility_test.dart
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/utils/version_compatibility.dart';
|
||||
|
||||
void main() {
|
||||
test('getVersionCompatibilityMessage', () {
|
||||
String? result;
|
||||
|
||||
result = getVersionCompatibilityMessage(1, 0, 2, 0);
|
||||
expect(result, 'Your app major version is not compatible with the server!');
|
||||
|
||||
result = getVersionCompatibilityMessage(1, 106, 1, 105);
|
||||
expect(
|
||||
result,
|
||||
'Your app minor version is not compatible with the server! Please update your server to version v1.106.0 or newer to login',
|
||||
);
|
||||
|
||||
result = getVersionCompatibilityMessage(1, 107, 1, 105);
|
||||
expect(
|
||||
result,
|
||||
'Your app minor version is not compatible with the server! Please update your server to version v1.106.0 or newer to login',
|
||||
);
|
||||
|
||||
result = getVersionCompatibilityMessage(1, 106, 1, 106);
|
||||
expect(result, null);
|
||||
|
||||
result = getVersionCompatibilityMessage(1, 107, 1, 106);
|
||||
expect(result, null);
|
||||
|
||||
result = getVersionCompatibilityMessage(1, 107, 1, 108);
|
||||
expect(result, null);
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue