Files
pulse/internal/monitoring/docker_host_identity_test.go
T
rcourtman 3c31aa4805 Stop Docker records flip-flopping under shared same-hostname tokens
The Docker analog of the #1753 estate was still broken: two live
machines reusing one short hostname and one pasted unified install
token collapsed into a single flip-flopping DockerHost record, because
the hostname+token identity fallback adopts a record whose machine ID
disagrees with the report's. That fold is deliberate for recreated
containers (whose /etc/machine-id regenerates), so it cannot simply be
guarded by machine-ID inequality - the discriminator is a revisit: a
recreated container transitions to its new machine ID exactly once,
while two live machines alternate. Removing the collapsed record then
revoked the shared token unconditionally, rejecting every surviving
module - host reports included, since a unified install shares one
credential - with 401 "Unauthorized access attempt".

Consult the identity flap tracker before the hostname fallbacks adopt a
machine-ID-disagreeing record: a report whose machine ID returns to a
value already seen behind that identity is a second live machine and is
not folded. The machine whose identifiers minted the record reclaims it,
so the first site keeps its record and history, and the other site falls
through to the token binding check, converging on the documented "Each
Docker / Podman module must use a unique API token" rejection instead of
silently overwriting the record every cycle. RemoveDockerHost now skips
token revocation while any host or Docker record still authenticates
with the credential, mirroring the host-agent removal guard.

Regression coverage: an end-to-end router test walking the two-site
shared-token estate (host + Docker reports, alternating cycles, removal)
asserting the first site's identity stays stable, the second site gets
the unique-token guidance, and the shared token survives removal; a
router test proving removal of a machine's Docker record keeps the
unified token its host record still uses; and a state-layer test pinning
the reclaim/no-flip-flop convergence. Recreated-container adoption and
the existing token-uniqueness rejections keep their tests unchanged.
2026-09-01 15:26:48 +01:00

980 lines
23 KiB
Go

