feat: passkeys (#32194)
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev> (cherry picked from commit 6233a79bf7e4cec11e9ea1094908ff3b228fcbe0)
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
import { Selectable } from 'kysely';
|
||||
import { PasskeyTable } from 'src/schema/tables/passkey.table.js';
|
||||
import { PasskeyLike } from 'test/factories/types.js';
|
||||
import { newDate, newUuid, newUuidV7 } from 'test/small.factory.js';
|
||||
|
||||
export class PasskeyFactory {
|
||||
private constructor(private value: Selectable<PasskeyTable>) {}
|
||||
|
||||
static create(dto: PasskeyLike = {}) {
|
||||
return PasskeyFactory.from(dto).build();
|
||||
}
|
||||
|
||||
static from(dto: PasskeyLike = {}) {
|
||||
return new PasskeyFactory({
|
||||
id: newUuid(),
|
||||
name: 'Passkey',
|
||||
credentialId: 'credential-id',
|
||||
publicKey: Buffer.from('public-key'),
|
||||
counter: 0,
|
||||
transports: ['internal'],
|
||||
backedUp: false,
|
||||
deviceType: 'singleDevice',
|
||||
userId: newUuid(),
|
||||
createdAt: newDate(),
|
||||
updatedAt: newDate(),
|
||||
usedAt: null,
|
||||
updateId: newUuidV7(),
|
||||
...dto,
|
||||
});
|
||||
}
|
||||
|
||||
build() {
|
||||
return { ...this.value };
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import { AssetTable } from 'src/schema/tables/asset.table.js';
|
||||
import { ClusterGroupTable } from 'src/schema/tables/cluster-group.table.js';
|
||||
import { MemoryTable } from 'src/schema/tables/memory.table.js';
|
||||
import { PartnerTable } from 'src/schema/tables/partner.table.js';
|
||||
import { PasskeyTable } from 'src/schema/tables/passkey.table.js';
|
||||
import { PersonGroupTable } from 'src/schema/tables/person-group.table.js';
|
||||
import { PersonTable } from 'src/schema/tables/person.table.js';
|
||||
import { SessionTable } from 'src/schema/tables/session.table.js';
|
||||
@@ -45,5 +46,6 @@ export type MemoryLike = Partial<Selectable<MemoryTable>>;
|
||||
export type PartnerLike = Partial<Selectable<PartnerTable>>;
|
||||
export type ActivityLike = Partial<Selectable<ActivityTable>>;
|
||||
export type ApiKeyLike = Partial<Selectable<ApiKeyTable>>;
|
||||
export type PasskeyLike = Partial<Selectable<PasskeyTable>>;
|
||||
export type SessionLike = Partial<Selectable<SessionTable>>;
|
||||
export type OAuthProfileLike = Partial<OAuthProfile>;
|
||||
|
||||
@@ -30,6 +30,7 @@ import { AssetEditRepository } from 'src/repositories/asset-edit.repository.js';
|
||||
import { AssetFileRepository } from 'src/repositories/asset-file.repository.js';
|
||||
import { AssetJobRepository } from 'src/repositories/asset-job.repository.js';
|
||||
import { AssetRepository } from 'src/repositories/asset.repository.js';
|
||||
import { AuthChallengeRepository } from 'src/repositories/auth-challenge.repository.js';
|
||||
import { ClusterGroupRepository } from 'src/repositories/cluster-group.repository.js';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository.js';
|
||||
import { CronRepository } from 'src/repositories/cron.repository.js';
|
||||
@@ -50,6 +51,7 @@ import { MetadataRepository } from 'src/repositories/metadata.repository.js';
|
||||
import { NotificationRepository } from 'src/repositories/notification.repository.js';
|
||||
import { OcrRepository } from 'src/repositories/ocr.repository.js';
|
||||
import { PartnerRepository } from 'src/repositories/partner.repository.js';
|
||||
import { PasskeyRepository } from 'src/repositories/passkey.repository.js';
|
||||
import { PersonUserRepository } from 'src/repositories/person-user.repository.js';
|
||||
import { PersonRepository } from 'src/repositories/person.repository.js';
|
||||
import { PluginRepository } from 'src/repositories/plugin.repository.js';
|
||||
@@ -66,6 +68,7 @@ import { TagRepository } from 'src/repositories/tag.repository.js';
|
||||
import { TelemetryRepository } from 'src/repositories/telemetry.repository.js';
|
||||
import { UserRepository } from 'src/repositories/user.repository.js';
|
||||
import { VersionHistoryRepository } from 'src/repositories/version-history.repository.js';
|
||||
import { WebAuthnRepository } from 'src/repositories/webauthn.repository.js';
|
||||
import { WebsocketRepository } from 'src/repositories/websocket.repository.js';
|
||||
import { WorkflowRepository } from 'src/repositories/workflow.repository.js';
|
||||
import { DB } from 'src/schema/index.js';
|
||||
@@ -77,6 +80,7 @@ import { AssetMetadataTable } from 'src/schema/tables/asset-metadata.table.js';
|
||||
import { AssetTable } from 'src/schema/tables/asset.table.js';
|
||||
import { FaceSearchTable } from 'src/schema/tables/face-search.table.js';
|
||||
import { MemoryTable } from 'src/schema/tables/memory.table.js';
|
||||
import { PasskeyTable } from 'src/schema/tables/passkey.table.js';
|
||||
import { PersonUserTable } from 'src/schema/tables/person-user.table.js';
|
||||
import { PersonTable } from 'src/schema/tables/person.table.js';
|
||||
import { SessionTable } from 'src/schema/tables/session.table.js';
|
||||
@@ -177,6 +181,12 @@ export class MediumTestContext<S extends ClassConstructor<typeof BaseService> =
|
||||
return { user, result };
|
||||
}
|
||||
|
||||
async newPasskey(dto: Partial<Insertable<PasskeyTable>> & { userId: string }) {
|
||||
const passkey = mediumFactory.passkeyInsert(dto);
|
||||
const result = await this.get(PasskeyRepository).create(passkey);
|
||||
return { passkey, result };
|
||||
}
|
||||
|
||||
async newPartner(dto: { sharedById: string; sharedWithId: string; inTimeline?: boolean }) {
|
||||
const partner = { inTimeline: true, ...dto };
|
||||
const result = await this.get(PartnerRepository).create(partner);
|
||||
@@ -494,7 +504,9 @@ const newRealRepository = <T extends BaseServiceDeps[number]>(key: T, db: Kysely
|
||||
case LibraryRepository:
|
||||
case NotificationRepository:
|
||||
case OcrRepository:
|
||||
case AuthChallengeRepository:
|
||||
case PartnerRepository:
|
||||
case PasskeyRepository:
|
||||
case PersonRepository:
|
||||
case PersonUserRepository:
|
||||
case SearchRepository:
|
||||
@@ -512,7 +524,8 @@ const newRealRepository = <T extends BaseServiceDeps[number]>(key: T, db: Kysely
|
||||
}
|
||||
|
||||
case ConfigRepository:
|
||||
case CryptoRepository: {
|
||||
case CryptoRepository:
|
||||
case WebAuthnRepository: {
|
||||
return new key() as InstanceType<T>;
|
||||
}
|
||||
|
||||
@@ -557,6 +570,7 @@ const newMockRepository = <T>(key: ClassConstructor<T>) => {
|
||||
case AlbumRepository:
|
||||
case AssetRepository:
|
||||
case AssetJobRepository:
|
||||
case AuthChallengeRepository:
|
||||
case ConfigRepository:
|
||||
case CryptoRepository:
|
||||
case LibraryRepository:
|
||||
@@ -565,6 +579,7 @@ const newMockRepository = <T>(key: ClassConstructor<T>) => {
|
||||
case NotificationRepository:
|
||||
case OcrRepository:
|
||||
case PartnerRepository:
|
||||
case PasskeyRepository:
|
||||
case PersonRepository:
|
||||
case SessionRepository:
|
||||
case SyncRepository:
|
||||
@@ -779,6 +794,30 @@ const sessionInsert = ({
|
||||
};
|
||||
};
|
||||
|
||||
const passkeyInsert = ({
|
||||
id = newUuid(),
|
||||
userId,
|
||||
...passkey
|
||||
}: Partial<Insertable<PasskeyTable>> & { userId: string }) => {
|
||||
const defaults: Insertable<PasskeyTable> = {
|
||||
id,
|
||||
userId,
|
||||
name: 'Passkey',
|
||||
credentialId: `credential-${id}`,
|
||||
publicKey: Buffer.from('public-key'),
|
||||
counter: 0,
|
||||
transports: ['internal'],
|
||||
backedUp: false,
|
||||
deviceType: 'singleDevice',
|
||||
};
|
||||
|
||||
return {
|
||||
...defaults,
|
||||
...passkey,
|
||||
id,
|
||||
};
|
||||
};
|
||||
|
||||
const userInsert = (user: Partial<Insertable<UserTable>> & { clusterGroupId: string }) => {
|
||||
const id = user.id || newUuid();
|
||||
|
||||
@@ -899,6 +938,7 @@ export const mediumFactory = {
|
||||
faceInsert,
|
||||
personInsert,
|
||||
personUserInsert,
|
||||
passkeyInsert,
|
||||
sessionInsert,
|
||||
syncStream,
|
||||
userInsert,
|
||||
|
||||
@@ -0,0 +1,308 @@
|
||||
import { Kysely } from 'kysely';
|
||||
import { AuthChallengeType, SystemMetadataKey } from 'src/enum.js';
|
||||
import { AccessRepository } from 'src/repositories/access.repository.js';
|
||||
import { AuthChallengeRepository } from 'src/repositories/auth-challenge.repository.js';
|
||||
import { ClusterGroupRepository } from 'src/repositories/cluster-group.repository.js';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository.js';
|
||||
import { CryptoRepository } from 'src/repositories/crypto.repository.js';
|
||||
import { DatabaseRepository } from 'src/repositories/database.repository.js';
|
||||
import { EventRepository } from 'src/repositories/event.repository.js';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository.js';
|
||||
import { PasskeyRepository } from 'src/repositories/passkey.repository.js';
|
||||
import { SessionRepository } from 'src/repositories/session.repository.js';
|
||||
import { StorageRepository } from 'src/repositories/storage.repository.js';
|
||||
import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository.js';
|
||||
import { TelemetryRepository } from 'src/repositories/telemetry.repository.js';
|
||||
import { UserRepository } from 'src/repositories/user.repository.js';
|
||||
import { WebAuthnRepository } from 'src/repositories/webauthn.repository.js';
|
||||
import { DB } from 'src/schema/index.js';
|
||||
import { PasskeyService } from 'src/services/passkey.service.js';
|
||||
import { mediumFactory, newMediumService } from 'test/medium.factory.js';
|
||||
import { factory } from 'test/small.factory.js';
|
||||
import { getKyselyDB } from 'test/utils.js';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
const origin = 'https://immich.example';
|
||||
const headers = { origin };
|
||||
|
||||
const registrationResponse = {
|
||||
id: 'credential-id',
|
||||
rawId: 'credential-id',
|
||||
type: 'public-key',
|
||||
clientExtensionResults: {},
|
||||
response: {
|
||||
clientDataJSON: 'client-data',
|
||||
attestationObject: 'attestation-object',
|
||||
},
|
||||
};
|
||||
|
||||
const authenticationResponse = {
|
||||
id: 'credential-id',
|
||||
rawId: 'credential-id',
|
||||
type: 'public-key',
|
||||
clientExtensionResults: {},
|
||||
response: {
|
||||
clientDataJSON: 'client-data',
|
||||
authenticatorData: 'authenticator-data',
|
||||
signature: 'signature',
|
||||
},
|
||||
};
|
||||
|
||||
const setup = (db?: Kysely<DB>) => {
|
||||
return newMediumService(PasskeyService, {
|
||||
database: db || defaultDatabase,
|
||||
real: [
|
||||
AccessRepository,
|
||||
AuthChallengeRepository,
|
||||
ClusterGroupRepository,
|
||||
ConfigRepository,
|
||||
CryptoRepository,
|
||||
DatabaseRepository,
|
||||
PasskeyRepository,
|
||||
SessionRepository,
|
||||
SystemMetadataRepository,
|
||||
UserRepository,
|
||||
WebAuthnRepository,
|
||||
],
|
||||
mock: [LoggingRepository, StorageRepository, EventRepository, TelemetryRepository],
|
||||
});
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
defaultDatabase = await getKyselyDB();
|
||||
const { ctx } = setup();
|
||||
await ctx.get(SystemMetadataRepository).set(SystemMetadataKey.SystemConfig, {
|
||||
passkey: { enabled: true, domain: 'https://immich.example', additionalDomains: [] },
|
||||
});
|
||||
});
|
||||
|
||||
describe(PasskeyService.name, () => {
|
||||
describe('startRegistration', () => {
|
||||
it('should generate registration options', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
|
||||
await expect(sut.startRegistration(factory.auth({ user, session: {} }), headers)).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
challenge: expect.any(String),
|
||||
rp: { id: 'immich.example', name: 'Immich' },
|
||||
user: expect.objectContaining({ name: user.email, displayName: user.name }),
|
||||
excludeCredentials: [],
|
||||
authenticatorSelection: expect.objectContaining({ residentKey: 'required', userVerification: 'required' }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should store a single-use challenge for the user', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
|
||||
const { challenge } = await sut.startRegistration(factory.auth({ user, session: {} }), headers);
|
||||
const repository = ctx.get(AuthChallengeRepository);
|
||||
|
||||
await expect(
|
||||
repository.consume({ type: AuthChallengeType.PasskeyAuthentication, challenge }),
|
||||
).resolves.toBeUndefined();
|
||||
await expect(repository.consume({ type: AuthChallengeType.PasskeyRegistration, challenge })).resolves.toEqual({
|
||||
id: expect.any(String),
|
||||
userId: user.id,
|
||||
});
|
||||
await expect(
|
||||
repository.consume({ type: AuthChallengeType.PasskeyRegistration, challenge }),
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('should exclude the existing passkeys of the user', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const { passkey } = await ctx.newPasskey({ userId: user.id });
|
||||
|
||||
await expect(sut.startRegistration(factory.auth({ user, session: {} }), headers)).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
excludeCredentials: [{ id: passkey.credentialId, type: 'public-key', transports: passkey.transports }],
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('finishRegistration', () => {
|
||||
it('should reject an invalid response', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
|
||||
await expect(
|
||||
sut.finishRegistration(
|
||||
factory.auth({ user, session: {} }),
|
||||
{ response: registrationResponse },
|
||||
headers,
|
||||
mediumFactory.loginDetails(),
|
||||
),
|
||||
).rejects.toThrow('Passkey registration failed');
|
||||
|
||||
await expect(ctx.get(PasskeyRepository).search({ userId: user.id })).resolves.toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('startAuthentication', () => {
|
||||
it('should store a single-use challenge without a user', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
|
||||
const { challenge } = await sut.startAuthentication(headers);
|
||||
const repository = ctx.get(AuthChallengeRepository);
|
||||
|
||||
await expect(repository.consume({ type: AuthChallengeType.PasskeyAuthentication, challenge })).resolves.toEqual({
|
||||
id: expect.any(String),
|
||||
userId: null,
|
||||
});
|
||||
await expect(
|
||||
repository.consume({ type: AuthChallengeType.PasskeyAuthentication, challenge }),
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
it('should generate authentication options', async () => {
|
||||
const { sut } = setup();
|
||||
|
||||
await expect(sut.startAuthentication(headers)).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
challenge: expect.any(String),
|
||||
rpId: 'immich.example',
|
||||
userVerification: 'required',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('finishAuthentication', () => {
|
||||
it('should reject an unknown credential', async () => {
|
||||
const { sut } = setup();
|
||||
|
||||
await expect(
|
||||
sut.finishAuthentication({ response: authenticationResponse }, headers, mediumFactory.loginDetails()),
|
||||
).rejects.toThrow('Invalid passkey');
|
||||
});
|
||||
|
||||
it('should reject an invalid response for a known credential', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const { passkey } = await ctx.newPasskey({ userId: user.id, credentialId: authenticationResponse.id });
|
||||
|
||||
await expect(
|
||||
sut.finishAuthentication({ response: authenticationResponse }, headers, mediumFactory.loginDetails()),
|
||||
).rejects.toThrow('Invalid passkey');
|
||||
|
||||
await expect(ctx.get(PasskeyRepository).get(passkey.id)).resolves.toEqual(
|
||||
expect.objectContaining({ usedAt: null }),
|
||||
);
|
||||
await expect(ctx.get(SessionRepository).getByUserId(user.id)).resolves.toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('search', () => {
|
||||
it('should return an empty list', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
|
||||
await expect(sut.search(factory.auth({ user }), {})).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it('should only return the passkeys of the current user', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const { user: otherUser } = await ctx.newUser();
|
||||
const { passkey } = await ctx.newPasskey({ userId: user.id });
|
||||
await ctx.newPasskey({ userId: otherUser.id });
|
||||
|
||||
await expect(sut.search(factory.auth({ user }), {})).resolves.toEqual([
|
||||
expect.objectContaining({ id: passkey.id, name: passkey.name, usedAt: null }),
|
||||
]);
|
||||
});
|
||||
|
||||
it('should filter by id', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const { passkey } = await ctx.newPasskey({ userId: user.id });
|
||||
await ctx.newPasskey({ userId: user.id });
|
||||
|
||||
await expect(sut.search(factory.auth({ user }), { id: passkey.id })).resolves.toEqual([
|
||||
expect.objectContaining({ id: passkey.id }),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('get', () => {
|
||||
it('should reject the passkey of another user', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const { user: otherUser } = await ctx.newUser();
|
||||
const { passkey } = await ctx.newPasskey({ userId: otherUser.id });
|
||||
|
||||
await expect(sut.get(factory.auth({ user }), passkey.id)).rejects.toThrow('Not found or no passkey.read access');
|
||||
});
|
||||
|
||||
it('should get a passkey', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const { passkey } = await ctx.newPasskey({ userId: user.id });
|
||||
|
||||
await expect(sut.get(factory.auth({ user }), passkey.id)).resolves.toEqual({
|
||||
id: passkey.id,
|
||||
name: passkey.name,
|
||||
createdAt: expect.any(Date),
|
||||
updatedAt: expect.any(Date),
|
||||
usedAt: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('should reject the passkey of another user', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const { user: otherUser } = await ctx.newUser();
|
||||
const { passkey } = await ctx.newPasskey({ userId: otherUser.id });
|
||||
|
||||
await expect(sut.update(factory.auth({ user }), passkey.id, { name: 'Renamed' })).rejects.toThrow(
|
||||
'Not found or no passkey.update access',
|
||||
);
|
||||
});
|
||||
|
||||
it('should rename a passkey', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const { passkey } = await ctx.newPasskey({ userId: user.id });
|
||||
|
||||
await expect(sut.update(factory.auth({ user }), passkey.id, { name: 'Renamed' })).resolves.toEqual(
|
||||
expect.objectContaining({ id: passkey.id, name: 'Renamed' }),
|
||||
);
|
||||
await expect(ctx.get(PasskeyRepository).get(passkey.id)).resolves.toEqual(
|
||||
expect.objectContaining({ name: 'Renamed' }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
it('should reject the passkey of another user', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const { user: otherUser } = await ctx.newUser();
|
||||
const { passkey } = await ctx.newPasskey({ userId: otherUser.id });
|
||||
|
||||
await expect(sut.delete(factory.auth({ user }), passkey.id)).rejects.toThrow(
|
||||
'Not found or no passkey.delete access',
|
||||
);
|
||||
await expect(ctx.get(PasskeyRepository).get(passkey.id)).resolves.toEqual(
|
||||
expect.objectContaining({ id: passkey.id }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should delete a passkey', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const { passkey } = await ctx.newPasskey({ userId: user.id });
|
||||
|
||||
await sut.delete(factory.auth({ user }), passkey.id);
|
||||
|
||||
await expect(ctx.get(PasskeyRepository).get(passkey.id)).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -68,6 +68,10 @@ export const newAccessRepositoryMock = (): IAccessRepositoryMock => {
|
||||
checkUpdateAccess: vitest.fn().mockResolvedValue(new Set()),
|
||||
},
|
||||
|
||||
passkey: {
|
||||
checkOwnerAccess: vitest.fn().mockResolvedValue(new Set()),
|
||||
},
|
||||
|
||||
session: {
|
||||
checkOwnerAccess: vitest.fn().mockResolvedValue(new Set()),
|
||||
},
|
||||
|
||||
@@ -29,6 +29,7 @@ import { AssetEditRepository } from 'src/repositories/asset-edit.repository.js';
|
||||
import { AssetFileRepository } from 'src/repositories/asset-file.repository.js';
|
||||
import { AssetJobRepository } from 'src/repositories/asset-job.repository.js';
|
||||
import { AssetRepository } from 'src/repositories/asset.repository.js';
|
||||
import { AuthChallengeRepository } from 'src/repositories/auth-challenge.repository.js';
|
||||
import { ClusterGroupRepository } from 'src/repositories/cluster-group.repository.js';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository.js';
|
||||
import { CronRepository } from 'src/repositories/cron.repository.js';
|
||||
@@ -52,6 +53,7 @@ import { NotificationRepository } from 'src/repositories/notification.repository
|
||||
import { OAuthRepository } from 'src/repositories/oauth.repository.js';
|
||||
import { OcrRepository } from 'src/repositories/ocr.repository.js';
|
||||
import { PartnerRepository } from 'src/repositories/partner.repository.js';
|
||||
import { PasskeyRepository } from 'src/repositories/passkey.repository.js';
|
||||
import { PersonUserRepository } from 'src/repositories/person-user.repository.js';
|
||||
import { PersonRepository } from 'src/repositories/person.repository.js';
|
||||
import { PluginRepository } from 'src/repositories/plugin.repository.js';
|
||||
@@ -73,6 +75,7 @@ import { UserRepository } from 'src/repositories/user.repository.js';
|
||||
import { VersionHistoryRepository } from 'src/repositories/version-history.repository.js';
|
||||
import { VideoStreamRepository } from 'src/repositories/video-stream.repository.js';
|
||||
import { ViewRepository } from 'src/repositories/view-repository.js';
|
||||
import { WebAuthnRepository } from 'src/repositories/webauthn.repository.js';
|
||||
import { WebsocketRepository } from 'src/repositories/websocket.repository.js';
|
||||
import { WorkflowRepository } from 'src/repositories/workflow.repository.js';
|
||||
import { DB } from 'src/schema/index.js';
|
||||
@@ -246,6 +249,7 @@ export type ServiceOverrides = {
|
||||
assetEdit: AssetEditRepository;
|
||||
assetFile: AssetFileRepository;
|
||||
assetJob: AssetJobRepository;
|
||||
authChallenge: AuthChallengeRepository;
|
||||
clusterGroup: ClusterGroupRepository;
|
||||
config: ConfigRepository;
|
||||
cron: CronRepository;
|
||||
@@ -269,6 +273,7 @@ export type ServiceOverrides = {
|
||||
ocr: OcrRepository;
|
||||
oauth: OAuthRepository;
|
||||
partner: PartnerRepository;
|
||||
passkey: PasskeyRepository;
|
||||
person: PersonRepository;
|
||||
personUser: PersonUserRepository;
|
||||
plugin: PluginRepository;
|
||||
@@ -290,6 +295,7 @@ export type ServiceOverrides = {
|
||||
versionHistory: VersionHistoryRepository;
|
||||
videoStream: VideoStreamRepository;
|
||||
view: ViewRepository;
|
||||
webAuthn: WebAuthnRepository;
|
||||
websocket: WebsocketRepository;
|
||||
workflow: WorkflowRepository;
|
||||
};
|
||||
@@ -333,6 +339,7 @@ export const getMocks = () => {
|
||||
assetEdit: automock(AssetEditRepository),
|
||||
assetFile: automock(AssetFileRepository),
|
||||
assetJob: automock(AssetJobRepository),
|
||||
authChallenge: automock(AuthChallengeRepository),
|
||||
clusterGroup: automock(ClusterGroupRepository),
|
||||
app: automock(AppRepository, { strict: false }),
|
||||
config: newConfigRepositoryMock(),
|
||||
@@ -356,6 +363,7 @@ export const getMocks = () => {
|
||||
ocr: automock(OcrRepository, { strict: false }),
|
||||
oauth: automock(OAuthRepository, { args: [loggerMock] }),
|
||||
partner: automock(PartnerRepository, { strict: false }),
|
||||
passkey: automock(PasskeyRepository),
|
||||
person: automock(PersonRepository, { strict: false }),
|
||||
personUser: automock(PersonUserRepository, { strict: false }),
|
||||
plugin: automock(PluginRepository, { strict: true, args: [databaseMock, loggerMock] }),
|
||||
@@ -380,6 +388,7 @@ export const getMocks = () => {
|
||||
versionHistory: automock(VersionHistoryRepository),
|
||||
videoStream: automock(VideoStreamRepository, { strict: false }),
|
||||
view: automock(ViewRepository),
|
||||
webAuthn: automock(WebAuthnRepository),
|
||||
// eslint-disable-next-line no-sparse-arrays
|
||||
websocket: automock(WebsocketRepository, { args: [, loggerMock], strict: false }),
|
||||
workflow: automock(WorkflowRepository, { strict: true }),
|
||||
@@ -409,6 +418,7 @@ export const newTestService = <T extends BaseService>(
|
||||
overrides.assetEdit || (mocks.assetEdit as As<AssetEditRepository>),
|
||||
overrides.assetFile || (mocks.assetFile as As<AssetFileRepository>),
|
||||
overrides.assetJob || (mocks.assetJob as As<AssetJobRepository>),
|
||||
overrides.authChallenge || (mocks.authChallenge as As<AuthChallengeRepository>),
|
||||
overrides.clusterGroup || (mocks.clusterGroup as As<ClusterGroupRepository>),
|
||||
overrides.config || (mocks.config as As<ConfigRepository> as ConfigRepository),
|
||||
overrides.cron || (mocks.cron as As<CronRepository>),
|
||||
@@ -431,6 +441,7 @@ export const newTestService = <T extends BaseService>(
|
||||
overrides.oauth || (mocks.oauth as As<OAuthRepository>),
|
||||
overrides.ocr || (mocks.ocr as As<OcrRepository>),
|
||||
overrides.partner || (mocks.partner as As<PartnerRepository>),
|
||||
overrides.passkey || (mocks.passkey as As<PasskeyRepository>),
|
||||
overrides.person || (mocks.person as As<PersonRepository>),
|
||||
overrides.personUser || (mocks.personUser as As<PersonUserRepository>),
|
||||
overrides.plugin || (mocks.plugin as As<PluginRepository>),
|
||||
@@ -452,6 +463,7 @@ export const newTestService = <T extends BaseService>(
|
||||
overrides.versionHistory || (mocks.versionHistory as As<VersionHistoryRepository>),
|
||||
overrides.videoStream || (mocks.videoStream as As<VideoStreamRepository>),
|
||||
overrides.view || (mocks.view as As<ViewRepository>),
|
||||
overrides.webAuthn || (mocks.webAuthn as As<WebAuthnRepository>),
|
||||
overrides.websocket || (mocks.websocket as As<WebsocketRepository>),
|
||||
overrides.workflow || (mocks.workflow as As<WorkflowRepository>),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user