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> = (