tailcfg: send health update if DisplayMessage URL changes

Updates tailscale/corp#27759

Signed-off-by: James Sanderson <jsanderson@tailscale.com>
This commit is contained in:
James Sanderson
2025-07-14 17:54:56 +01:00
committed by James 'zofrex' Sanderson
parent 7a3221177e
commit e0fcd596bf
3 changed files with 161 additions and 115 deletions

View File

@@ -555,98 +555,88 @@ func TestControlHealth(t *testing.T) {
}) })
} }
func TestControlHealthNotifiesOnSet(t *testing.T) { func TestControlHealthNotifies(t *testing.T) {
ht := Tracker{} type test struct {
ht.SetIPNState("NeedsLogin", true) name string
ht.GotStreamedMapResponse() initialState map[tailcfg.DisplayMessageID]tailcfg.DisplayMessage
newState map[tailcfg.DisplayMessageID]tailcfg.DisplayMessage
gotNotified := false wantNotify bool
ht.registerSyncWatcher(func(_ Change) {
gotNotified = true
})
ht.SetControlHealth(map[tailcfg.DisplayMessageID]tailcfg.DisplayMessage{
"test": {},
})
if !gotNotified {
t.Errorf("watcher did not get called, want it to be called")
} }
} tests := []test{
{
func TestControlHealthNotifiesOnChange(t *testing.T) { name: "no-change",
ht := Tracker{} initialState: map[tailcfg.DisplayMessageID]tailcfg.DisplayMessage{
ht.SetIPNState("NeedsLogin", true) "test": {},
ht.GotStreamedMapResponse() },
newState: map[tailcfg.DisplayMessageID]tailcfg.DisplayMessage{
ht.SetControlHealth(map[tailcfg.DisplayMessageID]tailcfg.DisplayMessage{ "test": {},
"test-1": {}, },
}) wantNotify: false,
gotNotified := false
ht.registerSyncWatcher(func(_ Change) {
gotNotified = true
})
ht.SetControlHealth(map[tailcfg.DisplayMessageID]tailcfg.DisplayMessage{
"test-2": {},
})
if !gotNotified {
t.Errorf("watcher did not get called, want it to be called")
}
}
func TestControlHealthNotifiesOnDetailsChange(t *testing.T) {
ht := Tracker{}
ht.SetIPNState("NeedsLogin", true)
ht.GotStreamedMapResponse()
ht.SetControlHealth(map[tailcfg.DisplayMessageID]tailcfg.DisplayMessage{
"test-1": {
Title: "Title",
}, },
}) {
name: "on-set",
gotNotified := false initialState: map[tailcfg.DisplayMessageID]tailcfg.DisplayMessage{},
ht.registerSyncWatcher(func(_ Change) { newState: map[tailcfg.DisplayMessageID]tailcfg.DisplayMessage{
gotNotified = true "test": {},
}) },
wantNotify: true,
ht.SetControlHealth(map[tailcfg.DisplayMessageID]tailcfg.DisplayMessage{ },
"test-1": { {
Title: "Updated title", name: "details-change",
initialState: map[tailcfg.DisplayMessageID]tailcfg.DisplayMessage{
"test": {
Title: "Title",
},
},
newState: map[tailcfg.DisplayMessageID]tailcfg.DisplayMessage{
"test": {
Title: "Updated title",
},
},
wantNotify: true,
},
{
name: "action-changes",
initialState: map[tailcfg.DisplayMessageID]tailcfg.DisplayMessage{
"test": {
PrimaryAction: &tailcfg.DisplayMessageAction{
URL: "http://www.example.com/a/123456",
Label: "Sign in",
},
},
},
newState: map[tailcfg.DisplayMessageID]tailcfg.DisplayMessage{
"test": {
PrimaryAction: &tailcfg.DisplayMessageAction{
URL: "http://www.example.com/a/abcdefg",
Label: "Sign in",
},
},
},
wantNotify: true,
}, },
})
if !gotNotified {
t.Errorf("watcher did not get called, want it to be called")
} }
} for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
ht := Tracker{}
ht.SetIPNState("NeedsLogin", true)
ht.GotStreamedMapResponse()
func TestControlHealthNoNotifyOnUnchanged(t *testing.T) { if len(test.initialState) != 0 {
ht := Tracker{} ht.SetControlHealth(test.initialState)
ht.SetIPNState("NeedsLogin", true) }
ht.GotStreamedMapResponse()
// Set up an existing control health issue gotNotified := false
ht.SetControlHealth(map[tailcfg.DisplayMessageID]tailcfg.DisplayMessage{ ht.registerSyncWatcher(func(_ Change) {
"test": {}, gotNotified = true
}) })
// Now register our watcher ht.SetControlHealth(test.newState)
gotNotified := false
ht.registerSyncWatcher(func(_ Change) {
gotNotified = true
})
// Send the same control health message again - should not notify if gotNotified != test.wantNotify {
ht.SetControlHealth(map[tailcfg.DisplayMessageID]tailcfg.DisplayMessage{ t.Errorf("notified: got %v, want %v", gotNotified, test.wantNotify)
"test": {}, }
}) })
if gotNotified {
t.Errorf("watcher got called, want it to not be called")
} }
} }

