2
2
import { it , afterEach , vi , expect } from "vitest"
3
3
import { SSHConfig } from "./sshConfig"
4
4
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]+$`
6
10
7
11
const mockFileSystem = {
8
- readFile : vi . fn ( ) ,
9
12
mkdir : vi . fn ( ) ,
13
+ readFile : vi . fn ( ) ,
14
+ rename : vi . fn ( ) ,
15
+ stat : vi . fn ( ) ,
10
16
writeFile : vi . fn ( ) ,
11
17
}
12
18
@@ -16,6 +22,7 @@ afterEach(() => {
16
22
17
23
it ( "creates a new file and adds config with empty label" , async ( ) => {
18
24
mockFileSystem . readFile . mockRejectedValueOnce ( "No file found" )
25
+ mockFileSystem . stat . mockRejectedValueOnce ( { code : "ENOENT" } )
19
26
20
27
const sshConfig = new SSHConfig ( sshFilePath , mockFileSystem )
21
28
await sshConfig . load ( )
@@ -38,11 +45,20 @@ Host coder-vscode--*
38
45
# --- END CODER VSCODE ---`
39
46
40
47
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 )
42
57
} )
43
58
44
59
it ( "creates a new file and adds the config" , async ( ) => {
45
60
mockFileSystem . readFile . mockRejectedValueOnce ( "No file found" )
61
+ mockFileSystem . stat . mockRejectedValueOnce ( { code : "ENOENT" } )
46
62
47
63
const sshConfig = new SSHConfig ( sshFilePath , mockFileSystem )
48
64
await sshConfig . load ( )
@@ -65,7 +81,15 @@ Host coder-vscode.dev.coder.com--*
65
81
# --- END CODER VSCODE dev.coder.com ---`
66
82
67
83
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 )
69
93
} )
70
94
71
95
it ( "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 () => {
77
101
StrictHostKeyChecking=no
78
102
UserKnownHostsFile=/dev/null`
79
103
mockFileSystem . readFile . mockResolvedValueOnce ( existentSSHConfig )
104
+ mockFileSystem . stat . mockResolvedValueOnce ( { mode : 0o644 } )
80
105
81
106
const sshConfig = new SSHConfig ( sshFilePath , mockFileSystem )
82
107
await sshConfig . load ( )
@@ -100,10 +125,11 @@ Host coder-vscode.dev.coder.com--*
100
125
UserKnownHostsFile /dev/null
101
126
# --- END CODER VSCODE dev.coder.com ---`
102
127
103
- expect ( mockFileSystem . writeFile ) . toBeCalledWith ( sshFilePath , expectedOutput , {
128
+ expect ( mockFileSystem . writeFile ) . toBeCalledWith ( expect . stringMatching ( sshTempFilePathExpr ) , expectedOutput , {
104
129
encoding : "utf-8" ,
105
- mode : 384 ,
130
+ mode : 0o644 ,
106
131
} )
132
+ expect ( mockFileSystem . rename ) . toBeCalledWith ( expect . stringMatching ( sshTempFilePathExpr ) , sshFilePath )
107
133
} )
108
134
109
135
it ( "updates an existent coder config" , async ( ) => {
@@ -138,6 +164,7 @@ Host coder-vscode.dev.coder.com--*
138
164
Host *
139
165
SetEnv TEST=1`
140
166
mockFileSystem . readFile . mockResolvedValueOnce ( existentSSHConfig )
167
+ mockFileSystem . stat . mockResolvedValueOnce ( { mode : 0o644 } )
141
168
142
169
const sshConfig = new SSHConfig ( sshFilePath , mockFileSystem )
143
170
await sshConfig . load ( )
@@ -164,10 +191,11 @@ Host coder-vscode.dev-updated.coder.com--*
164
191
Host *
165
192
SetEnv TEST=1`
166
193
167
- expect ( mockFileSystem . writeFile ) . toBeCalledWith ( sshFilePath , expectedOutput , {
194
+ expect ( mockFileSystem . writeFile ) . toBeCalledWith ( expect . stringMatching ( sshTempFilePathExpr ) , expectedOutput , {
168
195
encoding : "utf-8" ,
169
- mode : 384 ,
196
+ mode : 0o644 ,
170
197
} )
198
+ expect ( mockFileSystem . rename ) . toBeCalledWith ( expect . stringMatching ( sshTempFilePathExpr ) , sshFilePath )
171
199
} )
172
200
173
201
it ( "does not remove deployment-unaware SSH config and adds the new one" , async ( ) => {
@@ -186,6 +214,7 @@ Host coder-vscode--*
186
214
UserKnownHostsFile=/dev/null
187
215
# --- END CODER VSCODE ---`
188
216
mockFileSystem . readFile . mockResolvedValueOnce ( existentSSHConfig )
217
+ mockFileSystem . stat . mockResolvedValueOnce ( { mode : 0o644 } )
189
218
190
219
const sshConfig = new SSHConfig ( sshFilePath , mockFileSystem )
191
220
await sshConfig . load ( )
@@ -209,16 +238,18 @@ Host coder-vscode.dev.coder.com--*
209
238
UserKnownHostsFile /dev/null
210
239
# --- END CODER VSCODE dev.coder.com ---`
211
240
212
- expect ( mockFileSystem . writeFile ) . toBeCalledWith ( sshFilePath , expectedOutput , {
241
+ expect ( mockFileSystem . writeFile ) . toBeCalledWith ( expect . stringMatching ( sshTempFilePathExpr ) , expectedOutput , {
213
242
encoding : "utf-8" ,
214
- mode : 384 ,
243
+ mode : 0o644 ,
215
244
} )
245
+ expect ( mockFileSystem . rename ) . toBeCalledWith ( expect . stringMatching ( sshTempFilePathExpr ) , sshFilePath )
216
246
} )
217
247
218
248
it ( "it does not remove a user-added block that only matches the host of an old coder SSH config" , async ( ) => {
219
249
const existentSSHConfig = `Host coder-vscode--*
220
250
ForwardAgent=yes`
221
251
mockFileSystem . readFile . mockResolvedValueOnce ( existentSSHConfig )
252
+ mockFileSystem . stat . mockResolvedValueOnce ( { mode : 0o644 } )
222
253
223
254
const sshConfig = new SSHConfig ( sshFilePath , mockFileSystem )
224
255
await sshConfig . load ( )
@@ -243,10 +274,11 @@ Host coder-vscode.dev.coder.com--*
243
274
UserKnownHostsFile /dev/null
244
275
# --- END CODER VSCODE dev.coder.com ---`
245
276
246
- expect ( mockFileSystem . writeFile ) . toBeCalledWith ( sshFilePath , expectedOutput , {
277
+ expect ( mockFileSystem . writeFile ) . toBeCalledWith ( expect . stringMatching ( sshTempFilePathExpr ) , expectedOutput , {
247
278
encoding : "utf-8" ,
248
- mode : 384 ,
279
+ mode : 0o644 ,
249
280
} )
281
+ expect ( mockFileSystem . rename ) . toBeCalledWith ( expect . stringMatching ( sshTempFilePathExpr ) , sshFilePath )
250
282
} )
251
283
252
284
it ( "throws an error if there is a missing end block" , async ( ) => {
@@ -476,6 +508,7 @@ Host afterconfig
476
508
477
509
const sshConfig = new SSHConfig ( sshFilePath , mockFileSystem )
478
510
mockFileSystem . readFile . mockResolvedValueOnce ( existentSSHConfig )
511
+ mockFileSystem . stat . mockResolvedValueOnce ( { mode : 0o644 } )
479
512
await sshConfig . load ( )
480
513
481
514
const expectedOutput = `Host beforeconfig
@@ -517,14 +550,17 @@ Host afterconfig
517
550
LogLevel : "ERROR" ,
518
551
} )
519
552
520
- expect ( mockFileSystem . writeFile ) . toBeCalledWith ( sshFilePath , expectedOutput , {
553
+ expect ( mockFileSystem . writeFile ) . toBeCalledWith ( expect . stringMatching ( sshTempFilePathExpr ) , expectedOutput , {
521
554
encoding : "utf-8" ,
522
- mode : 384 ,
555
+ mode : 0o644 ,
523
556
} )
557
+ expect ( mockFileSystem . rename ) . toBeCalledWith ( expect . stringMatching ( sshTempFilePathExpr ) , sshFilePath )
524
558
} )
525
559
526
560
it ( "override values" , async ( ) => {
527
561
mockFileSystem . readFile . mockRejectedValueOnce ( "No file found" )
562
+ mockFileSystem . stat . mockRejectedValueOnce ( { code : "ENOENT" } )
563
+
528
564
const sshConfig = new SSHConfig ( sshFilePath , mockFileSystem )
529
565
await sshConfig . load ( )
530
566
await sshConfig . update (
@@ -561,5 +597,62 @@ Host coder-vscode.dev.coder.com--*
561
597
# --- END CODER VSCODE dev.coder.com ---`
562
598
563
599
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 / )
565
658
} )
0 commit comments