arblant
firefox-vpn-client / core / internal / service / statecode_test.go
statecode_test.go133 lines · 3.9 KB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133
package service

import (
	"strings"
	"testing"
	"time"

	"github.com/armanlife/firefox-vpn-client/core/internal/accounts"
	"github.com/armanlife/firefox-vpn-client/core/internal/config"
)

// waitCode waits until /api/status reports the state code, and returns the
// state.
func waitCode(t *testing.T, c *dashboardClient, code string) map[string]any {
	t.Helper()
	var state map[string]any
	deadline := time.Now().Add(5 * time.Second)
	for time.Now().Before(deadline) {
		if state = statusState(t, c); state["code"] == code {
			return state
		}
		time.Sleep(10 * time.Millisecond)
	}
	t.Fatalf("state %v, want the code %q", state, code)
	return nil
}

// The waiting states carry a stable code beside the core's English
// message, so the apps can word them: no network, no account, and no
// usable account.
func TestWaitingStatesHaveCodes(t *testing.T) {
	online := setNetwork(t)
	d := newTestDaemon(t)
	d.mu.Lock()
	d.cfg.Dashboard.Listen = "127.0.0.1:0"
	d.mu.Unlock()
	done := make(chan error, 1)
	go func() { done <- d.Run() }()
	defer func() {
		d.Shutdown()
		<-done
	}()
	c := newDashboardClient(t, d)

	state := waitCode(t, c, CodeWaitingForNetwork)
	if state["phase"] != PhaseWaiting || state["message"] != MessageWaitingForNetwork {
		t.Fatalf("waiting for the network: %v", state)
	}

	online.Store(true)
	state = waitCode(t, c, CodeNoAccounts)
	if state["phase"] != PhaseWaiting || state["message"] == "" {
		t.Fatalf("no accounts: %v", state)
	}

	// The only account must sign in again: none can be used.
	id := addTestAccount(t, d, "signed-out")
	acc, err := d.store.Get(id)
	if err != nil {
		t.Fatal(err)
	}
	acc.Status, acc.Reason = accounts.StatusReauthRequired, "sign-in required"
	if err := d.store.SaveState(acc); err != nil {
		t.Fatal(err)
	}
	if err := d.accountsChanged(); err != nil {
		t.Fatal(err)
	}
	d.RequestRestart()
	state = waitCode(t, c, CodeNoUsableAccount)
	if state["phase"] != PhaseWaiting || !strings.HasPrefix(state["message"].(string), "No usable account") {
		t.Fatalf("no usable account: %v", state)
	}
	if _, ok := state["address"]; ok {
		t.Fatalf("only the port code has an address: %v", state)
	}
}

// Another failed start is an error with the code start_failed and the
// core's message; it is retried. A proxy address that cannot be listened
// on is named, for apps to offer another port.
func TestStartFailureHasCode(t *testing.T) {
	d := newTestDaemon(t)
	d.mu.Lock()
	d.cfg.Dashboard.Listen = "127.0.0.1:0"
	d.cfg.Proxy.Listen = "127.0.0.1:not-a-port"
	d.mu.Unlock()
	done := make(chan error, 1)
	go func() { done <- d.Run() }()
	defer func() {
		d.Shutdown()
		<-done
	}()
	c := newDashboardClient(t, d)

	state := waitCode(t, c, CodeStartFailed)
	if state["phase"] != PhaseError || state["message"] == "" || state["retry_at"] == nil {
		t.Fatalf("start failed: %v", state)
	}
	if state["address"] != "127.0.0.1:not-a-port" {
		t.Fatalf("start failed without the proxy's address: %v", state)
	}
}

// Another program's setting in place of the system proxy shows in the
// status; asking for set while set asks to take it back, and a mode change
// forgets it.
func TestSystemProxyTakenShowsInTheStatus(t *testing.T) {
	d := newTestDaemon(t)
	if err := d.SetSystemProxy(config.SystemProxySet); err != nil {
		t.Fatal(err)
	}
	d.SetSystemProxyTaken(&SystemProxyTaken{By: "127.0.0.1:7890"})
	view, err := d.StatusView()
	if err != nil {
		t.Fatal(err)
	}
	if view.SystemProxyTaken == nil || view.SystemProxyTaken.By != "127.0.0.1:7890" || view.SystemProxyRetake != 0 {
		t.Fatalf("taken %+v retake %d", view.SystemProxyTaken, view.SystemProxyRetake)
	}
	if err := d.SetSystemProxy(config.SystemProxySet); err != nil {
		t.Fatal(err)
	}
	if view, _ = d.StatusView(); view.SystemProxyRetake != 1 {
		t.Fatalf("set again: retake %d, want 1", view.SystemProxyRetake)
	}
	if err := d.SetSystemProxy(config.SystemProxyClear); err != nil {
		t.Fatal(err)
	}
	if view, _ = d.StatusView(); view.SystemProxyTaken != nil {
		t.Fatalf("still taken after clear: %+v", view.SystemProxyTaken)
	}
}