Skip to content
This repository was archived by the owner on Jul 22, 2025. It is now read-only.

Commit bba22fd

Browse files
committed
test(postgres): add comprehensive tests for HtDataPostgresClient
- Implements a full test suite for `HtDataPostgresClient`. - Mocks dependencies (`Connection`, `Logger`) using `mocktail`. - Covers success and failure paths for all CRUD operations (`create`, `read`, `update`, `delete`). - Verifies correct SQL generation for simple and complex queries (`readAllByQuery`). - Tests exception mapping from `ServerException` to standard `HtHttpException` subtypes. - Validates that logging calls are made at the correct levels for different outcomes.
1 parent 3da9c5c commit bba22fd

File tree

2 files changed

+264
-3
lines changed

2 files changed

+264
-3
lines changed

pubspec.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ dependencies:
1717
postgres: ^3.5.6
1818

1919
dev_dependencies:
20+
equatable: ^2.0.7
2021
mocktail: ^1.0.4
2122
test: ^1.26.2
2223
very_good_analysis: ^9.0.0
Lines changed: 263 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,272 @@
11
// Not required for test files
22
// ignore_for_file: prefer_const_constructors
3+
4+
import 'package:equatable/equatable.dart';
5+
import 'package:ht_data_client/ht_data_client.dart';
36
import 'package:ht_data_postgres/ht_data_postgres.dart';
7+
import 'package:ht_shared/ht_shared.dart' hide ServerException;
8+
import 'package:logging/logging.dart';
9+
import 'package:mocktail/mocktail.dart';
10+
import 'package:postgres/postgres.dart';
411
import 'package:test/test.dart';
512

