Skip to content

Commit fc433ea

Browse files
committed
refactor(drizzle): extract shared test helpers and fix code review issues
Extract duplicated mock factory into shared test-utils.ts, remove unused variables and dead code, strengthen assertions with encrypted-value param checks, fix fallthrough tests to verify SQL output, remove unnecessary as-any cast, and add MatchIndexOpts schema-extraction test.
1 parent 2576b48 commit fc433ea

File tree

4 files changed

+853
-30
lines changed

4 files changed

+853
-30
lines changed
Lines changed: 116 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,22 @@
1-
import type { ProtectClient } from '@cipherstash/protect/client'
2-
import { PgDialect, pgTable } from 'drizzle-orm/pg-core'
3-
import { describe, expect, it, vi } from 'vitest'
4-
import { createProtectOperators, encryptedType } from '../src/pg'
1+
import { pgTable } from 'drizzle-orm/pg-core'
2+
import { describe, expect, it } from 'vitest'
3+
import { encryptedType } from '../src/pg'
4+
import { ProtectOperatorError } from '../src/pg/operators'
5+
import { setup } from './test-utils'
56

67
const docsTable = pgTable('json_docs', {
78
metadata: encryptedType<Record<string, unknown>>('metadata', {
89
dataType: 'json',
910
searchableJson: true,
1011
}),
12+
noJsonConfig: encryptedType<string>('no_json_config', {
13+
equality: true,
14+
}),
1115
})
1216

