arblant
firefox-vpn-client / core / internal / service / tun_test.go
tun_test.go100 lines · 3.6 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
package service

import (
	"encoding/json"
	"net/http"
	"slices"
	"testing"
	"time"

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

// status is the TUN's state as GET /api/status reports it.
func status(t *testing.T, d *Daemon) *TunStatus {
	t.Helper()
	done := make(chan StatusView, 1)
	go func() {
		view, err := d.StatusView()
		if err != nil {
			t.Error(err)
		}
		done <- view
	}()
	select {
	case view := <-done:
		return view.Tun
	case <-time.After(5 * time.Second):
		t.Fatal("the status did not come: a lock is taken twice")
		return nil
	}
}

// The running programs come only from the desktop app's core, which lists
// them; elsewhere the API has none to show.
func TestTheRunningProgramsComeFromTheDesktopCore(t *testing.T) {
	d := newTestDaemon(t)
	c := newDashboardClient(t, d)
	if rec := c.do("GET", "/api/tun/programs", nil); rec.Code != http.StatusNotFound {
		t.Fatalf("without the TUN mode: %d %s", rec.Code, rec.Body)
	}
	d.TunSupported = true
	d.RunningPrograms = func() ([]RunningProgram, error) {
		return []RunningProgram{{Name: "steam.exe", Path: `C:\Games\Steam\steam.exe`}}, nil
	}
	rec := c.do("GET", "/api/tun/programs", nil)
	var got struct{ Programs []RunningProgram }
	if rec.Code != http.StatusOK || json.Unmarshal(rec.Body.Bytes(), &got) != nil ||
		!slices.Equal(got.Programs, []RunningProgram{{Name: "steam.exe", Path: `C:\Games\Steam\steam.exe`}}) {
		t.Fatalf("%d %s", rec.Code, rec.Body)
	}
	d.RunningPrograms = func() ([]RunningProgram, error) { return nil, nil }
	if rec := c.do("GET", "/api/tun/programs", nil); rec.Body.String() != "{\"programs\":[]}\n" {
		t.Fatalf("none running: %s", rec.Body)
	}
}

// The program whose call the TUN refused shows while the TUN is on and the
// program does not skip the VPN: once the settings send it directly (by its
// name, or by a pattern) it is gone, and the TUN going off forgets it.
func TestTheRefusedProgramShowsWhileTheTUNRuns(t *testing.T) {
	d := newTestDaemon(t)
	d.TunSupported = true
	d.SetTunStatus(TunStatus{State: TunOn})
	if s := status(t, d); s.RefusedUDP != "" || s.ProgramsSupported != tunProgramsSupported {
		t.Fatalf("status %+v", s)
	}
	d.NoteRefusedUDP("Discord.exe", `C:\Apps\Discord.exe`)
	if s := status(t, d); s.RefusedUDP != "Discord.exe" || s.RefusedUDPPath != `C:\Apps\Discord.exe` {
		t.Fatalf("refused %q at %q, want Discord.exe", s.RefusedUDP, s.RefusedUDPPath)
	}
	cfg := d.config()
	cfg.Tun.Programs = []config.TunProgram{{Name: `C:\Apps\discord.exe`, UDPOnly: true}}
	if _, err := d.updateSettings(cfg, nil); err != nil {
		t.Fatal(err)
	}
	// The folder given with the name is kept as its path.
	if want := []config.TunProgram{{Name: "discord.exe", UDPOnly: true, Path: `C:\Apps\discord.exe`}}; !slices.Equal(d.TunPrograms(), want) {
		t.Fatalf("programs %+v, want %+v", d.TunPrograms(), want)
	}
	if s := status(t, d); s.RefusedUDP != "" {
		t.Fatalf("a program that skips the VPN is still offered: %q", s.RefusedUDP)
	}
	d.NoteRefusedUDP("SteamWebHelper.exe", "")
	if s := status(t, d); s.RefusedUDP != "SteamWebHelper.exe" {
		t.Fatalf("refused %q, want SteamWebHelper.exe", s.RefusedUDP)
	}
	cfg.Tun.Programs = append(cfg.Tun.Programs, config.TunProgram{Name: "steam*", UDPOnly: true})
	if _, err := d.updateSettings(cfg, nil); err != nil {
		t.Fatal(err)
	}
	if s := status(t, d); s.RefusedUDP != "" {
		t.Fatalf("a program a pattern sends directly is still offered: %q", s.RefusedUDP)
	}
	d.NoteRefusedUDP("Game.exe", "")
	d.SetTunStatus(TunStatus{State: TunOff})
	d.SetTunStatus(TunStatus{State: TunOn})
	if s := status(t, d); s.RefusedUDP != "" {
		t.Fatalf("the TUN off kept %q", s.RefusedUDP)
	}
}