chore: transition to nullable user.oauthId (#31560)

This commit is contained in:
Jason Rasmussen
2026-09-15 09:33:15 -04:00
committed by GitHub
parent 76e8513316
commit 99fd561144
25 changed files with 244 additions and 45 deletions
@@ -4,10 +4,13 @@ enum ServerCapability {
// Feature Support
cloudIdMetadata(SemVer(major: 2, minor: 4, patch: 0)),
bulkCloudIdMetadata(SemVer(major: 2, minor: 5, patch: 0)),
assetEdits(SemVer(major: 2, minor: 6, patch: 0)),
assetFacesV2(SemVer(major: 2, minor: 6, patch: 0)),
// Sync
syncV2(SemVer(major: 3, minor: 0, patch: 0)),
assetOcr(SemVer(major: 3, minor: 0, patch: 0)),
syncAssetEditsV1(SemVer(major: 2, minor: 6, patch: 0)),
syncAssetFacesV2(SemVer(major: 2, minor: 6, patch: 0)),
syncAssetOcrV1(SemVer(major: 3, minor: 0, patch: 0)),
syncAuthUsersV2(SemVer(major: 3, minor: 3, patch: 0)),
// Migrations
assetPayloadChange20260128(SemVer(major: 2, minor: 5, patch: 0)),
@@ -194,6 +194,8 @@ class SyncStreamService {
switch (type) {
case SyncEntityType.authUserV1:
return _syncStreamRepository.updateAuthUsersV1(data.cast());
case SyncEntityType.authUserV2:
return _syncStreamRepository.updateAuthUsersV2(data.cast());
case SyncEntityType.userV1:
return _syncStreamRepository.updateUsersV1(data.cast());
case SyncEntityType.userDeleteV1:
@@ -43,11 +43,11 @@ class SyncApiRepository {
request.body = jsonEncode(
SyncStreamDto(
types: [
SyncRequestType.authUsersV1,
serverVersion.supports(.syncAuthUsersV2) ? SyncRequestType.authUsersV2 : SyncRequestType.authUsersV1,
SyncRequestType.usersV1,
serverVersion.supports(.syncV2) ? SyncRequestType.assetsV2 : SyncRequestType.assetsV1,
SyncRequestType.assetExifsV1,
if (serverVersion.supports(.assetEdits)) SyncRequestType.assetEditsV1,
if (serverVersion.supports(.syncAssetEditsV1)) SyncRequestType.assetEditsV1,
SyncRequestType.assetMetadataV1,
SyncRequestType.partnersV1,
serverVersion.supports(.syncV2) ? SyncRequestType.partnerAssetsV2 : SyncRequestType.partnerAssetsV1,
@@ -63,8 +63,8 @@ class SyncApiRepository {
SyncRequestType.partnerStacksV1,
SyncRequestType.userMetadataV1,
SyncRequestType.peopleV1,
serverVersion.supports(.assetFacesV2) ? SyncRequestType.assetFacesV2 : SyncRequestType.assetFacesV1,
if (serverVersion.supports(.assetOcr)) SyncRequestType.assetOcrV1,
serverVersion.supports(.syncAssetFacesV2) ? SyncRequestType.assetFacesV2 : SyncRequestType.assetFacesV1,
if (serverVersion.supports(.syncAssetOcrV1)) SyncRequestType.assetOcrV1,
],
).toJson(),
);
@@ -140,6 +140,7 @@ class SyncApiRepository {
const _kResponseMap = <SyncEntityType, Function(Object)>{
SyncEntityType.authUserV1: SyncAuthUserV1.fromJson,
SyncEntityType.authUserV2: SyncAuthUserV2.fromJson,
SyncEntityType.userV1: SyncUserV1.fromJson,
SyncEntityType.userDeleteV1: SyncUserDeleteV1.fromJson,
SyncEntityType.partnerV1: SyncPartnerV1.fromJson,
@@ -116,6 +116,35 @@ class SyncStreamRepository extends DatabaseAccessor<Drift> with $SyncStreamRepos
}
}
Future<void> updateAuthUsersV2(Iterable<SyncAuthUserV2> data) async {
try {
await _db.batch((batch) {
for (final user in data) {
final companion = AuthUserEntityCompanion(
name: Value(user.name),
email: Value(user.email),
hasProfileImage: Value(user.hasProfileImage),
profileChangedAt: Value(user.profileChangedAt),
avatarColor: Value(user.avatarColor.orElse(null)?.toAvatarColor() ?? AvatarColor.primary),
isAdmin: Value(user.isAdmin),
pinCode: Value(user.pinCode),
quotaSizeInBytes: Value(user.quotaSizeInBytes ?? 0),
quotaUsageInBytes: Value(user.quotaUsageInBytes),
);
batch.insert(
_db.authUserEntity,
companion.copyWith(id: Value(user.id)),
onConflict: DoUpdate((_) => companion),
);
}
});
} catch (error, stack) {
_logger.severe('Error: SyncAuthUserV2', error, stack);
rethrow;
}
}
Future<void> deleteUsersV1(Iterable<SyncUserDeleteV1> data) async {
try {
await _db.batch((batch) {
@@ -20,7 +20,7 @@ import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/utils/error_handler.dart';
final _stateProvider = Provider.family.autoDispose<RemoteAsset?, ActionSource>((ref, source) {
final isSupported = ref.watch(serverInfoProvider.select((state) => state.serverVersion.supports(.assetEdits)));
final isSupported = ref.watch(serverInfoProvider.select((state) => state.serverVersion.supports(.syncAssetEditsV1)));
if (!isSupported) {
return null;
}
+94
View File
@@ -30710,6 +30710,98 @@
],
"type": "object"
},
"SyncAuthUserV2": {
"properties": {
"avatarColor": {
"allOf": [
{
"$ref": "#/components/schemas/UserAvatarColor"
}
],
"nullable": true
},
"deletedAt": {
"description": "User deleted at",
"example": "2024-01-01T00:00:00.000Z",
"format": "date-time",
"nullable": true,
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$",
"type": "string"
},
"email": {
"description": "User email",
"type": "string"
},
"hasProfileImage": {
"description": "User has profile image",
"type": "boolean"
},
"id": {
"description": "User ID",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$",
"type": "string"
},
"isAdmin": {
"description": "User is admin",
"type": "boolean"
},
"name": {
"description": "User name",
"type": "string"
},
"oauthId": {
"description": "User OAuth ID",
"nullable": true,
"type": "string"
},
"pinCode": {
"description": "User pin code",
"nullable": true,
"type": "string"
},
"profileChangedAt": {
"description": "User profile changed at",
"example": "2024-01-01T00:00:00.000Z",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$",
"type": "string"
},
"quotaSizeInBytes": {
"description": "Quota size in bytes",
"maximum": 9007199254740991,
"minimum": -9007199254740991,
"nullable": true,
"type": "integer"
},
"quotaUsageInBytes": {
"description": "Quota usage in bytes",
"maximum": 9007199254740991,
"minimum": -9007199254740991,
"type": "integer"
},
"storageLabel": {
"description": "User storage label",
"nullable": true,
"type": "string"
}
},
"required": [
"deletedAt",
"email",
"hasProfileImage",
"id",
"isAdmin",
"name",
"oauthId",
"pinCode",
"profileChangedAt",
"quotaSizeInBytes",
"quotaUsageInBytes",
"storageLabel"
],
"type": "object"
},
"SyncCompleteV1": {
"properties": {},
"type": "object"
@@ -30718,6 +30810,7 @@
"description": "Sync entity type",
"enum": [
"AuthUserV1",
"AuthUserV2",
"UserV1",
"UserDeleteV1",
"AssetV1",
@@ -31082,6 +31175,7 @@
"AssetMetadataV1",
"AssetOcrV1",
"AuthUsersV1",
"AuthUsersV2",
"MemoriesV1",
"MemoryToAssetsV1",
"PartnersV1",
+29
View File
@@ -3594,6 +3594,33 @@ export type SyncAuthUserV1 = {
/** User storage label */
storageLabel: string | null;
};
export type SyncAuthUserV2 = {
avatarColor?: (UserAvatarColor) | null;
/** User deleted at */
deletedAt: string | null;
/** User email */
email: string;
/** User has profile image */
hasProfileImage: boolean;
/** User ID */
id: string;
/** User is admin */
isAdmin: boolean;
/** User name */
name: string;
/** User OAuth ID */
oauthId: string | null;
/** User pin code */
pinCode: string | null;
/** User profile changed at */
profileChangedAt: string;
/** Quota size in bytes */
quotaSizeInBytes: number | null;
/** Quota usage in bytes */
quotaUsageInBytes: number;
/** User storage label */
storageLabel: string | null;
};
export type SyncCompleteV1 = {};
export type SyncMemoryAssetDeleteV1 = {
/** Asset ID */
@@ -8306,6 +8333,7 @@ export enum AssetIdErrorReason {
}
export enum SyncEntityType {
AuthUserV1 = "AuthUserV1",
AuthUserV2 = "AuthUserV2",
UserV1 = "UserV1",
UserDeleteV1 = "UserDeleteV1",
AssetV1 = "AssetV1",
@@ -8380,6 +8408,7 @@ export enum SyncRequestType {
AssetMetadataV1 = "AssetMetadataV1",
AssetOcrV1 = "AssetOcrV1",
AuthUsersV1 = "AuthUsersV1",
AuthUsersV2 = "AuthUsersV2",
MemoriesV1 = "MemoriesV1",
MemoryToAssetsV1 = "MemoryToAssetsV1",
PartnersV1 = "PartnersV1",
+1 -1
View File
@@ -140,7 +140,7 @@ export type UserAdmin = User & {
createdAt: Date;
updatedAt: Date;
deletedAt: Date | null;
oauthId: string;
oauthId: string | null;
quotaSizeInBytes: number | null;
quotaUsageInBytes: number;
status: UserStatus;
+7
View File
@@ -40,6 +40,10 @@ const SyncAuthUserV1Schema = SyncUserV1Schema.merge(
}),
).meta({ id: 'SyncAuthUserV1' });
const SyncAuthUserV2Schema = SyncAuthUserV1Schema.extend({
oauthId: z.string().nullable().describe('User OAuth ID'),
}).meta({ id: 'SyncAuthUserV2' });
const SyncUserDeleteV1Schema = z.object({ userId: z.uuidv4().describe('User ID') }).meta({ id: 'SyncUserDeleteV1' });
const SyncPartnerV1Schema = z
@@ -112,6 +116,8 @@ class SyncUserV1 extends createZodDto(SyncUserV1Schema) {}
@ExtraModel()
class SyncAuthUserV1 extends createZodDto(SyncAuthUserV1Schema) {}
@ExtraModel()
class SyncAuthUserV2 extends createZodDto(SyncAuthUserV2Schema) {}
@ExtraModel()
class SyncUserDeleteV1 extends createZodDto(SyncUserDeleteV1Schema) {}
@ExtraModel()
class SyncPartnerV1 extends createZodDto(SyncPartnerV1Schema) {}
@@ -463,6 +469,7 @@ class SyncCompleteV1 extends createZodDto(SyncCompleteV1Schema) {}
export type SyncItem = {
[SyncEntityType.AuthUserV1]: SyncAuthUserV1;
[SyncEntityType.AuthUserV2]: SyncAuthUserV2;
[SyncEntityType.UserV1]: SyncUserV1;
[SyncEntityType.UserDeleteV1]: SyncUserDeleteV1;
[SyncEntityType.PartnerV1]: SyncPartnerV1;
+2 -1
View File
@@ -151,7 +151,8 @@ export function mapUserAdmin(entity: UserAdmin): UserAdminResponseDto {
createdAt: entity.createdAt,
deletedAt: entity.deletedAt,
updatedAt: entity.updatedAt,
oauthId: entity.oauthId,
// TODO(v4): remove the mapping and make `oauthId` nullable
oauthId: entity.oauthId ?? '',
quotaSizeInBytes: entity.quotaSizeInBytes,
quotaUsageInBytes: entity.quotaUsageInBytes,
status: entity.status,
+2
View File
@@ -1024,6 +1024,7 @@ export enum SyncRequestType {
AssetMetadataV1 = 'AssetMetadataV1',
AssetOcrV1 = 'AssetOcrV1',
AuthUsersV1 = 'AuthUsersV1',
AuthUsersV2 = 'AuthUsersV2',
MemoriesV1 = 'MemoriesV1',
MemoryToAssetsV1 = 'MemoryToAssetsV1',
PartnersV1 = 'PartnersV1',
@@ -1048,6 +1049,7 @@ export const SyncRequestTypeSchema = z
export enum SyncEntityType {
AuthUserV1 = 'AuthUserV1',
AuthUserV2 = 'AuthUserV2',
UserV1 = 'UserV1',
UserDeleteV1 = 'UserDeleteV1',
+1 -1
View File
@@ -130,7 +130,7 @@ type UserEvent = {
isAdmin: boolean;
shouldChangePassword: boolean;
avatarColor: UserAvatarColor | null;
oauthId: string;
oauthId: string | null;
storageLabel: string | null;
quotaSizeInBytes: number | null;
quotaUsageInBytes: number;
@@ -0,0 +1,13 @@
import { Kysely, sql } from 'kysely';
export async function up(db: Kysely<any>): Promise<void> {
await sql`ALTER TABLE "user" ALTER COLUMN "oauthId" DROP NOT NULL;`.execute(db);
await sql`ALTER TABLE "user" ALTER COLUMN "oauthId" SET DEFAULT NULL;`.execute(db);
await sql`UPDATE "user" SET "oauthId" = NULL WHERE "oauthId" = '';`.execute(db);
}
export async function down(db: Kysely<any>): Promise<void> {
await sql`UPDATE "user" SET "oauthId" = '' WHERE "oauthId" IS NULL;`.execute(db);
await sql`ALTER TABLE "user" ALTER COLUMN "oauthId" SET DEFAULT '';`.execute(db);
await sql`ALTER TABLE "user" ALTER COLUMN "oauthId" SET NOT NULL;`.execute(db);
}
+1
View File
@@ -94,3 +94,4 @@
1786972746372-AssetOcrSyncReset
1787148183729-ClusterGroups
1787148183730-DeleteMismatchedMemoryAssets
1789419229196-ConvertUserOAuthIdEmptyStringToNull
+2 -2
View File
@@ -57,8 +57,8 @@ export class UserTable {
@DeleteDateColumn()
deletedAt!: Timestamp | null;
@Column({ default: '' })
oauthId!: Generated<string>;
@Column({ nullable: true })
oauthId!: string | null;
@UpdateDateColumn()
updatedAt!: Generated<Timestamp>;
+1 -2
View File
@@ -5,7 +5,6 @@ import { BaseService } from 'src/services/base.service.js';
@Injectable()
export class AuthAdminService extends BaseService {
async unlinkAll(_auth: AuthDto) {
// TODO replace '' with null
await this.userRepository.updateAll({ oauthId: '' });
await this.userRepository.updateAll({ oauthId: null });
}
}
+14 -10
View File
@@ -1057,12 +1057,13 @@ describe(AuthService.name, () => {
});
it('should not sync the profile picture if the user already has one', async () => {
const user = UserFactory.create({ oauthId: 'oauth-id', profileImagePath: 'not-empty' });
const oauthId = 'oauth-id';
const user = UserFactory.create({ oauthId, profileImagePath: 'not-empty' });
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.oauthEnabled);
mocks.oauth.getProfileAndOAuthSid.mockResolvedValue({
profile: OAuthProfileFactory.create({
sub: user.oauthId,
sub: oauthId,
email: user.email,
picture: 'https://auth.immich.cloud/profiles/1.jpg',
}),
@@ -1181,11 +1182,12 @@ describe(AuthService.name, () => {
});
it('should promote an existing user to admin if the role claim contains admin on login', async () => {
const user = UserFactory.create({ isAdmin: false, oauthId: 'oauth-id' });
const oauthId = 'oauth-id';
const user = UserFactory.create({ isAdmin: false, oauthId });
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.oauthEnabled);
mocks.oauth.getProfileAndOAuthSid.mockResolvedValue({
profile: OAuthProfileFactory.create({ sub: user.oauthId, immich_role: 'admin' }),
profile: OAuthProfileFactory.create({ sub: oauthId, immich_role: 'admin' }),
});
mocks.user.getByOAuthId.mockResolvedValue(user);
mocks.user.update.mockResolvedValue({ ...user, isAdmin: true });
@@ -1201,11 +1203,12 @@ describe(AuthService.name, () => {
});
it('should demote an existing admin if the role claim only contains user on login', async () => {
const user = UserFactory.create({ isAdmin: true, oauthId: 'oauth-id' });
const oauthId = 'oauth-id';
const user = UserFactory.create({ isAdmin: true, oauthId });
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.oauthEnabled);
mocks.oauth.getProfileAndOAuthSid.mockResolvedValue({
profile: OAuthProfileFactory.create({ sub: user.oauthId, immich_role: ['user'] }),
profile: OAuthProfileFactory.create({ sub: oauthId, immich_role: ['user'] }),
});
mocks.user.getByOAuthId.mockResolvedValue(user);
mocks.user.update.mockResolvedValue({ ...user, isAdmin: false });
@@ -1221,11 +1224,12 @@ describe(AuthService.name, () => {
});
it('should not change isAdmin for an existing user if the role claim is blank', async () => {
const user = UserFactory.create({ isAdmin: true, oauthId: 'oauth-id' });
const oauthId = 'oauth-id';
const user = UserFactory.create({ isAdmin: true, oauthId });
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.oauthEnabled);
mocks.oauth.getProfileAndOAuthSid.mockResolvedValue({
profile: OAuthProfileFactory.create({ sub: user.oauthId }),
profile: OAuthProfileFactory.create({ sub: oauthId }),
});
mocks.user.getByOAuthId.mockResolvedValue(user);
mocks.session.create.mockResolvedValue(SessionFactory.create());
@@ -1334,7 +1338,7 @@ describe(AuthService.name, () => {
await sut.unlink(auth);
expect(mocks.user.update).toHaveBeenCalledWith(auth.user.id, { oauthId: '' });
expect(mocks.user.update).toHaveBeenCalledWith(auth.user.id, { oauthId: null });
});
it('should unlink an account and remove the OAuth data from the session', async () => {
@@ -1349,7 +1353,7 @@ describe(AuthService.name, () => {
await sut.unlink(auth);
expect(mocks.session.update).toHaveBeenCalledWith(session.id, { oauthSid: null, oauthBearerToken: null });
expect(mocks.user.update).toHaveBeenCalledWith(auth.user.id, { oauthId: '' });
expect(mocks.user.update).toHaveBeenCalledWith(auth.user.id, { oauthId: null });
});
});
+1 -1
View File
@@ -441,7 +441,7 @@ export class AuthService extends BaseService {
await this.sessionRepository.update(auth.session.id, { oauthSid: null, oauthBearerToken: null });
}
const user = await this.userRepository.update(auth.user.id, { oauthId: '' });
const user = await this.userRepository.update(auth.user.id, { oauthId: null });
return mapUserAdmin(user);
}
+14
View File
@@ -63,6 +63,7 @@ const sendEntityBackfillCompleteAck = async (response: Writable, ackType: SyncEn
export const SYNC_TYPES_ORDER = [
SyncRequestType.AuthUsersV1,
SyncRequestType.AuthUsersV2,
SyncRequestType.UsersV1,
SyncRequestType.PartnersV1,
SyncRequestType.AssetsV1,
@@ -190,6 +191,7 @@ export class SyncService extends BaseService {
[SyncRequestType.AlbumAssetsV1]: () => this.syncAlbumAssetsV1(),
[SyncRequestType.AuthUsersV1]: () => this.syncAuthUsersV1(options, response, checkpointMap),
[SyncRequestType.AuthUsersV2]: () => this.syncAuthUsersV2(options, response, checkpointMap),
[SyncRequestType.UsersV1]: () => this.syncUsersV1(options, response, checkpointMap),
[SyncRequestType.PartnersV1]: () => this.syncPartnersV1(options, response, checkpointMap),
[SyncRequestType.AssetsV2]: () => this.syncAssetsV2(options, response, checkpointMap),
@@ -266,6 +268,18 @@ export class SyncService extends BaseService {
private async syncAuthUsersV1(options: SyncQueryOptions, response: Writable, checkpointMap: CheckpointMap) {
const upsertType = SyncEntityType.AuthUserV1;
const upserts = this.syncRepository.authUser.getUpserts({ ...options, ack: checkpointMap[upsertType] });
for await (const { updateId, profileImagePath, ...data } of upserts) {
await send(response, {
type: upsertType,
ids: [updateId],
data: { ...data, oauthId: data.oauthId ?? '', hasProfileImage: !!profileImagePath },
});
}
}
private async syncAuthUsersV2(options: SyncQueryOptions, response: Writable, checkpointMap: CheckpointMap) {
const upsertType = SyncEntityType.AuthUserV2;
const upserts = this.syncRepository.authUser.getUpserts({ ...options, ack: checkpointMap[upsertType] });
for await (const { updateId, profileImagePath, ...data } of upserts) {
await send(response, {
type: upsertType,
+1 -1
View File
@@ -27,7 +27,7 @@ export class UserFactory {
shouldChangePassword: false,
avatarColor: null,
deletedAt: null,
oauthId: '',
oauthId: null,
updatedAt: newDate(),
storageLabel: null,
name: 'Test User',
+2 -2
View File
@@ -11,7 +11,7 @@ export const userStub = {
name: 'admin_name',
id: 'admin_id',
storageLabel: 'admin',
oauthId: '',
oauthId: null,
shouldChangePassword: false,
avatarColor: null,
profileImagePath: '',
@@ -29,7 +29,7 @@ export const userStub = {
profileChangedAt: new Date('2021-01-01'),
name: 'immich_name',
storageLabel: null,
oauthId: '',
oauthId: null,
shouldChangePassword: false,
avatarColor: null,
profileImagePath: '',
+1 -1
View File
@@ -761,7 +761,7 @@ const userInsert = (user: Partial<Insertable<UserTable>> & { clusterGroupId: str
shouldChangePassword: true,
storageLabel: null,
pinCode: null,
oauthId: '',
oauthId: null,
avatarColor: null,
quotaSizeInBytes: null,
quotaUsageInBytes: 0,
@@ -31,7 +31,7 @@ describe(AuthAdminService.name, () => {
await expect(sut.unlinkAll(auth)).resolves.toBeUndefined();
await expect(userRepo.get(user.id, { withDeleted: true })).resolves.toEqual(
expect.objectContaining({ oauthId: '' }),
expect.objectContaining({ oauthId: null }),
);
});
@@ -43,7 +43,7 @@ describe(AuthAdminService.name, () => {
await expect(sut.unlinkAll(auth)).resolves.toBeUndefined();
await expect(userRepo.get(user.id, { withDeleted: true })).resolves.toEqual(
expect.objectContaining({ oauthId: '' }),
expect.objectContaining({ oauthId: null }),
);
});
@@ -56,10 +56,10 @@ describe(AuthAdminService.name, () => {
await expect(sut.unlinkAll(auth)).resolves.toBeUndefined();
await expect(userRepo.get(user1.id, { withDeleted: true })).resolves.toEqual(
expect.objectContaining({ oauthId: '' }),
expect.objectContaining({ oauthId: null }),
);
await expect(userRepo.get(user2.id, { withDeleted: true })).resolves.toEqual(
expect.objectContaining({ oauthId: '' }),
expect.objectContaining({ oauthId: null }),
);
});
});
@@ -17,11 +17,11 @@ beforeAll(async () => {
defaultDatabase = await getKyselyDB();
});
describe(SyncEntityType.AuthUserV1, () => {
describe(SyncEntityType.AuthUserV2, () => {
it('should detect and sync the first user', async () => {
const { auth, user, ctx } = await setup(await getKyselyDB());
const response = await ctx.syncStream(auth, [SyncRequestType.AuthUsersV1]);
const response = await ctx.syncStream(auth, [SyncRequestType.AuthUsersV2]);
expect(response).toEqual([
{
ack: expect.any(String),
@@ -40,13 +40,13 @@ describe(SyncEntityType.AuthUserV1, () => {
quotaUsageInBytes: user.quotaUsageInBytes,
storageLabel: user.storageLabel,
},
type: 'AuthUserV1',
type: 'AuthUserV2',
},
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]);
await ctx.syncAckAll(auth, response);
await ctx.assertSyncIsComplete(auth, [SyncRequestType.AuthUsersV1]);
await ctx.assertSyncIsComplete(auth, [SyncRequestType.AuthUsersV2]);
});
it('should sync a change and then another change to that same user', async () => {
@@ -54,7 +54,7 @@ describe(SyncEntityType.AuthUserV1, () => {
const userRepo = ctx.get(UserRepository);
const response = await ctx.syncStream(auth, [SyncRequestType.AuthUsersV1]);
const response = await ctx.syncStream(auth, [SyncRequestType.AuthUsersV2]);
expect(response).toEqual([
{
ack: expect.any(String),
@@ -62,7 +62,7 @@ describe(SyncEntityType.AuthUserV1, () => {
id: user.id,
isAdmin: false,
}),
type: 'AuthUserV1',
type: 'AuthUserV2',
},
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]);
@@ -71,7 +71,7 @@ describe(SyncEntityType.AuthUserV1, () => {
await userRepo.update(user.id, { isAdmin: true });
const newResponse = await ctx.syncStream(auth, [SyncRequestType.AuthUsersV1]);
const newResponse = await ctx.syncStream(auth, [SyncRequestType.AuthUsersV2]);
expect(newResponse).toEqual([
{
ack: expect.any(String),
@@ -79,7 +79,7 @@ describe(SyncEntityType.AuthUserV1, () => {
id: user.id,
isAdmin: true,
}),
type: 'AuthUserV1',
type: 'AuthUserV2',
},
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]);
@@ -90,7 +90,7 @@ describe(SyncEntityType.AuthUserV1, () => {
await ctx.newUser();
const response = await ctx.syncStream(auth, [SyncRequestType.AuthUsersV1]);
const response = await ctx.syncStream(auth, [SyncRequestType.AuthUsersV2]);
expect(response).toEqual([
{
ack: expect.any(String),
@@ -98,7 +98,7 @@ describe(SyncEntityType.AuthUserV1, () => {
id: user.id,
isAdmin: false,
}),
type: 'AuthUserV1',
type: 'AuthUserV2',
},
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]);
+1 -1
View File
@@ -120,7 +120,7 @@ const userAdminFactory = (user: Partial<UserAdmin> = {}) => {
createdAt = newDate(),
updatedAt = newDate(),
deletedAt = null,
oauthId = '',
oauthId = null,
quotaSizeInBytes = null,
quotaUsageInBytes = 0,
status = UserStatus.Active,