22import { it , afterEach , vi , expect } from "vitest"
33import { SSHConfig } from "./sshConfig"
44
5- const sshFilePath = "~/.config/ssh"
5+ // This is not the usual path to ~/.ssh/config, but
6+ // setting it to a different path makes it easier to test
7+ // and makes mistakes abundantly clear.
8+ const sshFilePath = "/Path/To/UserHomeDir/.sshConfigDir/sshConfigFile"
9+ const sshTempFilePathExpr = `^/Path/To/UserHomeDir/\\.sshConfigDir/\\.sshConfigFile\\.vscode-coder-tmp\\.[a-z0-9]+$`
610
711const mockFileSystem = {
8- readFile : vi . fn ( ) ,
912 mkdir : vi . fn ( ) ,
13+ readFile : vi . fn ( ) ,
14+ rename : vi . fn ( ) ,
15+ stat : vi . fn ( ) ,
1016 writeFile : vi . fn ( ) ,
1117}
1218
@@ -16,6 +22,7 @@ afterEach(() => {
1622
1723it ( "creates a new file and adds config with empty label" , async ( ) => {
1824 mockFileSystem . readFile . mockRejectedValueOnce ( "No file found" )
25+ mockFileSystem . stat . mockRejectedValueOnce ( { code : "ENOENT" } )
1926
2027 const sshConfig = new SSHConfig ( sshFilePath , mockFileSystem )
2128 await sshConfig . load ( )
@@ -38,11 +45,20 @@ Host coder-vscode--*
3845# --- END CODER VSCODE ---`
3946
4047 expect ( mockFileSystem . readFile ) . toBeCalledWith ( sshFilePath , expect . anything ( ) )
41- expect ( mockFileSystem . writeFile ) . toBeCalledWith ( sshFilePath , expectedOutput , expect . anything ( ) )
48+ expect ( mockFileSystem . writeFile ) . toBeCalledWith (
49+ expect . stringMatching ( sshTempFilePathExpr ) ,
50+ expectedOutput ,
51+ expect . objectContaining ( {
52+ encoding : "utf-8" ,
53+ mode : 0o600 , // Default mode for new files.
54+ } ) ,
55+ )
56+ expect ( mockFileSystem . rename ) . toBeCalledWith ( expect . stringMatching ( sshTempFilePathExpr ) , sshFilePath )
4257} )
4358
4459it ( "creates a new file and adds the config" , async ( ) => {
4560 mockFileSystem . readFile . mockRejectedValueOnce ( "No file found" )
61+ mockFileSystem . stat . mockRejectedValueOnce ( { code : "ENOENT" } )
4662
4763 const sshConfig = new SSHConfig ( sshFilePath , mockFileSystem )
4864 await sshConfig . load ( )
@@ -65,7 +81,15 @@ Host coder-vscode.dev.coder.com--*
6581# --- END CODER VSCODE dev.coder.com ---`
6682
6783 expect ( mockFileSystem . readFile ) . toBeCalledWith ( sshFilePath , expect . anything ( ) )
68- expect ( mockFileSystem . writeFile ) . toBeCalledWith ( sshFilePath , expectedOutput , expect . anything ( ) )
84+ expect ( mockFileSystem . writeFile ) . toBeCalledWith (
85+ expect . stringMatching ( sshTempFilePathExpr ) ,
86+ expectedOutput ,
87+ expect . objectContaining ( {
88+ encoding : "utf-8" ,
89+ mode : 0o600 , // Default mode for new files.
90+ } ) ,
91+ )
92+ expect ( mockFileSystem . rename ) . toBeCalledWith ( expect . stringMatching ( sshTempFilePathExpr ) , sshFilePath )
6993} )
7094
7195it ( "adds a new coder config in an existent SSH configuration" , async ( ) => {
@@ -77,6 +101,7 @@ it("adds a new coder config in an existent SSH configuration", async () => {
77101 StrictHostKeyChecking=no
78102 UserKnownHostsFile=/dev/null`
79103 mockFileSystem . readFile . mockResolvedValueOnce ( existentSSHConfig )
104+ mockFileSystem . stat . mockResolvedValueOnce ( { mode : 0o644 } )
80105
81106 const sshConfig = new SSHConfig ( sshFilePath , mockFileSystem )
82107 await sshConfig . load ( )
@@ -100,10 +125,11 @@ Host coder-vscode.dev.coder.com--*
100125 UserKnownHostsFile /dev/null
101126# --- END CODER VSCODE dev.coder.com ---`
102127
103- expect ( mockFileSystem . writeFile ) . toBeCalledWith ( sshFilePath , expectedOutput , {
128+ expect ( mockFileSystem . writeFile ) . toBeCalledWith ( expect . stringMatching ( sshTempFilePathExpr ) , expectedOutput , {
104129 encoding : "utf-8" ,
105- mode : 384 ,
130+ mode : 0o644 ,
106131 } )
132+ expect ( mockFileSystem . rename ) . toBeCalledWith ( expect . stringMatching ( sshTempFilePathExpr ) , sshFilePath )
107133} )
108134
109135it ( "updates an existent coder config" , async ( ) => {
@@ -138,6 +164,7 @@ Host coder-vscode.dev.coder.com--*
138164Host *
139165 SetEnv TEST=1`
140166 mockFileSystem . readFile . mockResolvedValueOnce ( existentSSHConfig )
167+ mockFileSystem . stat . mockResolvedValueOnce ( { mode : 0o644 } )
141168
142169 const sshConfig = new SSHConfig ( sshFilePath , mockFileSystem )
143170 await sshConfig . load ( )
@@ -164,10 +191,11 @@ Host coder-vscode.dev-updated.coder.com--*
164191Host *
165192 SetEnv TEST=1`
166193
167- expect ( mockFileSystem . writeFile ) . toBeCalledWith ( sshFilePath , expectedOutput , {
194+ expect ( mockFileSystem . writeFile ) . toBeCalledWith ( expect . stringMatching ( sshTempFilePathExpr ) , expectedOutput , {
168195 encoding : "utf-8" ,
169- mode : 384 ,
196+ mode : 0o644 ,
170197 } )
198+ expect ( mockFileSystem . rename ) . toBeCalledWith ( expect . stringMatching ( sshTempFilePathExpr ) , sshFilePath )
171199} )
172200
173201it ( "does not remove deployment-unaware SSH config and adds the new one" , async ( ) => {
@@ -186,6 +214,7 @@ Host coder-vscode--*
186214 UserKnownHostsFile=/dev/null
187215# --- END CODER VSCODE ---`
188216 mockFileSystem . readFile . mockResolvedValueOnce ( existentSSHConfig )
217+ mockFileSystem . stat . mockResolvedValueOnce ( { mode : 0o644 } )
189218
190219 const sshConfig = new SSHConfig ( sshFilePath , mockFileSystem )
191220 await sshConfig . load ( )
@@ -209,16 +238,18 @@ Host coder-vscode.dev.coder.com--*
209238 UserKnownHostsFile /dev/null
210239# --- END CODER VSCODE dev.coder.com ---`
211240
212- expect ( mockFileSystem . writeFile ) . toBeCalledWith ( sshFilePath , expectedOutput , {
241+ expect ( mockFileSystem . writeFile ) . toBeCalledWith ( expect . stringMatching ( sshTempFilePathExpr ) , expectedOutput , {
213242 encoding : "utf-8" ,
214- mode : 384 ,
243+ mode : 0o644 ,
215244 } )
245+ expect ( mockFileSystem . rename ) . toBeCalledWith ( expect . stringMatching ( sshTempFilePathExpr ) , sshFilePath )
216246} )
217247
218248it ( "it does not remove a user-added block that only matches the host of an old coder SSH config" , async ( ) => {
219249 const existentSSHConfig = `Host coder-vscode--*
220250 ForwardAgent=yes`
221251 mockFileSystem . readFile . mockResolvedValueOnce ( existentSSHConfig )
252+ mockFileSystem . stat . mockResolvedValueOnce ( { mode : 0o644 } )
222253
223254 const sshConfig = new SSHConfig ( sshFilePath , mockFileSystem )
224255 await sshConfig . load ( )
@@ -243,10 +274,11 @@ Host coder-vscode.dev.coder.com--*
243274 UserKnownHostsFile /dev/null
244275# --- END CODER VSCODE dev.coder.com ---`
245276
246- expect ( mockFileSystem . writeFile ) . toBeCalledWith ( sshFilePath , expectedOutput , {
277+ expect ( mockFileSystem . writeFile ) . toBeCalledWith ( expect . stringMatching ( sshTempFilePathExpr ) , expectedOutput , {
247278 encoding : "utf-8" ,
248- mode : 384 ,
279+ mode : 0o644 ,
249280 } )
281+ expect ( mockFileSystem . rename ) . toBeCalledWith ( expect . stringMatching ( sshTempFilePathExpr ) , sshFilePath )
250282} )
251283
252284it ( "throws an error if there is a missing end block" , async ( ) => {
@@ -476,6 +508,7 @@ Host afterconfig
476508
477509 const sshConfig = new SSHConfig ( sshFilePath , mockFileSystem )
478510 mockFileSystem . readFile . mockResolvedValueOnce ( existentSSHConfig )
511+ mockFileSystem . stat . mockResolvedValueOnce ( { mode : 0o644 } )
479512 await sshConfig . load ( )
480513
481514 const expectedOutput = `Host beforeconfig
@@ -517,14 +550,17 @@ Host afterconfig
517550 LogLevel : "ERROR" ,
518551 } )
519552
520- expect ( mockFileSystem . writeFile ) . toBeCalledWith ( sshFilePath , expectedOutput , {
553+ expect ( mockFileSystem . writeFile ) . toBeCalledWith ( expect . stringMatching ( sshTempFilePathExpr ) , expectedOutput , {
521554 encoding : "utf-8" ,
522- mode : 384 ,
555+ mode : 0o644 ,
523556 } )
557+ expect ( mockFileSystem . rename ) . toBeCalledWith ( expect . stringMatching ( sshTempFilePathExpr ) , sshFilePath )
524558} )
525559
526560it ( "override values" , async ( ) => {
527561 mockFileSystem . readFile . mockRejectedValueOnce ( "No file found" )
562+ mockFileSystem . stat . mockRejectedValueOnce ( { code : "ENOENT" } )
563+
528564 const sshConfig = new SSHConfig ( sshFilePath , mockFileSystem )
529565 await sshConfig . load ( )
530566 await sshConfig . update (
@@ -561,5 +597,62 @@ Host coder-vscode.dev.coder.com--*
561597# --- END CODER VSCODE dev.coder.com ---`
562598
563599 expect ( mockFileSystem . readFile ) . toBeCalledWith ( sshFilePath , expect . anything ( ) )
564- expect ( mockFileSystem . writeFile ) . toBeCalledWith ( sshFilePath , expectedOutput , expect . anything ( ) )
600+ expect ( mockFileSystem . writeFile ) . toBeCalledWith (
601+ expect . stringMatching ( sshTempFilePathExpr ) ,
602+ expectedOutput ,
603+ expect . objectContaining ( {
604+ encoding : "utf-8" ,
605+ mode : 0o600 , // Default mode for new files.
606+ } ) ,
607+ )
608+ expect ( mockFileSystem . rename ) . toBeCalledWith ( expect . stringMatching ( sshTempFilePathExpr ) , sshFilePath )
609+ } )
610+
611+ it ( "fails if we are unable to write the temporary file" , async ( ) => {
612+ const existentSSHConfig = `Host beforeconfig
613+ HostName before.config.tld
614+ User before`
615+
616+ const sshConfig = new SSHConfig ( sshFilePath , mockFileSystem )
617+ mockFileSystem . readFile . mockResolvedValueOnce ( existentSSHConfig )
618+ mockFileSystem . stat . mockResolvedValueOnce ( { mode : 0o600 } )
619+ mockFileSystem . writeFile . mockRejectedValueOnce ( new Error ( "EACCES" ) )
620+
621+ await sshConfig . load ( )
622+
623+ expect ( mockFileSystem . readFile ) . toBeCalledWith ( sshFilePath , expect . anything ( ) )
624+ await expect (
625+ sshConfig . update ( "dev.coder.com" , {
626+ Host : "coder-vscode.dev.coder.com--*" ,
627+ ProxyCommand : "some-command-here" ,
628+ ConnectTimeout : "0" ,
629+ StrictHostKeyChecking : "no" ,
630+ UserKnownHostsFile : "/dev/null" ,
631+ LogLevel : "ERROR" ,
632+ } ) ,
633+ ) . rejects . toThrow ( / F a i l e d t o w r i t e t e m p o r a r y S S H c o n f i g f i l e .* E A C C E S / )
634+ } )
635+
636+ it ( "fails if we are unable to rename the temporary file" , async ( ) => {
637+ const existentSSHConfig = `Host beforeconfig
638+ HostName before.config.tld
639+ User before`
640+
641+ const sshConfig = new SSHConfig ( sshFilePath , mockFileSystem )
642+ mockFileSystem . readFile . mockResolvedValueOnce ( existentSSHConfig )
643+ mockFileSystem . stat . mockResolvedValueOnce ( { mode : 0o600 } )
644+ mockFileSystem . writeFile . mockResolvedValueOnce ( "" )
645+ mockFileSystem . rename . mockRejectedValueOnce ( new Error ( "EACCES" ) )
646+
647+ await sshConfig . load ( )
648+ await expect (
649+ sshConfig . update ( "dev.coder.com" , {
650+ Host : "coder-vscode.dev.coder.com--*" ,
651+ ProxyCommand : "some-command-here" ,
652+ ConnectTimeout : "0" ,
653+ StrictHostKeyChecking : "no" ,
654+ UserKnownHostsFile : "/dev/null" ,
655+ LogLevel : "ERROR" ,
656+ } ) ,
657+ ) . rejects . toThrow ( / F a i l e d t o r e n a m e t e m p o r a r y S S H c o n f i g f i l e .* E A C C E S / )
565658} )
0 commit comments