using RackPeek.Domain.Discovery;
using RackPeek.Domain.Resources;
using RackPeek.Domain.Resources.Connections;
using RackPeek.Domain.Resources.Services;
using RackPeek.Domain.Resources.SystemResources;
namespace Tests.Discovery;
///
/// A placeholder name is a stand-in, and a later collector that knows the machine's
/// real name should be allowed to replace it.
///
/// A sweep of a routed subnet can see that something answers and nothing else, so
/// it writes host-1a2b3c4d. Run the firewall collector and that same machine has
/// a DHCP name; ask the hypervisor and it has a guest name. Without this the
/// inventory would keep the hash for ever and the better name would be discarded
/// on every run.
///
///
/// The line it must not cross is a name a person typed. That is what
/// records, and once set it is never unset.
///
///
public class PlaceholderNameUpgradeTests {
private const string _mac = "bc:24:11:00:3a:01";
private static string PlaceholderFor(string discoveryId) =>
$"host-{DiscoveryId.ShortSuffix(discoveryId)}";
/// A sweep's card: an address, a MAC, and a name that is just its own hash.
private static SystemResource Stored(string? name = null, bool? userNamed = null) {
var id = DiscoveryId.Create(DiscoveryId.NetworkScheme, _mac);
return new SystemResource {
Kind = SystemResource.KindLabel,
Name = name ?? PlaceholderFor(id),
DiscoveryId = id,
UserNamed = userNamed,
Ip = "192.0.2.50",
Labels = { ["mac"] = _mac }
};
}
/// The firewall's view of the same box: same identity, but it knows the name.
private static SystemResource Incoming(string name = "forgejo") => new() {
Kind = SystemResource.KindLabel,
Name = name,
DiscoveryId = DiscoveryId.Create(DiscoveryId.NetworkScheme, _mac),
Ip = "192.0.2.50",
Labels = { ["mac"] = _mac }
};
private static List Resolve(
Resource stored,
Resource incoming,
IReadOnlyList? storedConnections = null,
params Resource[] alsoStored) {
List existing = [stored, .. alsoStored];
DiscoveryIdResolver.ResolveNames(
existing,
[incoming],
null,
storedConnections,
true);
return existing;
}
[Fact]
public void A_real_name_replaces_a_placeholder_nobody_chose() {
SystemResource stored = Stored();
Resolve(stored, Incoming());
Assert.Equal("forgejo", stored.Name);
}
[Fact]
public void A_name_a_person_typed_is_never_touched() {
SystemResource stored = Stored("the-blue-one", true);
Resolve(stored, Incoming());
Assert.Equal("the-blue-one", stored.Name);
}
[Fact]
public void A_placeholder_a_person_chose_to_keep_is_still_theirs() {
// Renaming a card back to its hash is a strange thing to do, but it is a choice,
// and the flag is what records that rather than the shape of the name.
SystemResource stored = Stored(userNamed: true);
var before = stored.Name;
Resolve(stored, Incoming());
Assert.Equal(before, stored.Name);
}
[Fact]
public void A_placeholder_never_replaces_a_real_name() {
// The reverse direction: the sweep runs after the firewall and knows less. The
// upgrade is one-way or the card would flip names on alternate runs.
SystemResource stored = Stored("forgejo");
var placeholder = PlaceholderFor(stored.DiscoveryId!);
Resolve(stored, Incoming(placeholder));
Assert.Equal("forgejo", stored.Name);
}
[Fact]
public void One_real_name_does_not_replace_another() {
// Two collectors that each know a different name for one box would otherwise
// rename it back and forth every run. First real name wins and stays.
SystemResource stored = Stored("forgejo");
Resolve(stored, Incoming("git-server"));
Assert.Equal("forgejo", stored.Name);
}
[Fact]
public void A_hand_written_resource_counts_as_user_named_without_the_flag() {
// Nothing but a person could have written a resource with no discovery id, so
// the absent flag must not be read as permission.
var stored = new SystemResource {
Kind = SystemResource.KindLabel,
Name = "forgejo",
Ip = "192.0.2.50"
};
Assert.True(stored.IsUserNamed());
Assert.False(Stored().IsUserNamed());
}
[Fact]
public void Everything_pointing_at_the_old_name_follows_it() {
SystemResource stored = Stored();
var placeholder = stored.Name;
var service = new Service {
Kind = Service.KindLabel,
Name = "forgejo-https",
RunsOn = [placeholder]
};
Resolve(stored, Incoming(), null, service);
Assert.Equal(["forgejo"], service.RunsOn);
}
[Fact]
public void Stored_connections_follow_it_too() {
SystemResource stored = Stored();
var placeholder = stored.Name;
var connection = new Connection {
A = new PortReference { Resource = placeholder },
B = new PortReference { Resource = "core-switch", PortIndex = 12 }
};
Resolve(stored, Incoming(), [connection]);
Assert.Equal("forgejo", connection.A.Resource);
}
[Fact]
public void A_re_push_of_exported_yaml_follows_the_rename_too() {
// The payload still calls the machine by the name it was stored under, because
// that is what was exported. Its links have to land on the upgraded card.
SystemResource stored = Stored();
var placeholder = stored.Name;
var arriving = new Service {
Kind = Service.KindLabel,
Name = "forgejo-https",
DiscoveryId = DiscoveryId.Create(DiscoveryId.NetworkScheme, $"{_mac}:443"),
RunsOn = [placeholder]
};
DiscoveryIdResolver.ResolveNames([stored], [Incoming(), arriving], null, null, true);
Assert.Equal("forgejo", stored.Name);
Assert.Equal(["forgejo"], arriving.RunsOn);
}
[Fact]
public void A_service_named_after_the_host_follows_it() {
// The sweep names what it finds on a port after the host it found it on, so the
// leftover reads host-1a2b3c4d-ssh running on forgejo until this carries it over.
SystemResource stored = Stored();
var placeholder = stored.Name;
var ssh = new Service {
Kind = Service.KindLabel,
Name = $"{placeholder}-ssh",
DiscoveryId = DiscoveryId.Create(DiscoveryId.NetworkScheme, $"{_mac}:22"),
RunsOn = [placeholder]
};
Resolve(stored, Incoming(), null, ssh);
Assert.Equal("forgejo-ssh", ssh.Name);
Assert.Equal(["forgejo"], ssh.RunsOn);
}
[Fact]
public void A_service_a_person_named_keeps_its_name() {
SystemResource stored = Stored();
var placeholder = stored.Name;
var ssh = new Service {
Kind = Service.KindLabel,
Name = $"{placeholder}-ssh",
DiscoveryId = DiscoveryId.Create(DiscoveryId.NetworkScheme, $"{_mac}:22"),
UserNamed = true,
RunsOn = [placeholder]
};
Resolve(stored, Incoming(), null, ssh);
Assert.Equal($"{placeholder}-ssh", ssh.Name);
}
[Fact]
public void A_service_that_only_looks_like_the_host_is_not_renamed() {
// Named for the old host but running somewhere else entirely: a coincidence,
// and coincidences are not evidence of anything.
SystemResource stored = Stored();
var placeholder = stored.Name;
var stray = new Service {
Kind = Service.KindLabel,
Name = $"{placeholder}-ssh",
DiscoveryId = DiscoveryId.Create(DiscoveryId.NetworkScheme, "elsewhere:22"),
RunsOn = ["some-other-box"]
};
Resolve(stored, Incoming(), null, stray);
Assert.Equal($"{placeholder}-ssh", stray.Name);
}
[Fact]
public void A_service_whose_new_name_is_taken_keeps_the_old_one() {
SystemResource stored = Stored();
var placeholder = stored.Name;
var ssh = new Service {
Kind = Service.KindLabel,
Name = $"{placeholder}-ssh",
DiscoveryId = DiscoveryId.Create(DiscoveryId.NetworkScheme, $"{_mac}:22"),
RunsOn = [placeholder]
};
var occupier = new Service {
Kind = Service.KindLabel,
Name = "forgejo-ssh",
DiscoveryId = DiscoveryId.Create(DiscoveryId.NetworkScheme, "other:22"),
RunsOn = ["some-other-box"]
};
Resolve(stored, Incoming(), null, ssh, occupier);
Assert.Equal($"{placeholder}-ssh", ssh.Name);
}
[Fact]
public void A_connection_to_a_renamed_service_follows_it() {
SystemResource stored = Stored();
var placeholder = stored.Name;
var ssh = new Service {
Kind = Service.KindLabel,
Name = $"{placeholder}-ssh",
DiscoveryId = DiscoveryId.Create(DiscoveryId.NetworkScheme, $"{_mac}:22"),
RunsOn = [placeholder]
};
var connection = new Connection {
A = new PortReference { Resource = $"{placeholder}-ssh" },
B = new PortReference { Resource = "core-switch", PortIndex = 3 }
};
DiscoveryIdResolver.ResolveNames(
[stored, ssh], [Incoming()], null, [connection], true);
Assert.Equal("forgejo-ssh", connection.A.Resource);
}
[Fact]
public void A_name_already_in_use_is_left_alone() {
// Renaming onto an occupied name would collide two unrelated resources in the
// merge, which keys on name. Keeping the placeholder is the safe outcome.
SystemResource stored = Stored();
var placeholder = stored.Name;
var other = new SystemResource {
Kind = SystemResource.KindLabel,
Name = "forgejo",
Ip = "192.0.2.99"
};
Resolve(stored, Incoming(), null, other);
Assert.Equal(placeholder, stored.Name);
}
[Fact]
public void A_dry_run_leaves_the_stored_name_where_it_was() {
// The upgrade rewrites the inventory, so a caller that is only reporting what
// would happen must not ask for it — the default is off for exactly this reason.
SystemResource stored = Stored();
var placeholder = stored.Name;
DiscoveryIdResolver.ResolveNames([stored], [Incoming()]);
Assert.Equal(placeholder, stored.Name);
}
}