test: add unit tests for Control Room data models
Add comprehensive test coverage for: - ProxyHostModel: 37 tests for JSON converters and serialization - ContainerModel: 44 tests for deserialization, entity conversion, state parsing - StackModel: 55 tests for type/status parsing, timestamps, entity properties 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
50f643dd90
commit
67d2f777c8
@@ -0,0 +1,458 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:tatlock_ui/features/control_room/containers/data/models/container_model.dart';
|
||||
import 'package:tatlock_ui/features/control_room/containers/domain/entities/container.dart';
|
||||
|
||||
void main() {
|
||||
group('ContainerModel', () {
|
||||
group('fromJson', () {
|
||||
test('deserializes minimal container', () {
|
||||
final json = {
|
||||
'Id': 'abc123def456',
|
||||
'Names': ['/my-container'],
|
||||
'Image': 'nginx:latest',
|
||||
'State': 'running',
|
||||
'Status': 'Up 2 hours',
|
||||
};
|
||||
|
||||
final model = ContainerModel.fromJson(json);
|
||||
|
||||
expect(model.id, equals('abc123def456'));
|
||||
expect(model.names, equals(['/my-container']));
|
||||
expect(model.image, equals('nginx:latest'));
|
||||
expect(model.state, equals('running'));
|
||||
expect(model.status, equals('Up 2 hours'));
|
||||
});
|
||||
|
||||
test('deserializes full container with all fields', () {
|
||||
final json = {
|
||||
'Id': 'abc123def456789xyz',
|
||||
'Names': ['/my-container', '/alias'],
|
||||
'Image': 'nginx:latest',
|
||||
'State': 'running',
|
||||
'Status': 'Up 2 hours',
|
||||
'Labels': {
|
||||
'com.docker.compose.project': 'mystack',
|
||||
'maintainer': 'test@example.com',
|
||||
},
|
||||
'Ports': [
|
||||
{'PrivatePort': 80, 'PublicPort': 8080, 'Type': 'tcp'},
|
||||
],
|
||||
'Mounts': [
|
||||
{
|
||||
'Type': 'bind',
|
||||
'Source': '/host/path',
|
||||
'Destination': '/container/path',
|
||||
'Mode': 'rw',
|
||||
'RW': true,
|
||||
},
|
||||
],
|
||||
'NetworkSettings': {
|
||||
'Networks': {'bridge': {}, 'custom': {}},
|
||||
},
|
||||
'Created': 1704067200, // 2024-01-01 00:00:00 UTC
|
||||
'SizeRw': 1024,
|
||||
'SizeRootFs': 2048,
|
||||
};
|
||||
|
||||
final model = ContainerModel.fromJson(json);
|
||||
|
||||
expect(model.labels['com.docker.compose.project'], equals('mystack'));
|
||||
expect(model.ports.length, equals(1));
|
||||
expect(model.mounts.length, equals(1));
|
||||
expect(model.networkSettings?.networks.length, equals(2));
|
||||
expect(model.created, equals(1704067200));
|
||||
expect(model.sizeRw, equals(1024));
|
||||
expect(model.sizeRootFs, equals(2048));
|
||||
});
|
||||
|
||||
test('handles empty lists and maps', () {
|
||||
final json = {
|
||||
'Id': 'abc123',
|
||||
'Names': ['/container'],
|
||||
'Image': 'alpine',
|
||||
'State': 'exited',
|
||||
'Status': 'Exited (0)',
|
||||
'Labels': <String, String>{},
|
||||
'Ports': <Map<String, dynamic>>[],
|
||||
'Mounts': <Map<String, dynamic>>[],
|
||||
};
|
||||
|
||||
final model = ContainerModel.fromJson(json);
|
||||
|
||||
expect(model.labels, isEmpty);
|
||||
expect(model.ports, isEmpty);
|
||||
expect(model.mounts, isEmpty);
|
||||
});
|
||||
});
|
||||
|
||||
group('toEntity', () {
|
||||
test('converts to entity with truncated ID', () {
|
||||
final model = ContainerModel(
|
||||
id: 'abc123def456789xyz',
|
||||
names: ['/my-container'],
|
||||
image: 'nginx:latest',
|
||||
state: 'running',
|
||||
status: 'Up 2 hours',
|
||||
);
|
||||
|
||||
final entity = model.toEntity();
|
||||
|
||||
expect(entity.id, equals('abc123def456')); // Truncated to 12 chars
|
||||
expect(entity.fullId, equals('abc123def456789xyz'));
|
||||
});
|
||||
|
||||
test('strips leading slash from container name', () {
|
||||
final model = ContainerModel(
|
||||
id: 'abc123def456',
|
||||
names: ['/my-container'],
|
||||
image: 'nginx:latest',
|
||||
state: 'running',
|
||||
status: 'Up 2 hours',
|
||||
);
|
||||
|
||||
final entity = model.toEntity();
|
||||
|
||||
expect(entity.name, equals('my-container'));
|
||||
});
|
||||
|
||||
test('uses ID as name when names list is empty', () {
|
||||
final model = ContainerModel(
|
||||
id: 'abc123def456',
|
||||
names: [],
|
||||
image: 'nginx:latest',
|
||||
state: 'running',
|
||||
status: 'Up 2 hours',
|
||||
);
|
||||
|
||||
final entity = model.toEntity();
|
||||
|
||||
expect(entity.name, equals('abc123def456'));
|
||||
});
|
||||
|
||||
test('extracts stack name from compose labels', () {
|
||||
final model = ContainerModel(
|
||||
id: 'abc123def456',
|
||||
names: ['/container'],
|
||||
image: 'nginx',
|
||||
state: 'running',
|
||||
status: 'Up',
|
||||
labels: {'com.docker.compose.project': 'my-stack'},
|
||||
);
|
||||
|
||||
final entity = model.toEntity();
|
||||
|
||||
expect(entity.stackName, equals('my-stack'));
|
||||
expect(entity.stackId, equals('my-stack'));
|
||||
});
|
||||
|
||||
test('handles missing stack labels', () {
|
||||
final model = ContainerModel(
|
||||
id: 'abc123def456',
|
||||
names: ['/container'],
|
||||
image: 'nginx',
|
||||
state: 'running',
|
||||
status: 'Up',
|
||||
labels: {},
|
||||
);
|
||||
|
||||
final entity = model.toEntity();
|
||||
|
||||
expect(entity.stackName, isNull);
|
||||
expect(entity.stackId, isNull);
|
||||
});
|
||||
|
||||
test('converts created timestamp to DateTime', () {
|
||||
final model = ContainerModel(
|
||||
id: 'abc123def456',
|
||||
names: ['/container'],
|
||||
image: 'nginx',
|
||||
state: 'running',
|
||||
status: 'Up',
|
||||
created: 1704067200, // 2024-01-01 00:00:00 UTC
|
||||
);
|
||||
|
||||
final entity = model.toEntity();
|
||||
|
||||
expect(entity.createdAt, isNotNull);
|
||||
expect(entity.createdAt!.year, equals(2024));
|
||||
expect(entity.createdAt!.month, equals(1));
|
||||
expect(entity.createdAt!.day, equals(1));
|
||||
});
|
||||
|
||||
test('handles null created timestamp', () {
|
||||
final model = ContainerModel(
|
||||
id: 'abc123def456',
|
||||
names: ['/container'],
|
||||
image: 'nginx',
|
||||
state: 'running',
|
||||
status: 'Up',
|
||||
created: null,
|
||||
);
|
||||
|
||||
final entity = model.toEntity();
|
||||
|
||||
expect(entity.createdAt, isNull);
|
||||
});
|
||||
|
||||
test('extracts networks from network settings', () {
|
||||
final model = ContainerModel(
|
||||
id: 'abc123def456',
|
||||
names: ['/container'],
|
||||
image: 'nginx',
|
||||
state: 'running',
|
||||
status: 'Up',
|
||||
networkSettings: const NetworkSettingsModel(
|
||||
networks: {'bridge': {}, 'custom-network': {}},
|
||||
),
|
||||
);
|
||||
|
||||
final entity = model.toEntity();
|
||||
|
||||
expect(entity.networks, containsAll(['bridge', 'custom-network']));
|
||||
});
|
||||
|
||||
test('handles null network settings', () {
|
||||
final model = ContainerModel(
|
||||
id: 'abc123def456',
|
||||
names: ['/container'],
|
||||
image: 'nginx',
|
||||
state: 'running',
|
||||
status: 'Up',
|
||||
networkSettings: null,
|
||||
);
|
||||
|
||||
final entity = model.toEntity();
|
||||
|
||||
expect(entity.networks, isEmpty);
|
||||
});
|
||||
});
|
||||
|
||||
group('_parseState', () {
|
||||
final testCases = {
|
||||
'created': ContainerState.created,
|
||||
'Created': ContainerState.created,
|
||||
'CREATED': ContainerState.created,
|
||||
'running': ContainerState.running,
|
||||
'Running': ContainerState.running,
|
||||
'RUNNING': ContainerState.running,
|
||||
'paused': ContainerState.paused,
|
||||
'Paused': ContainerState.paused,
|
||||
'restarting': ContainerState.restarting,
|
||||
'Restarting': ContainerState.restarting,
|
||||
'removing': ContainerState.removing,
|
||||
'Removing': ContainerState.removing,
|
||||
'exited': ContainerState.exited,
|
||||
'Exited': ContainerState.exited,
|
||||
'dead': ContainerState.dead,
|
||||
'Dead': ContainerState.dead,
|
||||
'unknown': ContainerState.exited, // Unknown defaults to exited
|
||||
'': ContainerState.exited, // Empty defaults to exited
|
||||
'invalid': ContainerState.exited, // Invalid defaults to exited
|
||||
};
|
||||
|
||||
testCases.forEach((input, expected) {
|
||||
test('parses "$input" to ${expected.name}', () {
|
||||
final model = ContainerModel(
|
||||
id: 'abc123def456',
|
||||
names: ['/container'],
|
||||
image: 'nginx',
|
||||
state: input,
|
||||
status: 'Status',
|
||||
);
|
||||
|
||||
final entity = model.toEntity();
|
||||
|
||||
expect(entity.state, equals(expected));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
group('PortModel', () {
|
||||
group('fromJson', () {
|
||||
test('deserializes port with all fields', () {
|
||||
final json = {
|
||||
'IP': '0.0.0.0',
|
||||
'PrivatePort': 80,
|
||||
'PublicPort': 8080,
|
||||
'Type': 'tcp',
|
||||
};
|
||||
|
||||
final model = PortModel.fromJson(json);
|
||||
|
||||
expect(model.ip, equals('0.0.0.0'));
|
||||
expect(model.privatePort, equals(80));
|
||||
expect(model.publicPort, equals(8080));
|
||||
expect(model.type, equals('tcp'));
|
||||
});
|
||||
|
||||
test('deserializes port with minimal fields', () {
|
||||
final json = {
|
||||
'PrivatePort': 443,
|
||||
};
|
||||
|
||||
final model = PortModel.fromJson(json);
|
||||
|
||||
expect(model.ip, isNull);
|
||||
expect(model.privatePort, equals(443));
|
||||
expect(model.publicPort, isNull);
|
||||
expect(model.type, equals('tcp')); // Default
|
||||
});
|
||||
|
||||
test('handles UDP type', () {
|
||||
final json = {
|
||||
'PrivatePort': 53,
|
||||
'Type': 'udp',
|
||||
};
|
||||
|
||||
final model = PortModel.fromJson(json);
|
||||
|
||||
expect(model.type, equals('udp'));
|
||||
});
|
||||
});
|
||||
|
||||
group('toEntity', () {
|
||||
test('converts to PortMapping entity', () {
|
||||
const model = PortModel(
|
||||
ip: '127.0.0.1',
|
||||
privatePort: 80,
|
||||
publicPort: 8080,
|
||||
type: 'tcp',
|
||||
);
|
||||
|
||||
final entity = model.toEntity();
|
||||
|
||||
expect(entity.hostIp, equals('127.0.0.1'));
|
||||
expect(entity.hostPort, equals(8080));
|
||||
expect(entity.containerPort, equals(80));
|
||||
expect(entity.protocol, equals('tcp'));
|
||||
});
|
||||
|
||||
test('handles null public port', () {
|
||||
const model = PortModel(
|
||||
privatePort: 80,
|
||||
publicPort: null,
|
||||
);
|
||||
|
||||
final entity = model.toEntity();
|
||||
|
||||
expect(entity.hostPort, isNull);
|
||||
expect(entity.containerPort, equals(80));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
group('MountModel', () {
|
||||
group('fromJson', () {
|
||||
test('deserializes bind mount', () {
|
||||
final json = {
|
||||
'Type': 'bind',
|
||||
'Source': '/host/path',
|
||||
'Destination': '/container/path',
|
||||
'Mode': 'rw',
|
||||
'RW': true,
|
||||
};
|
||||
|
||||
final model = MountModel.fromJson(json);
|
||||
|
||||
expect(model.type, equals('bind'));
|
||||
expect(model.source, equals('/host/path'));
|
||||
expect(model.destination, equals('/container/path'));
|
||||
expect(model.mode, equals('rw'));
|
||||
expect(model.rw, isTrue);
|
||||
});
|
||||
|
||||
test('deserializes volume mount', () {
|
||||
final json = {
|
||||
'Type': 'volume',
|
||||
'Source': 'my-volume',
|
||||
'Destination': '/data',
|
||||
'Mode': 'ro',
|
||||
'RW': false,
|
||||
};
|
||||
|
||||
final model = MountModel.fromJson(json);
|
||||
|
||||
expect(model.type, equals('volume'));
|
||||
expect(model.rw, isFalse);
|
||||
});
|
||||
|
||||
test('uses defaults for missing fields', () {
|
||||
final json = {
|
||||
'Type': 'bind',
|
||||
'Source': '/src',
|
||||
'Destination': '/dst',
|
||||
};
|
||||
|
||||
final model = MountModel.fromJson(json);
|
||||
|
||||
expect(model.mode, equals('rw'));
|
||||
expect(model.rw, isTrue);
|
||||
});
|
||||
});
|
||||
|
||||
group('toEntity', () {
|
||||
test('converts read-write mount to entity', () {
|
||||
const model = MountModel(
|
||||
type: 'bind',
|
||||
source: '/host',
|
||||
destination: '/container',
|
||||
rw: true,
|
||||
);
|
||||
|
||||
final entity = model.toEntity();
|
||||
|
||||
expect(entity.type, equals('bind'));
|
||||
expect(entity.source, equals('/host'));
|
||||
expect(entity.destination, equals('/container'));
|
||||
expect(entity.mode, equals('rw'));
|
||||
});
|
||||
|
||||
test('converts read-only mount to entity', () {
|
||||
const model = MountModel(
|
||||
type: 'volume',
|
||||
source: 'vol',
|
||||
destination: '/data',
|
||||
rw: false,
|
||||
);
|
||||
|
||||
final entity = model.toEntity();
|
||||
|
||||
expect(entity.mode, equals('ro'));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
group('NetworkSettingsModel', () {
|
||||
test('deserializes network settings', () {
|
||||
final json = {
|
||||
'Networks': {
|
||||
'bridge': {'IPAddress': '172.17.0.2'},
|
||||
'custom': {'IPAddress': '10.0.0.5'},
|
||||
},
|
||||
};
|
||||
|
||||
final model = NetworkSettingsModel.fromJson(json);
|
||||
|
||||
expect(model.networks.keys, containsAll(['bridge', 'custom']));
|
||||
});
|
||||
|
||||
test('handles empty networks', () {
|
||||
final json = {
|
||||
'Networks': <String, dynamic>{},
|
||||
};
|
||||
|
||||
final model = NetworkSettingsModel.fromJson(json);
|
||||
|
||||
expect(model.networks, isEmpty);
|
||||
});
|
||||
|
||||
test('uses empty map as default', () {
|
||||
final json = <String, dynamic>{};
|
||||
|
||||
final model = NetworkSettingsModel.fromJson(json);
|
||||
|
||||
expect(model.networks, isEmpty);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,335 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:tatlock_ui/features/control_room/npm/data/models/proxy_host_model.dart';
|
||||
|
||||
void main() {
|
||||
group('NullableIntOrBoolConverter', () {
|
||||
const converter = NullableIntOrBoolConverter();
|
||||
|
||||
group('fromJson', () {
|
||||
test('returns null for null input', () {
|
||||
expect(converter.fromJson(null), isNull);
|
||||
});
|
||||
|
||||
test('returns null for false input', () {
|
||||
expect(converter.fromJson(false), isNull);
|
||||
});
|
||||
|
||||
test('returns null for true input (treats as invalid)', () {
|
||||
// true is not a valid ID, so we return null
|
||||
expect(converter.fromJson(true), isNull);
|
||||
});
|
||||
|
||||
test('returns int for int input', () {
|
||||
expect(converter.fromJson(42), equals(42));
|
||||
});
|
||||
|
||||
test('returns int for positive int', () {
|
||||
expect(converter.fromJson(1), equals(1));
|
||||
});
|
||||
|
||||
test('returns int for zero', () {
|
||||
expect(converter.fromJson(0), equals(0));
|
||||
});
|
||||
|
||||
test('converts double to int', () {
|
||||
expect(converter.fromJson(42.0), equals(42));
|
||||
});
|
||||
|
||||
test('converts double with decimals to int (truncates)', () {
|
||||
expect(converter.fromJson(42.9), equals(42));
|
||||
});
|
||||
|
||||
test('returns null for string input', () {
|
||||
expect(converter.fromJson('42'), isNull);
|
||||
});
|
||||
|
||||
test('returns null for empty string', () {
|
||||
expect(converter.fromJson(''), isNull);
|
||||
});
|
||||
});
|
||||
|
||||
group('toJson', () {
|
||||
test('returns null for null input', () {
|
||||
expect(converter.toJson(null), isNull);
|
||||
});
|
||||
|
||||
test('returns int for int input', () {
|
||||
expect(converter.toJson(42), equals(42));
|
||||
});
|
||||
|
||||
test('returns zero for zero input', () {
|
||||
expect(converter.toJson(0), equals(0));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
group('IntOrBoolConverter', () {
|
||||
const converter = IntOrBoolConverter();
|
||||
|
||||
group('fromJson', () {
|
||||
test('returns 0 for null input', () {
|
||||
expect(converter.fromJson(null), equals(0));
|
||||
});
|
||||
|
||||
test('returns 0 for false input', () {
|
||||
expect(converter.fromJson(false), equals(0));
|
||||
});
|
||||
|
||||
test('returns 1 for true input', () {
|
||||
expect(converter.fromJson(true), equals(1));
|
||||
});
|
||||
|
||||
test('returns int for int input', () {
|
||||
expect(converter.fromJson(42), equals(42));
|
||||
});
|
||||
|
||||
test('returns 0 for zero input', () {
|
||||
expect(converter.fromJson(0), equals(0));
|
||||
});
|
||||
|
||||
test('returns 1 for one input', () {
|
||||
expect(converter.fromJson(1), equals(1));
|
||||
});
|
||||
|
||||
test('converts double to int', () {
|
||||
expect(converter.fromJson(42.0), equals(42));
|
||||
});
|
||||
|
||||
test('converts double with decimals to int (truncates)', () {
|
||||
expect(converter.fromJson(42.9), equals(42));
|
||||
});
|
||||
|
||||
test('returns 0 for string input', () {
|
||||
expect(converter.fromJson('42'), equals(0));
|
||||
});
|
||||
|
||||
test('returns 0 for empty string', () {
|
||||
expect(converter.fromJson(''), equals(0));
|
||||
});
|
||||
|
||||
test('returns 0 for invalid types', () {
|
||||
expect(converter.fromJson([1, 2, 3]), equals(0));
|
||||
expect(converter.fromJson({'key': 'value'}), equals(0));
|
||||
});
|
||||
});
|
||||
|
||||
group('toJson', () {
|
||||
test('returns int for int input', () {
|
||||
expect(converter.toJson(42), equals(42));
|
||||
});
|
||||
|
||||
test('returns 0 for zero input', () {
|
||||
expect(converter.toJson(0), equals(0));
|
||||
});
|
||||
|
||||
test('returns 1 for one input', () {
|
||||
expect(converter.toJson(1), equals(1));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
group('ProxyHostModel JSON serialization', () {
|
||||
test('deserializes with boolean certificate_id (false)', () {
|
||||
final json = {
|
||||
'id': 1,
|
||||
'domain_names': ['example.com'],
|
||||
'forward_scheme': 'http',
|
||||
'forward_host': 'localhost',
|
||||
'forward_port': 8080,
|
||||
'certificate_id': false, // NPM returns false for no certificate
|
||||
};
|
||||
|
||||
final model = ProxyHostModel.fromJson(json);
|
||||
|
||||
expect(model.certificateId, isNull);
|
||||
});
|
||||
|
||||
test('deserializes with int certificate_id', () {
|
||||
final json = {
|
||||
'id': 1,
|
||||
'domain_names': ['example.com'],
|
||||
'forward_scheme': 'http',
|
||||
'forward_host': 'localhost',
|
||||
'forward_port': 8080,
|
||||
'certificate_id': 42,
|
||||
};
|
||||
|
||||
final model = ProxyHostModel.fromJson(json);
|
||||
|
||||
expect(model.certificateId, equals(42));
|
||||
});
|
||||
|
||||
test('deserializes with null certificate_id', () {
|
||||
final json = {
|
||||
'id': 1,
|
||||
'domain_names': ['example.com'],
|
||||
'forward_scheme': 'http',
|
||||
'forward_host': 'localhost',
|
||||
'forward_port': 8080,
|
||||
'certificate_id': null,
|
||||
};
|
||||
|
||||
final model = ProxyHostModel.fromJson(json);
|
||||
|
||||
expect(model.certificateId, isNull);
|
||||
});
|
||||
|
||||
test('deserializes with boolean http2_support (true)', () {
|
||||
final json = {
|
||||
'id': 1,
|
||||
'domain_names': ['example.com'],
|
||||
'forward_scheme': 'http',
|
||||
'forward_host': 'localhost',
|
||||
'forward_port': 8080,
|
||||
'http2_support': true, // NPM can return boolean
|
||||
};
|
||||
|
||||
final model = ProxyHostModel.fromJson(json);
|
||||
|
||||
expect(model.http2Support, equals(1));
|
||||
});
|
||||
|
||||
test('deserializes with boolean http2_support (false)', () {
|
||||
final json = {
|
||||
'id': 1,
|
||||
'domain_names': ['example.com'],
|
||||
'forward_scheme': 'http',
|
||||
'forward_host': 'localhost',
|
||||
'forward_port': 8080,
|
||||
'http2_support': false,
|
||||
};
|
||||
|
||||
final model = ProxyHostModel.fromJson(json);
|
||||
|
||||
expect(model.http2Support, equals(0));
|
||||
});
|
||||
|
||||
test('deserializes with int http2_support', () {
|
||||
final json = {
|
||||
'id': 1,
|
||||
'domain_names': ['example.com'],
|
||||
'forward_scheme': 'http',
|
||||
'forward_host': 'localhost',
|
||||
'forward_port': 8080,
|
||||
'http2_support': 1,
|
||||
};
|
||||
|
||||
final model = ProxyHostModel.fromJson(json);
|
||||
|
||||
expect(model.http2Support, equals(1));
|
||||
});
|
||||
|
||||
test('deserializes with all boolean flags', () {
|
||||
final json = {
|
||||
'id': 1,
|
||||
'domain_names': ['example.com'],
|
||||
'forward_scheme': 'https',
|
||||
'forward_host': 'localhost',
|
||||
'forward_port': 443,
|
||||
'ssl_forced': true,
|
||||
'certificate_id': false,
|
||||
'enabled': true,
|
||||
'http2_support': true,
|
||||
'hsts_enabled': false,
|
||||
'access_list_id': false,
|
||||
'caching_enabled': true,
|
||||
'block_exploits': true,
|
||||
'allow_websocket_upgrade': false,
|
||||
};
|
||||
|
||||
final model = ProxyHostModel.fromJson(json);
|
||||
|
||||
expect(model.sslForced, isTrue);
|
||||
expect(model.certificateId, isNull);
|
||||
expect(model.enabled, equals(1));
|
||||
expect(model.http2Support, equals(1));
|
||||
expect(model.hstsEnabled, equals(0));
|
||||
expect(model.accessListId, isNull);
|
||||
expect(model.cachingEnabled, equals(1));
|
||||
expect(model.blockExploits, equals(1));
|
||||
expect(model.allowWebsocketUpgrade, equals(0));
|
||||
});
|
||||
|
||||
test('deserializes with all int flags', () {
|
||||
final json = {
|
||||
'id': 1,
|
||||
'domain_names': ['example.com'],
|
||||
'forward_scheme': 'https',
|
||||
'forward_host': 'localhost',
|
||||
'forward_port': 443,
|
||||
'ssl_forced': false,
|
||||
'certificate_id': 5,
|
||||
'enabled': 1,
|
||||
'http2_support': 1,
|
||||
'hsts_enabled': 0,
|
||||
'access_list_id': 3,
|
||||
'caching_enabled': 1,
|
||||
'block_exploits': 1,
|
||||
'allow_websocket_upgrade': 0,
|
||||
};
|
||||
|
||||
final model = ProxyHostModel.fromJson(json);
|
||||
|
||||
expect(model.sslForced, isFalse);
|
||||
expect(model.certificateId, equals(5));
|
||||
expect(model.enabled, equals(1));
|
||||
expect(model.http2Support, equals(1));
|
||||
expect(model.hstsEnabled, equals(0));
|
||||
expect(model.accessListId, equals(3));
|
||||
expect(model.cachingEnabled, equals(1));
|
||||
expect(model.blockExploits, equals(1));
|
||||
expect(model.allowWebsocketUpgrade, equals(0));
|
||||
});
|
||||
|
||||
test('toEntity converts correctly', () {
|
||||
final model = ProxyHostModel(
|
||||
id: 1,
|
||||
domainNames: ['example.com', 'www.example.com'],
|
||||
forwardScheme: 'https',
|
||||
forwardHost: 'backend',
|
||||
forwardPort: 8080,
|
||||
sslForced: true,
|
||||
certificateId: 5,
|
||||
enabled: 1,
|
||||
http2Support: 1,
|
||||
hstsEnabled: 1,
|
||||
cachingEnabled: 0,
|
||||
blockExploits: 1,
|
||||
allowWebsocketUpgrade: 1,
|
||||
);
|
||||
|
||||
final entity = model.toEntity();
|
||||
|
||||
expect(entity.id, equals(1));
|
||||
expect(entity.domainNames, equals(['example.com', 'www.example.com']));
|
||||
expect(entity.forwardScheme, equals('https'));
|
||||
expect(entity.forwardHost, equals('backend'));
|
||||
expect(entity.forwardPort, equals(8080));
|
||||
expect(entity.forceSSL, isTrue);
|
||||
expect(entity.sslEnabled, isTrue); // certificateId > 0
|
||||
expect(entity.certificateId, equals(5));
|
||||
expect(entity.enabled, isTrue);
|
||||
expect(entity.http2Support, isTrue);
|
||||
expect(entity.hstsEnabled, isTrue);
|
||||
expect(entity.cacheAssets, isFalse);
|
||||
expect(entity.blockExploits, isTrue);
|
||||
expect(entity.websocketSupport, isTrue);
|
||||
});
|
||||
|
||||
test('toEntity handles null certificateId correctly', () {
|
||||
final model = ProxyHostModel(
|
||||
id: 1,
|
||||
domainNames: ['example.com'],
|
||||
forwardScheme: 'http',
|
||||
forwardHost: 'backend',
|
||||
forwardPort: 80,
|
||||
certificateId: null,
|
||||
);
|
||||
|
||||
final entity = model.toEntity();
|
||||
|
||||
expect(entity.sslEnabled, isFalse);
|
||||
expect(entity.certificateId, isNull);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,395 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:tatlock_ui/features/control_room/stacks/data/models/stack_model.dart';
|
||||
import 'package:tatlock_ui/features/control_room/stacks/domain/entities/stack.dart';
|
||||
|
||||
void main() {
|
||||
group('StackModel', () {
|
||||
group('fromJson', () {
|
||||
test('deserializes minimal stack', () {
|
||||
final json = {
|
||||
'id': 'stack-123',
|
||||
'name': 'my-stack',
|
||||
};
|
||||
|
||||
final model = StackModel.fromJson(json);
|
||||
|
||||
expect(model.id, equals('stack-123'));
|
||||
expect(model.name, equals('my-stack'));
|
||||
expect(model.containerCount, equals(0)); // Default
|
||||
expect(model.runningCount, equals(0)); // Default
|
||||
});
|
||||
|
||||
test('deserializes full stack with all fields', () {
|
||||
final json = {
|
||||
'id': 'stack-456',
|
||||
'name': 'production-stack',
|
||||
'type': 'compose',
|
||||
'status': 'active',
|
||||
'container_count': 5,
|
||||
'running_count': 5,
|
||||
'compose_file': '/path/to/docker-compose.yml',
|
||||
'environment': 'production',
|
||||
'created_at': '2024-01-15T10:30:00Z',
|
||||
'updated_at': '2024-01-20T15:45:00Z',
|
||||
};
|
||||
|
||||
final model = StackModel.fromJson(json);
|
||||
|
||||
expect(model.id, equals('stack-456'));
|
||||
expect(model.name, equals('production-stack'));
|
||||
expect(model.typeString, equals('compose'));
|
||||
expect(model.statusString, equals('active'));
|
||||
expect(model.containerCount, equals(5));
|
||||
expect(model.runningCount, equals(5));
|
||||
expect(model.composeFile, equals('/path/to/docker-compose.yml'));
|
||||
expect(model.environment, equals('production'));
|
||||
expect(model.createdAt, equals('2024-01-15T10:30:00Z'));
|
||||
expect(model.updatedAt, equals('2024-01-20T15:45:00Z'));
|
||||
});
|
||||
|
||||
test('handles null optional fields', () {
|
||||
final json = {
|
||||
'id': 'stack-789',
|
||||
'name': 'minimal-stack',
|
||||
'type': null,
|
||||
'status': null,
|
||||
'compose_file': null,
|
||||
'environment': null,
|
||||
'created_at': null,
|
||||
'updated_at': null,
|
||||
};
|
||||
|
||||
final model = StackModel.fromJson(json);
|
||||
|
||||
expect(model.typeString, isNull);
|
||||
expect(model.statusString, isNull);
|
||||
expect(model.composeFile, isNull);
|
||||
expect(model.environment, isNull);
|
||||
expect(model.createdAt, isNull);
|
||||
expect(model.updatedAt, isNull);
|
||||
});
|
||||
|
||||
test('uses defaults for missing container counts', () {
|
||||
final json = {
|
||||
'id': 'stack-abc',
|
||||
'name': 'test-stack',
|
||||
};
|
||||
|
||||
final model = StackModel.fromJson(json);
|
||||
|
||||
expect(model.containerCount, equals(0));
|
||||
expect(model.runningCount, equals(0));
|
||||
});
|
||||
});
|
||||
|
||||
group('toEntity', () {
|
||||
test('converts to entity with all fields', () {
|
||||
const model = StackModel(
|
||||
id: 'stack-123',
|
||||
name: 'my-stack',
|
||||
typeString: 'compose',
|
||||
statusString: 'active',
|
||||
containerCount: 3,
|
||||
runningCount: 2,
|
||||
composeFile: '/compose.yml',
|
||||
environment: 'staging',
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
updatedAt: '2024-01-02T12:00:00Z',
|
||||
);
|
||||
|
||||
final entity = model.toEntity();
|
||||
|
||||
expect(entity.id, equals('stack-123'));
|
||||
expect(entity.name, equals('my-stack'));
|
||||
expect(entity.type, equals(StackType.compose));
|
||||
expect(entity.status, equals(StackStatus.active));
|
||||
expect(entity.containerCount, equals(3));
|
||||
expect(entity.runningCount, equals(2));
|
||||
expect(entity.composeFile, equals('/compose.yml'));
|
||||
expect(entity.environment, equals('staging'));
|
||||
expect(entity.createdAt, isNotNull);
|
||||
expect(entity.updatedAt, isNotNull);
|
||||
});
|
||||
|
||||
test('handles null timestamps', () {
|
||||
const model = StackModel(
|
||||
id: 'stack-123',
|
||||
name: 'my-stack',
|
||||
createdAt: null,
|
||||
updatedAt: null,
|
||||
);
|
||||
|
||||
final entity = model.toEntity();
|
||||
|
||||
expect(entity.createdAt, isNull);
|
||||
expect(entity.updatedAt, isNull);
|
||||
});
|
||||
|
||||
test('handles invalid timestamp format', () {
|
||||
const model = StackModel(
|
||||
id: 'stack-123',
|
||||
name: 'my-stack',
|
||||
createdAt: 'invalid-date',
|
||||
updatedAt: 'not-a-date',
|
||||
);
|
||||
|
||||
final entity = model.toEntity();
|
||||
|
||||
expect(entity.createdAt, isNull);
|
||||
expect(entity.updatedAt, isNull);
|
||||
});
|
||||
|
||||
test('parses valid ISO 8601 timestamps', () {
|
||||
const model = StackModel(
|
||||
id: 'stack-123',
|
||||
name: 'my-stack',
|
||||
createdAt: '2024-06-15T14:30:00.000Z',
|
||||
updatedAt: '2024-06-16T09:15:30.500Z',
|
||||
);
|
||||
|
||||
final entity = model.toEntity();
|
||||
|
||||
expect(entity.createdAt?.year, equals(2024));
|
||||
expect(entity.createdAt?.month, equals(6));
|
||||
expect(entity.createdAt?.day, equals(15));
|
||||
expect(entity.updatedAt?.hour, equals(9));
|
||||
expect(entity.updatedAt?.minute, equals(15));
|
||||
});
|
||||
});
|
||||
|
||||
group('_parseStackType', () {
|
||||
final testCases = {
|
||||
'compose': StackType.compose,
|
||||
'Compose': StackType.compose,
|
||||
'COMPOSE': StackType.compose,
|
||||
'swarm': StackType.swarm,
|
||||
'Swarm': StackType.swarm,
|
||||
'SWARM': StackType.swarm,
|
||||
'kubernetes': StackType.kubernetes,
|
||||
'Kubernetes': StackType.kubernetes,
|
||||
'KUBERNETES': StackType.kubernetes,
|
||||
'k8s': StackType.kubernetes,
|
||||
'K8S': StackType.kubernetes,
|
||||
'K8s': StackType.kubernetes,
|
||||
'unknown': StackType.compose, // Defaults to compose
|
||||
'': StackType.compose, // Empty defaults to compose
|
||||
'invalid': StackType.compose, // Invalid defaults to compose
|
||||
};
|
||||
|
||||
testCases.forEach((input, expected) {
|
||||
test('parses "$input" to ${expected.name}', () {
|
||||
final model = StackModel(
|
||||
id: 'stack-123',
|
||||
name: 'test',
|
||||
typeString: input,
|
||||
);
|
||||
|
||||
final entity = model.toEntity();
|
||||
|
||||
expect(entity.type, equals(expected));
|
||||
});
|
||||
});
|
||||
|
||||
test('handles null type', () {
|
||||
const model = StackModel(
|
||||
id: 'stack-123',
|
||||
name: 'test',
|
||||
typeString: null,
|
||||
);
|
||||
|
||||
final entity = model.toEntity();
|
||||
|
||||
expect(entity.type, equals(StackType.compose));
|
||||
});
|
||||
});
|
||||
|
||||
group('_parseStackStatus', () {
|
||||
final testCases = {
|
||||
'active': StackStatus.active,
|
||||
'Active': StackStatus.active,
|
||||
'ACTIVE': StackStatus.active,
|
||||
'running': StackStatus.active,
|
||||
'Running': StackStatus.active,
|
||||
'RUNNING': StackStatus.active,
|
||||
'inactive': StackStatus.inactive,
|
||||
'Inactive': StackStatus.inactive,
|
||||
'INACTIVE': StackStatus.inactive,
|
||||
'stopped': StackStatus.inactive,
|
||||
'Stopped': StackStatus.inactive,
|
||||
'STOPPED': StackStatus.inactive,
|
||||
'error': StackStatus.error,
|
||||
'Error': StackStatus.error,
|
||||
'ERROR': StackStatus.error,
|
||||
'unknown': StackStatus.unknown, // Defaults to unknown
|
||||
'': StackStatus.unknown, // Empty defaults to unknown
|
||||
'invalid': StackStatus.unknown, // Invalid defaults to unknown
|
||||
};
|
||||
|
||||
testCases.forEach((input, expected) {
|
||||
test('parses "$input" to ${expected.name}', () {
|
||||
final model = StackModel(
|
||||
id: 'stack-123',
|
||||
name: 'test',
|
||||
statusString: input,
|
||||
);
|
||||
|
||||
final entity = model.toEntity();
|
||||
|
||||
expect(entity.status, equals(expected));
|
||||
});
|
||||
});
|
||||
|
||||
test('handles null status', () {
|
||||
const model = StackModel(
|
||||
id: 'stack-123',
|
||||
name: 'test',
|
||||
statusString: null,
|
||||
);
|
||||
|
||||
final entity = model.toEntity();
|
||||
|
||||
expect(entity.status, equals(StackStatus.unknown));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
group('Stack entity', () {
|
||||
group('isHealthy', () {
|
||||
test('returns true when all containers are running', () {
|
||||
const stack = Stack(
|
||||
id: 'stack-1',
|
||||
name: 'healthy-stack',
|
||||
containerCount: 5,
|
||||
runningCount: 5,
|
||||
);
|
||||
|
||||
expect(stack.isHealthy, isTrue);
|
||||
});
|
||||
|
||||
test('returns false when some containers are not running', () {
|
||||
const stack = Stack(
|
||||
id: 'stack-1',
|
||||
name: 'partial-stack',
|
||||
containerCount: 5,
|
||||
runningCount: 3,
|
||||
);
|
||||
|
||||
expect(stack.isHealthy, isFalse);
|
||||
});
|
||||
|
||||
test('returns false when no containers are running', () {
|
||||
const stack = Stack(
|
||||
id: 'stack-1',
|
||||
name: 'stopped-stack',
|
||||
containerCount: 5,
|
||||
runningCount: 0,
|
||||
);
|
||||
|
||||
expect(stack.isHealthy, isFalse);
|
||||
});
|
||||
|
||||
test('returns false when containerCount is zero', () {
|
||||
const stack = Stack(
|
||||
id: 'stack-1',
|
||||
name: 'empty-stack',
|
||||
containerCount: 0,
|
||||
runningCount: 0,
|
||||
);
|
||||
|
||||
expect(stack.isHealthy, isFalse);
|
||||
});
|
||||
});
|
||||
|
||||
group('hasRunningContainers', () {
|
||||
test('returns true when some containers are running', () {
|
||||
const stack = Stack(
|
||||
id: 'stack-1',
|
||||
name: 'running-stack',
|
||||
containerCount: 5,
|
||||
runningCount: 2,
|
||||
);
|
||||
|
||||
expect(stack.hasRunningContainers, isTrue);
|
||||
});
|
||||
|
||||
test('returns false when no containers are running', () {
|
||||
const stack = Stack(
|
||||
id: 'stack-1',
|
||||
name: 'stopped-stack',
|
||||
containerCount: 5,
|
||||
runningCount: 0,
|
||||
);
|
||||
|
||||
expect(stack.hasRunningContainers, isFalse);
|
||||
});
|
||||
|
||||
test('returns true when all containers are running', () {
|
||||
const stack = Stack(
|
||||
id: 'stack-1',
|
||||
name: 'healthy-stack',
|
||||
containerCount: 3,
|
||||
runningCount: 3,
|
||||
);
|
||||
|
||||
expect(stack.hasRunningContainers, isTrue);
|
||||
});
|
||||
});
|
||||
|
||||
group('isPartial', () {
|
||||
test('returns true when some but not all containers are running', () {
|
||||
const stack = Stack(
|
||||
id: 'stack-1',
|
||||
name: 'partial-stack',
|
||||
containerCount: 5,
|
||||
runningCount: 3,
|
||||
);
|
||||
|
||||
expect(stack.isPartial, isTrue);
|
||||
});
|
||||
|
||||
test('returns false when all containers are running', () {
|
||||
const stack = Stack(
|
||||
id: 'stack-1',
|
||||
name: 'healthy-stack',
|
||||
containerCount: 5,
|
||||
runningCount: 5,
|
||||
);
|
||||
|
||||
expect(stack.isPartial, isFalse);
|
||||
});
|
||||
|
||||
test('returns false when no containers are running', () {
|
||||
const stack = Stack(
|
||||
id: 'stack-1',
|
||||
name: 'stopped-stack',
|
||||
containerCount: 5,
|
||||
runningCount: 0,
|
||||
);
|
||||
|
||||
expect(stack.isPartial, isFalse);
|
||||
});
|
||||
|
||||
test('returns false when containerCount is zero', () {
|
||||
const stack = Stack(
|
||||
id: 'stack-1',
|
||||
name: 'empty-stack',
|
||||
containerCount: 0,
|
||||
runningCount: 0,
|
||||
);
|
||||
|
||||
expect(stack.isPartial, isFalse);
|
||||
});
|
||||
|
||||
test('returns true when only one of many containers is running', () {
|
||||
const stack = Stack(
|
||||
id: 'stack-1',
|
||||
name: 'degraded-stack',
|
||||
containerCount: 10,
|
||||
runningCount: 1,
|
||||
);
|
||||
|
||||
expect(stack.isPartial, isTrue);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user