13+
class MockConnection extends Mock implements Connection {}
14+
15+
class MockLogger extends Mock implements Logger {}
16+
17+
class MockResult extends Mock implements Result {}
18+
19+
class MockRow extends Mock implements Row {}
20+
21+
class FakeSql extends Fake implements Sql {}
22+
23+
class TestModel extends Equatable {
24+
const TestModel({required this.id, required this.name});
25+
26+
factory TestModel.fromJson(Map<String, dynamic> json) {
27+
return TestModel(
28+
id: json['id'] as String,
29+
name: json['name'] as String,
30+
);
31+
}
32+
33+
final String id;
34+
final String name;
35+
36+
Map<String, dynamic> toJson() => {'id': id, 'name': name};
37+
38+
@override
39+
List<Object?> get props => [id, name];
40+
}
41+
642
void main() {
7-
group('HtDataPostgres', () {
8-
test('can be instantiated', () {
9-
expect(HtDataPostgres(), isNotNull);
43+
group('HtDataPostgresClient', () {
44+
late HtDataPostgresClient<TestModel> sut;
45+
late MockConnection mockConnection;
46+
late MockLogger mockLogger;
47+
48+
const tableName = 'test_models';
49+
final testModel = TestModel(id: '1', name: 'Test');
50+
final testModelJson = {'id': '1', 'name': 'Test'};
51+
52+
setUpAll(() {
53+
registerFallbackValue(FakeSql());
54+
});
55+
56+
setUp(() {
57+
mockConnection = MockConnection();
58+
mockLogger = MockLogger();
59+
sut = HtDataPostgresClient<TestModel>(
60+
connection: mockConnection,
61+
tableName: tableName,
62+
fromJson: TestModel.fromJson,
63+
toJson: (m) => m.toJson(),
64+
log: mockLogger,
65+
);
66+
});
67+
68+
group('create', () {
69+
test('should return created item on success', () async {
70+
final mockResult = MockResult();
71+
final mockRow = MockRow();
72+
when(() => mockRow.toColumnMap()).thenReturn(testModelJson);
73+
when(() => mockResult.first).thenReturn(mockRow);
74+
when(
75+
() => mockConnection.execute(
76+
any(),
77+
parameters: any(named: 'parameters'),
78+
),
79+
).thenAnswer((_) async => mockResult);
80+
81+
final result = await sut.create(item: testModel);
82+
83+
expect(result.data, testModel);
84+
verify(
85+
() => mockConnection.execute(
86+
any(
87+
that: isA<Sql>().having(
88+
(s) => s.statement,
89+
'statement',
90+
'INSERT INTO test_models (id, name) VALUES (@id, @name) RETURNING *;',
91+
),
92+
),
93+
parameters: testModelJson,
94+
),
95+
).called(1);
96+
verify(() => mockLogger.fine(any())).called(1);
97+
verify(() => mockLogger.finer(any())).called(1);
98+
});
99+
100+
test('should throw ConflictException on unique violation', () {
101+
final exception = ServerException(
102+
'unique violation',
103+
code: '23505',
104+
severity: Severity.error,
105+
);
106+
when(
107+
() => mockConnection.execute(
108+
any(),
109+
parameters: any(named: 'parameters'),
110+
),
111+
).thenThrow(exception);
112+
113+
expect(
114+
() => sut.create(item: testModel),
115+
throwsA(isA<ConflictException>()),
116+
);
117+
verify(() => mockLogger.severe(any(), any(), any())).called(1);
118+
});
119+
});
120+
121+
group('read', () {
122+
test('should return item when found', () async {
123+
final mockResult = MockResult();
124+
final mockRow = MockRow();
125+
when(() => mockRow.toColumnMap()).thenReturn(testModelJson);
126+
when(() => mockResult.isEmpty).thenReturn(false);
127+
when(() => mockResult.first).thenReturn(mockRow);
128+
when(
129+
() => mockConnection.execute(
130+
any(),
131+
parameters: any(named: 'parameters'),
132+
),
133+
).thenAnswer((_) async => mockResult);
134+
135+
final result = await sut.read(id: '1');
136+
137+
expect(result.data, testModel);
138+
verify(
139+
() => mockConnection.execute(
140+
any(
141+
that: isA<Sql>().having(
142+
(s) => s.statement,
143+
'statement',
144+
'SELECT * FROM test_models WHERE id = @id;',
145+
),
146+
),
147+
parameters: {'id': '1'},
148+
),
149+
).called(1);
150+
});
151+
152+
test('should throw NotFoundException when item is not found', () {
153+
final mockResult = MockResult();
154+
when(() => mockResult.isEmpty).thenReturn(true);
155+
when(
156+
() => mockConnection.execute(
157+
any(),
158+
parameters: any(named: 'parameters'),
159+
),
160+
).thenAnswer((_) async => mockResult);
161+
162+
expect(
163+
() => sut.read(id: '1'),
164+
throwsA(isA<NotFoundException>()),
165+
);
166+
});
167+
});
168+
169+
group('update', () {
170+
test('should return updated item on success', () async {
171+
final mockResult = MockResult();
172+
final mockRow = MockRow();
173+
when(() => mockRow.toColumnMap()).thenReturn(testModelJson);
174+
when(() => mockResult.isEmpty).thenReturn(false);
175+
when(() => mockResult.first).thenReturn(mockRow);
176+
when(
177+
() => mockConnection.execute(
178+
any(),
179+
parameters: any(named: 'parameters'),
180+
),
181+
).thenAnswer((_) async => mockResult);
182+
183+
final result = await sut.update(id: '1', item: testModel);
184+
185+
expect(result.data, testModel);
186+
verify(
187+
() => mockConnection.execute(
188+
any(
189+
that: isA<Sql>().having(
190+
(s) => s.statement,
191+
'statement',
192+
'UPDATE test_models SET name = @name WHERE id = @id RETURNING *;',
193+
),
194+
),
195+
parameters: {'name': 'Test', 'id': '1'},
196+
),
197+
).called(1);
198+
});
199+
});
200+
201+
group('delete', () {
202+
test('should complete normally on success', () async {
203+
final mockResult = MockResult();
204+
when(() => mockResult.affectedRows).thenReturn(1);
205+
when(
206+
() => mockConnection.execute(
207+
any(),
208+
parameters: any(named: 'parameters'),
209+
),
210+
).thenAnswer((_) async => mockResult);
211+
212+
await expectLater(sut.delete(id: '1'), completes);
213+
verify(() => mockLogger.finer(any())).called(1);
214+
});
215+
216+
test('should throw NotFoundException when item to delete is not found',
217+
() {
218+
final mockResult = MockResult();
219+
when(() => mockResult.affectedRows).thenReturn(0);
220+
when(
221+
() => mockConnection.execute(
222+
any(),
223+
parameters: any(named: 'parameters'),
224+
),
225+
).thenAnswer((_) async => mockResult);
226+
227+
expect(
228+
() => sut.delete(id: '1'),
229+
throwsA(isA<NotFoundException>()),
230+
);
231+
});
232+
});
233+
234+
group('readAllByQuery', () {
235+
test('should build correct query for `_in` operator', () async {
236+
final mockResult = MockResult();
237+
when(() => mockResult.map(any())).thenReturn([]);
238+
when(
239+
() => mockConnection.execute(
240+
any(),
241+
parameters: any(named: 'parameters'),
242+
),
243+
).thenAnswer((_) async => mockResult);
244+
245+
await sut.readAllByQuery({'id_in': '1,2,3'});
246+
247+
final captured = verify(
248+
() => mockConnection.execute(
249+
captureAny(),
250+
parameters: captureAny(named: 'parameters'),
251+
),
252+
).captured;
253+
254+
final sql = captured[0] as Sql;
255+
final params = captured[1] as Map<String, dynamic>;
256+
257+
expect(
258+
sql.statement,
259+
'SELECT * FROM test_models WHERE id IN (@p0, @p1, @p2) LIMIT 11;',
260+
);
261+
expect(params, {'p0': '1', 'p1': '2', 'p2': '3'});
262+
});
263+
264+
test('should throw ArgumentError for invalid column name', () {
265+
expect(
266+
() => sut.readAllByQuery({'id; DROP TABLE test_models;': '1'}),
267+
throwsA(isA<ArgumentError>()),
268+
);
269+
});
10270
});
11271
});
12272
}

0 commit comments

Comments
 (0)