13-
function createMockProtectClient() {
14-
const encryptedSelector = '{"v":"encrypted-selector"}'
15-
const encryptQuery = vi.fn(async (terms: unknown[]) => ({
16-
data: terms.map(() => encryptedSelector),
17-
}))
18-
19-
return {
20-
client: { encryptQuery } as unknown as ProtectClient,
21-
encryptQuery,
22-
encryptedSelector,
23-
}
24-
}
25-
2617
describe('createProtectOperators JSONB selector typing', () => {
2718
it('casts jsonbPathQueryFirst selector params to eql_v2_encrypted', async () => {
28-
const { client, encryptQuery, encryptedSelector } =
29-
createMockProtectClient()
30-
const protectOps = createProtectOperators(client)
31-
const dialect = new PgDialect()
19+
const { encryptQuery, protectOps, dialect } = setup()
3220

3321
const condition = await protectOps.jsonbPathQueryFirst(
3422
docsTable.metadata,
@@ -40,17 +28,15 @@ describe('createProtectOperators JSONB selector typing', () => {
4028
/eql_v2\.jsonb_path_query_first\([^,]+,\s*\$\d+::eql_v2_encrypted\)/,
4129
)
4230
expect(query.params).toHaveLength(1)
43-
expect(typeof query.params[0]).toBe('string')
31+
expect(query.params[0]).toContain('encrypted-value')
4432
expect(encryptQuery).toHaveBeenCalledTimes(1)
4533
expect(encryptQuery.mock.calls[0]?.[0]).toMatchObject([
4634
{ queryType: 'steVecSelector' },
4735
])
4836
})
4937

5038
it('casts jsonbPathExists selector params to eql_v2_encrypted', async () => {
51-
const { client } = createMockProtectClient()
52-
const protectOps = createProtectOperators(client)
53-
const dialect = new PgDialect()
39+
const { protectOps, dialect } = setup()
5440

5541
const condition = await protectOps.jsonbPathExists(
5642
docsTable.metadata,
@@ -62,13 +48,11 @@ describe('createProtectOperators JSONB selector typing', () => {
6248
/eql_v2\.jsonb_path_exists\([^,]+,\s*\$\d+::eql_v2_encrypted\)/,
6349
)
6450
expect(query.params).toHaveLength(1)
65-
expect(typeof query.params[0]).toBe('string')
51+
expect(query.params[0]).toContain('encrypted-value')
6652
})
6753

6854
it('casts jsonbGet selector params to eql_v2_encrypted', async () => {
69-
const { client } = createMockProtectClient()
70-
const protectOps = createProtectOperators(client)
71-
const dialect = new PgDialect()
55+
const { protectOps, dialect } = setup()
7256

7357
const condition = await protectOps.jsonbGet(
7458
docsTable.metadata,
@@ -78,6 +62,108 @@ describe('createProtectOperators JSONB selector typing', () => {
7862

7963
expect(query.sql).toMatch(/->\s*\$\d+::eql_v2_encrypted/)
8064
expect(query.params).toHaveLength(1)
81-
expect(typeof query.params[0]).toBe('string')
65+
expect(query.params[0]).toContain('encrypted-value')
66+
})
67+
})
68+
69+
describe('JSONB operator error paths', () => {
70+
it('throws ProtectOperatorError when column lacks searchableJson config', () => {
71+
const { protectOps } = setup()
72+
73+
expect(() =>
74+
protectOps.jsonbPathQueryFirst(docsTable.noJsonConfig, '$.path'),
75+
).toThrow(ProtectOperatorError)
76+
77+
expect(() =>
78+
protectOps.jsonbPathQueryFirst(docsTable.noJsonConfig, '$.path'),
79+
).toThrow(/searchableJson/)
80+
})
81+
82+
it('throws ProtectOperatorError for jsonbPathExists without searchableJson', () => {
83+
const { protectOps } = setup()
84+
85+
expect(() =>
86+
protectOps.jsonbPathExists(docsTable.noJsonConfig, '$.path'),
87+
).toThrow(ProtectOperatorError)
88+
})
89+
90+
it('throws ProtectOperatorError for jsonbGet without searchableJson', () => {
91+
const { protectOps } = setup()
92+
93+
expect(() =>
94+
protectOps.jsonbGet(docsTable.noJsonConfig, '$.path'),
95+
).toThrow(ProtectOperatorError)
96+
})
97+
98+
it('error includes column name and operator context', () => {
99+
const { protectOps } = setup()
100+
101+
try {
102+
protectOps.jsonbPathQueryFirst(docsTable.noJsonConfig, '$.path')
103+
expect.fail('Should have thrown')
104+
} catch (error) {
105+
expect(error).toBeInstanceOf(ProtectOperatorError)
106+
const opError = error as ProtectOperatorError
107+
expect(opError.context?.columnName).toBe('no_json_config')
108+
expect(opError.context?.operator).toBe('jsonbPathQueryFirst')
109+
}
110+
})
111+
})
112+
113+
describe('JSONB batched operations', () => {
114+
it('batches jsonbPathQueryFirst and jsonbGet in protectOps.and()', async () => {
115+
const { encryptQuery, protectOps, dialect } = setup()
116+
117+
const condition = await protectOps.and(
118+
protectOps.jsonbPathQueryFirst(docsTable.metadata, '$.profile.email'),
119+
protectOps.jsonbGet(docsTable.metadata, '$.profile.name'),
120+
)
121+
const query = dialect.sqlToQuery(condition)
122+
123+
expect(query.sql).toContain('eql_v2.jsonb_path_query_first')
124+
expect(query.sql).toContain('->')
125+
// Both values should be encrypted
126+
expect(encryptQuery).toHaveBeenCalled()
127+
})
128+
129+
it('batches jsonbPathExists and jsonbPathQueryFirst in protectOps.or()', async () => {
130+
const { encryptQuery, protectOps, dialect } = setup()
131+
132+
const condition = await protectOps.or(
133+
protectOps.jsonbPathExists(docsTable.metadata, '$.profile.email'),
134+
protectOps.jsonbPathQueryFirst(docsTable.metadata, '$.profile.name'),
135+
)
136+
const query = dialect.sqlToQuery(condition)
137+
138+
expect(query.sql).toContain('eql_v2.jsonb_path_exists')
139+
expect(query.sql).toContain('eql_v2.jsonb_path_query_first')
140+
// Both values should be encrypted
141+
expect(encryptQuery).toHaveBeenCalled()
142+
})
143+
144+
it('generates SQL combining conditions with AND', async () => {
145+
const { protectOps, dialect } = setup()
146+
147+
const condition = await protectOps.and(
148+
protectOps.jsonbPathQueryFirst(docsTable.metadata, '$.a'),
149+
protectOps.jsonbPathExists(docsTable.metadata, '$.b'),
150+
)
151+
const query = dialect.sqlToQuery(condition)
152+
153+
// AND combines conditions
154+
expect(query.sql).toContain(' and ')
155+
})
156+
157+
it('generates SQL combining conditions with OR', async () => {
158+
const { protectOps, dialect } = setup()
159+
160+
const condition = await protectOps.or(
161+
protectOps.jsonbPathQueryFirst(docsTable.metadata, '$.a'),
162+
protectOps.jsonbPathExists(docsTable.metadata, '$.b'),
163+
)
164+
const query = dialect.sqlToQuery(condition)
165+
166+
// OR combines conditions
167+
expect(query.sql).toContain(' or ')
82168
})
83169
})

0 commit comments

Comments
 (0)