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>
186 lines
5.1 KiB
Dart
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}',
|
|
));
|
|
}
|
|
}
|
|
}
|