mirror of
https://github.com/immich-app/immich.git
synced 2026-02-04 08:49:01 +03:00
fix: add asset upload medium test (#25144)
This commit is contained in:
14
mobile/openapi/lib/api/assets_api.dart
generated
14
mobile/openapi/lib/api/assets_api.dart
generated
@@ -1353,8 +1353,6 @@ class AssetsApi {
|
|||||||
///
|
///
|
||||||
/// * [DateTime] fileModifiedAt (required):
|
/// * [DateTime] fileModifiedAt (required):
|
||||||
///
|
///
|
||||||
/// * [List<AssetMetadataUpsertItemDto>] metadata (required):
|
|
||||||
///
|
|
||||||
/// * [String] key:
|
/// * [String] key:
|
||||||
///
|
///
|
||||||
/// * [String] slug:
|
/// * [String] slug:
|
||||||
@@ -1370,10 +1368,12 @@ class AssetsApi {
|
|||||||
///
|
///
|
||||||
/// * [String] livePhotoVideoId:
|
/// * [String] livePhotoVideoId:
|
||||||
///
|
///
|
||||||
|
/// * [List<AssetMetadataUpsertItemDto>] metadata:
|
||||||
|
///
|
||||||
/// * [MultipartFile] sidecarData:
|
/// * [MultipartFile] sidecarData:
|
||||||
///
|
///
|
||||||
/// * [AssetVisibility] visibility:
|
/// * [AssetVisibility] visibility:
|
||||||
Future<Response> uploadAssetWithHttpInfo(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, List<AssetMetadataUpsertItemDto> metadata, { String? key, String? slug, String? xImmichChecksum, String? duration, String? filename, bool? isFavorite, String? livePhotoVideoId, MultipartFile? sidecarData, AssetVisibility? visibility, }) async {
|
Future<Response> uploadAssetWithHttpInfo(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? xImmichChecksum, String? duration, String? filename, bool? isFavorite, String? livePhotoVideoId, List<AssetMetadataUpsertItemDto>? metadata, MultipartFile? sidecarData, AssetVisibility? visibility, }) async {
|
||||||
// ignore: prefer_const_declarations
|
// ignore: prefer_const_declarations
|
||||||
final apiPath = r'/assets';
|
final apiPath = r'/assets';
|
||||||
|
|
||||||
@@ -1480,8 +1480,6 @@ class AssetsApi {
|
|||||||
///
|
///
|
||||||
/// * [DateTime] fileModifiedAt (required):
|
/// * [DateTime] fileModifiedAt (required):
|
||||||
///
|
///
|
||||||
/// * [List<AssetMetadataUpsertItemDto>] metadata (required):
|
|
||||||
///
|
|
||||||
/// * [String] key:
|
/// * [String] key:
|
||||||
///
|
///
|
||||||
/// * [String] slug:
|
/// * [String] slug:
|
||||||
@@ -1497,11 +1495,13 @@ class AssetsApi {
|
|||||||
///
|
///
|
||||||
/// * [String] livePhotoVideoId:
|
/// * [String] livePhotoVideoId:
|
||||||
///
|
///
|
||||||
|
/// * [List<AssetMetadataUpsertItemDto>] metadata:
|
||||||
|
///
|
||||||
/// * [MultipartFile] sidecarData:
|
/// * [MultipartFile] sidecarData:
|
||||||
///
|
///
|
||||||
/// * [AssetVisibility] visibility:
|
/// * [AssetVisibility] visibility:
|
||||||
Future<AssetMediaResponseDto?> uploadAsset(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, List<AssetMetadataUpsertItemDto> metadata, { String? key, String? slug, String? xImmichChecksum, String? duration, String? filename, bool? isFavorite, String? livePhotoVideoId, MultipartFile? sidecarData, AssetVisibility? visibility, }) async {
|
Future<AssetMediaResponseDto?> uploadAsset(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? xImmichChecksum, String? duration, String? filename, bool? isFavorite, String? livePhotoVideoId, List<AssetMetadataUpsertItemDto>? metadata, MultipartFile? sidecarData, AssetVisibility? visibility, }) async {
|
||||||
final response = await uploadAssetWithHttpInfo(assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, metadata, key: key, slug: slug, xImmichChecksum: xImmichChecksum, duration: duration, filename: filename, isFavorite: isFavorite, livePhotoVideoId: livePhotoVideoId, sidecarData: sidecarData, visibility: visibility, );
|
final response = await uploadAssetWithHttpInfo(assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, key: key, slug: slug, xImmichChecksum: xImmichChecksum, duration: duration, filename: filename, isFavorite: isFavorite, livePhotoVideoId: livePhotoVideoId, metadata: metadata, sidecarData: sidecarData, visibility: visibility, );
|
||||||
if (response.statusCode >= HttpStatus.badRequest) {
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15605,8 +15605,7 @@
|
|||||||
"deviceAssetId",
|
"deviceAssetId",
|
||||||
"deviceId",
|
"deviceId",
|
||||||
"fileCreatedAt",
|
"fileCreatedAt",
|
||||||
"fileModifiedAt",
|
"fileModifiedAt"
|
||||||
"metadata"
|
|
||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -484,7 +484,7 @@ export type AssetMediaCreateDto = {
|
|||||||
filename?: string;
|
filename?: string;
|
||||||
isFavorite?: boolean;
|
isFavorite?: boolean;
|
||||||
livePhotoVideoId?: string;
|
livePhotoVideoId?: string;
|
||||||
metadata: AssetMetadataUpsertItemDto[];
|
metadata?: AssetMetadataUpsertItemDto[];
|
||||||
sidecarData?: Blob;
|
sidecarData?: Blob;
|
||||||
visibility?: AssetVisibility;
|
visibility?: AssetVisibility;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ export class AssetMediaCreateDto extends AssetMediaBase {
|
|||||||
@Optional()
|
@Optional()
|
||||||
@ValidateNested({ each: true })
|
@ValidateNested({ each: true })
|
||||||
@IsArray()
|
@IsArray()
|
||||||
metadata!: AssetMetadataUpsertItemDto[];
|
metadata?: AssetMetadataUpsertItemDto[];
|
||||||
|
|
||||||
@ApiProperty({ type: 'string', format: 'binary', required: false })
|
@ApiProperty({ type: 'string', format: 'binary', required: false })
|
||||||
[UploadFieldName.SIDECAR_DATA]?: any;
|
[UploadFieldName.SIDECAR_DATA]?: any;
|
||||||
|
|||||||
@@ -258,6 +258,10 @@ export class AssetRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
upsertMetadata(id: string, items: Array<{ key: string; value: object }>) {
|
upsertMetadata(id: string, items: Array<{ key: string; value: object }>) {
|
||||||
|
if (items.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
return this.db
|
return this.db
|
||||||
.insertInto('asset_metadata')
|
.insertInto('asset_metadata')
|
||||||
.values(items.map((item) => ({ assetId: id, ...item })))
|
.values(items.map((item) => ({ assetId: id, ...item })))
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ import { UserTable } from 'src/schema/tables/user.table';
|
|||||||
import { BASE_SERVICE_DEPENDENCIES, BaseService } from 'src/services/base.service';
|
import { BASE_SERVICE_DEPENDENCIES, BaseService } from 'src/services/base.service';
|
||||||
import { MetadataService } from 'src/services/metadata.service';
|
import { MetadataService } from 'src/services/metadata.service';
|
||||||
import { SyncService } from 'src/services/sync.service';
|
import { SyncService } from 'src/services/sync.service';
|
||||||
|
import { UploadFile } from 'src/types';
|
||||||
import { mockEnvData } from 'test/repositories/config.repository.mock';
|
import { mockEnvData } from 'test/repositories/config.repository.mock';
|
||||||
import { newTelemetryRepositoryMock } from 'test/repositories/telemetry.repository.mock';
|
import { newTelemetryRepositoryMock } from 'test/repositories/telemetry.repository.mock';
|
||||||
import { factory, newDate, newEmbedding, newUuid } from 'test/small.factory';
|
import { factory, newDate, newEmbedding, newUuid } from 'test/small.factory';
|
||||||
@@ -746,6 +747,17 @@ const loginResponse = (): LoginResponseDto => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const uploadFile = (file: Partial<UploadFile> = {}) => {
|
||||||
|
return {
|
||||||
|
uuid: newUuid(),
|
||||||
|
checksum: randomBytes(32),
|
||||||
|
originalPath: '/path/to/file.jpg',
|
||||||
|
originalName: 'file.jpg',
|
||||||
|
size: 123_456,
|
||||||
|
...file,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const mediumFactory = {
|
export const mediumFactory = {
|
||||||
assetInsert,
|
assetInsert,
|
||||||
assetFaceInsert,
|
assetFaceInsert,
|
||||||
@@ -760,4 +772,5 @@ export const mediumFactory = {
|
|||||||
loginDetails,
|
loginDetails,
|
||||||
loginResponse,
|
loginResponse,
|
||||||
tagInsert,
|
tagInsert,
|
||||||
|
uploadFile,
|
||||||
};
|
};
|
||||||
|
|||||||
100
server/test/medium/specs/services/asset-media.service.spec.ts
Normal file
100
server/test/medium/specs/services/asset-media.service.spec.ts
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import { Kysely } from 'kysely';
|
||||||
|
import { AssetMediaStatus } from 'src/dtos/asset-media-response.dto';
|
||||||
|
import { AccessRepository } from 'src/repositories/access.repository';
|
||||||
|
import { AssetRepository } from 'src/repositories/asset.repository';
|
||||||
|
import { EventRepository } from 'src/repositories/event.repository';
|
||||||
|
import { JobRepository } from 'src/repositories/job.repository';
|
||||||
|
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||||
|
import { StorageRepository } from 'src/repositories/storage.repository';
|
||||||
|
import { UserRepository } from 'src/repositories/user.repository';
|
||||||
|
import { DB } from 'src/schema';
|
||||||
|
import { AssetMediaService } from 'src/services/asset-media.service';
|
||||||
|
import { AssetService } from 'src/services/asset.service';
|
||||||
|
import { mediumFactory, newMediumService } from 'test/medium.factory';
|
||||||
|
import { factory } from 'test/small.factory';
|
||||||
|
import { getKyselyDB } from 'test/utils';
|
||||||
|
|
||||||
|
let defaultDatabase: Kysely<DB>;
|
||||||
|
|
||||||
|
const setup = (db?: Kysely<DB>) => {
|
||||||
|
return newMediumService(AssetMediaService, {
|
||||||
|
database: db || defaultDatabase,
|
||||||
|
real: [AccessRepository, AssetRepository, UserRepository],
|
||||||
|
mock: [EventRepository, LoggingRepository, JobRepository, StorageRepository],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
defaultDatabase = await getKyselyDB();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(AssetService.name, () => {
|
||||||
|
describe('uploadAsset', () => {
|
||||||
|
it('should work', async () => {
|
||||||
|
const { sut, ctx } = setup();
|
||||||
|
|
||||||
|
ctx.getMock(StorageRepository).utimes.mockResolvedValue();
|
||||||
|
ctx.getMock(EventRepository).emit.mockResolvedValue();
|
||||||
|
ctx.getMock(JobRepository).queue.mockResolvedValue();
|
||||||
|
|
||||||
|
const { user } = await ctx.newUser();
|
||||||
|
const { asset } = await ctx.newAsset({ ownerId: user.id });
|
||||||
|
await ctx.newExif({ assetId: asset.id, fileSizeInByte: 12_345 });
|
||||||
|
const auth = factory.auth({ user: { id: user.id } });
|
||||||
|
const file = mediumFactory.uploadFile();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
sut.uploadAsset(
|
||||||
|
auth,
|
||||||
|
{
|
||||||
|
deviceId: 'some-id',
|
||||||
|
deviceAssetId: 'some-id',
|
||||||
|
fileModifiedAt: new Date(),
|
||||||
|
fileCreatedAt: new Date(),
|
||||||
|
assetData: Buffer.from('some data'),
|
||||||
|
},
|
||||||
|
file,
|
||||||
|
),
|
||||||
|
).resolves.toEqual({
|
||||||
|
id: expect.any(String),
|
||||||
|
status: AssetMediaStatus.CREATED,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(ctx.getMock(EventRepository).emit).toHaveBeenCalledWith('AssetCreate', {
|
||||||
|
asset: expect.objectContaining({ deviceAssetId: 'some-id' }),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with an empty metadata list', async () => {
|
||||||
|
const { sut, ctx } = setup();
|
||||||
|
|
||||||
|
ctx.getMock(StorageRepository).utimes.mockResolvedValue();
|
||||||
|
ctx.getMock(EventRepository).emit.mockResolvedValue();
|
||||||
|
ctx.getMock(JobRepository).queue.mockResolvedValue();
|
||||||
|
|
||||||
|
const { user } = await ctx.newUser();
|
||||||
|
const { asset } = await ctx.newAsset({ ownerId: user.id });
|
||||||
|
await ctx.newExif({ assetId: asset.id, fileSizeInByte: 12_345 });
|
||||||
|
const auth = factory.auth({ user: { id: user.id } });
|
||||||
|
const file = mediumFactory.uploadFile();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
sut.uploadAsset(
|
||||||
|
auth,
|
||||||
|
{
|
||||||
|
deviceId: 'some-id',
|
||||||
|
deviceAssetId: 'some-id',
|
||||||
|
fileModifiedAt: new Date(),
|
||||||
|
fileCreatedAt: new Date(),
|
||||||
|
assetData: Buffer.from('some data'),
|
||||||
|
metadata: [],
|
||||||
|
},
|
||||||
|
file,
|
||||||
|
),
|
||||||
|
).resolves.toEqual({
|
||||||
|
id: expect.any(String),
|
||||||
|
status: AssetMediaStatus.CREATED,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user