0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-03-17 22:31:28 -05:00

test(core): add ut for queires (#287)

* test(core): add ut for queires

add ut for queries

* test(core): add user query ut

add user query ut

* fix(core): remove test code

remove console log
This commit is contained in:
simeng-li 2022-02-28 14:30:27 +08:00 committed by GitHub
parent c9400d0a4d
commit fb6a1dc236
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 1469 additions and 34 deletions

View file

@ -3,7 +3,7 @@ import snakecaseKeys from 'snakecase-keys';
import {
consumeInstanceById,
destoryInstanceById,
destroyInstanceById,
findPayloadById,
findPayloadByPayloadField,
revokeInstanceByGrantId,
@ -23,7 +23,7 @@ jest.mock('@/queries/oidc-model-instance', () => ({
findPayloadById: jest.fn(),
findPayloadByPayloadField: jest.fn(),
consumeInstanceById: jest.fn(),
destoryInstanceById: jest.fn(),
destroyInstanceById: jest.fn(),
revokeInstanceByGrantId: jest.fn(),
}));
@ -102,7 +102,7 @@ describe('postgres Adapter', () => {
expect(consumeInstanceById).toBeCalledWith(modelName, id);
await adapter.destroy(id);
expect(destoryInstanceById).toBeCalledWith(modelName, id);
expect(destroyInstanceById).toBeCalledWith(modelName, id);
await adapter.revokeByGrantId(grantId);
expect(revokeInstanceByGrantId).toBeCalledWith(modelName, grantId);

View file

@ -6,7 +6,7 @@ import snakecaseKeys from 'snakecase-keys';
import { findApplicationById } from '@/queries/application';
import {
consumeInstanceById,
destoryInstanceById,
destroyInstanceById,
findPayloadById,
findPayloadByPayloadField,
revokeInstanceByGrantId,
@ -57,7 +57,7 @@ export default function postgresAdapter(modelName: string): ReturnType<AdapterFa
findByUserCode: async (userCode) => findPayloadByPayloadField(modelName, 'userCode', userCode),
findByUid: async (uid) => findPayloadByPayloadField(modelName, 'uid', uid),
consume: async (id) => consumeInstanceById(modelName, id),
destroy: async (id) => destoryInstanceById(modelName, id),
destroy: async (id) => destroyInstanceById(modelName, id),
revokeByGrantId: async (grantId) => revokeInstanceByGrantId(modelName, grantId),
};
}

View file

@ -1,12 +1,5 @@
import { Applications } from '@logto/schemas';
import {
createMockPool,
createMockQueryResult,
sql,
QueryResultType,
QueryResultRowType,
} from 'slonik';
import { PrimitiveValueExpressionType } from 'slonik/dist/src/types.d';
import { createMockPool, createMockQueryResult, sql } from 'slonik';
import { snakeCase } from 'snake-case';
import {
@ -16,7 +9,7 @@ import {
} from '@/database/utils';
import { DeletionError } from '@/errors/SlonikError';
import { mockApplication } from '@/utils/mock';
import { expectSqlAssert } from '@/utils/test-utils';
import { expectSqlAssert, QueryType } from '@/utils/test-utils';
import {
findTotalNumberOfApplications,
@ -27,12 +20,7 @@ import {
deleteApplicationById,
} from './application';
type MockQuery = (
sql: string,
values: PrimitiveValueExpressionType
) => Promise<QueryResultType<QueryResultRowType>>;
const mockQuery: jest.MockedFunction<MockQuery> = jest.fn();
const mockQuery: jest.MockedFunction<QueryType> = jest.fn();
jest.mock('@/database/pool', () =>
createMockPool({
@ -42,7 +30,7 @@ jest.mock('@/database/pool', () =>
})
);
describe('appliaction query', () => {
describe('application query', () => {
const { table, fields } = convertToIdentifiers(Applications);
it('findTotalNumberOfApplications', async () => {

View file

@ -0,0 +1,132 @@
import { Connectors, CreateConnector } from '@logto/schemas';
import { createMockPool, createMockQueryResult, sql, QueryResultRowType } from 'slonik';
import { convertToIdentifiers } from '@/database/utils';
import { expectSqlAssert, QueryType } from '@/utils/test-utils';
import {
findAllConnectors,
findConnectorById,
hasConnector,
insertConnector,
updateConnector,
} from './connector';
const mockQuery: jest.MockedFunction<QueryType> = jest.fn();
jest.mock('@/database/pool', () =>
createMockPool({
query: async (sql, values) => {
return mockQuery(sql, values);
},
})
);
describe('connector queries', () => {
const { table, fields } = convertToIdentifiers(Connectors);
it('findAllConnectors', async () => {
const rowData = { id: 'foo' };
const expectSql = sql`
select ${sql.join(Object.values(fields), sql`, `)}
from ${table}
`;
mockQuery.mockImplementationOnce(async (sql, values) => {
expectSqlAssert(sql, expectSql.sql);
expect(values).toEqual([]);
return createMockQueryResult([rowData]);
});
await expect(findAllConnectors()).resolves.toEqual([rowData]);
});
it('findConnectorById', async () => {
const id = 'foo';
const rowData = { id };
const expectSql = sql`
select ${sql.join(Object.values(fields), sql`, `)}
from ${table}
where ${fields.id}=$1
`;
mockQuery.mockImplementationOnce(async (sql, values) => {
expectSqlAssert(sql, expectSql.sql);
expect(values).toEqual([id]);
return createMockQueryResult([rowData]);
});
await expect(findConnectorById(id)).resolves.toEqual(rowData);
});
it('hasConnector', async () => {
const id = 'foo';
const expectSql = sql`
SELECT EXISTS(
select ${sql.join(Object.values(fields), sql`, `)}
from ${table}
where ${fields.id}=$1
)
`;
mockQuery.mockImplementationOnce(async (sql, values) => {
expectSqlAssert(sql, expectSql.sql);
expect(values).toEqual([id]);
return createMockQueryResult([{ exists: true }]);
});
await expect(hasConnector(id)).resolves.toEqual(true);
});
it('insertConnector', async () => {
const connector: CreateConnector & QueryResultRowType = {
id: 'foo',
enabled: true,
};
const expectSql = `
insert into "connectors" ("id", "enabled")
values ($1, $2)
returning *
`;
mockQuery.mockImplementationOnce(async (sql, values) => {
expectSqlAssert(sql, expectSql);
expect(values).toEqual([connector.id, connector.enabled]);
return createMockQueryResult([connector]);
});
await expect(insertConnector(connector)).resolves.toEqual(connector);
});
it('updateConnector', async () => {
const id = 'foo';
const enabled = false;
const expectSql = sql`
update ${table}
set ${fields.enabled}=$1
where ${fields.id}=$2
returning *
`;
mockQuery.mockImplementationOnce(async (sql, values) => {
expectSqlAssert(sql, expectSql.sql);
expect(values).toEqual([enabled, id]);
return createMockQueryResult([{ id, enabled }]);
});
await expect(updateConnector({ where: { id }, set: { enabled } })).resolves.toEqual({
id,
enabled,
});
});
});

View file

@ -0,0 +1,166 @@
import { OidcModelInstances, CreateOidcModelInstance } from '@logto/schemas';
import { createMockPool, createMockQueryResult, sql } from 'slonik';
import { convertToIdentifiers } from '@/database/utils';
import { expectSqlAssert, QueryType } from '@/utils/test-utils';
import {
upsertInstance,
findPayloadById,
findPayloadByPayloadField,
consumeInstanceById,
destroyInstanceById,
revokeInstanceByGrantId,
} from './oidc-model-instance';
const mockQuery: jest.MockedFunction<QueryType> = jest.fn();
jest.mock('@/database/pool', () =>
createMockPool({
query: async (sql, values) => {
return mockQuery(sql, values);
},
})
);
jest.mock('@/database/utils', () => ({
...jest.requireActual('@/database/utils'),
convertToTimestamp: () => 100,
}));
describe('oidc-model-instance query', () => {
const { table, fields } = convertToIdentifiers(OidcModelInstances);
const expiresAt = Date.now();
const instance: CreateOidcModelInstance = {
modelName: 'access_token',
id: 'foo',
payload: {},
expiresAt,
};
const databaseValue = {
...instance,
payload: JSON.stringify(instance.payload),
};
it('upsertInstance', async () => {
const expectSql = sql`
insert into ${table} ("model_name", "id", "payload", "expires_at")
values ($1, $2, $3, to_timestamp($4))
on conflict ("model_name", "id") do update
set "payload"=excluded."payload", "expires_at"=excluded."expires_at"
`;
mockQuery.mockImplementationOnce(async (sql, values) => {
expectSqlAssert(sql, expectSql.sql);
expect(values).toEqual([
instance.modelName,
instance.id,
JSON.stringify(instance.payload),
instance.expiresAt / 1000,
]);
return createMockQueryResult([databaseValue]);
});
await expect(upsertInstance(instance)).resolves.toEqual(databaseValue);
});
it('findPayloadById', async () => {
const expectSql = sql`
select ${fields.payload}, ${fields.consumedAt}
from ${table}
where "model_name"=$1
and "id"=$2
`;
mockQuery.mockImplementationOnce(async (sql, values) => {
expectSqlAssert(sql, expectSql.sql);
expect(values).toEqual([instance.modelName, instance.id]);
return createMockQueryResult([{ consumedAt: 10 }]);
});
await expect(findPayloadById(instance.modelName, instance.id)).resolves.toEqual({
consumed: true,
});
});
it('findPayloadByPayloadField', async () => {
const uid_key = 'uid';
const uid_value = 'foo';
const expectSql = sql`
select ${fields.payload}, ${fields.consumedAt}
from ${table}
where ${fields.modelName}=$1
and ${fields.payload}->>$2=$3
`;
mockQuery.mockImplementationOnce(async (sql, values) => {
expectSqlAssert(sql, expectSql.sql);
expect(values).toEqual([instance.modelName, uid_key, uid_value]);
return createMockQueryResult([{ consumedAt: 10 }]);
});
await expect(
findPayloadByPayloadField(instance.modelName, uid_key, uid_value)
).resolves.toEqual({
consumed: true,
});
});
it('consumeInstanceById', async () => {
const expectSql = sql`
update ${table}
set ${fields.consumedAt}=$1
where ${fields.modelName}=$2
and ${fields.id}=$3
`;
mockQuery.mockImplementationOnce(async (sql, values) => {
expectSqlAssert(sql, expectSql.sql);
expect(values).toEqual([100, instance.modelName, instance.id]);
return createMockQueryResult([]);
});
await consumeInstanceById(instance.modelName, instance.id);
});
it('destroyInstanceById', async () => {
const expectSql = sql`
delete from ${table}
where ${fields.modelName}=$1
and ${fields.id}=$2
`;
mockQuery.mockImplementationOnce(async (sql, values) => {
expectSqlAssert(sql, expectSql.sql);
expect(values).toEqual([instance.modelName, instance.id]);
return createMockQueryResult([]);
});
await destroyInstanceById(instance.modelName, instance.id);
});
it('revokeInstanceByGrantId', async () => {
const grantId = 'grant';
const expectSql = sql`
delete from ${table}
where ${fields.modelName}=$1
and ${fields.payload}->>'grantId'=$2
`;
mockQuery.mockImplementationOnce(async (sql, values) => {
expectSqlAssert(sql, expectSql.sql);
expect(values).toEqual([instance.modelName, grantId]);
return createMockQueryResult([]);
});
await revokeInstanceByGrantId(instance.modelName, grantId);
});
});

View file

@ -71,7 +71,7 @@ export const consumeInstanceById = async (modelName: string, id: string) => {
`);
};
export const destoryInstanceById = async (modelName: string, id: string) => {
export const destroyInstanceById = async (modelName: string, id: string) => {
await pool.query(sql`
delete from ${table}
where ${fields.modelName}=${modelName}

View file

@ -0,0 +1,191 @@
import { Passcodes, PasscodeType } from '@logto/schemas';
import { createMockPool, createMockQueryResult, sql } from 'slonik';
import { snakeCase } from 'snake-case';
import {
convertToIdentifiers,
convertToPrimitiveOrSql,
excludeAutoSetFields,
} from '@/database/utils';
import { DeletionError } from '@/errors/SlonikError';
import { mockPasscode } from '@/utils/mock';
import { expectSqlAssert, QueryType } from '@/utils/test-utils';
import {
findUnconsumedPasscodeByJtiAndType,
findUnconsumedPasscodesByJtiAndType,
insertPasscode,
updatePasscode,
deletePasscodeById,
deletePasscodesByIds,
} from './passcode';
const mockQuery: jest.MockedFunction<QueryType> = jest.fn();
jest.mock('@/database/pool', () =>
createMockPool({
query: async (sql, values) => {
return mockQuery(sql, values);
},
})
);
describe('passcode query', () => {
const { table, fields } = convertToIdentifiers(Passcodes);
it('findUnconsumedPasscodeByJtiAndType', async () => {
const jti = 'foo';
const type = PasscodeType.SignIn;
const expectSql = sql`
select ${sql.join(Object.values(fields), sql`, `)}
from ${table}
where ${fields.interactionJti}=$1 and ${fields.type}=$2 and ${fields.consumed} = false
`;
mockQuery.mockImplementationOnce(async (sql, values) => {
expectSqlAssert(sql, expectSql.sql);
expect(values).toEqual([jti, type]);
return createMockQueryResult([mockPasscode]);
});
await expect(findUnconsumedPasscodeByJtiAndType(jti, type)).resolves.toEqual(mockPasscode);
});
it('findUnconsumedPasscodesByJtiAndType', async () => {
const jti = 'foo';
const type = PasscodeType.SignIn;
const expectSql = sql`
select ${sql.join(Object.values(fields), sql`, `)}
from ${table}
where ${fields.interactionJti}=$1 and ${fields.type}=$2 and ${fields.consumed} = false
`;
mockQuery.mockImplementationOnce(async (sql, values) => {
expectSqlAssert(sql, expectSql.sql);
expect(values).toEqual([jti, type]);
return createMockQueryResult([mockPasscode]);
});
await expect(findUnconsumedPasscodesByJtiAndType(jti, type)).resolves.toEqual([mockPasscode]);
});
it('insertPasscode', async () => {
const keys = excludeAutoSetFields(Passcodes.fieldKeys);
const expectSql = `
insert into "passcodes" (${keys.map((k) => `"${snakeCase(k)}"`).join(', ')})
values (${keys.map((_, index) => `$${index + 1}`).join(', ')})
returning *
`;
mockQuery.mockImplementationOnce(async (sql, values) => {
expectSqlAssert(sql, expectSql);
expect(values).toEqual(keys.map((k) => convertToPrimitiveOrSql(k, mockPasscode[k])));
return createMockQueryResult([mockPasscode]);
});
await expect(insertPasscode(mockPasscode)).resolves.toEqual(mockPasscode);
});
it('updatePasscode', async () => {
const id = 'foo';
const tryCount = 3;
const expectSql = sql`
update ${table}
set ${fields.tryCount}=$1
where ${fields.id}=$2
returning *
`;
mockQuery.mockImplementationOnce(async (sql, values) => {
expectSqlAssert(sql, expectSql.sql);
expect(values).toEqual([tryCount, id]);
return createMockQueryResult([{ ...mockPasscode, tryCount }]);
});
await expect(updatePasscode({ where: { id }, set: { tryCount } })).resolves.toEqual({
...mockPasscode,
tryCount,
});
});
it('deletePasscodeById', async () => {
const id = 'foo';
const expectSql = sql`
delete from ${table}
where ${fields.id}=$1
`;
mockQuery.mockImplementationOnce(async (sql, values) => {
expectSqlAssert(sql, expectSql.sql);
expect(values).toEqual([id]);
return createMockQueryResult([mockPasscode]);
});
await deletePasscodeById(id);
});
it('deletePasscodeById throw error if return row count is 0', async () => {
const id = 'foo';
const expectSql = sql`
delete from ${table}
where ${fields.id}=$1
`;
mockQuery.mockImplementationOnce(async (sql, values) => {
expectSqlAssert(sql, expectSql.sql);
expect(values).toEqual([id]);
return createMockQueryResult([]);
});
await expect(deletePasscodeById(id)).rejects.toMatchError(
new DeletionError(Passcodes.table, id)
);
});
it('deletePasscodesByIds', async () => {
const ids = ['foo', 'foo2'];
const expectSql = sql`
delete from ${table}
where ${fields.id} in (${ids.join(',')})
`;
mockQuery.mockImplementationOnce(async (sql, values) => {
expectSqlAssert(sql, expectSql.sql);
expect(values).toEqual([ids.join(',')]);
return createMockQueryResult([mockPasscode, mockPasscode]);
});
await deletePasscodesByIds(ids);
});
it('deletePasscodesByIds throw error if return row count not match requested id length', async () => {
const ids = ['foo', 'foo2'];
const expectSql = sql`
delete from ${table}
where ${fields.id} in (${ids.join(',')})
`;
mockQuery.mockImplementationOnce(async (sql, values) => {
expectSqlAssert(sql, expectSql.sql);
expect(values).toEqual([ids.join(',')]);
return createMockQueryResult([mockPasscode]);
});
await expect(deletePasscodesByIds(ids)).rejects.toMatchError(
new DeletionError(Passcodes.table, `${ids.join(',')}`)
);
});
});

View file

@ -32,7 +32,7 @@ export const updatePasscode = buildUpdateWhere<CreatePasscode, Passcode>(pool, P
export const deletePasscodeById = async (id: string) => {
const { rowCount } = await pool.query(sql`
delete from ${table}
where id=${id}
where ${fields.id}=${id}
`);
if (rowCount < 1) {
@ -43,7 +43,7 @@ export const deletePasscodeById = async (id: string) => {
export const deletePasscodesByIds = async (ids: string[]) => {
const { rowCount } = await pool.query(sql`
delete from ${table}
where id in (${ids.join(',')})
where ${fields.id} in (${ids.join(',')})
`);
if (rowCount !== ids.length) {

View file

@ -0,0 +1,179 @@
import { Resources } from '@logto/schemas';
import { createMockPool, createMockQueryResult, sql } from 'slonik';
import { convertToIdentifiers, convertToPrimitiveOrSql } from '@/database/utils';
import { DeletionError } from '@/errors/SlonikError';
import { mockResource } from '@/utils/mock';
import { expectSqlAssert, QueryType } from '@/utils/test-utils';
import {
findTotalNumberOfResources,
findAllResources,
findResourceById,
findResourceByIndicator,
insertResource,
updateResourceById,
deleteResourceById,
} from './resource';
const mockQuery: jest.MockedFunction<QueryType> = jest.fn();
jest.mock('@/database/pool', () =>
createMockPool({
query: async (sql, values) => {
return mockQuery(sql, values);
},
})
);
describe('resource query', () => {
const { table, fields } = convertToIdentifiers(Resources);
it('findTotalNumberOfResources', async () => {
const expectSql = sql`
select count(*)
from ${table}
`;
mockQuery.mockImplementationOnce(async (sql, values) => {
expectSqlAssert(sql, expectSql.sql);
expect(values).toEqual(expectSql.values);
return createMockQueryResult([{ count: 10 }]);
});
await expect(findTotalNumberOfResources()).resolves.toEqual({ count: 10 });
});
it('findAllResources', async () => {
const limit = 10;
const offset = 1;
const expectSql = sql`
select ${sql.join(Object.values(fields), sql`, `)}
from ${table}
limit $1
offset $2
`;
mockQuery.mockImplementationOnce(async (sql, values) => {
expectSqlAssert(sql, expectSql.sql);
expect(values).toEqual([limit, offset]);
return createMockQueryResult([mockResource]);
});
await expect(findAllResources(limit, offset)).resolves.toEqual([mockResource]);
});
it('findResourcesById', async () => {
const id = 'foo';
const expectSql = sql`
select ${sql.join(Object.values(fields), sql`, `)}
from ${table}
where ${fields.id}=$1
`;
mockQuery.mockImplementationOnce(async (sql, values) => {
expectSqlAssert(sql, expectSql.sql);
expect(values).toEqual([id]);
return createMockQueryResult([mockResource]);
});
await expect(findResourceById(id)).resolves.toEqual(mockResource);
});
it('findResourceByIndicator', async () => {
const indicator = 'foo';
const expectSql = sql`
select ${sql.join(Object.values(fields), sql`, `)}
from ${table}
where ${fields.indicator}=$1
`;
mockQuery.mockImplementationOnce(async (sql, values) => {
expectSqlAssert(sql, expectSql.sql);
expect(values).toEqual([indicator]);
return createMockQueryResult([mockResource]);
});
await expect(findResourceByIndicator(indicator)).resolves.toEqual(mockResource);
});
it('insertResource', async () => {
const expectSql = sql`
insert into ${table} (${sql.join(Object.values(fields), sql`, `)})
values (${sql.join(
Object.values(fields).map((_, index) => `$${index + 1}`),
sql`, `
)})
returning *
`;
mockQuery.mockImplementationOnce(async (sql, values) => {
expectSqlAssert(sql, expectSql.sql);
expect(values).toEqual(
Resources.fieldKeys.map((k) => convertToPrimitiveOrSql(k, mockResource[k]))
);
return createMockQueryResult([mockResource]);
});
await expect(insertResource(mockResource)).resolves.toEqual(mockResource);
});
it('updateResourceById', async () => {
const id = 'foo';
const name = 'foo';
const expectSql = sql`
update ${table}
set ${fields.name}=$1
where ${fields.id}=$2
returning *
`;
mockQuery.mockImplementationOnce(async (sql, values) => {
expectSqlAssert(sql, expectSql.sql);
expect(values).toEqual([name, id]);
return createMockQueryResult([mockResource]);
});
await expect(updateResourceById(id, { name })).resolves.toEqual(mockResource);
});
it('deleteResourceById', async () => {
const id = 'foo';
const expectSql = sql`
delete from ${table}
where ${fields.id}=$1
`;
mockQuery.mockImplementationOnce(async (sql, values) => {
expectSqlAssert(sql, expectSql.sql);
expect(values).toEqual([id]);
return createMockQueryResult([mockResource]);
});
await deleteResourceById(id);
});
it('deleteResourceById throw error if return row count is 0', async () => {
const id = 'foo';
mockQuery.mockImplementationOnce(async () => {
return createMockQueryResult([]);
});
await expect(deleteResourceById(id)).rejects.toMatchError(
new DeletionError(Resources.table, id)
);
});
});

View file

@ -19,14 +19,14 @@ export const findAllResources = async (limit: number, offset: number) =>
export const findResourceByIndicator = async (indicator: string) =>
pool.maybeOne<Resource>(sql`
select ${sql.join(Object.values(fields), sql`,`)}
select ${sql.join(Object.values(fields), sql`, `)}
from ${table}
where ${fields.indicator}=${indicator}
`);
export const findResourceById = async (id: string) =>
pool.one<Resource>(sql`
select ${sql.join(Object.values(fields), sql`,`)}
select ${sql.join(Object.values(fields), sql`, `)}
from ${table}
where ${fields.id}=${id}
`);
@ -45,7 +45,7 @@ export const updateResourceById = async (
export const deleteResourceById = async (id: string) => {
const { rowCount } = await pool.query(sql`
delete from ${table}
where id=${id}
where ${fields.id}=${id}
`);
if (rowCount < 1) {

View file

@ -0,0 +1,57 @@
import { Roles } from '@logto/schemas';
import { createMockPool, createMockQueryResult, sql } from 'slonik';
import { convertToIdentifiers } from '@/database/utils';
import { mockRole } from '@/utils/mock';
import { expectSqlAssert, QueryType } from '@/utils/test-utils';
import { findAllRoles, findRolesByRoleNames } from './roles';
const mockQuery: jest.MockedFunction<QueryType> = jest.fn();
jest.mock('@/database/pool', () =>
createMockPool({
query: async (sql, values) => {
return mockQuery(sql, values);
},
})
);
describe('roles query', () => {
const { table, fields } = convertToIdentifiers(Roles);
it('findAllRoles', async () => {
const expectSql = sql`
select ${sql.join(Object.values(fields), sql`, `)}
from ${table}
`;
mockQuery.mockImplementationOnce(async (sql, values) => {
expectSqlAssert(sql, expectSql.sql);
expect(values).toEqual([]);
return createMockQueryResult([mockRole]);
});
await expect(findAllRoles()).resolves.toEqual([mockRole]);
});
it('findRolesByRoleNames', async () => {
const roleNames = ['foo'];
const expectSql = sql`
select ${sql.join(Object.values(fields), sql`, `)}
from ${table}
where ${fields.name} in (${sql.join(roleNames, sql`, `)})
`;
mockQuery.mockImplementationOnce(async (sql, values) => {
expectSqlAssert(sql, expectSql.sql);
expect(values).toEqual([roleNames.join(', ')]);
return createMockQueryResult([mockRole]);
});
await expect(findRolesByRoleNames(roleNames)).resolves.toEqual([mockRole]);
});
});

View file

@ -14,7 +14,7 @@ export const findAllRoles = async () =>
export const findRolesByRoleNames = async (roleNames: string[]) =>
pool.any<Role>(sql`
select ${sql.join(Object.values(fields), sql`,`)}
select ${sql.join(Object.values(fields), sql`, `)}
from ${table}
where ${fields.name} in (${sql.join(roleNames, sql`,`)})
where ${fields.name} in (${sql.join(roleNames, sql`, `)})
`);

View file

@ -0,0 +1,99 @@
import { ResourceScopes } from '@logto/schemas';
import { createMockPool, createMockQueryResult, sql } from 'slonik';
import { convertToIdentifiers, convertToPrimitiveOrSql } from '@/database/utils';
import { DeletionError } from '@/errors/SlonikError';
import { mockScope } from '@/utils/mock';
import { expectSqlAssert, QueryType } from '@/utils/test-utils';
import { findAllScopesWithResourceId, insertScope, deleteScopeById } from './scope';
const mockQuery: jest.MockedFunction<QueryType> = jest.fn();
jest.mock('@/database/pool', () =>
createMockPool({
query: async (sql, values) => {
return mockQuery(sql, values);
},
})
);
describe('scope query', () => {
const { table, fields } = convertToIdentifiers(ResourceScopes);
it('findAllScopesWithResourceId', async () => {
const expectSql = sql`
select ${sql.join(Object.values(fields), sql`, `)}
from ${table}
where ${fields.resourceId}=$1
`;
mockQuery.mockImplementationOnce(async (sql, values) => {
expectSqlAssert(sql, expectSql.sql);
expect(values).toEqual([mockScope.resourceId]);
return createMockQueryResult([mockScope]);
});
await expect(findAllScopesWithResourceId(mockScope.resourceId)).resolves.toEqual([mockScope]);
});
it('insertScope', async () => {
const expectSql = sql`
insert into ${table} (${sql.join(Object.values(fields), sql`, `)})
values (${sql.join(
Object.values(fields).map((_, index) => `$${index + 1}`),
sql`, `
)})
returning *
`;
mockQuery.mockImplementationOnce(async (sql, values) => {
expectSqlAssert(sql, expectSql.sql);
expect(values).toEqual(
ResourceScopes.fieldKeys.map((k) => convertToPrimitiveOrSql(k, mockScope[k]))
);
return createMockQueryResult([mockScope]);
});
await expect(insertScope(mockScope)).resolves.toEqual(mockScope);
});
it('deleteScopeById', async () => {
const id = 'foo';
const expectSql = sql`
delete from ${table}
where ${fields.id}=$1
`;
mockQuery.mockImplementationOnce(async (sql, values) => {
expectSqlAssert(sql, expectSql.sql);
expect(values).toEqual([id]);
return createMockQueryResult([mockScope]);
});
await deleteScopeById(id);
});
it('deleteScopeById throw error if return row count is 0', async () => {
const id = 'foo';
const expectSql = sql`
delete from ${table}
where ${fields.id}=$1
`;
mockQuery.mockImplementationOnce(async (sql, values) => {
expectSqlAssert(sql, expectSql.sql);
expect(values).toEqual([id]);
return createMockQueryResult([]);
});
await expect(deleteScopeById(id)).rejects.toMatchError(
new DeletionError(ResourceScopes.table, id)
);
});
});

View file

@ -10,7 +10,7 @@ const { table, fields } = convertToIdentifiers(ResourceScopes);
export const findAllScopesWithResourceId = async (resourceId: string) =>
pool.any<ResourceScope>(sql`
select ${sql.join(Object.values(fields), sql`,`)}
select ${sql.join(Object.values(fields), sql`, `)}
from ${table}
where ${fields.resourceId}=${resourceId}
`);
@ -26,7 +26,7 @@ export const insertScope = buildInsertInto<CreateResourceScope, ResourceScope>(
export const deleteScopeById = async (id: string) => {
const { rowCount } = await pool.query(sql`
delete from ${table}
where id=${id}
where ${fields.id}=${id}
`);
if (rowCount < 1) {

View file

@ -0,0 +1,60 @@
import { Settings } from '@logto/schemas';
import { createMockPool, createMockQueryResult, sql } from 'slonik';
import { convertToIdentifiers } from '@/database/utils';
import { mockSetting } from '@/utils/mock';
import { expectSqlAssert, QueryType } from '@/utils/test-utils';
import { defaultSettingId, getSetting, updateSetting } from './setting';
const mockQuery: jest.MockedFunction<QueryType> = jest.fn();
jest.mock('@/database/pool', () =>
createMockPool({
query: async (sql, values) => {
return mockQuery(sql, values);
},
})
);
describe('setting query', () => {
const { table, fields } = convertToIdentifiers(Settings);
const dbvalue = { ...mockSetting, adminConsole: JSON.stringify(mockSetting.adminConsole) };
it('getSetting', async () => {
const expectSql = sql`
select ${sql.join(Object.values(fields), sql`, `)}
from ${table}
where ${fields.id}=$1
`;
mockQuery.mockImplementationOnce(async (sql, values) => {
expectSqlAssert(sql, expectSql.sql);
expect(values).toEqual([defaultSettingId]);
return createMockQueryResult([dbvalue]);
});
await expect(getSetting()).resolves.toEqual(dbvalue);
});
it('updateSetting', async () => {
const customDomain = 'logto.io';
const expectSql = sql`
update ${table}
set ${fields.customDomain}=$1
where ${fields.id}=$2
returning *
`;
mockQuery.mockImplementationOnce(async (sql, values) => {
expectSqlAssert(sql, expectSql.sql);
expect(values).toEqual([customDomain, defaultSettingId]);
return createMockQueryResult([dbvalue]);
});
await expect(updateSetting({ customDomain })).resolves.toEqual(dbvalue);
});
});

View file

@ -0,0 +1,71 @@
import { SignInExperiences } from '@logto/schemas';
import { createMockPool, createMockQueryResult, sql } from 'slonik';
import { convertToIdentifiers } from '@/database/utils';
import { mockSignInExperience } from '@/utils/mock';
import { expectSqlAssert, QueryType } from '@/utils/test-utils';
import { findDefaultSignInExperience, updateSignInExperienceById } from './sign-in-experience';
const mockQuery: jest.MockedFunction<QueryType> = jest.fn();
jest.mock('@/database/pool', () =>
createMockPool({
query: async (sql, values) => {
return mockQuery(sql, values);
},
})
);
describe('sign-in-experience query', () => {
const { table, fields } = convertToIdentifiers(SignInExperiences);
const dbvalue = {
...mockSignInExperience,
companyInfo: JSON.stringify(mockSignInExperience.companyInfo),
branding: JSON.stringify(mockSignInExperience.branding),
termsOfUse: JSON.stringify(mockSignInExperience.termsOfUse),
localization: JSON.stringify(mockSignInExperience.localization),
signInMethods: JSON.stringify(mockSignInExperience.signInMethods),
};
it('findDefaultSignInExperience', async () => {
const expectSql = sql`
select ${sql.join(Object.values(fields), sql`, `)}
from ${table}
`;
mockQuery.mockImplementationOnce(async (sql, values) => {
expectSqlAssert(sql, expectSql.sql);
expect(values).toEqual([]);
return createMockQueryResult([dbvalue]);
});
await expect(findDefaultSignInExperience()).resolves.toEqual(dbvalue);
});
it('updateSignInExperienceById', async () => {
const id = 'foo';
const termsOfUse = {
enabled: false,
};
const expectSql = sql`
update ${table}
set
${fields.termsOfUse}=
coalesce(${fields.termsOfUse},'{}'::jsonb)|| $1
where ${fields.id}=$2
returning *
`;
mockQuery.mockImplementationOnce(async (sql, values) => {
expectSqlAssert(sql, expectSql.sql);
expect(values).toEqual([JSON.stringify(termsOfUse), id]);
return createMockQueryResult([dbvalue]);
});
await expect(updateSignInExperienceById(id, { termsOfUse })).resolves.toEqual(dbvalue);
});
});

View file

@ -0,0 +1,66 @@
import { UserLogs } from '@logto/schemas';
import { createMockPool, createMockQueryResult, sql } from 'slonik';
import { snakeCase } from 'snake-case';
import {
convertToIdentifiers,
excludeAutoSetFields,
convertToPrimitiveOrSql,
} from '@/database/utils';
import { mockUserLog } from '@/utils/mock';
import { expectSqlAssert, QueryType } from '@/utils/test-utils';
import { insertUserLog, findLogsByUserId } from './user-log';
const mockQuery: jest.MockedFunction<QueryType> = jest.fn();
jest.mock('@/database/pool', () =>
createMockPool({
query: async (sql, values) => {
return mockQuery(sql, values);
},
})
);
describe('user-log query', () => {
const { table, fields } = convertToIdentifiers(UserLogs);
const dbvalue = { ...mockUserLog, payload: JSON.stringify(mockUserLog.payload) };
it('findLogsByUserId', async () => {
const userId = 'foo';
const expectSql = sql`
select ${sql.join(Object.values(fields), sql`,`)}
from ${table}
where ${fields.userId}=${userId}
order by created_at desc
`;
mockQuery.mockImplementationOnce(async (sql, values) => {
expectSqlAssert(sql, expectSql.sql);
expect(values).toEqual([userId]);
return createMockQueryResult([dbvalue]);
});
await expect(findLogsByUserId(userId)).resolves.toEqual([dbvalue]);
});
it('insertUserLog', async () => {
const keys = excludeAutoSetFields(UserLogs.fieldKeys);
// eslint-disable-next-line sql/no-unsafe-query
const expectSql = `
insert into "user_logs" (${keys.map((k) => `"${snakeCase(k)}"`).join(', ')})
values (${keys.map((_, index) => `$${index + 1}`).join(', ')})
`;
mockQuery.mockImplementationOnce(async (sql, values) => {
expectSqlAssert(sql, expectSql);
expect(values).toEqual(keys.map((k) => convertToPrimitiveOrSql(k, mockUserLog[k])));
return createMockQueryResult([]);
});
await insertUserLog(mockUserLog);
});
});

View file

@ -0,0 +1,378 @@
import { Users } from '@logto/schemas';
import { createMockPool, createMockQueryResult, sql } from 'slonik';
import { convertToIdentifiers, convertToPrimitiveOrSql } from '@/database/utils';
import { DeletionError } from '@/errors/SlonikError';
import { mockUser } from '@/utils/mock';
import { expectSqlAssert, QueryType } from '@/utils/test-utils';
import {
findUserByUsername,
findUserByEmail,
findUserByPhone,
findUserById,
findUserByIdentity,
hasUser,
hasUserWithId,
hasUserWithEmail,
hasUserWithIdentity,
hasUserWithPhone,
insertUser,
countUsers,
findUsers,
updateUserById,
deleteUserById,
clearUserCustomDataById,
} from './user';
const mockQuery: jest.MockedFunction<QueryType> = jest.fn();
jest.mock('@/database/pool', () =>
createMockPool({
query: async (sql, values) => {
return mockQuery(sql, values);
},
})
);
describe('user query', () => {
const { table, fields } = convertToIdentifiers(Users);
const dbvalue = {
...mockUser,
roleNames: JSON.stringify(mockUser.roleNames),
identities: JSON.stringify(mockUser.identities),
customData: JSON.stringify(mockUser.customData),
};
it('findUserByUsername', async () => {
const expectSql = sql`
select ${sql.join(Object.values(fields), sql`,`)}
from ${table}
where ${fields.username}=$1
`;
mockQuery.mockImplementationOnce(async (sql, values) => {
expectSqlAssert(sql, expectSql.sql);
expect(values).toEqual([mockUser.username]);
return createMockQueryResult([dbvalue]);
});
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
await expect(findUserByUsername(mockUser.username!)).resolves.toEqual(dbvalue);
});
it('findUserByEmail', async () => {
const expectSql = sql`
select ${sql.join(Object.values(fields), sql`,`)}
from ${table}
where ${fields.primaryEmail}=$1
`;
mockQuery.mockImplementationOnce(async (sql, values) => {
expectSqlAssert(sql, expectSql.sql);
expect(values).toEqual([mockUser.primaryEmail]);
return createMockQueryResult([dbvalue]);
});
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
await expect(findUserByEmail(mockUser.primaryEmail!)).resolves.toEqual(dbvalue);
});
it('findUserByPhone', async () => {
const expectSql = sql`
select ${sql.join(Object.values(fields), sql`,`)}
from ${table}
where ${fields.primaryPhone}=$1
`;
mockQuery.mockImplementationOnce(async (sql, values) => {
expectSqlAssert(sql, expectSql.sql);
expect(values).toEqual([mockUser.primaryPhone]);
return createMockQueryResult([dbvalue]);
});
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
await expect(findUserByPhone(mockUser.primaryPhone!)).resolves.toEqual(dbvalue);
});
it('findUserById', async () => {
const expectSql = sql`
select ${sql.join(Object.values(fields), sql`,`)}
from ${table}
where ${fields.id}=$1
`;
mockQuery.mockImplementationOnce(async (sql, values) => {
expectSqlAssert(sql, expectSql.sql);
expect(values).toEqual([mockUser.id]);
return createMockQueryResult([dbvalue]);
});
await expect(findUserById(mockUser.id)).resolves.toEqual(dbvalue);
});
it('findUserByIdentity', async () => {
const connectorId = 'github_foo';
const expectSql = sql`
select ${sql.join(Object.values(fields), sql`,`)}
from ${table}
where ${fields.identities}::json#>>'{${sql.identifier([connectorId])},userId}' = $1
`;
mockQuery.mockImplementationOnce(async (sql, values) => {
expectSqlAssert(sql, expectSql.sql);
expect(values).toEqual([mockUser.id]);
return createMockQueryResult([dbvalue]);
});
await expect(findUserByIdentity(connectorId, mockUser.id)).resolves.toEqual(dbvalue);
});
it('hasUser', async () => {
const expectSql = sql`
SELECT EXISTS(
select ${fields.id}
from ${table}
where ${fields.username}=$1
)
`;
mockQuery.mockImplementationOnce(async (sql, values) => {
expectSqlAssert(sql, expectSql.sql);
expect(values).toEqual([mockUser.username]);
return createMockQueryResult([{ exists: true }]);
});
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
await expect(hasUser(mockUser.username!)).resolves.toEqual(true);
});
it('hasUserWithId', async () => {
const expectSql = sql`
SELECT EXISTS(
select ${fields.id}
from ${table}
where ${fields.id}=$1
)
`;
mockQuery.mockImplementationOnce(async (sql, values) => {
expectSqlAssert(sql, expectSql.sql);
expect(values).toEqual([mockUser.id]);
return createMockQueryResult([{ exists: true }]);
});
await expect(hasUserWithId(mockUser.id)).resolves.toEqual(true);
});
it('hasUserWithEmail', async () => {
const expectSql = sql`
SELECT EXISTS(
select ${fields.primaryEmail}
from ${table}
where ${fields.primaryEmail}=$1
)
`;
mockQuery.mockImplementationOnce(async (sql, values) => {
expectSqlAssert(sql, expectSql.sql);
expect(values).toEqual([mockUser.primaryEmail]);
return createMockQueryResult([{ exists: true }]);
});
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
await expect(hasUserWithEmail(mockUser.primaryEmail!)).resolves.toEqual(true);
});
it('hasUserWithPhone', async () => {
const expectSql = sql`
SELECT EXISTS(
select ${fields.primaryPhone}
from ${table}
where ${fields.primaryPhone}=$1
)
`;
mockQuery.mockImplementationOnce(async (sql, values) => {
expectSqlAssert(sql, expectSql.sql);
expect(values).toEqual([mockUser.primaryPhone]);
return createMockQueryResult([{ exists: true }]);
});
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
await expect(hasUserWithPhone(mockUser.primaryPhone!)).resolves.toEqual(true);
});
it('hasUserWithIdentity', async () => {
const connectorId = 'github_foo';
const expectSql = sql`
SELECT EXISTS(
select ${fields.id}
from ${table}
where ${fields.identities}::json#>>'{${sql.identifier([connectorId])},userId}' = $1
)
`;
mockQuery.mockImplementationOnce(async (sql, values) => {
expectSqlAssert(sql, expectSql.sql);
expect(values).toEqual([mockUser.id]);
return createMockQueryResult([{ exists: true }]);
});
await expect(hasUserWithIdentity(connectorId, mockUser.id)).resolves.toEqual(true);
});
it('insertUser', async () => {
const expectSql = sql`
insert into ${table} (${sql.join(Object.values(fields), sql`, `)})
values (${sql.join(
Object.values(fields).map((_, index) => `$${index + 1}`),
sql`, `
)})
returning *
`;
mockQuery.mockImplementationOnce(async (sql, values) => {
expectSqlAssert(sql, expectSql.sql);
expect(values).toEqual(Users.fieldKeys.map((k) => convertToPrimitiveOrSql(k, mockUser[k])));
return createMockQueryResult([dbvalue]);
});
await expect(insertUser(mockUser)).resolves.toEqual(dbvalue);
});
it('countUsers', async () => {
const search = 'foo';
const expectSql = sql`
select count(*)
from ${table}
where ${fields.primaryEmail} like $1 or ${fields.primaryPhone} like $2 or ${fields.username} like $3 or ${fields.name} like $4
`;
mockQuery.mockImplementationOnce(async (sql, values) => {
expectSqlAssert(sql, expectSql.sql);
expect(values).toEqual([`%${search}%`, `%${search}%`, `%${search}%`, `%${search}%`]);
return createMockQueryResult([dbvalue]);
});
await expect(countUsers(search)).resolves.toEqual(dbvalue);
});
it('findUsers', async () => {
const search = 'foo';
const limit = 100;
const offset = 1;
const expectSql = sql`
select ${sql.join(Object.values(fields), sql`,`)}
from ${table}
where ${fields.primaryEmail} like $1 or ${fields.primaryPhone} like $2 or ${
fields.username
} like $3 or ${fields.name} like $4
limit $5
offset $6
`;
mockQuery.mockImplementationOnce(async (sql, values) => {
expectSqlAssert(sql, expectSql.sql);
expect(values).toEqual([
`%${search}%`,
`%${search}%`,
`%${search}%`,
`%${search}%`,
limit,
offset,
]);
return createMockQueryResult([dbvalue]);
});
await expect(findUsers(limit, offset, search)).resolves.toEqual([dbvalue]);
});
it('updateUserById', async () => {
const username = 'Joe';
const id = 'foo';
const expectSql = sql`
update ${table}
set ${fields.username}=$1
where ${fields.id}=$2
returning *
`;
mockQuery.mockImplementationOnce(async (sql, values) => {
expectSqlAssert(sql, expectSql.sql);
expect(values).toEqual([username, id]);
return createMockQueryResult([dbvalue]);
});
await expect(updateUserById(id, { username })).resolves.toEqual(dbvalue);
});
it('deleteUserById', async () => {
const id = 'foo';
const expectSql = sql`
delete from ${table}
where ${fields.id}=$1
`;
mockQuery.mockImplementationOnce(async (sql, values) => {
expectSqlAssert(sql, expectSql.sql);
expect(values).toEqual([id]);
return createMockQueryResult([dbvalue]);
});
await deleteUserById(id);
});
it('deleteUserById should throw with zero response', async () => {
const id = 'foo';
const expectSql = sql`
delete from ${table}
where ${fields.id}=$1
`;
mockQuery.mockImplementationOnce(async (sql, values) => {
expectSqlAssert(sql, expectSql.sql);
expect(values).toEqual([id]);
return createMockQueryResult([]);
});
await expect(deleteUserById(id)).rejects.toMatchError(new DeletionError(Users.table, id));
});
it('clearUserCustomDataById', 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([dbvalue]);
});
await clearUserCustomDataById(id);
});
});

View file

@ -118,7 +118,7 @@ export const updateUserById = async (id: string, set: Partial<OmitAutoSetFields<
export const deleteUserById = async (id: string) => {
const { rowCount } = await pool.query(sql`
delete from ${table}
where id=${id}
where ${fields.id}=${id}
`);
if (rowCount < 1) {
@ -130,7 +130,7 @@ export const clearUserCustomDataById = async (id: string) => {
const { rowCount } = await pool.query<User>(sql`
update ${table}
set ${fields.customData}='{}'::jsonb
where id=${id}
where ${fields.id}=${id}
`);
if (rowCount < 1) {

View file

@ -10,6 +10,12 @@ import {
SignInExperience,
BrandingStyle,
Language,
Connector,
Passcode,
PasscodeType,
UserLog,
UserLogType,
UserLogResult,
} from '@logto/schemas';
import pick from 'lodash.pick';
@ -174,3 +180,31 @@ export const mockSignInExperience: SignInExperience = {
disabled: [],
},
};
export const mockConnector: Connector = {
id: 'foo',
enabled: true,
config: {},
createdAt: 1_645_334_775_356,
};
export const mockPasscode: Passcode = {
id: 'foo',
interactionJti: 'jti',
phone: '888 888 8888',
email: 'foo@logto.io',
type: PasscodeType.SignIn,
code: 'asdfghjkl',
consumed: false,
tryCount: 2,
createdAt: 10,
};
export const mockUserLog: UserLog = {
id: 'foo',
userId: 'foo',
type: UserLogType.RegisterEmail,
result: UserLogResult.Success,
payload: {},
createdAt: 10,
};

View file

@ -2,12 +2,15 @@ import { createMockContext, Options } from '@shopify/jest-koa-mocks';
import Koa, { MiddlewareType, Context, Middleware } from 'koa';
import Router, { IRouterParamContext } from 'koa-router';
import { Provider } from 'oidc-provider';
import { createMockPool, createMockQueryResult, QueryResultRowType } from 'slonik';
import { createMockPool, createMockQueryResult, QueryResultType, QueryResultRowType } from 'slonik';
import { PrimitiveValueExpressionType } from 'slonik/dist/src/types.d';
import request from 'supertest';
import { AuthedRouter, AnonymousRouter } from '@/routes/types';
/**
* Slonik Query Mock Utils
**/
export const expectSqlAssert = (sql: string, expectSql: string) => {
expect(
sql
@ -22,6 +25,11 @@ export const expectSqlAssert = (sql: string, expectSql: string) => {
);
};
export type QueryType = (
sql: string,
values: readonly PrimitiveValueExpressionType[]
) => Promise<QueryResultType<QueryResultRowType>>;
export const createTestPool = <T extends QueryResultRowType>(
expectSql?: string,
returning?: T | ((sql: string, values: readonly PrimitiveValueExpressionType[]) => T)
@ -38,6 +46,9 @@ export const createTestPool = <T extends QueryResultRowType>(
},
});
/**
* Middleware & Context Mock Utils
**/
export const emptyMiddleware =
<StateT, ContextT>(): MiddlewareType<StateT, ContextT> =>
// Intend to mock the async middleware
@ -60,6 +71,9 @@ export const createContextWithRouteParameters = (
};
};
/**
* Supertest Request Mock Utils
**/
type RouteLauncher<T extends AuthedRouter | AnonymousRouter> = (router: T) => void;
type ProviderRouteLauncher<T extends AuthedRouter | AnonymousRouter> = (