Complete account schema foundation
This commit is contained in:
@@ -28,6 +28,7 @@ import { SyncController } from '../../src/modules/sync/sync.controller';
|
||||
import { UploadsController } from '../../src/modules/uploads/uploads.controller';
|
||||
import { UploadsService } from '../../src/modules/uploads/uploads.service';
|
||||
import { PrismaService } from '../../src/infrastructure/database/prisma.service';
|
||||
import { AccountController } from '../../src/modules/users/account.controller';
|
||||
|
||||
function sampleMp3Bytes(seed: string): Buffer {
|
||||
return Buffer.concat([
|
||||
@@ -98,6 +99,9 @@ function createPrismaMock() {
|
||||
id: data.id ?? randomUUID(),
|
||||
createdAt: data.createdAt ?? now,
|
||||
updatedAt: data.updatedAt ?? now,
|
||||
accountKind: data.accountKind ?? 'LEGACY_DEFAULT',
|
||||
accountStatus: data.accountStatus ?? 'ACTIVE',
|
||||
libraryNamespace: data.libraryNamespace ?? randomUUID(),
|
||||
...data,
|
||||
libraryCursor:
|
||||
data.libraryCursor == null ? 0n : BigInt(data.libraryCursor),
|
||||
@@ -138,6 +142,21 @@ function createPrismaMock() {
|
||||
users.set(created.id, created);
|
||||
return created;
|
||||
}),
|
||||
findUnique: jest.fn().mockImplementation(async ({ where, select }) => {
|
||||
if (where.id) {
|
||||
return applySelect(users.get(where.id) ?? null, select);
|
||||
}
|
||||
|
||||
if (where.slug) {
|
||||
const matchingUser =
|
||||
[...users.values()].find((user) => user.slug === where.slug) ??
|
||||
null;
|
||||
|
||||
return applySelect(matchingUser, select);
|
||||
}
|
||||
|
||||
return null;
|
||||
}),
|
||||
create: jest.fn().mockImplementation(async ({ data }) => {
|
||||
const created = createUserRecord(data);
|
||||
users.set(created.id, created);
|
||||
@@ -437,6 +456,7 @@ describe('Velody API wiring (e2e)', () => {
|
||||
let app: NestExpressApplication;
|
||||
let prismaMock: ReturnType<typeof createPrismaMock>['prismaMock'];
|
||||
let assetsController: AssetsController;
|
||||
let accountController: AccountController;
|
||||
let artworkController: ArtworkController;
|
||||
let healthController: HealthController;
|
||||
let devicesController: DevicesController;
|
||||
@@ -552,6 +572,7 @@ describe('Velody API wiring (e2e)', () => {
|
||||
await app.init();
|
||||
|
||||
assetsController = moduleRef.get(AssetsController);
|
||||
accountController = moduleRef.get(AccountController);
|
||||
artworkController = moduleRef.get(ArtworkController);
|
||||
healthController = moduleRef.get(HealthController);
|
||||
devicesController = moduleRef.get(DevicesController);
|
||||
@@ -587,13 +608,22 @@ describe('Velody API wiring (e2e)', () => {
|
||||
update: {
|
||||
displayName: 'Default Owner',
|
||||
isDefault: true,
|
||||
accountKind: 'LEGACY_DEFAULT',
|
||||
accountStatus: 'ACTIVE',
|
||||
},
|
||||
create: {
|
||||
slug: 'default-owner',
|
||||
displayName: 'Default Owner',
|
||||
isDefault: true,
|
||||
accountKind: 'LEGACY_DEFAULT',
|
||||
accountStatus: 'ACTIVE',
|
||||
},
|
||||
});
|
||||
expect(prismaState.defaultUser).toMatchObject({
|
||||
accountKind: 'LEGACY_DEFAULT',
|
||||
accountStatus: 'ACTIVE',
|
||||
libraryNamespace: expect.any(String),
|
||||
});
|
||||
});
|
||||
|
||||
it('registers a device and accepts heartbeat', async () => {
|
||||
@@ -631,6 +661,32 @@ describe('Velody API wiring (e2e)', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('returns current account metadata from device bearer auth only', async () => {
|
||||
const registerResponse = await devicesController.register({
|
||||
platform: 'MACOS',
|
||||
deviceName: 'Account Mac',
|
||||
appVersion: '0.1.0',
|
||||
});
|
||||
|
||||
const meResponse = await runAsDevice(registerResponse.deviceAccessToken, () =>
|
||||
accountController.getMe(),
|
||||
);
|
||||
|
||||
expect(meResponse).toEqual({
|
||||
accountId: prismaState.defaultUser.id,
|
||||
accountKind: 'LEGACY_DEFAULT',
|
||||
accountStatus: 'ACTIVE',
|
||||
libraryNamespace: prismaState.defaultUser.libraryNamespace,
|
||||
currentDeviceId: registerResponse.deviceId,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns 401 from /me when Authorization is missing', async () => {
|
||||
await expect(accountController.getMe()).rejects.toBeInstanceOf(
|
||||
UnauthorizedException,
|
||||
);
|
||||
});
|
||||
|
||||
it('registers a linked device under the authenticated device owner when Authorization is present', async () => {
|
||||
const linkedOwnerId = randomUUID();
|
||||
const existingDevice = seedDevice({
|
||||
@@ -659,6 +715,78 @@ describe('Velody API wiring (e2e)', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('ignores raw deviceId owner fallback during unauthenticated registration', async () => {
|
||||
const foreignOwnerId = randomUUID();
|
||||
const foreignDeviceId = randomUUID();
|
||||
prismaState.users.set(
|
||||
foreignOwnerId,
|
||||
{
|
||||
id: foreignOwnerId,
|
||||
slug: 'foreign-owner',
|
||||
displayName: 'Foreign Owner',
|
||||
isDefault: false,
|
||||
accountKind: 'LEGACY_DEFAULT',
|
||||
accountStatus: 'ACTIVE',
|
||||
libraryNamespace: randomUUID(),
|
||||
libraryCursor: 0n,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
);
|
||||
seedDevice({
|
||||
userId: foreignOwnerId,
|
||||
deviceId: foreignDeviceId,
|
||||
deviceName: 'Foreign Existing Device',
|
||||
});
|
||||
|
||||
const response = await requestContextService.run(async () => {
|
||||
requestContextService.setLegacyDeviceId(foreignDeviceId);
|
||||
|
||||
return devicesController.register({
|
||||
platform: 'IPHONE',
|
||||
deviceName: 'Fresh iPhone',
|
||||
appVersion: '0.1.0',
|
||||
});
|
||||
});
|
||||
|
||||
expect(prismaState.devices.get(response.deviceId)?.userId).toBe(
|
||||
prismaState.defaultUser.id,
|
||||
);
|
||||
});
|
||||
|
||||
it('uses authenticated device owner during registration even when raw deviceId points elsewhere', async () => {
|
||||
const authenticatedOwnerId = randomUUID();
|
||||
const rawDeviceOwnerId = randomUUID();
|
||||
const rawDeviceId = randomUUID();
|
||||
const authenticatedDevice = seedDevice({
|
||||
userId: authenticatedOwnerId,
|
||||
deviceAccessToken: 'authenticated-link-token',
|
||||
deviceName: 'Authenticated Existing Device',
|
||||
});
|
||||
seedDevice({
|
||||
userId: rawDeviceOwnerId,
|
||||
deviceId: rawDeviceId,
|
||||
deviceName: 'Raw Device',
|
||||
});
|
||||
|
||||
const response = await requestContextService.run(async () => {
|
||||
requestContextService.setLegacyDeviceId(rawDeviceId);
|
||||
await deviceAuthService.authenticateAuthorizationHeader(
|
||||
`Bearer ${authenticatedDevice.deviceAccessToken}`,
|
||||
);
|
||||
|
||||
return devicesController.register({
|
||||
platform: 'IPHONE',
|
||||
deviceName: 'Linked iPhone',
|
||||
appVersion: '0.1.0',
|
||||
});
|
||||
});
|
||||
|
||||
expect(prismaState.devices.get(response.deviceId)?.userId).toBe(
|
||||
authenticatedOwnerId,
|
||||
);
|
||||
});
|
||||
|
||||
it('returns 401 when register receives an invalid Authorization header', async () => {
|
||||
await expect(
|
||||
registerDeviceWithAuthorization(
|
||||
|
||||
Reference in New Issue
Block a user