diff --git a/packages/core/package.json b/packages/core/package.json index de2faf7bf..0db848dca 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -67,6 +67,7 @@ "nock": "^13.2.2", "openapi-types": "^9.1.0", "prettier": "^2.3.2", + "snake-case": "^3.0.4", "supertest": "^6.2.2", "ts-jest": "^27.0.5", "tsc-watch": "^4.4.0", diff --git a/packages/core/src/queries/application.test.ts b/packages/core/src/queries/application.test.ts new file mode 100644 index 000000000..ae34be48d --- /dev/null +++ b/packages/core/src/queries/application.test.ts @@ -0,0 +1,176 @@ +import { Applications } from '@logto/schemas'; +import { + createMockPool, + createMockQueryResult, + sql, + QueryResultType, + QueryResultRowType, +} from 'slonik'; +import { PrimitiveValueExpressionType } from 'slonik/dist/src/types.d'; +import { snakeCase } from 'snake-case'; + +import { + convertToIdentifiers, + convertToPrimitiveOrSql, + excludeAutoSetFields, +} from '@/database/utils'; +import { DeletionError } from '@/errors/SlonikError'; +import { mockApplication } from '@/utils/mock'; +import { expectSqlAssert } from '@/utils/test-utils'; + +import { + findTotalNumberOfApplications, + findAllApplications, + findApplicationById, + insertApplication, + updateApplicationById, + deleteApplicationById, +} from './application'; + +type MockQuery = ( + sql: string, + values: PrimitiveValueExpressionType +) => Promise>; + +const mockQuery: jest.MockedFunction = jest.fn(); + +jest.mock('@/database/pool', () => + createMockPool({ + query: async (sql, values) => { + return mockQuery(sql, values); + }, + }) +); + +describe('appliaction query', () => { + const { table, fields } = convertToIdentifiers(Applications); + + it('findTotalNumberOfApplications', 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(findTotalNumberOfApplications()).resolves.toEqual({ count: 10 }); + }); + + it('findAllApplications', async () => { + const limit = 10; + const offset = 1; + const rowData = { id: 'foo' }; + + 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([rowData]); + }); + + await expect(findAllApplications(limit, offset)).resolves.toEqual([rowData]); + }); + + it('findApplicationById', 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 findApplicationById(id); + }); + + it('insertApplication', async () => { + const keys = excludeAutoSetFields(Applications.fieldKeys); + + const expectSql = ` + insert into "applications" (${keys.map((k) => `"${snakeCase(k)}"`).join(', ')}) + values (${keys.map((_, index) => `$${index + 1}`).join(', ')}) + returning * + `; + + mockQuery.mockImplementationOnce(async (sql, values) => { + const rowData = { id: 'foo' }; + expectSqlAssert(sql, expectSql); + + expect(values).toEqual(keys.map((k) => convertToPrimitiveOrSql(k, mockApplication[k]))); + + return createMockQueryResult([rowData]); + }); + + await insertApplication(mockApplication); + }); + + it('updateApplicationById', async () => { + const id = 'foo'; + const description = 'des'; + + const expectSql = sql` + update ${table} + set ${fields.description}=$1 + where ${fields.id}=$2 + returning * + `; + + mockQuery.mockImplementationOnce(async (sql, values) => { + expectSqlAssert(sql, expectSql.sql); + expect(values).toEqual([description, id]); + + return createMockQueryResult([{ id, description }]); + }); + + await updateApplicationById(id, { description }); + }); + + it('deleteApplicationById', 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([{ id }]); + }); + + await deleteApplicationById(id); + }); + + it('deleteApplicationById throw error if return row count is 0', async () => { + const id = 'foo'; + + mockQuery.mockImplementationOnce(async () => { + return createMockQueryResult([]); + }); + + await expect(deleteApplicationById(id)).rejects.toMatchError( + new DeletionError(Applications.table, id) + ); + }); +}); diff --git a/packages/core/src/queries/application.ts b/packages/core/src/queries/application.ts index 6c3ec83ff..4b22e5218 100644 --- a/packages/core/src/queries/application.ts +++ b/packages/core/src/queries/application.ts @@ -46,7 +46,7 @@ export const updateApplicationById = async ( export const deleteApplicationById = 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/utils/test-utils.ts b/packages/core/src/utils/test-utils.ts index d6517155f..810cc8443 100644 --- a/packages/core/src/utils/test-utils.ts +++ b/packages/core/src/utils/test-utils.ts @@ -8,6 +8,20 @@ import request from 'supertest'; import { AuthedRouter, AnonymousRouter } from '@/routes/types'; +export const expectSqlAssert = (sql: string, expectSql: string) => { + expect( + sql + .split('\n') + .map((row) => row.trim()) + .filter((row) => row) + ).toEqual( + expectSql + .split('\n') + .map((row) => row.trim()) + .filter((row) => row) + ); +}; + export const createTestPool = ( expectSql?: string, returning?: T | ((sql: string, values: readonly PrimitiveValueExpressionType[]) => T) @@ -15,12 +29,7 @@ export const createTestPool = ( createMockPool({ query: async (sql, values) => { if (expectSql) { - expect( - sql - .split('\n') - .map((row) => row.trim()) - .filter((row) => row) - ).toEqual(expectSql.split('\n')); + expectSqlAssert(sql, expectSql); } return createMockQueryResult( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 884a8d744..c246443a0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -110,6 +110,7 @@ importers: query-string: ^7.0.1 slonik: ^23.8.3 slonik-interceptor-preset: ^1.2.10 + snake-case: ^3.0.4 snakecase-keys: ^5.1.0 supertest: ^6.2.2 ts-jest: ^27.0.5 @@ -165,6 +166,7 @@ importers: nock: 13.2.2 openapi-types: 9.3.1 prettier: 2.5.1 + snake-case: 3.0.4 supertest: 6.2.2 ts-jest: 27.1.1_0ef321b3552d50570980838f9f6677eb tsc-watch: 4.5.0_typescript@4.5.5 @@ -5066,7 +5068,6 @@ packages: dependencies: no-case: 3.0.4 tslib: 2.3.1 - dev: false /dot-prop/5.3.0: resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} @@ -6285,6 +6286,7 @@ packages: /graceful-fs/4.2.9: resolution: {integrity: sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==} + requiresBuild: true dev: true /handlebars/4.7.7: @@ -8704,7 +8706,6 @@ packages: resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} dependencies: tslib: 2.3.1 - dev: false /lowercase-keys/1.0.1: resolution: {integrity: sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==} @@ -9179,7 +9180,6 @@ packages: dependencies: lower-case: 2.0.2 tslib: 2.3.1 - dev: false /nock/13.2.2: resolution: {integrity: sha512-PcBHuvl9i6zfaJ50A7LS55oU+nFLv8htXIhffJO+FxyfibdZ4jEvd9kTuvkrJireBFIGMZ+oUIRpMK5gU9h//g==} @@ -11588,7 +11588,6 @@ packages: dependencies: dot-case: 3.0.4 tslib: 2.3.1 - dev: false /snakecase-keys/5.1.2: resolution: {integrity: sha512-fvtDQZqPBqYb0dEY97TGuOMbN2NJ05Tj4MaoKwjTKkmjcG6mrd58JYGr23UWZRi6Aqv49Fk4HtjTIStOQenaug==}