@@ -5,14 +5,16 @@ import (
5
5
"fmt"
6
6
"os/user"
7
7
"path/filepath"
8
+ "regexp"
9
+ "strings"
8
10
"testing"
9
11
10
12
"github.com/github/git-bundle-server/internal/daemon"
11
13
"github.com/stretchr/testify/assert"
12
14
"github.com/stretchr/testify/mock"
13
15
)
14
16
15
- var launchdCreateTests = []struct {
17
+ var launchdCreateBehaviorTests = []struct {
16
18
title string
17
19
18
20
// Inputs
@@ -97,6 +99,74 @@ var launchdCreateTests = []struct {
97
99
},
98
100
}
99
101
102
+ var launchdCreatePlistTests = []struct {
103
+ title string
104
+
105
+ // Inputs
106
+ config * daemon.DaemonConfig
107
+
108
+ // Expected values
109
+ expectedPlistLines []string
110
+ }{
111
+ {
112
+ title : "Created plist is correct" ,
113
+ config : & basicDaemonConfig ,
114
+ expectedPlistLines : []string {
115
+ `<?xml version="1.0" encoding="UTF-8"?>` ,
116
+ `<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">` ,
117
+ `<plist version="1.0">` ,
118
+ "<dict>" ,
119
+
120
+ "<key>Label</key>" ,
121
+ fmt .Sprintf ("<string>%s</string>" , basicDaemonConfig .Label ),
122
+
123
+ "<key>Program</key>" ,
124
+ fmt .Sprintf ("<string>%s</string>" , basicDaemonConfig .Program ),
125
+
126
+ "<key>StandardOutPath</key>" ,
127
+ "<string>/dev/null</string>" ,
128
+
129
+ "<key>StandardErrorPath</key>" ,
130
+ "<string>/dev/null</string>" ,
131
+
132
+ "</dict>" ,
133
+ "</plist>" ,
134
+ },
135
+ },
136
+ {
137
+ title : "Plist contents are escaped" ,
138
+ config : & daemon.DaemonConfig {
139
+ // All of <'&"\t> should be replaced by the associated escape code
140
+ // 🤔 is in-range for XML (no replacement), but (\uFFFF) is
141
+ // out-of-range and replaced with � (\uFFFD)
142
+ // See https://www.w3.org/TR/xml11/Overview.html#charsets for details
143
+ Label : "test-escape<'&\" 🤔>" ,
144
+ Program : "/path/to/the/program with a space" ,
145
+ },
146
+ expectedPlistLines : []string {
147
+ `<?xml version="1.0" encoding="UTF-8"?>` ,
148
+ `<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">` ,
149
+ `<plist version="1.0">` ,
150
+ "<dict>" ,
151
+
152
+ "<key>Label</key>" ,
153
+ "<string>test-escape<'&"	🤔�></string>" ,
154
+
155
+ "<key>Program</key>" ,
156
+ "<string>/path/to/the/program with a space</string>" ,
157
+
158
+ "<key>StandardOutPath</key>" ,
159
+ "<string>/dev/null</string>" ,
160
+
161
+ "<key>StandardErrorPath</key>" ,
162
+ "<string>/dev/null</string>" ,
163
+
164
+ "</dict>" ,
165
+ "</plist>" ,
166
+ },
167
+ },
168
+ }
169
+
100
170
func TestLaunchd_Create (t * testing.T ) {
101
171
// Set up mocks
102
172
testUser := & user.User {
@@ -114,7 +184,7 @@ func TestLaunchd_Create(t *testing.T) {
114
184
launchd := daemon .NewLaunchdProvider (testUserProvider , testCommandExecutor , testFileSystem )
115
185
116
186
// Verify launchd commands called
117
- for _ , tt := range launchdCreateTests {
187
+ for _ , tt := range launchdCreateBehaviorTests {
118
188
forceArg := tt .force .toBoolList ()
119
189
for _ , force := range forceArg {
120
190
t .Run (fmt .Sprintf ("%s (force='%t')" , tt .title , force ), func (t * testing.T ) {
@@ -168,53 +238,61 @@ func TestLaunchd_Create(t *testing.T) {
168
238
}
169
239
170
240
// Verify content of created file
171
- t .Run ("Created file content and path are correct" , func (t * testing.T ) {
172
- var actualFilename string
173
- var actualFileBytes []byte
174
-
175
- // Mock responses for successful fresh write
176
- testCommandExecutor .On ("Run" ,
177
- "launchctl" ,
178
- mock .MatchedBy (func (args []string ) bool { return args [0 ] == "print" }),
179
- ).Return (daemon .LaunchdServiceNotFoundErrorCode , nil ).Once ()
180
- testCommandExecutor .On ("Run" ,
181
- "launchctl" ,
182
- mock .MatchedBy (func (args []string ) bool { return args [0 ] == "bootstrap" }),
183
- ).Return (0 , nil ).Once ()
184
- testFileSystem .On ("FileExists" ,
185
- mock .AnythingOfType ("string" ),
186
- ).Return (false , nil ).Once ()
187
-
188
- // Use mock to save off input args
189
- testFileSystem .On ("WriteFile" ,
190
- mock .MatchedBy (func (filename string ) bool {
191
- actualFilename = filename
192
- return true
193
- }),
194
- mock .MatchedBy (func (fileBytes any ) bool {
195
- // Save off value and always match
196
- actualFileBytes = fileBytes .([]byte )
197
- return true
198
- }),
199
- ).Return (nil ).Once ()
200
-
201
- err := launchd .Create (& basicDaemonConfig , false )
202
- assert .Nil (t , err )
203
- mock .AssertExpectationsForObjects (t , testCommandExecutor , testFileSystem )
204
-
205
- // Check filename
206
- expectedFilename := filepath .Clean (fmt .Sprintf ("/my/test/dir/Library/LaunchAgents/%s.plist" , basicDaemonConfig .Label ))
207
- assert .Equal (t , expectedFilename , actualFilename )
208
-
209
- // Check file contents
210
- err = xml .Unmarshal (actualFileBytes , new (interface {}))
211
- if err != nil {
212
- assert .Fail (t , "plist XML is malformed" )
213
- }
214
- fileContents := string (actualFileBytes )
215
- assert .Contains (t , fileContents , fmt .Sprintf ("<key>Label</key><string>%s</string>" , basicDaemonConfig .Label ))
216
- assert .Contains (t , fileContents , fmt .Sprintf ("<key>Program</key><string>%s</string>" , basicDaemonConfig .Program ))
217
- })
241
+ for _ , tt := range launchdCreatePlistTests {
242
+ t .Run (tt .title , func (t * testing.T ) {
243
+ var actualFilename string
244
+ var actualFileBytes []byte
245
+
246
+ // Mock responses for successful fresh write
247
+ testCommandExecutor .On ("Run" ,
248
+ "launchctl" ,
249
+ mock .MatchedBy (func (args []string ) bool { return args [0 ] == "print" }),
250
+ ).Return (daemon .LaunchdServiceNotFoundErrorCode , nil ).Once ()
251
+ testCommandExecutor .On ("Run" ,
252
+ "launchctl" ,
253
+ mock .MatchedBy (func (args []string ) bool { return args [0 ] == "bootstrap" }),
254
+ ).Return (0 , nil ).Once ()
255
+ testFileSystem .On ("FileExists" ,
256
+ mock .AnythingOfType ("string" ),
257
+ ).Return (false , nil ).Once ()
258
+
259
+ // Use mock to save off input args
260
+ testFileSystem .On ("WriteFile" ,
261
+ mock .MatchedBy (func (filename string ) bool {
262
+ actualFilename = filename
263
+ return true
264
+ }),
265
+ mock .MatchedBy (func (fileBytes any ) bool {
266
+ // Save off value and always match
267
+ actualFileBytes = fileBytes .([]byte )
268
+ return true
269
+ }),
270
+ ).Return (nil ).Once ()
271
+
272
+ err := launchd .Create (tt .config , false )
273
+ assert .Nil (t , err )
274
+ mock .AssertExpectationsForObjects (t , testCommandExecutor , testFileSystem )
275
+
276
+ // Check filename
277
+ expectedFilename := filepath .Clean (fmt .Sprintf ("/my/test/dir/Library/LaunchAgents/%s.plist" , tt .config .Label ))
278
+ assert .Equal (t , expectedFilename , actualFilename )
279
+
280
+ // Check XML
281
+ err = xml .Unmarshal (actualFileBytes , new (interface {}))
282
+ if err != nil {
283
+ assert .Fail (t , "plist XML is malformed" )
284
+ }
285
+ fileContents := strings .TrimSpace (string (actualFileBytes ))
286
+ plistLines := strings .Split (
287
+ regexp .MustCompile (`>\s*<` ).ReplaceAllString (fileContents , ">\n <" ), "\n " )
288
+
289
+ assert .ElementsMatch (t , tt .expectedPlistLines , plistLines )
290
+
291
+ // Reset mocks
292
+ testCommandExecutor .Mock = mock.Mock {}
293
+ testFileSystem .Mock = mock.Mock {}
294
+ })
295
+ }
218
296
}
219
297
220
298
func TestLaunchd_Start (t * testing.T ) {
0 commit comments