package monitoring
import (
"testing"
"github.com/rcourtman/pulse-go-rewrite/internal/config"
"github.com/rcourtman/pulse-go-rewrite/internal/models"
"github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
agentsdocker "github.com/rcourtman/pulse-go-rewrite/pkg/agents/docker"
)
func dockerHostViewsForTest(hosts []models.DockerHost) []*unifiedresources.DockerHostView {
registry := unifiedresources.NewRegistry(nil)
registry.IngestSnapshot(models.StateSnapshot{DockerHosts: hosts})
return registry.DockerHosts()
}
func TestUniqueNonEmptyStrings(t *testing.T) {
t.Parallel()
tests := []struct {
name string
input []string
expected []string
}{
{
name: "empty input",
input: []string{},
expected: []string{},
},
{
name: "all empty strings",
input: []string{"", "", ""},
expected: []string{},
},
{
name: "all whitespace strings",
input: []string{" ", "\t", "\n"},
expected: []string{},
},
{
name: "duplicates removed",
input: []string{"foo", "bar", "foo", "baz"},
expected: []string{"foo", "bar", "baz"},
},
{
name: "order preserved",
input: []string{"first", "second", "third"},
expected: []string{"first", "second", "third"},
},
{
name: "whitespace trimmed",
input: []string{" foo ", "bar", " foo"},
expected: []string{"foo", "bar"},
},
{
name: "mixed empty and non-empty",
input: []string{"", "foo", "", "bar", "foo"},
expected: []string{"foo", "bar"},
},
{
name: "single value",
input: []string{"value"},
expected: []string{"value"},
},
{
name: "nil input",
input: nil,
expected: []string{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
result := uniqueNonEmptyStrings(tt.input...)
if len(result) != len(tt.expected) {
t.Errorf("length mismatch: got %d, want %d", len(result), len(tt.expected))
return
}
for i := range result {
if result[i] != tt.expected[i] {
t.Errorf("index %d: got %q, want %q", i, result[i], tt.expected[i])
}
}
})
}
}
func TestSanitizeDockerHostSuffix(t *testing.T) {
t.Parallel()
tests := []struct {
name string
input string
expected string
}{
{
name: "empty string",
input: "",
expected: "",
},
{
name: "whitespace only",
input: " ",
expected: "",
},
{
name: "lowercase conversion",
input: "UPPERCASE",
expected: "uppercase",
},
{
name: "special chars to hyphens",
input: "foo@bar!baz",
expected: "foo-bar-baz",
},
{
name: "consecutive special chars become single hyphen",
input: "foo@@@bar",
expected: "foo-bar",
},
{
name: "leading hyphen trimmed",
input: "@@@foo",
expected: "foo",
},
{
name: "trailing hyphen trimmed",
input: "foo@@@",
expected: "foo",
},
{
name: "leading and trailing hyphens trimmed",
input: "@@foo@@",
expected: "foo",
},
{
name: "truncation at 48 runes",
input: "abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz",
expected: "abcdefghijklmnopqrstuvwxyz0123456789abcdefghijkl",
},
{
name: "unicode letters preserved",
input: "café",
expected: "café",
},
{
name: "digits preserved",
input: "host123",
expected: "host123",
},
{
name: "all special chars",
input: "@@@@",
expected: "",
},
{
name: "mixed case with spaces",
input: "My Docker Host",
expected: "my-docker-host",
},
{
name: "truncation with trailing special char",
input: "abcdefghijklmnopqrstuvwxyz0123456789abcdefghij@@@",
expected: "abcdefghijklmnopqrstuvwxyz0123456789abcdefghij",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
result := sanitizeDockerHostSuffix(tt.input)
if result != tt.expected {
t.Errorf("got %q, want %q", result, tt.expected)
}
})
}
}
func TestTokenHintFromRecord(t *testing.T) {
t.Parallel()
tests := []struct {
name string
record *config.APITokenRecord
expected string
}{
{
name: "nil record",
record: nil,
expected: "",
},
{
name: "both prefix and suffix",
record: &config.APITokenRecord{
Prefix: "ps_abc",
Suffix: "xyz",
},
expected: "ps_abc…xyz",
},
{
name: "only prefix",
record: &config.APITokenRecord{
Prefix: "ps_abc",
},
expected: "ps_abc…",
},
{
name: "only suffix",
record: &config.APITokenRecord{
Suffix: "xyz",
},
expected: "…xyz",
},
{
name: "neither prefix nor suffix",
record: &config.APITokenRecord{},
expected: "",
},
{
name: "empty strings",
record: &config.APITokenRecord{
Prefix: "",
Suffix: "",
},
expected: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
result := tokenHintFromRecord(tt.record)
if result != tt.expected {
t.Errorf("got %q, want %q", result, tt.expected)
}
})
}
}
func TestDockerHostIDExists(t *testing.T) {
t.Parallel()
hosts := []models.DockerHost{
{ID: "host1"},
{ID: "host2"},
{ID: "host3"},
}
tests := []struct {
name string
id string
hosts []models.DockerHost
expected bool
}{
{
name: "empty id",
id: "",
hosts: hosts,
expected: false,
},
{
name: "whitespace id",
id: " ",
hosts: hosts,
expected: false,
},
{
name: "id found",
id: "host2",
hosts: hosts,
expected: true,
},
{
name: "id not found",
id: "host4",
hosts: hosts,
expected: false,
},
{
name: "empty hosts list",
id: "host1",
hosts: []models.DockerHost{},
expected: false,
},
{
name: "nil hosts list",
id: "host1",
hosts: nil,
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
result := dockerHostIDExists(tt.id, dockerHostViewsForTest(tt.hosts))
if result != tt.expected {
t.Errorf("got %v, want %v", result, tt.expected)
}
})
}
}
func TestDockerHostSuffixCandidates(t *testing.T) {
t.Parallel()
tests := []struct {
name string
report agentsdocker.Report
tokenRecord *config.APITokenRecord
expected []string
}{
{
name: "nil tokenRecord, empty report",
report: agentsdocker.Report{},
tokenRecord: nil,
expected: []string{},
},
{
name: "tokenRecord with ID",
report: agentsdocker.Report{
Agent: agentsdocker.AgentInfo{ID: "agent-123"},
},
tokenRecord: &config.APITokenRecord{ID: "token-456"},
expected: []string{"token-token-456", "agent-agent-123"},
},
{
name: "agent ID only",
report: agentsdocker.Report{
Agent: agentsdocker.AgentInfo{ID: "agent-abc"},
},
tokenRecord: nil,
expected: []string{"agent-agent-abc"},
},
{
name: "machine ID only",
report: agentsdocker.Report{
Host: agentsdocker.HostInfo{MachineID: "machine-xyz"},
},
tokenRecord: nil,
expected: []string{"machine-machine-xyz"},
},
{
name: "hostname only",
report: agentsdocker.Report{
Host: agentsdocker.HostInfo{Hostname: "my-host"},
},
tokenRecord: nil,
expected: []string{"host-my-host"},
},
{
name: "display name different from hostname",
report: agentsdocker.Report{
Host: agentsdocker.HostInfo{
Hostname: "host1",
Name: "Custom Name",
},
},
tokenRecord: nil,
expected: []string{"host-host1", "name-custom-name"},
},
{
name: "display name same as hostname",
report: agentsdocker.Report{
Host: agentsdocker.HostInfo{
Hostname: "my-host",
Name: "my-host",
},
},
tokenRecord: nil,
expected: []string{"host-my-host"},
},
{
name: "all fields present",
report: agentsdocker.Report{
Agent: agentsdocker.AgentInfo{ID: "agent-1"},
Host: agentsdocker.HostInfo{
MachineID: "machine-1",
Hostname: "host-1",
Name: "Display-1",
},
},
tokenRecord: &config.APITokenRecord{ID: "token-1"},
expected: []string{"token-token-1", "agent-agent-1", "machine-machine-1", "host-host-1", "name-display-1"},
},
{
name: "sanitization applies",
report: agentsdocker.Report{
Agent: agentsdocker.AgentInfo{ID: "Agent@123"},
},
tokenRecord: nil,
expected: []string{"agent-agent-123"},
},
{
name: "duplicates removed after sanitization",
report: agentsdocker.Report{
Host: agentsdocker.HostInfo{
Hostname: "my-host",
Name: "MY-HOST",
},
},
tokenRecord: nil,
expected: []string{"host-my-host"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
result := dockerHostSuffixCandidates(tt.report, tt.tokenRecord)
if len(result) != len(tt.expected) {
t.Errorf("length mismatch: got %d, want %d\ngot: %v\nwant: %v", len(result), len(tt.expected), result, tt.expected)
return
}
for i := range result {
if result[i] != tt.expected[i] {
t.Errorf("index %d: got %q, want %q", i, result[i], tt.expected[i])
}
}
})
}
}
func TestFallbackDockerHostID(t *testing.T) {
t.Parallel()
tests := []struct {
name string
report agentsdocker.Report
tokenRecord *config.APITokenRecord
expectEmpty bool
expectHash bool
}{
{
name: "empty candidates",
report: agentsdocker.Report{},
tokenRecord: nil,
expectEmpty: true,
},
{
name: "has candidates",
report: agentsdocker.Report{
Agent: agentsdocker.AgentInfo{ID: "agent-123"},
},
tokenRecord: nil,
expectHash: true,
},
{
name: "multiple candidates",
report: agentsdocker.Report{
Agent: agentsdocker.AgentInfo{ID: "agent-123"},
Host: agentsdocker.HostInfo{
MachineID: "machine-456",
Hostname: "my-host",
},
},
tokenRecord: nil,
expectHash: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
result := fallbackDockerHostID(tt.report, tt.tokenRecord)
if tt.expectEmpty {
if result != "" {
t.Errorf("expected empty string, got %q", result)
}
}
if tt.expectHash {
if result == "" {
t.Errorf("expected non-empty hash, got empty string")
}
if len(result) < len("docker-host-") {
t.Errorf("expected docker-host- prefix, got %q", result)
}
if result[:12] != "docker-host-" {
t.Errorf("expected docker-host- prefix, got %q", result[:12])
}
// Should be 12 hex chars after prefix (6 bytes)
if len(result) != 12+12 {
t.Errorf("expected length %d, got %d: %q", 12+12, len(result), result)
}
}
})
}
}
func TestFindMatchingDockerHost(t *testing.T) {
t.Parallel()
hosts := []models.DockerHost{
{
ID: "host1",
AgentID: "agent-1",
TokenID: "token-1",
MachineID: "machine-1",
Hostname: "hostname-1",
},
{
ID: "host2",
AgentID: "agent-2",
TokenID: "",
MachineID: "machine-2",
Hostname: "hostname-2",
},
{
ID: "host3",
AgentID: "",
TokenID: "",
MachineID: "machine-3",
Hostname: "hostname-3",
},
{
ID: "host4",
AgentID: "agent-4",
TokenID: "token-4",
MachineID: "machine-4",
Hostname: "hostname-4",
},
}
tests := []struct {
name string
hosts []models.DockerHost
report agentsdocker.Report
tokenRecord *config.APITokenRecord
expectedID string
expectMatch bool
}{
{
name: "empty hosts",
hosts: []models.DockerHost{},
report: agentsdocker.Report{},
tokenRecord: nil,
expectMatch: false,
},
{
name: "match by agentID with matching token",
hosts: hosts,
report: agentsdocker.Report{
Agent: agentsdocker.AgentInfo{ID: "agent-1"},
},
tokenRecord: &config.APITokenRecord{ID: "token-1"},
expectedID: "host1",
expectMatch: true,
},
{
name: "match by agentID with no token",
hosts: hosts,
report: agentsdocker.Report{
Agent: agentsdocker.AgentInfo{ID: "agent-2"},
},
tokenRecord: nil,
expectedID: "host2",
expectMatch: true,
},
{
name: "match by machineID and hostname with matching token",
hosts: hosts,
report: agentsdocker.Report{
Host: agentsdocker.HostInfo{
MachineID: "machine-4",
Hostname: "hostname-4",
},
},
tokenRecord: &config.APITokenRecord{ID: "token-4"},
expectedID: "host4",
expectMatch: true,
},
{
name: "match by machineID and hostname with matching token when hostname differs only by fqdn form",
hosts: []models.DockerHost{
{
ID: "host-fqdn",
TokenID: "token-fqdn",
MachineID: "machine-fqdn",
Hostname: "qnap.local",
},
},
report: agentsdocker.Report{
Host: agentsdocker.HostInfo{
MachineID: "machine-fqdn",
Hostname: "qnap",
},
},
tokenRecord: &config.APITokenRecord{ID: "token-fqdn"},
expectedID: "host-fqdn",
expectMatch: true,
},
{
name: "match by machineID and hostname with no token",
hosts: hosts,
report: agentsdocker.Report{
Host: agentsdocker.HostInfo{
MachineID: "machine-3",
Hostname: "hostname-3",
},
},
tokenRecord: nil,
expectedID: "host3",
expectMatch: true,
},
{
name: "match by machineID only (no token)",
hosts: hosts,
report: agentsdocker.Report{
Host: agentsdocker.HostInfo{
MachineID: "machine-3",
},
},
tokenRecord: nil,
expectedID: "host3",
expectMatch: true,
},
{
name: "match by hostname only (no token)",
hosts: hosts,
report: agentsdocker.Report{
Host: agentsdocker.HostInfo{
Hostname: "hostname-3",
},
},
tokenRecord: nil,
expectedID: "host3",
expectMatch: true,
},
{
name: "no match - token mismatch on agentID",
hosts: hosts,
report: agentsdocker.Report{
Agent: agentsdocker.AgentInfo{ID: "agent-1"},
},
tokenRecord: &config.APITokenRecord{ID: "wrong-token"},
expectMatch: false,
},
{
name: "no match - token mismatch on machineID+hostname",
hosts: hosts,
report: agentsdocker.Report{
Host: agentsdocker.HostInfo{
MachineID: "machine-1",
Hostname: "hostname-1",
},
},
tokenRecord: &config.APITokenRecord{ID: "wrong-token"},
expectMatch: false,
},
{
name: "no match - unknown agent",
hosts: hosts,
report: agentsdocker.Report{
Agent: agentsdocker.AgentInfo{ID: "agent-unknown"},
},
tokenRecord: nil,
expectMatch: false,
},
{
name: "whitespace trimming on agentID",
hosts: hosts,
report: agentsdocker.Report{
Agent: agentsdocker.AgentInfo{ID: " agent-1 "},
},
tokenRecord: &config.APITokenRecord{ID: "token-1"},
expectedID: "host1",
expectMatch: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
result, found := findMatchingDockerHost(dockerHostViewsForTest(tt.hosts), tt.report, tt.tokenRecord, nil)
if found != tt.expectMatch {
t.Errorf("found mismatch: got %v, want %v", found, tt.expectMatch)
}
if tt.expectMatch {
gotID := result.HostSourceID()
if gotID == "" {
gotID = result.ID()
}
if gotID != tt.expectedID {
t.Errorf("ID mismatch: got %q, want %q", gotID, tt.expectedID)
}
}
})
}
}
func TestGenerateDockerHostIdentifier(t *testing.T) {
t.Parallel()
tests := []struct {
name string
base string
report agentsdocker.Report
token *config.APITokenRecord
hosts []models.DockerHost
expected string
}{
{
name: "empty base with fallback from report",
base: "",
report: agentsdocker.Report{Agent: agentsdocker.AgentInfo{ID: "agent-1"}},
token: nil,
hosts: []models.DockerHost{},
expected: "docker-host-546dbe5233c6::agent-agent-1",
},
{
name: "empty base with fallback from token",
base: "",
report: agentsdocker.Report{},
token: &config.APITokenRecord{ID: "token-abc"},
hosts: []models.DockerHost{},
expected: "docker-host-a9bb97f6f774::token-token-abc",
},
{
name: "empty base no fallback uses docker-host",
base: "",
report: agentsdocker.Report{},
token: nil,
hosts: []models.DockerHost{},
expected: "docker-host::hash-bd318191f5b8",
},
{
name: "whitespace base no fallback uses docker-host",
base: " ",
report: agentsdocker.Report{},
token: nil,
hosts: []models.DockerHost{},
expected: "docker-host::hash-bd318191f5b8",
},
{
name: "first suffix candidate works",
base: "my-host",
report: agentsdocker.Report{Agent: agentsdocker.AgentInfo{ID: "agent-1"}},
token: nil,
hosts: []models.DockerHost{
{ID: "other-host"},
},
expected: "my-host::agent-agent-1",
},
{
name: "second suffix candidate when first taken",
base: "my-host",
report: agentsdocker.Report{
Agent: agentsdocker.AgentInfo{ID: "agent-1"},
Host: agentsdocker.HostInfo{MachineID: "machine-1"},
},
token: nil,
hosts: []models.DockerHost{
{ID: "my-host::agent-agent-1"},
},
expected: "my-host::machine-machine-1",
},
{
name: "hash suffix when all candidates taken",
base: "my-host",
report: agentsdocker.Report{Agent: agentsdocker.AgentInfo{ID: "agent-1"}},
token: nil,
hosts: []models.DockerHost{
{ID: "my-host::agent-agent-1"},
},
expected: "my-host::hash-546dbe5233c6",
},
{
name: "numeric suffix when hash also taken",
base: "my-host",
report: agentsdocker.Report{Agent: agentsdocker.AgentInfo{ID: "agent-1"}},
token: nil,
hosts: []models.DockerHost{
{ID: "my-host::agent-agent-1"},
{ID: "my-host::hash-546dbe5233c6"},
},
expected: "my-host::2",
},
{
name: "numeric suffix increments",
base: "my-host",
report: agentsdocker.Report{Agent: agentsdocker.AgentInfo{ID: "agent-1"}},
token: nil,
hosts: []models.DockerHost{
{ID: "my-host::agent-agent-1"},
{ID: "my-host::hash-546dbe5233c6"},
{ID: "my-host::2"},
{ID: "my-host::3"},
},
expected: "my-host::4",
},
{
name: "empty suffixes list seed becomes base",
base: "my-host",
report: agentsdocker.Report{},
token: nil,
hosts: []models.DockerHost{},
expected: "my-host::hash-6dd480c53846",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
result := generateDockerHostIdentifier(tt.base, tt.report, tt.token, dockerHostViewsForTest(tt.hosts))
if result != tt.expected {
t.Errorf("got %q, want %q", result, tt.expected)
}
})
}
}
func TestResolveDockerHostIdentifier(t *testing.T) {
t.Parallel()
tests := []struct {
name string
report agentsdocker.Report
tokenRecord *config.APITokenRecord
hosts []models.DockerHost
expectMatch bool
expectFallbacks int
expectedID string
checkIDFormat bool
}{
{
name: "existing host match",
report: agentsdocker.Report{
Agent: agentsdocker.AgentInfo{ID: "agent-1"},
},
tokenRecord: &config.APITokenRecord{ID: "token-1"},
hosts: []models.DockerHost{
{ID: "existing-host", AgentID: "agent-1", TokenID: "token-1"},
},
expectMatch: true,
expectedID: "existing-host",
expectFallbacks: 1,
},
{
name: "no match, base available",
report: agentsdocker.Report{
Agent: agentsdocker.AgentInfo{ID: "new-agent"},
},
tokenRecord: nil,
hosts: []models.DockerHost{{ID: "other-host"}},
expectMatch: false,
expectedID: "new-agent",
expectFallbacks: 1,
},
{
name: "no match, base taken, generates unique",
report: agentsdocker.Report{
Agent: agentsdocker.AgentInfo{ID: "agent-1"},
Host: agentsdocker.HostInfo{MachineID: "machine-1"},
},
tokenRecord: nil,
hosts: []models.DockerHost{
{ID: "agent-1"},
},
expectMatch: false,
checkIDFormat: true,
expectFallbacks: 2,
},
{
name: "empty report uses fallback",
report: agentsdocker.Report{
Host: agentsdocker.HostInfo{Hostname: "my-host"},
},
tokenRecord: nil,
hosts: []models.DockerHost{},
expectMatch: false,
expectedID: "my-host",
expectFallbacks: 1,
},
{
name: "completely empty report",
report: agentsdocker.Report{},
tokenRecord: nil,
hosts: []models.DockerHost{},
expectMatch: false,
expectedID: "docker-host",
expectFallbacks: 0,
},
{
name: "multiple fallback candidates",
report: agentsdocker.Report{
Agent: agentsdocker.AgentInfo{ID: "agent-1"},
Host: agentsdocker.HostInfo{
MachineID: "machine-1",
Hostname: "host-1",
},
},
tokenRecord: nil,
hosts: []models.DockerHost{},
expectMatch: false,
expectedID: "agent-1",
expectFallbacks: 3,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
id, fallbacks, existing, found := resolveDockerHostIdentifier(tt.report, tt.tokenRecord, dockerHostViewsForTest(tt.hosts), nil)
if found != tt.expectMatch {
t.Errorf("found mismatch: got %v, want %v", found, tt.expectMatch)
}
if tt.expectMatch {
gotID := existing.HostSourceID()
if gotID == "" {
gotID = existing.ID()
}
if gotID != tt.expectedID {
t.Errorf("existing ID mismatch: got %q, want %q", gotID, tt.expectedID)
}
}
if !tt.expectMatch && existing != nil {
t.Errorf("expected empty existing host, got %q", existing.ID())
}
if tt.expectedID != "" && !tt.checkIDFormat && id != tt.expectedID {
t.Errorf("ID mismatch: got %q, want %q", id, tt.expectedID)
}
if tt.checkIDFormat && id == "" {
t.Errorf("expected non-empty ID")
}
if len(fallbacks) != tt.expectFallbacks {
t.Errorf("fallback count mismatch: got %d, want %d", len(fallbacks), tt.expectFallbacks)
}
})
}
}
func TestSanitizeDockerHostSuffix_UnicodeRunes(t *testing.T) {
t.Parallel()
// Test that rune counting works correctly (not byte counting)
input := "🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀" // 12 emoji (48+ bytes but 12 runes)
result := sanitizeDockerHostSuffix(input)
// All emojis should become single hyphen since they're not letters/digits
expected := ""
if result != expected {
t.Errorf("got %q, want %q", result, expected)
}
// Test with mix of emoji and letters
input = "abc🚀def"
result = sanitizeDockerHostSuffix(input)
expected = "abc-def"
if result != expected {
t.Errorf("got %q, want %q", result, expected)
}
}
func TestFindMatchingDockerHost_RejectsConflictingPhysicalIdentity(t *testing.T) {
t.Parallel()
hosts := []models.DockerHost{
{
ID: "host1",
AgentID: "agent-1",
TokenID: "token-1",
MachineID: "machine-1",
Hostname: "hostname-1",
},
}
// Same agentID + token, but the report comes from a physically different
// machine — it must NOT be collapsed into the existing host (#1366).
report := agentsdocker.Report{
Agent: agentsdocker.AgentInfo{ID: "agent-1"},
Host: agentsdocker.HostInfo{MachineID: "machine-2", Hostname: "hostname-2"},
}
if _, ok := findMatchingDockerHost(dockerHostViewsForTest(hosts), report, &config.APITokenRecord{ID: "token-1"}, nil); ok {
t.Fatal("expected no match when the report's physical identity conflicts with the existing host")
}
}