test(local): cover shared link access and video readiness; fix lint dependency

This commit is contained in:
Codex
2026-10-14 21:37:42 +03:00
parent a06a589936
commit d25c946c29
4 changed files with 125 additions and 4 deletions
@@ -0,0 +1,63 @@
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/providers/asset_viewer/video_player_provider.dart';
import 'package:mocktail/mocktail.dart';
import 'package:native_video_player/native_video_player.dart';
class _Controller extends Mock implements NativeVideoPlayerController {}
class _PlaybackInfo extends Mock implements PlaybackInfo {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
const wakelock = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.wakelock_plus_platform_interface.WakelockPlusApi.toggle',
StandardMessageCodec(),
);
late _Controller controller;
late VideoPlayerNotifier notifier;
setUp(() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockDecodedMessageHandler<Object?>(
wakelock,
(_) async => <Object?>[null],
);
final playback = _PlaybackInfo();
when(() => playback.status).thenReturn(PlaybackStatus.stopped);
when(() => playback.position).thenReturn(0);
controller = _Controller();
when(() => controller.playbackInfo).thenReturn(playback);
when(() => controller.videoInfo).thenReturn(VideoInfo.fromJson({'width': 1920, 'height': 1080, 'duration': 5000}));
when(() => controller.play()).thenAnswer((_) async {});
notifier = VideoPlayerNotifier()..attachController(controller);
});
tearDown(() async {
notifier.dispose();
await Future<void>.delayed(Duration.zero);
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockDecodedMessageHandler<Object?>(
wakelock,
null,
);
});
test('newly loaded stopped video offers playback', () {
notifier.onNativePlaybackReady();
expect(notifier.state.status, VideoPlaybackStatus.paused);
expect(notifier.state.duration, const Duration(seconds: 5));
});
testWidgets('stopped ready event cancels the pending buffering indicator', (tester) async {
await notifier.play();
notifier.onNativePlaybackReady();
await tester.pump(const Duration(seconds: 2));
expect(notifier.state.status, VideoPlaybackStatus.paused);
});
test('ready event preserves replay after playback has completed', () {
notifier.onNativeStatusChanged();
expect(notifier.state.status, VideoPlaybackStatus.completed);
notifier.onNativePlaybackReady();
expect(notifier.state.status, VideoPlaybackStatus.completed);
});
}
+4 -4
View File
@@ -10,7 +10,7 @@ overrides:
sharp: ^0.35.3
tsc-alias: 1.9.2
packageExtensionsChecksum: sha256-W6pFzyf+6QXnV91iA6oob0OGVkergPXDN1afLgoF53k=
packageExtensionsChecksum: sha256-y1PgMgGqpI3vgcVUp03tHO1Ia2xP1NSL0js1RewfUP0=
pnpmfileChecksum: sha256-un98do36L0wZyqsjcLozQ3YUadCAn2yz5bXcBbOuyDA=
@@ -939,7 +939,7 @@ importers:
version: 10.6.0
'@koddsson/eslint-plugin-tscompat':
specifier: ^0.2.0
version: 0.2.0(@typescript/typescript6@6.0.2)(eslint@10.11.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)
version: 0.2.0(eslint@10.11.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)
'@socket.io/component-emitter':
specifier: ^3.1.0
version: 3.1.2
@@ -16646,16 +16646,16 @@ snapshots:
'@keyv/serialize@1.1.1': {}
'@koddsson/eslint-plugin-tscompat@0.2.0(@typescript/typescript6@6.0.2)(eslint@10.11.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)':
'@koddsson/eslint-plugin-tscompat@0.2.0(eslint@10.11.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)':
dependencies:
'@mdn/browser-compat-data': 6.1.5
'@typescript-eslint/type-utils': 8.70.1(@typescript/typescript6@6.0.2)(eslint@10.11.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)
'@typescript-eslint/utils': 8.70.1(@typescript/typescript6@6.0.2)(eslint@10.11.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)
browserslist: 4.29.0
typescript: '@typescript/typescript6@6.0.2'
transitivePeerDependencies:
- eslint
- supports-color
- typescript
'@kwsites/file-exists@1.1.1(supports-color@8.1.1)':
dependencies:
+3
View File
@@ -34,6 +34,9 @@ overrides:
# 1.9.3-1.9.5 are confirmed broken
tsc-alias: 1.9.2
packageExtensions:
'@koddsson/eslint-plugin-tscompat':
dependencies:
typescript: 'npm:@typescript/typescript6@^6.0.2'
nestjs-kysely:
dependencies:
tslib: '*'
@@ -0,0 +1,55 @@
import { Kysely } from 'kysely';
import { randomBytes } from 'node:crypto';
import { AssetType, SharedLinkType } from 'src/enum.js';
import { AccessRepository } from 'src/repositories/access.repository.js';
import { LoggingRepository } from 'src/repositories/logging.repository.js';
import { SharedLinkRepository } from 'src/repositories/shared-link.repository.js';
import { DB } from 'src/schema/index.js';
import { BaseService } from 'src/services/base.service.js';
import { newMediumService } from 'test/medium.factory.js';
import { factory } from 'test/small.factory.js';
import { getKyselyDB } from 'test/utils.js';
let db: Kysely<DB>;
beforeAll(async () => {
db = await getKyselyDB();
});
afterAll(async () => {
await db.destroy();
});
describe('shared link asset access', () => {
for (const type of [SharedLinkType.Album, SharedLinkType.Individual]) {
it(`${type}: permits the photo and its Live Photo video, but excludes unrelated and trashed assets`, async () => {
const { ctx } = newMediumService(BaseService, { database: db, real: [], mock: [LoggingRepository] });
const { user } = await ctx.newUser();
const { asset: video } = await ctx.newAsset({ ownerId: user.id, type: AssetType.Video });
const { asset: photo } = await ctx.newAsset({ ownerId: user.id, livePhotoVideoId: video.id });
const { asset: unrelated } = await ctx.newAsset({ ownerId: user.id });
const { asset: trashed } = await ctx.newAsset({ ownerId: user.id, deletedAt: new Date().toISOString() });
const { album } = await ctx.newAlbum({ ownerId: user.id });
await ctx.newAlbumAsset({ albumId: album.id, assetId: photo.id });
await ctx.newAlbumAsset({ albumId: album.id, assetId: trashed.id });
const link = await ctx.get(SharedLinkRepository).create({
id: factory.uuid(),
key: randomBytes(16),
userId: user.id,
allowUpload: false,
type,
...(type === SharedLinkType.Album ? { albumId: album.id } : { assetIds: [photo.id, trashed.id] }),
});
const access = ctx.get(AccessRepository).asset;
const requested = new Set([photo.id, video.id, unrelated.id, trashed.id]);
await expect(access.checkSharedLinkAccess(link.id, requested)).resolves.toEqual(new Set([photo.id, video.id]));
await expect(access.checkSharedLinkAccess(factory.uuid(), requested)).resolves.toEqual(new Set());
await expect(access.checkSharedLinkAccess(link.id, new Set())).resolves.toEqual(new Set());
if (type !== SharedLinkType.Album) {
return;
}
await db.updateTable('album').set({ deletedAt: new Date().toISOString() }).where('id', '=', album.id).execute();
await expect(access.checkSharedLinkAccess(link.id, requested)).resolves.toEqual(new Set());
});
}
});