Harden protected route authentication
This commit is contained in:
+277
-182
@@ -520,12 +520,19 @@ describe('Velody API wiring (e2e)', () => {
|
||||
expect.any(Date),
|
||||
);
|
||||
|
||||
const heartbeatResponse = await devicesController.heartbeat({
|
||||
deviceId: registerResponse.deviceId,
|
||||
appVersion: '0.1.1',
|
||||
});
|
||||
const heartbeatResponse = await runAsDevice(
|
||||
registerResponse.deviceAccessToken,
|
||||
() =>
|
||||
devicesController.heartbeat({
|
||||
deviceId: registerResponse.deviceId,
|
||||
appVersion: '0.1.1',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(heartbeatResponse.ok).toBe(true);
|
||||
expect(prismaState.devices.get(registerResponse.deviceId)?.appVersion).toBe(
|
||||
'0.1.1',
|
||||
);
|
||||
});
|
||||
|
||||
it('registers a linked device under the authenticated device owner when Authorization is present', async () => {
|
||||
@@ -589,7 +596,69 @@ describe('Velody API wiring (e2e)', () => {
|
||||
).rejects.toBeInstanceOf(UnauthorizedException);
|
||||
});
|
||||
|
||||
it('accepts heartbeat updates for a legacy device id outside the bootstrap owner', async () => {
|
||||
it('returns 401 when heartbeat is requested without Authorization', async () => {
|
||||
await expect(
|
||||
devicesController.heartbeat({
|
||||
deviceId: randomUUID(),
|
||||
appVersion: '0.1.1',
|
||||
}),
|
||||
).rejects.toBeInstanceOf(UnauthorizedException);
|
||||
});
|
||||
|
||||
it('returns 401 when sync bootstrap is requested without Authorization', async () => {
|
||||
await expect(syncController.bootstrap()).rejects.toBeInstanceOf(
|
||||
UnauthorizedException,
|
||||
);
|
||||
});
|
||||
|
||||
it('returns 401 when upload prepare is requested without Authorization', async () => {
|
||||
const bytes = sampleMp3Bytes('missing-upload-auth');
|
||||
await expect(
|
||||
uploadsController.prepare({
|
||||
deviceId: randomUUID(),
|
||||
sha256: sha256Hex(bytes),
|
||||
originalFilename: 'missing-upload-auth.mp3',
|
||||
sizeBytes: bytes.length,
|
||||
}),
|
||||
).rejects.toBeInstanceOf(UnauthorizedException);
|
||||
});
|
||||
|
||||
it('returns 401 when asset download is requested without Authorization', async () => {
|
||||
await expect(
|
||||
assetsController.download(
|
||||
randomUUID(),
|
||||
{ deviceId: randomUUID() },
|
||||
{ setHeader() {} } as any,
|
||||
),
|
||||
).rejects.toBeInstanceOf(UnauthorizedException);
|
||||
});
|
||||
|
||||
it('returns 401 when artwork download is requested without Authorization', async () => {
|
||||
await expect(
|
||||
artworkController.download(
|
||||
randomUUID(),
|
||||
{ deviceId: randomUUID() },
|
||||
{ setHeader() {} } as any,
|
||||
),
|
||||
).rejects.toBeInstanceOf(UnauthorizedException);
|
||||
});
|
||||
|
||||
it('returns 401 when a protected route receives an invalid Authorization header', async () => {
|
||||
await requestContextService.run(async () => {
|
||||
await expect(
|
||||
deviceAuthService.authenticateAuthorizationHeader(
|
||||
'Bearer invalid-device-token',
|
||||
),
|
||||
).rejects.toBeInstanceOf(UnauthorizedException);
|
||||
});
|
||||
});
|
||||
|
||||
it('uses the authenticated device for heartbeat even when the body deviceId is spoofed', async () => {
|
||||
const ownerDevice = await devicesController.register({
|
||||
platform: 'MACOS',
|
||||
deviceName: 'Owner Mac',
|
||||
appVersion: '0.1.0',
|
||||
});
|
||||
const foreignDeviceId = randomUUID();
|
||||
prismaState.devices.set(foreignDeviceId, {
|
||||
id: foreignDeviceId,
|
||||
@@ -603,18 +672,32 @@ describe('Velody API wiring (e2e)', () => {
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
|
||||
const response = await devicesController.heartbeat({
|
||||
deviceId: foreignDeviceId,
|
||||
appVersion: '0.1.1',
|
||||
});
|
||||
const response = await runAsDevice(ownerDevice.deviceAccessToken, () =>
|
||||
devicesController.heartbeat({
|
||||
deviceId: foreignDeviceId,
|
||||
appVersion: '0.1.1',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(response.ok).toBe(true);
|
||||
expect(prismaState.devices.get(foreignDeviceId)?.appVersion).toBe('0.1.1');
|
||||
expect(prismaState.devices.get(ownerDevice.deviceId)?.appVersion).toBe(
|
||||
'0.1.1',
|
||||
);
|
||||
expect(prismaState.devices.get(foreignDeviceId)?.appVersion).toBe('0.1.0');
|
||||
});
|
||||
|
||||
it('returns sync bootstrap and changes payloads', async () => {
|
||||
const bootstrapResponse = await syncController.bootstrap();
|
||||
const changesResponse = await syncController.changes({ after: '0' });
|
||||
const device = await devicesController.register({
|
||||
platform: 'IPHONE',
|
||||
deviceName: 'Sync iPhone',
|
||||
appVersion: '0.1.0',
|
||||
});
|
||||
const bootstrapResponse = await runAsDevice(device.deviceAccessToken, () =>
|
||||
syncController.bootstrap(),
|
||||
);
|
||||
const changesResponse = await runAsDevice(device.deviceAccessToken, () =>
|
||||
syncController.changes({ after: '0' }),
|
||||
);
|
||||
|
||||
expect(bootstrapResponse.tracks).toEqual([]);
|
||||
expect(changesResponse.events).toEqual([]);
|
||||
@@ -622,6 +705,11 @@ describe('Velody API wiring (e2e)', () => {
|
||||
});
|
||||
|
||||
it('sync bootstrap and changes do not expose foreign-owner data', async () => {
|
||||
const device = await devicesController.register({
|
||||
platform: 'IPHONE',
|
||||
deviceName: 'Scoped Sync iPhone',
|
||||
appVersion: '0.1.0',
|
||||
});
|
||||
const foreignUserId = randomUUID();
|
||||
const foreignTrackId = randomUUID();
|
||||
|
||||
@@ -654,8 +742,12 @@ describe('Velody API wiring (e2e)', () => {
|
||||
createdAt: new Date('2026-05-29T08:02:00.000Z'),
|
||||
});
|
||||
|
||||
const bootstrapResponse = await syncController.bootstrap();
|
||||
const changesResponse = await syncController.changes({ after: '0' });
|
||||
const bootstrapResponse = await runAsDevice(device.deviceAccessToken, () =>
|
||||
syncController.bootstrap(),
|
||||
);
|
||||
const changesResponse = await runAsDevice(device.deviceAccessToken, () =>
|
||||
syncController.changes({ after: '0' }),
|
||||
);
|
||||
|
||||
expect(bootstrapResponse.tracks).toEqual([]);
|
||||
expect(changesResponse.events).toEqual([]);
|
||||
@@ -704,10 +796,12 @@ describe('Velody API wiring (e2e)', () => {
|
||||
},
|
||||
} as any;
|
||||
|
||||
const streamable = await assetsController.download(
|
||||
assetId,
|
||||
{ deviceId: registerResponse.deviceId },
|
||||
responseMock,
|
||||
const streamable = await runAsDevice(registerResponse.deviceAccessToken, () =>
|
||||
assetsController.download(
|
||||
assetId,
|
||||
{ deviceId: registerResponse.deviceId },
|
||||
responseMock,
|
||||
),
|
||||
);
|
||||
const downloadedBytes = await streamToBuffer(streamable.getStream());
|
||||
|
||||
@@ -741,10 +835,12 @@ describe('Velody API wiring (e2e)', () => {
|
||||
});
|
||||
|
||||
await expect(
|
||||
assetsController.download(
|
||||
assetId,
|
||||
{ deviceId: registerResponse.deviceId },
|
||||
{ setHeader() {} } as any,
|
||||
runAsDevice(registerResponse.deviceAccessToken, () =>
|
||||
assetsController.download(
|
||||
assetId,
|
||||
{ deviceId: registerResponse.deviceId },
|
||||
{ setHeader() {} } as any,
|
||||
),
|
||||
),
|
||||
).rejects.toBeInstanceOf(ForbiddenException);
|
||||
});
|
||||
@@ -778,10 +874,12 @@ describe('Velody API wiring (e2e)', () => {
|
||||
});
|
||||
|
||||
await expect(
|
||||
assetsController.download(
|
||||
assetId,
|
||||
{ deviceId: registerResponse.deviceId },
|
||||
{ setHeader() {} } as any,
|
||||
runAsDevice(registerResponse.deviceAccessToken, () =>
|
||||
assetsController.download(
|
||||
assetId,
|
||||
{ deviceId: registerResponse.deviceId },
|
||||
{ setHeader() {} } as any,
|
||||
),
|
||||
),
|
||||
).rejects.toBeInstanceOf(NotFoundException);
|
||||
});
|
||||
@@ -848,10 +946,12 @@ describe('Velody API wiring (e2e)', () => {
|
||||
},
|
||||
} as any;
|
||||
|
||||
const streamable = await artworkController.download(
|
||||
artworkId,
|
||||
{ deviceId: registerResponse.deviceId },
|
||||
responseMock,
|
||||
const streamable = await runAsDevice(registerResponse.deviceAccessToken, () =>
|
||||
artworkController.download(
|
||||
artworkId,
|
||||
{ deviceId: registerResponse.deviceId },
|
||||
responseMock,
|
||||
),
|
||||
);
|
||||
const downloadedBytes = await streamToBuffer(streamable.getStream());
|
||||
|
||||
@@ -882,10 +982,12 @@ describe('Velody API wiring (e2e)', () => {
|
||||
});
|
||||
|
||||
await expect(
|
||||
artworkController.download(
|
||||
artworkId,
|
||||
{ deviceId: registerResponse.deviceId },
|
||||
{ setHeader() {} } as any,
|
||||
runAsDevice(registerResponse.deviceAccessToken, () =>
|
||||
artworkController.download(
|
||||
artworkId,
|
||||
{ deviceId: registerResponse.deviceId },
|
||||
{ setHeader() {} } as any,
|
||||
),
|
||||
),
|
||||
).rejects.toBeInstanceOf(ForbiddenException);
|
||||
});
|
||||
@@ -917,10 +1019,12 @@ describe('Velody API wiring (e2e)', () => {
|
||||
});
|
||||
|
||||
await expect(
|
||||
artworkController.download(
|
||||
artworkId,
|
||||
{ deviceId: registerResponse.deviceId },
|
||||
{ setHeader() {} } as any,
|
||||
runAsDevice(registerResponse.deviceAccessToken, () =>
|
||||
artworkController.download(
|
||||
artworkId,
|
||||
{ deviceId: registerResponse.deviceId },
|
||||
{ setHeader() {} } as any,
|
||||
),
|
||||
),
|
||||
).rejects.toBeInstanceOf(NotFoundException);
|
||||
});
|
||||
@@ -1089,9 +1193,11 @@ describe('Velody API wiring (e2e)', () => {
|
||||
updatedAt: new Date('2026-05-29T08:02:30.000Z'),
|
||||
});
|
||||
|
||||
const response = await libraryController.getTracks({
|
||||
deviceId: primaryDevice.deviceId,
|
||||
});
|
||||
const response = await runAsDevice(primaryDevice.deviceAccessToken, () =>
|
||||
libraryController.getTracks({
|
||||
deviceId: primaryDevice.deviceId,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(response).toEqual({
|
||||
tracks: [
|
||||
@@ -1340,59 +1446,18 @@ describe('Velody API wiring (e2e)', () => {
|
||||
expect(linkedLibrary.tracks).toEqual(primaryLibrary.tracks);
|
||||
});
|
||||
|
||||
it('keeps the legacy library deviceId path working when Authorization is missing', async () => {
|
||||
it('returns 401 when library is requested without Authorization even if deviceId is supplied', async () => {
|
||||
const ownerDevice = await devicesController.register({
|
||||
platform: 'IPHONE',
|
||||
deviceName: 'Legacy iPhone',
|
||||
appVersion: '0.1.0',
|
||||
});
|
||||
const trackId = randomUUID();
|
||||
const assetId = randomUUID();
|
||||
|
||||
prismaState.audioAssets.set(assetId, {
|
||||
id: assetId,
|
||||
userId: prismaState.defaultUser.id,
|
||||
trackId,
|
||||
sha256: 'legacy-library-sha',
|
||||
storageKey: 'users/default/audio/legacy-library-sha.mp3',
|
||||
originalFilename: 'legacy-library.mp3',
|
||||
mimeType: 'audio/mpeg',
|
||||
fileExtension: 'mp3',
|
||||
fileSizeBytes: BigInt(42),
|
||||
durationMs: 210000,
|
||||
sourceDeviceId: ownerDevice.deviceId,
|
||||
createdAt: new Date('2026-05-29T08:00:00.000Z'),
|
||||
});
|
||||
prismaState.tracks.set(trackId, {
|
||||
id: trackId,
|
||||
userId: prismaState.defaultUser.id,
|
||||
primaryAudioAssetId: assetId,
|
||||
artworkAssetId: null,
|
||||
title: 'Legacy Library Track',
|
||||
artist: 'Velody',
|
||||
album: null,
|
||||
albumArtist: null,
|
||||
genre: null,
|
||||
discNumber: null,
|
||||
trackNumber: null,
|
||||
year: null,
|
||||
durationMs: 210000,
|
||||
status: 'ACTIVE',
|
||||
deletedAt: null,
|
||||
createdAt: new Date('2026-05-29T08:00:00.000Z'),
|
||||
updatedAt: new Date('2026-05-29T08:02:00.000Z'),
|
||||
});
|
||||
|
||||
const response = await libraryController.getTracks({
|
||||
deviceId: ownerDevice.deviceId,
|
||||
});
|
||||
|
||||
expect(response.tracks).toEqual([
|
||||
expect.objectContaining({
|
||||
trackId,
|
||||
assetId,
|
||||
await expect(
|
||||
libraryController.getTracks({
|
||||
deviceId: ownerDevice.deviceId,
|
||||
}),
|
||||
]);
|
||||
).rejects.toBeInstanceOf(UnauthorizedException);
|
||||
});
|
||||
|
||||
it('rejects invalid or revoked device tokens even when a legacy device id is supplied', async () => {
|
||||
@@ -1595,30 +1660,35 @@ describe('Velody API wiring (e2e)', () => {
|
||||
const bytes = sampleMp3Bytes('e2e-upload');
|
||||
const sha256 = sha256Hex(bytes);
|
||||
|
||||
const prepareResponse = await uploadsController.prepare({
|
||||
deviceId: registerResponse.deviceId,
|
||||
sha256,
|
||||
originalFilename: 'e2e-upload.mp3',
|
||||
sizeBytes: bytes.length,
|
||||
});
|
||||
const prepareResponse = await runAsDevice(registerResponse.deviceAccessToken, () =>
|
||||
uploadsController.prepare({
|
||||
deviceId: registerResponse.deviceId,
|
||||
sha256,
|
||||
originalFilename: 'e2e-upload.mp3',
|
||||
sizeBytes: bytes.length,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(prepareResponse.status).toBe('upload_required');
|
||||
|
||||
const uploadResponse = await uploadsService.uploadFile(
|
||||
prepareResponse.uploadId!,
|
||||
createUploadRequest(bytes),
|
||||
const uploadResponse = await runAsDevice(registerResponse.deviceAccessToken, () =>
|
||||
uploadsService.uploadFile(
|
||||
prepareResponse.uploadId!,
|
||||
createUploadRequest(bytes),
|
||||
),
|
||||
);
|
||||
|
||||
expect(uploadResponse.status).toBe('COMPLETED');
|
||||
|
||||
const finalizeResponse = await uploadsController.finalize(
|
||||
prepareResponse.uploadId!,
|
||||
{
|
||||
title: 'Uploaded Track',
|
||||
artist: 'Velody',
|
||||
album: 'Milestone 6',
|
||||
durationMs: 222000,
|
||||
},
|
||||
const finalizeResponse = await runAsDevice(
|
||||
registerResponse.deviceAccessToken,
|
||||
() =>
|
||||
uploadsController.finalize(prepareResponse.uploadId!, {
|
||||
title: 'Uploaded Track',
|
||||
artist: 'Velody',
|
||||
album: 'Milestone 6',
|
||||
durationMs: 222000,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(finalizeResponse.trackId).toBeDefined();
|
||||
@@ -1629,12 +1699,16 @@ describe('Velody API wiring (e2e)', () => {
|
||||
);
|
||||
expect(storedBytes.equals(bytes)).toBe(true);
|
||||
|
||||
const duplicatePrepare = await uploadsController.prepare({
|
||||
deviceId: registerResponse.deviceId,
|
||||
sha256,
|
||||
originalFilename: 'e2e-upload.mp3',
|
||||
sizeBytes: bytes.length,
|
||||
});
|
||||
const duplicatePrepare = await runAsDevice(
|
||||
registerResponse.deviceAccessToken,
|
||||
() =>
|
||||
uploadsController.prepare({
|
||||
deviceId: registerResponse.deviceId,
|
||||
sha256,
|
||||
originalFilename: 'e2e-upload.mp3',
|
||||
sizeBytes: bytes.length,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(duplicatePrepare.status).toBe('exists');
|
||||
expect(duplicatePrepare.uploadId).toBeDefined();
|
||||
@@ -1656,45 +1730,52 @@ describe('Velody API wiring (e2e)', () => {
|
||||
const sha256 = sha256Hex(bytes);
|
||||
const artworkSha256 = sha256Hex(artworkBytes);
|
||||
|
||||
const prepareResponse = await uploadsController.prepare({
|
||||
deviceId: registerResponse.deviceId,
|
||||
sha256,
|
||||
originalFilename: 'e2e-upload-artwork.mp3',
|
||||
sizeBytes: bytes.length,
|
||||
});
|
||||
const prepareResponse = await runAsDevice(registerResponse.deviceAccessToken, () =>
|
||||
uploadsController.prepare({
|
||||
deviceId: registerResponse.deviceId,
|
||||
sha256,
|
||||
originalFilename: 'e2e-upload-artwork.mp3',
|
||||
sizeBytes: bytes.length,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(prepareResponse.status).toBe('upload_required');
|
||||
|
||||
const uploadResponse = await uploadsService.uploadFile(
|
||||
prepareResponse.uploadId!,
|
||||
createUploadRequest(bytes),
|
||||
const uploadResponse = await runAsDevice(registerResponse.deviceAccessToken, () =>
|
||||
uploadsService.uploadFile(
|
||||
prepareResponse.uploadId!,
|
||||
createUploadRequest(bytes),
|
||||
),
|
||||
);
|
||||
|
||||
expect(uploadResponse.status).toBe('COMPLETED');
|
||||
|
||||
const finalizeResponse = await uploadsController.finalize(
|
||||
prepareResponse.uploadId!,
|
||||
{
|
||||
title: 'Uploaded Artwork Track',
|
||||
artist: 'Velody',
|
||||
album: 'Milestone 8.1',
|
||||
durationMs: 222000,
|
||||
artwork: {
|
||||
dataBase64: artworkBytes.toString('base64'),
|
||||
sha256: artworkSha256,
|
||||
mimeType: 'image/png',
|
||||
width: 1,
|
||||
height: 1,
|
||||
},
|
||||
},
|
||||
const finalizeResponse = await runAsDevice(
|
||||
registerResponse.deviceAccessToken,
|
||||
() =>
|
||||
uploadsController.finalize(prepareResponse.uploadId!, {
|
||||
title: 'Uploaded Artwork Track',
|
||||
artist: 'Velody',
|
||||
album: 'Milestone 8.1',
|
||||
durationMs: 222000,
|
||||
artwork: {
|
||||
dataBase64: artworkBytes.toString('base64'),
|
||||
sha256: artworkSha256,
|
||||
mimeType: 'image/png',
|
||||
width: 1,
|
||||
height: 1,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(finalizeResponse.trackId).toBeDefined();
|
||||
expect(prismaState.artworkAssets.size).toBe(1);
|
||||
|
||||
const remoteLibrary = await libraryController.getTracks({
|
||||
deviceId: registerResponse.deviceId,
|
||||
});
|
||||
const remoteLibrary = await runAsDevice(registerResponse.deviceAccessToken, () =>
|
||||
libraryController.getTracks({
|
||||
deviceId: registerResponse.deviceId,
|
||||
}),
|
||||
);
|
||||
expect(remoteLibrary.tracks).toEqual([
|
||||
expect.objectContaining({
|
||||
trackId: finalizeResponse.trackId,
|
||||
@@ -1727,10 +1808,12 @@ describe('Velody API wiring (e2e)', () => {
|
||||
},
|
||||
} as any;
|
||||
|
||||
const streamable = await artworkController.download(
|
||||
artworkAsset.id,
|
||||
{ deviceId: registerResponse.deviceId },
|
||||
responseMock,
|
||||
const streamable = await runAsDevice(registerResponse.deviceAccessToken, () =>
|
||||
artworkController.download(
|
||||
artworkAsset.id,
|
||||
{ deviceId: registerResponse.deviceId },
|
||||
responseMock,
|
||||
),
|
||||
);
|
||||
const downloadedArtworkBytes = await streamToBuffer(streamable.getStream());
|
||||
|
||||
@@ -1752,67 +1835,79 @@ describe('Velody API wiring (e2e)', () => {
|
||||
const sha256 = sha256Hex(bytes);
|
||||
const artworkSha256 = sha256Hex(artworkBytes);
|
||||
|
||||
const firstPrepare = await uploadsController.prepare({
|
||||
deviceId: registerResponse.deviceId,
|
||||
sha256,
|
||||
originalFilename: 'e2e-deduped-artwork.mp3',
|
||||
sizeBytes: bytes.length,
|
||||
});
|
||||
const firstPrepare = await runAsDevice(registerResponse.deviceAccessToken, () =>
|
||||
uploadsController.prepare({
|
||||
deviceId: registerResponse.deviceId,
|
||||
sha256,
|
||||
originalFilename: 'e2e-deduped-artwork.mp3',
|
||||
sizeBytes: bytes.length,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(firstPrepare.status).toBe('upload_required');
|
||||
|
||||
const uploadResponse = await uploadsService.uploadFile(
|
||||
firstPrepare.uploadId!,
|
||||
createUploadRequest(bytes),
|
||||
const uploadResponse = await runAsDevice(registerResponse.deviceAccessToken, () =>
|
||||
uploadsService.uploadFile(
|
||||
firstPrepare.uploadId!,
|
||||
createUploadRequest(bytes),
|
||||
),
|
||||
);
|
||||
|
||||
expect(uploadResponse.status).toBe('COMPLETED');
|
||||
|
||||
const firstFinalize = await uploadsController.finalize(
|
||||
firstPrepare.uploadId!,
|
||||
{
|
||||
title: 'Deduped Artwork Track',
|
||||
artist: 'Velody',
|
||||
album: 'Milestone 8.1',
|
||||
durationMs: 222000,
|
||||
},
|
||||
const firstFinalize = await runAsDevice(
|
||||
registerResponse.deviceAccessToken,
|
||||
() =>
|
||||
uploadsController.finalize(firstPrepare.uploadId!, {
|
||||
title: 'Deduped Artwork Track',
|
||||
artist: 'Velody',
|
||||
album: 'Milestone 8.1',
|
||||
durationMs: 222000,
|
||||
}),
|
||||
);
|
||||
|
||||
const secondPrepare = await uploadsController.prepare({
|
||||
deviceId: registerResponse.deviceId,
|
||||
sha256,
|
||||
originalFilename: 'e2e-deduped-artwork.mp3',
|
||||
sizeBytes: bytes.length,
|
||||
});
|
||||
const secondPrepare = await runAsDevice(
|
||||
registerResponse.deviceAccessToken,
|
||||
() =>
|
||||
uploadsController.prepare({
|
||||
deviceId: registerResponse.deviceId,
|
||||
sha256,
|
||||
originalFilename: 'e2e-deduped-artwork.mp3',
|
||||
sizeBytes: bytes.length,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(secondPrepare.status).toBe('exists');
|
||||
expect(secondPrepare.uploadId).toBeDefined();
|
||||
expect(prismaState.audioAssets.size).toBe(1);
|
||||
|
||||
const secondFinalize = await uploadsController.finalize(
|
||||
secondPrepare.uploadId!,
|
||||
{
|
||||
title: 'Deduped Artwork Track',
|
||||
artist: 'Velody',
|
||||
album: 'Milestone 8.1',
|
||||
durationMs: 222000,
|
||||
artwork: {
|
||||
dataBase64: artworkBytes.toString('base64'),
|
||||
sha256: artworkSha256,
|
||||
mimeType: 'image/png',
|
||||
width: 1,
|
||||
height: 1,
|
||||
},
|
||||
},
|
||||
const secondFinalize = await runAsDevice(
|
||||
registerResponse.deviceAccessToken,
|
||||
() =>
|
||||
uploadsController.finalize(secondPrepare.uploadId!, {
|
||||
title: 'Deduped Artwork Track',
|
||||
artist: 'Velody',
|
||||
album: 'Milestone 8.1',
|
||||
durationMs: 222000,
|
||||
artwork: {
|
||||
dataBase64: artworkBytes.toString('base64'),
|
||||
sha256: artworkSha256,
|
||||
mimeType: 'image/png',
|
||||
width: 1,
|
||||
height: 1,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(secondFinalize.trackId).toBe(firstFinalize.trackId);
|
||||
expect(secondFinalize.assetId).toBe(firstFinalize.assetId);
|
||||
expect(prismaState.artworkAssets.size).toBe(1);
|
||||
|
||||
const remoteLibrary = await libraryController.getTracks({
|
||||
deviceId: registerResponse.deviceId,
|
||||
});
|
||||
const remoteLibrary = await runAsDevice(registerResponse.deviceAccessToken, () =>
|
||||
libraryController.getTracks({
|
||||
deviceId: registerResponse.deviceId,
|
||||
}),
|
||||
);
|
||||
expect(remoteLibrary.tracks).toEqual([
|
||||
expect.objectContaining({
|
||||
trackId: firstFinalize.trackId,
|
||||
|
||||
Reference in New Issue
Block a user