WIP: rebase for 2026-05-18 #7
@@ -621,11 +621,9 @@ func (menu *Menu) rebuildExitNodeMenu(ctx context.Context) {
|
|||||||
title += strings.Split(sugg.Name, ".")[0]
|
title += strings.Split(sugg.Name, ".")[0]
|
||||||
}
|
}
|
||||||
menu.exitNodes.AddSeparator()
|
menu.exitNodes.AddSeparator()
|
||||||
rm := menu.exitNodes.AddSubMenuItemCheckbox(title, "", false)
|
active := recommendedIsActive(status, sugg.ID, sugg.Location.CountryCode(), sugg.Location.City())
|
||||||
|
rm := menu.exitNodes.AddSubMenuItemCheckbox(title, "", active)
|
||||||
setExitNodeOnClick(rm, sugg.ID)
|
setExitNodeOnClick(rm, sugg.ID)
|
||||||
if status.ExitNodeStatus != nil && sugg.ID == status.ExitNodeStatus.ID {
|
|
||||||
rm.Check()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -647,13 +645,11 @@ func (menu *Menu) rebuildExitNodeMenu(ctx context.Context) {
|
|||||||
if !ps.Online {
|
if !ps.Online {
|
||||||
name += " (offline)"
|
name += " (offline)"
|
||||||
}
|
}
|
||||||
sm := menu.exitNodes.AddSubMenuItemCheckbox(name, "", false)
|
active := status.ExitNodeStatus != nil && ps.ID == status.ExitNodeStatus.ID
|
||||||
|
sm := menu.exitNodes.AddSubMenuItemCheckbox(name, "", active)
|
||||||
if !ps.Online {
|
if !ps.Online {
|
||||||
sm.Disable()
|
sm.Disable()
|
||||||
}
|
}
|
||||||
if status.ExitNodeStatus != nil && ps.ID == status.ExitNodeStatus.ID {
|
|
||||||
sm.Check()
|
|
||||||
}
|
|
||||||
setExitNodeOnClick(sm, ps.ID)
|
setExitNodeOnClick(sm, ps.ID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -743,6 +739,30 @@ func (mc *mvCountry) sortedCities() []*mvCity {
|
|||||||
return cities
|
return cities
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// recommendedIsActive reports whether the suggested exit node corresponds to
|
||||||
|
// the currently active exit node in status.
|
||||||
|
func recommendedIsActive(status *ipnstate.Status, suggID tailcfg.StableNodeID, suggCountry, suggCity string) bool {
|
||||||
|
if status == nil || status.ExitNodeStatus == nil || status.ExitNodeStatus.ID.IsZero() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if suggID == status.ExitNodeStatus.ID {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if suggCountry == "" || suggCity == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, p := range status.Peer {
|
||||||
|
if p.ID != status.ExitNodeStatus.ID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if loc := p.Location; loc != nil && loc.CountryCode == suggCountry && loc.City == suggCity {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// countryFlag takes a 2-character ASCII string and returns the corresponding emoji flag.
|
// countryFlag takes a 2-character ASCII string and returns the corresponding emoji flag.
|
||||||
// It returns the empty string on error.
|
// It returns the empty string on error.
|
||||||
func countryFlag(code string) string {
|
func countryFlag(code string) string {
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & contributors
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
//go:build cgo || !darwin
|
||||||
|
|
||||||
|
package systray
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"tailscale.com/ipn/ipnstate"
|
||||||
|
"tailscale.com/tailcfg"
|
||||||
|
"tailscale.com/types/key"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRecommendedIsActive(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
const (
|
||||||
|
activeID = tailcfg.StableNodeID("active")
|
||||||
|
suggID = tailcfg.StableNodeID("suggestion")
|
||||||
|
)
|
||||||
|
usNYC := &tailcfg.Location{CountryCode: "US", City: "New York"}
|
||||||
|
usCHI := &tailcfg.Location{CountryCode: "US", City: "Chicago"}
|
||||||
|
seSTO := &tailcfg.Location{CountryCode: "SE", City: "Stockholm"}
|
||||||
|
|
||||||
|
statusWith := func(activePeer *ipnstate.PeerStatus) *ipnstate.Status {
|
||||||
|
s := &ipnstate.Status{
|
||||||
|
ExitNodeStatus: &ipnstate.ExitNodeStatus{ID: activeID},
|
||||||
|
}
|
||||||
|
if activePeer != nil {
|
||||||
|
s.Peer = map[key.NodePublic]*ipnstate.PeerStatus{{}: activePeer}
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
status *ipnstate.Status
|
||||||
|
suggID tailcfg.StableNodeID
|
||||||
|
suggCountry string
|
||||||
|
suggCity string
|
||||||
|
isActive bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "nil_status",
|
||||||
|
status: nil,
|
||||||
|
suggID: suggID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no_exit_node",
|
||||||
|
status: &ipnstate.Status{},
|
||||||
|
suggID: suggID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "exit_node_id_is_zero",
|
||||||
|
status: &ipnstate.Status{ExitNodeStatus: &ipnstate.ExitNodeStatus{}},
|
||||||
|
suggID: suggID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "exact_id_match_short-circuits",
|
||||||
|
status: statusWith(&ipnstate.PeerStatus{ID: activeID, Location: usCHI}),
|
||||||
|
suggID: activeID,
|
||||||
|
suggCountry: "US",
|
||||||
|
suggCity: "New York",
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "id_mismatch_but_same_city",
|
||||||
|
status: statusWith(&ipnstate.PeerStatus{ID: activeID, Location: usNYC}),
|
||||||
|
suggID: suggID,
|
||||||
|
suggCountry: "US",
|
||||||
|
suggCity: "New York",
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "different_city",
|
||||||
|
status: statusWith(&ipnstate.PeerStatus{ID: activeID, Location: usCHI}),
|
||||||
|
suggID: suggID,
|
||||||
|
suggCountry: "US",
|
||||||
|
suggCity: "New York",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "different_country",
|
||||||
|
status: statusWith(&ipnstate.PeerStatus{ID: activeID, Location: seSTO}),
|
||||||
|
suggID: suggID,
|
||||||
|
suggCountry: "US",
|
||||||
|
suggCity: "New York",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "id_mismatch_suggestion_has_no_location",
|
||||||
|
status: statusWith(&ipnstate.PeerStatus{ID: activeID, Location: usNYC}),
|
||||||
|
suggID: suggID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "id_mismatch_active_peer_has_no_location",
|
||||||
|
status: statusWith(&ipnstate.PeerStatus{ID: activeID}),
|
||||||
|
suggID: suggID,
|
||||||
|
suggCountry: "US",
|
||||||
|
suggCity: "New York",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "active_peer_not_in_status",
|
||||||
|
status: statusWith(nil),
|
||||||
|
suggID: suggID,
|
||||||
|
suggCountry: "US",
|
||||||
|
suggCity: "New York",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
isExitNodeActive := recommendedIsActive(tt.status, tt.suggID, tt.suggCountry, tt.suggCity)
|
||||||
|
if isExitNodeActive != tt.isActive {
|
||||||
|
t.Errorf("recommendedIsActive; got %v, want %v", isExitNodeActive, tt.isActive)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user