Complete account schema foundation

This commit is contained in:
diyaa
2026-06-24 07:36:57 +02:00
parent 538d259b63
commit babea87140
16 changed files with 789 additions and 57 deletions
+128
View File
@@ -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(