| 1 | //go:build windows |
| 2 | |
| 3 | package main |
| 4 | |
| 5 | import ( |
| 6 | "errors" |
| 7 | "io" |
| 8 | "log" |
| 9 | "os" |
| 10 | "path/filepath" |
| 11 | "testing" |
| 12 | "time" |
| 13 | |
| 14 | "reasonix/desktop/internal/instanceidentity" |
| 15 | ) |
| 16 | |
| 17 | func stubDesktopHandoff(t *testing.T) { |
| 18 | t.Helper() |
| 19 | verify, notify := verifyDesktopHandoffFn, notifyHandoffBlockedFn |
| 20 | t.Cleanup(func() { verifyDesktopHandoffFn = verify; notifyHandoffBlockedFn = notify }) |
| 21 | verifyDesktopHandoffFn = func(string, bool) error { return nil } |
| 22 | notifyHandoffBlockedFn = func(bool) {} |
| 23 | } |
| 24 | func TestDesktopHandoffConfirmsOnlyTargetInstance(t *testing.T) { |
| 25 | root, home := t.TempDir(), t.TempDir() |
| 26 | target := filepath.Join(root, "reasonix-desktop.exe") |
| 27 | old := filepath.Join(t.TempDir(), "reasonix-desktop.exe") |
| 28 | for _, p := range []string{target, old} { |
| 29 | if err := os.WriteFile(p, []byte("fixture"), 0600); err != nil { |
| 30 | t.Fatal(err) |
| 31 | } |
| 32 | } |
| 33 | id := instanceidentity.ForHome(home) |
| 34 | t.Setenv("REASONIX_HOME", home) |
| 35 | t.Setenv(instanceidentity.UpdateEnvironmentKey, id) |
| 36 | original, now, sleep := desktopEndpointImageFn, handoffNowFn, handoffSleepFn |
| 37 | t.Cleanup(func() { desktopEndpointImageFn = original; handoffNowFn = now; handoffSleepFn = sleep }) |
| 38 | for _, tc := range []struct { |
| 39 | name string |
| 40 | started bool |
| 41 | paths []string |
| 42 | probeErr error |
| 43 | wantErr bool |
| 44 | }{ |
| 45 | {name: "no owner before launch", paths: []string{""}}, |
| 46 | {name: "active target", started: true, paths: []string{target}}, |
| 47 | {name: "new owner ready", started: true, paths: []string{"", target}}, |
| 48 | {name: "old owner preserved", paths: []string{old}, wantErr: true}, |
| 49 | {name: "old owner wins race", started: true, paths: []string{"", old}, wantErr: true}, |
| 50 | {name: "access denied", probeErr: errors.New("access denied"), wantErr: true}, |
| 51 | {name: "timeout", started: true, paths: []string{""}, wantErr: true}, |
| 52 | } { |
| 53 | t.Run(tc.name, func(t *testing.T) { |
| 54 | clock := time.Unix(0, 0) |
| 55 | handoffNowFn = func() time.Time { return clock } |
| 56 | handoffSleepFn = func(d time.Duration) { clock = clock.Add(d) } |
| 57 | n := 0 |
| 58 | desktopEndpointImageFn = func(got string) (string, error) { |
| 59 | if got != id { |
| 60 | t.Fatalf("probed another home %q", got) |
| 61 | } |
| 62 | if tc.probeErr != nil { |
| 63 | return "", tc.probeErr |
| 64 | } |
| 65 | i := n |
| 66 | if i >= len(tc.paths) { |
| 67 | i = len(tc.paths) - 1 |
| 68 | } |
| 69 | n++ |
| 70 | return tc.paths[i], nil |
| 71 | } |
| 72 | err := verifyDesktopHandoff(root, tc.started) |
| 73 | if (err != nil) != tc.wantErr { |
| 74 | t.Fatalf("error=%v wantError=%v", err, tc.wantErr) |
| 75 | } |
| 76 | }) |
| 77 | } |
| 78 | } |
| 79 | func TestDesktopHandoffRejectsUnboundIdentity(t *testing.T) { |
| 80 | home := t.TempDir() |
| 81 | t.Setenv("REASONIX_HOME", home) |
| 82 | for _, id := range []string{"", instanceidentity.ForHome(t.TempDir())} { |
| 83 | t.Setenv(instanceidentity.UpdateEnvironmentKey, id) |
| 84 | if err := verifyDesktopHandoff(t.TempDir(), false); err == nil { |
| 85 | t.Fatal("unbound helper accepted") |
| 86 | } |
| 87 | } |
| 88 | } |
| 89 | |
| 90 | func TestPublishedRelaunchDoesNotReportSuccessWhenOldOwnerWins(t *testing.T) { |
| 91 | stubDesktopHandoff(t) |
| 92 | originalStart := startRelaunchFn |
| 93 | t.Cleanup(func() { startRelaunchFn = originalStart }) |
| 94 | root := t.TempDir() |
| 95 | target := filepath.Join(root, "reasonix-desktop.exe") |
| 96 | if err := os.WriteFile(target, []byte("target"), 0700); err != nil { |
| 97 | t.Fatal(err) |
| 98 | } |
| 99 | started, notified := false, false |
| 100 | startRelaunchFn = func(string, string) error { started = true; return nil } |
| 101 | verifyDesktopHandoffFn = func(_ string, after bool) error { |
| 102 | if after { |
| 103 | return errors.New("old owner acquired endpoint") |
| 104 | } |
| 105 | return nil |
| 106 | } |
| 107 | notifyHandoffBlockedFn = func(installed bool) { |
| 108 | if !installed { |
| 109 | t.Fatal("lost installed outcome") |
| 110 | } |
| 111 | notified = true |
| 112 | } |
| 113 | if code := relaunchPublishedInstall(log.New(io.Discard, "", 0), target, root, "restart"); code != 1 || !started || !notified { |
| 114 | t.Fatalf("code=%d started=%v notified=%v", code, started, notified) |
| 115 | } |
| 116 | } |
| 117 |