View File

@@ -2171,7 +2171,10 @@ func (m DisplayMessage) Equal(o DisplayMessage) bool {
return m.Title == o.Title && return m.Title == o.Title &&
m.Text == o.Text && m.Text == o.Text &&
m.Severity == o.Severity && m.Severity == o.Severity &&
m.ImpactsConnectivity == o.ImpactsConnectivity m.ImpactsConnectivity == o.ImpactsConnectivity &&
(m.PrimaryAction == nil) == (o.PrimaryAction == nil) &&
(m.PrimaryAction == nil || (m.PrimaryAction.URL == o.PrimaryAction.URL &&
m.PrimaryAction.Label == o.PrimaryAction.Label))
} }
// DisplayMessageSeverity represents how serious a [DisplayMessage] is. Analogous // DisplayMessageSeverity represents how serious a [DisplayMessage] is. Analogous

View File

@@ -881,76 +881,129 @@ func TestCheckTag(t *testing.T) {
} }
func TestDisplayMessageEqual(t *testing.T) { func TestDisplayMessageEqual(t *testing.T) {
base := DisplayMessage{
Title: "title",
Text: "text",
Severity: SeverityHigh,
ImpactsConnectivity: false,
}
type test struct { type test struct {
name string name string
value DisplayMessage value1 DisplayMessage
value2 DisplayMessage
wantEqual bool wantEqual bool
} }
for _, test := range []test{ for _, test := range []test{
{ {
name: "same", name: "same",
value: DisplayMessage{ value1: DisplayMessage{
Title: "title", Title: "title",
Text: "text", Text: "text",
Severity: SeverityHigh, Severity: SeverityHigh,
ImpactsConnectivity: false, ImpactsConnectivity: false,
PrimaryAction: &DisplayMessageAction{
URL: "https://example.com",
Label: "Open",
},
},
value2: DisplayMessage{
Title: "title",
Text: "text",
Severity: SeverityHigh,
ImpactsConnectivity: false,
PrimaryAction: &DisplayMessageAction{
URL: "https://example.com",
Label: "Open",
},
}, },
wantEqual: true, wantEqual: true,
}, },
{ {
name: "different-title", name: "different-title",
value: DisplayMessage{ value1: DisplayMessage{
Title: "different title", Title: "title",
Text: "text", },
Severity: SeverityHigh, value2: DisplayMessage{
ImpactsConnectivity: false, Title: "different title",
}, },
wantEqual: false, wantEqual: false,
}, },
{ {
name: "different-text", name: "different-text",
value: DisplayMessage{ value1: DisplayMessage{
Title: "title", Text: "some text",
Text: "different text", },
Severity: SeverityHigh, value2: DisplayMessage{
ImpactsConnectivity: false, Text: "different text",
}, },
wantEqual: false, wantEqual: false,
}, },
{ {
name: "different-severity", name: "different-severity",
value: DisplayMessage{ value1: DisplayMessage{
Title: "title", Severity: SeverityHigh,
Text: "text", },
Severity: SeverityMedium, value2: DisplayMessage{
ImpactsConnectivity: false, Severity: SeverityMedium,
}, },
wantEqual: false, wantEqual: false,
}, },
{ {
name: "different-impactsConnectivity", name: "different-impactsConnectivity",
value: DisplayMessage{ value1: DisplayMessage{
Title: "title",
Text: "text",
Severity: SeverityHigh,
ImpactsConnectivity: true, ImpactsConnectivity: true,
}, },
value2: DisplayMessage{
ImpactsConnectivity: false,
},
wantEqual: false,
},
{
name: "different-primaryAction-nil-non-nil",
value1: DisplayMessage{},
value2: DisplayMessage{
PrimaryAction: &DisplayMessageAction{
URL: "https://example.com",
Label: "Open",
},
},
wantEqual: false,
},
{
name: "different-primaryAction-url",
value1: DisplayMessage{
PrimaryAction: &DisplayMessageAction{
URL: "https://example.com",
Label: "Open",
},
},
value2: DisplayMessage{
PrimaryAction: &DisplayMessageAction{
URL: "https://zombo.com",
Label: "Open",
},
},
wantEqual: false,
},
{
name: "different-primaryAction-label",
value1: DisplayMessage{
PrimaryAction: &DisplayMessageAction{
URL: "https://example.com",
Label: "Open",
},
},
value2: DisplayMessage{
PrimaryAction: &DisplayMessageAction{
URL: "https://example.com",
Label: "Learn more",
},
},
wantEqual: false, wantEqual: false,
}, },
} { } {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
got := base.Equal(test.value) got := test.value1.Equal(test.value2)
if got != test.wantEqual { if got != test.wantEqual {
t.Errorf("Equal: got %t, want %t", got, test.wantEqual) value1 := must.Get(json.MarshalIndent(test.value1, "", " "))
value2 := must.Get(json.MarshalIndent(test.value2, "", " "))
t.Errorf("value1.Equal(value2): got %t, want %t\nvalue1:\n%s\nvalue2:\n%s", got, test.wantEqual, value1, value2)
} }
}) })
} }