From fb6a1dc2369ea7a9126fd8d8a2ccdb4d578c962f Mon Sep 17 00:00:00 2001 From: simeng-li <simeng@silverhand.io> Date: Mon, 28 Feb 2022 14:30:27 +0800 Subject: [PATCH] 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 --- packages/core/src/oidc/adapter.test.ts | 6 +- packages/core/src/oidc/adapter.ts | 4 +- packages/core/src/queries/application.test.ts | 20 +- packages/core/src/queries/connector.test.ts | 132 ++++++ .../src/queries/oidc-model-instance.test.ts | 166 ++++++++ .../core/src/queries/oidc-model-instance.ts | 2 +- packages/core/src/queries/passcode.test.ts | 191 +++++++++ packages/core/src/queries/passcode.ts | 4 +- packages/core/src/queries/resource.test.ts | 179 +++++++++ packages/core/src/queries/resource.ts | 6 +- packages/core/src/queries/roles.test.ts | 57 +++ packages/core/src/queries/roles.ts | 4 +- packages/core/src/queries/scope.test.ts | 99 +++++ packages/core/src/queries/scope.ts | 4 +- packages/core/src/queries/setting.test.ts | 60 +++ .../src/queries/sign-in-experience.test.ts | 71 ++++ packages/core/src/queries/user-log.test.ts | 66 +++ packages/core/src/queries/user.test.ts | 378 ++++++++++++++++++ packages/core/src/queries/user.ts | 4 +- packages/core/src/utils/mock.ts | 34 ++ packages/core/src/utils/test-utils.ts | 16 +- 21 files changed, 1469 insertions(+), 34 deletions(-) create mode 100644 packages/core/src/queries/connector.test.ts create mode 100644 packages/core/src/queries/oidc-model-instance.test.ts create mode 100644 packages/core/src/queries/passcode.test.ts create mode 100644 packages/core/src/queries/resource.test.ts create mode 100644 packages/core/src/queries/roles.test.ts create mode 100644 packages/core/src/queries/scope.test.ts create mode 100644 packages/core/src/queries/setting.test.ts create mode 100644 packages/core/src/queries/sign-in-experience.test.ts create mode 100644 packages/core/src/queries/user-log.test.ts create mode 100644 packages/core/src/queries/user.test.ts diff --git a/packages/core/src/oidc/adapter.test.ts b/packages/core/src/oidc/adapter.test.ts index 9ac7bc306..4f26519de 100644 --- a/packages/core/src/oidc/adapter.test.ts +++ b/packages/core/src/oidc/adapter.test.ts @@ -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); diff --git a/packages/core/src/oidc/adapter.ts b/packages/core/src/oidc/adapter.ts index 434cec3e5..7d0603218 100644 --- a/packages/core/src/oidc/adapter.ts +++ b/packages/core/src/oidc/adapter.ts @@ -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), }; } diff --git a/packages/core/src/queries/application.test.ts b/packages/core/src/queries/application.test.ts index ae34be48d..f62dee800 100644 --- a/packages/core/src/queries/application.test.ts +++ b/packages/core/src/queries/application.test.ts @@ -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 () => { diff --git a/packages/core/src/queries/connector.test.ts b/packages/core/src/queries/connector.test.ts new file mode 100644 index 000000000..e87bdbb97 --- /dev/null +++ b/packages/core/src/queries/connector.test.ts @@ -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, + }); + }); +}); diff --git a/packages/core/src/queries/oidc-model-instance.test.ts b/packages/core/src/queries/oidc-model-instance.test.ts new file mode 100644 index 000000000..a876ec3ba --- /dev/null +++ b/packages/core/src/queries/oidc-model-instance.test.ts @@ -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); + }); +}); diff --git a/packages/core/src/queries/oidc-model-instance.ts b/packages/core/src/queries/oidc-model-instance.ts index f663d92cb..73699565f 100644 --- a/packages/core/src/queries/oidc-model-instance.ts +++ b/packages/core/src/queries/oidc-model-instance.ts @@ -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} diff --git a/packages/core/src/queries/passcode.test.ts b/packages/core/src/queries/passcode.test.ts new file mode 100644 index 000000000..461c20fef --- /dev/null +++ b/packages/core/src/queries/passcode.test.ts @@ -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(',')}`) + ); + }); +}); diff --git a/packages/core/src/queries/passcode.ts b/packages/core/src/queries/passcode.ts index 0980b1126..5244e003d 100644 --- a/packages/core/src/queries/passcode.ts +++ b/packages/core/src/queries/passcode.ts @@ -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) { diff --git a/packages/core/src/queries/resource.test.ts b/packages/core/src/queries/resource.test.ts new file mode 100644 index 000000000..4ef583184 --- /dev/null +++ b/packages/core/src/queries/resource.test.ts @@ -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) + ); + }); +}); diff --git a/packages/core/src/queries/resource.ts b/packages/core/src/queries/resource.ts index 9f85c075c..90b81b442 100644 --- a/packages/core/src/queries/resource.ts +++ b/packages/core/src/queries/resource.ts @@ -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) { diff --git a/packages/core/src/queries/roles.test.ts b/packages/core/src/queries/roles.test.ts new file mode 100644 index 000000000..7b635f196 --- /dev/null +++ b/packages/core/src/queries/roles.test.ts @@ -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]); + }); +}); diff --git a/packages/core/src/queries/roles.ts b/packages/core/src/queries/roles.ts index 1c1bc92c4..da31d5d00 100644 --- a/packages/core/src/queries/roles.ts +++ b/packages/core/src/queries/roles.ts @@ -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`, `)}) `); diff --git a/packages/core/src/queries/scope.test.ts b/packages/core/src/queries/scope.test.ts new file mode 100644 index 000000000..94439f7ff --- /dev/null +++ b/packages/core/src/queries/scope.test.ts @@ -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) + ); + }); +}); diff --git a/packages/core/src/queries/scope.ts b/packages/core/src/queries/scope.ts index cce48f48d..fe90ccfe8 100644 --- a/packages/core/src/queries/scope.ts +++ b/packages/core/src/queries/scope.ts @@ -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) { diff --git a/packages/core/src/queries/setting.test.ts b/packages/core/src/queries/setting.test.ts new file mode 100644 index 000000000..314726bc0 --- /dev/null +++ b/packages/core/src/queries/setting.test.ts @@ -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); + }); +}); diff --git a/packages/core/src/queries/sign-in-experience.test.ts b/packages/core/src/queries/sign-in-experience.test.ts new file mode 100644 index 000000000..b3708da7b --- /dev/null +++ b/packages/core/src/queries/sign-in-experience.test.ts @@ -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); + }); +}); diff --git a/packages/core/src/queries/user-log.test.ts b/packages/core/src/queries/user-log.test.ts new file mode 100644 index 000000000..ec8a95454 --- /dev/null +++ b/packages/core/src/queries/user-log.test.ts @@ -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); + }); +}); diff --git a/packages/core/src/queries/user.test.ts b/packages/core/src/queries/user.test.ts new file mode 100644 index 000000000..81c144325 --- /dev/null +++ b/packages/core/src/queries/user.test.ts @@ -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); + }); +}); diff --git a/packages/core/src/queries/user.ts b/packages/core/src/queries/user.ts index dfb8036c1..449d50e2d 100644 --- a/packages/core/src/queries/user.ts +++ b/packages/core/src/queries/user.ts @@ -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) { diff --git a/packages/core/src/utils/mock.ts b/packages/core/src/utils/mock.ts index 62224bc6e..793da7f7e 100644 --- a/packages/core/src/utils/mock.ts +++ b/packages/core/src/utils/mock.ts @@ -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, +}; diff --git a/packages/core/src/utils/test-utils.ts b/packages/core/src/utils/test-utils.ts index 3571c5e88..a5db552d4 100644 --- a/packages/core/src/utils/test-utils.ts +++ b/packages/core/src/utils/test-utils.ts @@ -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> = (