Files
tatlock-ui/test/harness/mock_api_client.dart
Jeroen SchweitzerandClaude Opus 4.5 d6fd9aea60 test: add test harness and widget tests for DataGrid pages
Add comprehensive testing infrastructure:
- Test harness with mock auth, API client, and fixtures
- Mock Dio interceptor for canned API responses
- Fixtures for containers, domains, users, groups

Add widget tests for all DataGrid pages:
- ContainersListPage (21 tests)
- ProxyHostsPage (16 tests)
- UsersListPage (21 tests)
- GroupsListPage (18 tests)

Add unit tests:
- RoomRegistry (28 tests)
- DataGrid components (35 tests)

Test count: 282 -> 357 (+75 tests)
Coverage: 5.7% -> 19.1% (+504 lines)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 21:18:34 +01:00

186 lines
5.1 KiB
Dart

import 'package:dio/dio.dart';
/// Mock Dio client for testing API interactions.
///
/// Allows registering canned responses for specific endpoints.
class MockApiClient {
MockApiClient() {
_dio = Dio(BaseOptions(baseUrl: 'https://test.api'))
..interceptors.add(_MockInterceptor(this));
}
late final Dio _dio;
final Map<String, _MockResponse> _responses = {};
final List<RequestOptions> _requests = [];
/// Get the mock Dio instance.
Dio get dio => _dio;
/// Get all recorded requests.
List<RequestOptions> get requests => List.unmodifiable(_requests);
/// Get the last request made.
RequestOptions? get lastRequest => _requests.isEmpty ? null : _requests.last;
/// Clear all recorded requests.
void clearRequests() => _requests.clear();
/// Register a mock response for an endpoint.
///
/// [method] is the HTTP method (GET, POST, PUT, DELETE).
/// [path] is the endpoint path (can include wildcards like /users/*).
/// [response] is the response data.
/// [statusCode] is the HTTP status code (default 200).
void when({
required String method,
required String path,
required dynamic response,
int statusCode = 200,
Map<String, dynamic>? headers,
}) {
final key = '${method.toUpperCase()}:$path';
_responses[key] = _MockResponse(
data: response,
statusCode: statusCode,
headers: headers ?? {},
);
}
/// Register a GET response.
void whenGet(String path, dynamic response, {int statusCode = 200}) {
when(method: 'GET', path: path, response: response, statusCode: statusCode);
}
/// Register a POST response.
void whenPost(String path, dynamic response, {int statusCode = 200}) {
when(method: 'POST', path: path, response: response, statusCode: statusCode);
}
/// Register a PUT response.
void whenPut(String path, dynamic response, {int statusCode = 200}) {
when(method: 'PUT', path: path, response: response, statusCode: statusCode);
}
/// Register a DELETE response.
void whenDelete(String path, dynamic response, {int statusCode = 200}) {
when(method: 'DELETE', path: path, response: response, statusCode: statusCode);
}
/// Register an error response.
void whenError({
required String method,
required String path,
required int statusCode,
String? message,
}) {
when(
method: method,
path: path,
response: {'error': message ?? 'Mock error'},
statusCode: statusCode,
);
}
/// Find a mock response for the given request.
_MockResponse? _findResponse(String method, String path) {
// Try exact match first
final exactKey = '${method.toUpperCase()}:$path';
if (_responses.containsKey(exactKey)) {
return _responses[exactKey];
}
// Try wildcard matches
for (final entry in _responses.entries) {
final pattern = entry.key;
if (_matchesPattern(pattern, exactKey)) {
return entry.value;
}
}
return null;
}
bool _matchesPattern(String pattern, String key) {
// Simple wildcard matching: /users/* matches /users/123
if (!pattern.contains('*')) return false;
final patternParts = pattern.split('/');
final keyParts = key.split('/');
if (patternParts.length != keyParts.length) return false;
for (var i = 0; i < patternParts.length; i++) {
if (patternParts[i] == '*') continue;
if (patternParts[i] != keyParts[i]) return false;
}
return true;
}
/// Reset all mock responses and requests.
void reset() {
_responses.clear();
_requests.clear();
}
}
class _MockResponse {
_MockResponse({
required this.data,
required this.statusCode,
required this.headers,
});
final dynamic data;
final int statusCode;
final Map<String, dynamic> headers;
}
class _MockInterceptor extends Interceptor {
_MockInterceptor(this._client);
final MockApiClient _client;
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
_client._requests.add(options);
final response = _client._findResponse(options.method, options.path);
if (response != null) {
if (response.statusCode >= 200 && response.statusCode < 300) {
handler.resolve(Response(
requestOptions: options,
data: response.data,
statusCode: response.statusCode,
headers: Headers.fromMap(
response.headers.map((k, v) => MapEntry(k, [v.toString()])),
),
));
} else {
handler.reject(DioException(
requestOptions: options,
response: Response(
requestOptions: options,
data: response.data,
statusCode: response.statusCode,
),
type: DioExceptionType.badResponse,
));
}
} else {
// No mock registered - return 404
handler.reject(DioException(
requestOptions: options,
response: Response(
requestOptions: options,
data: {'error': 'No mock registered for ${options.method} ${options.path}'},
statusCode: 404,
),
type: DioExceptionType.badResponse,
message: 'No mock registered for ${options.method} ${options.path}',
));
}
}
}