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:
Jeroen Schweitzer
2026-01-01 15:00:04 +01:00
co-authored by Claude Opus 4.5
parent 50f643dd90
commit 67d2f777c8
3 changed files with 1188 additions and 0 deletions
@@ -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);
});
});
}