ipn: add watch opt to include actions in health messages

Updates tailscale/corp#27759

Signed-off-by: James Sanderson <jsanderson@tailscale.com>
This commit is contained in:
James Sanderson
2025-06-03 15:09:34 +01:00
committed by James 'zofrex' Sanderson
parent 1635ccca27
commit 5fde183754
3 changed files with 161 additions and 17 deletions
+105 -1
View File
@@ -5348,6 +5348,8 @@ func TestDisplayMessages(t *testing.T) {
ht.SetIPNState("NeedsLogin", true)
ht.GotStreamedMapResponse()
b.mu.Lock()
defer b.mu.Unlock()
b.setNetMapLocked(&netmap.NetworkMap{
DisplayMessages: map[tailcfg.DisplayMessageID]tailcfg.DisplayMessage{
"test-message": {
@@ -5374,7 +5376,8 @@ func TestDisplayMessagesURLFilter(t *testing.T) {
ht.SetIPNState("NeedsLogin", true)
ht.GotStreamedMapResponse()
defer b.lockAndGetUnlock()()
b.mu.Lock()
defer b.mu.Unlock()
b.setNetMapLocked(&netmap.NetworkMap{
DisplayMessages: map[tailcfg.DisplayMessageID]tailcfg.DisplayMessage{
"test-message": {
@@ -5405,3 +5408,104 @@ func TestDisplayMessagesURLFilter(t *testing.T) {
t.Errorf("Unexpected message content (-want/+got):\n%s", diff)
}
}
// TestDisplayMessageIPNBus checks that we send health messages appropriately
// based on whether the watcher has sent the [ipn.NotifyHealthActions] watch
// option or not.
func TestDisplayMessageIPNBus(t *testing.T) {
type test struct {
name string
mask ipn.NotifyWatchOpt
wantWarning health.UnhealthyState
}
msgs := map[tailcfg.DisplayMessageID]tailcfg.DisplayMessage{
"test-message": {
Title: "Message title",
Text: "Message text.",
Severity: tailcfg.SeverityMedium,
PrimaryAction: &tailcfg.DisplayMessageAction{
URL: "https://example.com",
Label: "Learn more",
},
},
}
for _, tt := range []test{
{
name: "older-client-no-actions",
mask: 0,
wantWarning: health.UnhealthyState{
WarnableCode: "test-message",
Severity: health.SeverityMedium,
Title: "Message title",
Text: "Message text. Learn more: https://example.com", // PrimaryAction appended to text
PrimaryAction: nil, // PrimaryAction not included
},
},
{
name: "new-client-with-actions",
mask: ipn.NotifyHealthActions,
wantWarning: health.UnhealthyState{
WarnableCode: "test-message",
Severity: health.SeverityMedium,
Title: "Message title",
Text: "Message text.",
PrimaryAction: &health.UnhealthyStateAction{
URL: "https://example.com",
Label: "Learn more",
},
},
},
} {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
lb := newLocalBackendWithTestControl(t, false, func(tb testing.TB, opts controlclient.Options) controlclient.Client {
return newClient(tb, opts)
})
ipnWatcher := newNotificationWatcher(t, lb, nil)
ipnWatcher.watch(tt.mask, []wantedNotification{{
name: "test",
cond: func(_ testing.TB, _ ipnauth.Actor, n *ipn.Notify) bool {
if n.Health == nil {
return false
}
got, ok := n.Health.Warnings["test-message"]
if ok {
if diff := cmp.Diff(tt.wantWarning, got); diff != "" {
t.Errorf("unexpected warning details (-want/+got):\n%s", diff)
return true // we failed the test so tell the watcher we've seen what we need to to stop it waiting
}
}
return ok
},
}})
lb.SetPrefsForTest(&ipn.Prefs{
ControlURL: "https://localhost:1/",
WantRunning: true,
LoggedOut: false,
})
if err := lb.Start(ipn.Options{}); err != nil {
t.Fatalf("(*LocalBackend).Start(): %v", err)
}
cc := lb.cc.(*mockControl)
// Assert that we are logged in and authorized, and also send our DisplayMessages
cc.send(nil, "", true, &netmap.NetworkMap{
SelfNode: (&tailcfg.Node{MachineAuthorized: true}).View(),
DisplayMessages: msgs,
})
// Tell the health tracker that we are in a map poll because
// mockControl doesn't tell it
lb.HealthTracker().GotStreamedMapResponse()
// Assert that we got the expected notification
ipnWatcher.check()
})
}
}