1- import { describe , expect , it , vi } from "vitest" ;
1+ import { afterEach , describe , expect , it , vi } from "vitest" ;
22
3- import {
3+ import * as tailscale from "./tailscale.js" ;
4+
5+ const {
46 ensureGoInstalled,
57 ensureTailscaledInstalled,
68 getTailnetHostname,
79 enableTailscaleServe,
810 disableTailscaleServe,
9- enableTailscaleFunnel ,
10- disableTailscaleFunnel ,
11- ensureFunnel
12- } from "./tailscale.js" ;
11+ ensureFunnel,
12+ } = tailscale ;
1313
1414describe ( "tailscale helpers" , ( ) => {
15+ afterEach ( ( ) => {
16+ vi . restoreAllMocks ( ) ;
17+ } ) ;
18+
1519 it ( "parses DNS name from tailscale status" , async ( ) => {
1620 const exec = vi . fn ( ) . mockResolvedValue ( {
1721 stdout : JSON . stringify ( {
@@ -61,7 +65,9 @@ describe("tailscale helpers", () => {
6165 it ( "enableTailscaleServe attempts normal first, then sudo" , async ( ) => {
6266 // 1. First attempt fails
6367 // 2. Second attempt (sudo) succeeds
64- const exec = vi . fn ( )
68+ vi . spyOn ( tailscale , "getTailscaleBinary" ) . mockResolvedValue ( "tailscale" ) ;
69+ const exec = vi
70+ . fn ( )
6571 . mockRejectedValueOnce ( new Error ( "permission denied" ) )
6672 . mockResolvedValueOnce ( { stdout : "" } ) ;
6773
@@ -71,18 +77,19 @@ describe("tailscale helpers", () => {
7177 1 ,
7278 "tailscale" ,
7379 expect . arrayContaining ( [ "serve" , "--bg" , "--yes" , "3000" ] ) ,
74- expect . any ( Object )
80+ expect . any ( Object ) ,
7581 ) ;
7682
7783 expect ( exec ) . toHaveBeenNthCalledWith (
7884 2 ,
7985 "sudo" ,
8086 expect . arrayContaining ( [ "-n" , "tailscale" , "serve" , "--bg" , "--yes" , "3000" ] ) ,
81- expect . any ( Object )
87+ expect . any ( Object ) ,
8288 ) ;
8389 } ) ;
8490
8591 it ( "enableTailscaleServe does NOT use sudo if first attempt succeeds" , async ( ) => {
92+ vi . spyOn ( tailscale , "getTailscaleBinary" ) . mockResolvedValue ( "tailscale" ) ;
8693 const exec = vi . fn ( ) . mockResolvedValue ( { stdout : "" } ) ;
8794
8895 await enableTailscaleServe ( 3000 , exec as never ) ;
@@ -91,13 +98,15 @@ describe("tailscale helpers", () => {
9198 expect ( exec ) . toHaveBeenCalledWith (
9299 "tailscale" ,
93100 expect . arrayContaining ( [ "serve" , "--bg" , "--yes" , "3000" ] ) ,
94- expect . any ( Object )
101+ expect . any ( Object ) ,
95102 ) ;
96103 } ) ;
97104
98105 it ( "disableTailscaleServe uses fallback" , async ( ) => {
99- const exec = vi . fn ( )
100- . mockRejectedValueOnce ( new Error ( "failed" ) )
106+ vi . spyOn ( tailscale , "getTailscaleBinary" ) . mockResolvedValue ( "tailscale" ) ;
107+ const exec = vi
108+ . fn ( )
109+ . mockRejectedValueOnce ( new Error ( "permission denied" ) )
101110 . mockResolvedValueOnce ( { stdout : "" } ) ;
102111
103112 await disableTailscaleServe ( exec as never ) ;
@@ -107,7 +116,7 @@ describe("tailscale helpers", () => {
107116 2 ,
108117 "sudo" ,
109118 expect . arrayContaining ( [ "-n" , "tailscale" , "serve" , "reset" ] ) ,
110- expect . any ( Object )
119+ expect . any ( Object ) ,
111120 ) ;
112121 } ) ;
113122
@@ -116,9 +125,11 @@ describe("tailscale helpers", () => {
116125 // 1. status (success)
117126 // 2. enable (fails)
118127 // 3. enable sudo (success)
119- const exec = vi . fn ( )
128+ vi . spyOn ( tailscale , "getTailscaleBinary" ) . mockResolvedValue ( "tailscale" ) ;
129+ const exec = vi
130+ . fn ( )
120131 . mockResolvedValueOnce ( { stdout : JSON . stringify ( { BackendState : "Running" } ) } ) // status
121- . mockRejectedValueOnce ( new Error ( "failed " ) ) // enable normal
132+ . mockRejectedValueOnce ( new Error ( "permission denied " ) ) // enable normal
122133 . mockResolvedValueOnce ( { stdout : "" } ) ; // enable sudo
123134
124135 const runtime = {
@@ -134,23 +145,47 @@ describe("tailscale helpers", () => {
134145 expect ( exec ) . toHaveBeenNthCalledWith (
135146 1 ,
136147 "tailscale" ,
137- expect . arrayContaining ( [ "funnel" , "status" , "--json" ] )
148+ expect . arrayContaining ( [ "funnel" , "status" , "--json" ] ) ,
138149 ) ;
139150
140151 // 2. enable normal
141152 expect ( exec ) . toHaveBeenNthCalledWith (
142153 2 ,
143154 "tailscale" ,
144155 expect . arrayContaining ( [ "funnel" , "--yes" , "--bg" , "8080" ] ) ,
145- expect . any ( Object )
156+ expect . any ( Object ) ,
146157 ) ;
147158
148159 // 3. enable sudo
149160 expect ( exec ) . toHaveBeenNthCalledWith (
150161 3 ,
151162 "sudo" ,
152163 expect . arrayContaining ( [ "-n" , "tailscale" , "funnel" , "--yes" , "--bg" , "8080" ] ) ,
153- expect . any ( Object )
164+ expect . any ( Object ) ,
154165 ) ;
155166 } ) ;
167+
168+ it ( "enableTailscaleServe skips sudo on non-permission errors" , async ( ) => {
169+ vi . spyOn ( tailscale , "getTailscaleBinary" ) . mockResolvedValue ( "tailscale" ) ;
170+ const exec = vi . fn ( ) . mockRejectedValueOnce ( new Error ( "boom" ) ) ;
171+
172+ await expect ( enableTailscaleServe ( 3000 , exec as never ) ) . rejects . toThrow ( "boom" ) ;
173+
174+ expect ( exec ) . toHaveBeenCalledTimes ( 1 ) ;
175+ } ) ;
176+
177+ it ( "enableTailscaleServe rethrows original error if sudo fails" , async ( ) => {
178+ vi . spyOn ( tailscale , "getTailscaleBinary" ) . mockResolvedValue ( "tailscale" ) ;
179+ const originalError = Object . assign ( new Error ( "permission denied" ) , {
180+ stderr : "permission denied" ,
181+ } ) ;
182+ const exec = vi
183+ . fn ( )
184+ . mockRejectedValueOnce ( originalError )
185+ . mockRejectedValueOnce ( new Error ( "sudo: a password is required" ) ) ;
186+
187+ await expect ( enableTailscaleServe ( 3000 , exec as never ) ) . rejects . toBe ( originalError ) ;
188+
189+ expect ( exec ) . toHaveBeenCalledTimes ( 2 ) ;
190+ } ) ;
156191} ) ;
0 commit comments