mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
feat(core): add DELETE /users/:userId/identities/:connectorId (#437)
* feat(core): add DELETE /users/:userId/identities/:connectorId * feat(core): add user query methods UT cases for better testing coverage * feat(core): rewrite deletion of connector info from user identities using postgresql operator
This commit is contained in:
parent
3f8cc6af69
commit
82d104a0d3
5 changed files with 129 additions and 7 deletions
|
@ -23,6 +23,7 @@ import {
|
|||
updateUserById,
|
||||
deleteUserById,
|
||||
clearUserCustomDataById,
|
||||
deleteUserIdentity,
|
||||
} from './user';
|
||||
|
||||
const mockQuery: jest.MockedFunction<QueryType> = jest.fn();
|
||||
|
@ -375,4 +376,51 @@ describe('user query', () => {
|
|||
|
||||
await clearUserCustomDataById(id);
|
||||
});
|
||||
|
||||
it('clearUserCustomDataById should throw when user can not be found by id', async () => {
|
||||
const id = 'foo';
|
||||
const expectSql = sql`
|
||||
update ${table}
|
||||
set ${fields.customData}='{}'::jsonb
|
||||
where ${fields.id}=$1
|
||||
`;
|
||||
|
||||
mockQuery.mockImplementationOnce(async (sql, values) => {
|
||||
expectSqlAssert(sql, expectSql.sql);
|
||||
expect(values).toEqual([id]);
|
||||
|
||||
return createMockQueryResult([]);
|
||||
});
|
||||
|
||||
await expect(clearUserCustomDataById(id)).rejects.toThrowError();
|
||||
});
|
||||
|
||||
it('deleteUserIdentity', async () => {
|
||||
const userId = 'foo';
|
||||
const connectorId = 'connector1';
|
||||
|
||||
const { connector1, ...restIdentities } = mockUser.identities;
|
||||
const finalDbvalue = {
|
||||
...mockUser,
|
||||
roleNames: JSON.stringify(mockUser.roleNames),
|
||||
identities: JSON.stringify(restIdentities),
|
||||
customData: JSON.stringify(mockUser.customData),
|
||||
};
|
||||
|
||||
const expectSql = sql`
|
||||
update ${table}
|
||||
set ${fields.identities}=${fields.identities}::jsonb-$1
|
||||
where ${fields.id}=$2
|
||||
returning *
|
||||
`;
|
||||
|
||||
mockQuery.mockImplementationOnce(async (sql, values) => {
|
||||
expectSqlAssert(sql, expectSql.sql);
|
||||
expect(values).toEqual([connectorId, mockUser.id]);
|
||||
|
||||
return createMockQueryResult([finalDbvalue]);
|
||||
});
|
||||
|
||||
await expect(deleteUserIdentity(userId, connectorId)).resolves.toEqual(finalDbvalue);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -137,3 +137,11 @@ export const clearUserCustomDataById = async (id: string) => {
|
|||
throw new UpdateError(Users, { set: { customData: {} }, where: { id } });
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteUserIdentity = async (userId: string, connectorId: string) =>
|
||||
pool.one<User>(sql`
|
||||
update ${table}
|
||||
set ${fields.identities}=${fields.identities}::jsonb-${connectorId}
|
||||
where ${fields.id}=${userId}
|
||||
returning *
|
||||
`);
|
||||
|
|
|
@ -7,6 +7,7 @@ import {
|
|||
hasUser,
|
||||
findUserById,
|
||||
updateUserById,
|
||||
deleteUserIdentity,
|
||||
deleteUserById,
|
||||
clearUserCustomDataById,
|
||||
} from '@/queries/user';
|
||||
|
@ -38,18 +39,15 @@ jest.mock('@/queries/user', () => ({
|
|||
...data,
|
||||
})
|
||||
),
|
||||
deleteUserById: jest.fn(async (_): Promise<void> => {
|
||||
/* Note: Do nothing */
|
||||
}),
|
||||
deleteUserById: jest.fn(),
|
||||
insertUser: jest.fn(
|
||||
async (user: CreateUser): Promise<User> => ({
|
||||
...mockUser,
|
||||
...user,
|
||||
})
|
||||
),
|
||||
clearUserCustomDataById: jest.fn(async (_): Promise<void> => {
|
||||
/* Note: Do nothing */
|
||||
}),
|
||||
clearUserCustomDataById: jest.fn(),
|
||||
deleteUserIdentity: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@/lib/user', () => ({
|
||||
|
@ -372,4 +370,47 @@ describe('adminUserRoutes', () => {
|
|||
expect(findUserById).toHaveBeenCalledTimes(1);
|
||||
expect(clearUserCustomDataById).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('DELETE /users/:userId/identities/:connectorId should throw if user not found', async () => {
|
||||
const notExistedUserId = 'notExisitedUserId';
|
||||
const arbitraryConnectorId = 'arbitraryConnectorId';
|
||||
const mockedFindUserById = findUserById as jest.Mock;
|
||||
mockedFindUserById.mockImplementationOnce((userId) => {
|
||||
if (userId === notExistedUserId) {
|
||||
throw new Error(' ');
|
||||
}
|
||||
});
|
||||
await expect(
|
||||
userRequest.delete(`/users/${notExistedUserId}/identities/${arbitraryConnectorId}`)
|
||||
).resolves.toHaveProperty('status', 500);
|
||||
expect(deleteUserIdentity).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('DELETE /users/:userId/identities/:connectorId should throw if user found and connector is not found', async () => {
|
||||
const arbitraryUserId = 'arbitraryUserId';
|
||||
const notExistedConnectorId = 'notExistedConnectorId';
|
||||
const mockedFindUserById = findUserById as jest.Mock;
|
||||
mockedFindUserById.mockImplementationOnce((userId) => {
|
||||
if (userId === arbitraryUserId) {
|
||||
return { identities: { connector1: {}, connector2: {} } };
|
||||
}
|
||||
});
|
||||
await expect(
|
||||
userRequest.delete(`/users/${arbitraryUserId}/identities/${notExistedConnectorId}`)
|
||||
).resolves.toHaveProperty('status', 404);
|
||||
expect(deleteUserIdentity).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('DELETE /users/:userId/identities/:connectorId', async () => {
|
||||
const arbitraryUserId = 'arbitraryUserId';
|
||||
const arbitraryConnectorId = 'arbitraryConnectorId';
|
||||
const mockedFindUserById = findUserById as jest.Mock;
|
||||
mockedFindUserById.mockImplementationOnce((userId) => {
|
||||
if (userId === arbitraryUserId) {
|
||||
return { identities: { connector1: {}, connector2: {}, arbitraryConnectorId: {} } };
|
||||
}
|
||||
});
|
||||
await userRequest.delete(`/users/${arbitraryUserId}/identities/${arbitraryConnectorId}`);
|
||||
expect(deleteUserIdentity).toHaveBeenCalledWith(arbitraryUserId, arbitraryConnectorId);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { arbitraryObjectGuard, userInfoSelectFields } from '@logto/schemas';
|
||||
import { has } from '@silverhand/essentials';
|
||||
import pick from 'lodash.pick';
|
||||
import { InvalidInputError } from 'slonik';
|
||||
import { object, string } from 'zod';
|
||||
|
@ -11,6 +12,7 @@ import { findRolesByRoleNames } from '@/queries/roles';
|
|||
import {
|
||||
clearUserCustomDataById,
|
||||
deleteUserById,
|
||||
deleteUserIdentity,
|
||||
findUsers,
|
||||
countUsers,
|
||||
findUserById,
|
||||
|
@ -264,4 +266,25 @@ export default function adminUserRoutes<T extends AuthedRouter>(router: T) {
|
|||
return next();
|
||||
}
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/users/:userId/identities/:connectorId',
|
||||
koaGuard({ params: object({ userId: string(), connectorId: string() }) }),
|
||||
async (ctx, next) => {
|
||||
const {
|
||||
params: { userId, connectorId },
|
||||
} = ctx.guard;
|
||||
|
||||
const { identities } = await findUserById(userId);
|
||||
|
||||
if (!has(identities, connectorId)) {
|
||||
throw new RequestError({ code: 'user.identity_not_exists', status: 404 });
|
||||
}
|
||||
|
||||
const updatedUser = await deleteUserIdentity(userId, connectorId);
|
||||
ctx.body = pick(updatedUser, ...userInfoSelectFields);
|
||||
|
||||
return next();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
@ -33,7 +33,9 @@ export const mockUser: User = {
|
|||
passwordEncryptionSalt: null,
|
||||
name: null,
|
||||
avatar: null,
|
||||
identities: {},
|
||||
identities: {
|
||||
connector1: { userId: 'connector1', details: {} },
|
||||
},
|
||||
customData: {},
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in a new issue