]> ruderich.org/simon Gitweb - nsscash/nsscash.git/blob - main_test.go
nsscash: main_test: test new server response which causes an update
[nsscash/nsscash.git] / main_test.go
1 // Copyright (C) 2019  Simon Ruderich
2 //
3 // This program is free software: you can redistribute it and/or modify
4 // it under the terms of the GNU Affero General Public License as published by
5 // the Free Software Foundation, either version 3 of the License, or
6 // (at your option) any later version.
7 //
8 // This program is distributed in the hope that it will be useful,
9 // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 // GNU Affero General Public License for more details.
12 //
13 // You should have received a copy of the GNU Affero General Public License
14 // along with this program.  If not, see <https://www.gnu.org/licenses/>.
15
16 package main
17
18 import (
19         "crypto/sha1"
20         "crypto/tls"
21         "encoding/hex"
22         "fmt"
23         "io/ioutil"
24         "log"
25         "net/http"
26         "net/http/httptest"
27         "os"
28         "reflect"
29         "runtime"
30         "strings"
31         "testing"
32         "time"
33 )
34
35 const (
36         configPath  = "testdata/config.toml"
37         statePath   = "testdata/state.json"
38         passwdPath  = "testdata/passwd.nsscash"
39         plainPath   = "testdata/plain"
40         groupPath   = "testdata/group.nsscash"
41         tlsCAPath   = "testdata/ca.crt"
42         tlsCertPath = "testdata/server.crt"
43         tlsKeyPath  = "testdata/server.key"
44         tlsCA2Path  = "testdata/ca2.crt"
45 )
46
47 type args struct {
48         t       *testing.T
49         url     string
50         handler *func(http.ResponseWriter, *http.Request)
51 }
52
53 // mustNotExist verifies that all given paths don't exist in the file system.
54 func mustNotExist(t *testing.T, paths ...string) {
55         for _, p := range paths {
56                 f, err := os.Open(p)
57                 if err != nil {
58                         if !os.IsNotExist(err) {
59                                 t.Errorf("path %q: unexpected error: %v",
60                                         p, err)
61                         }
62                 } else {
63                         t.Errorf("path %q exists", p)
64                         f.Close()
65                 }
66         }
67 }
68
69 // mustHaveHash checks if the given path content has the given SHA-1 string
70 // (in hex).
71 func mustHaveHash(t *testing.T, path string, hash string) {
72         x, err := ioutil.ReadFile(path)
73         if err != nil {
74                 t.Fatal(err)
75         }
76
77         h := sha1.New()
78         h.Write(x)
79         y := hex.EncodeToString(h.Sum(nil))
80
81         if y != hash {
82                 t.Errorf("%q has unexpected hash %q", path, y)
83         }
84 }
85
86 // mustBeErrorWithSubstring checks if the given error, represented as string,
87 // contains the given substring. This is somewhat ugly but the simplest way to
88 // check for proper errors.
89 func mustBeErrorWithSubstring(t *testing.T, err error, substring string) {
90         if err == nil {
91                 t.Errorf("err is nil")
92         } else if !strings.Contains(err.Error(), substring) {
93                 t.Errorf("err %q does not contain string %q", err, substring)
94         }
95 }
96
97 func mustWriteConfig(t *testing.T, config string) {
98         err := ioutil.WriteFile(configPath, []byte(config), 0644)
99         if err != nil {
100                 t.Fatal(err)
101         }
102 }
103
104 func mustWritePasswdConfig(t *testing.T, url string) {
105         mustWriteConfig(t, fmt.Sprintf(`
106 statepath = "%[1]s"
107
108 [[file]]
109 type = "passwd"
110 url = "%[2]s/passwd"
111 path = "%[3]s"
112 ca = "%[4]s"
113 `, statePath, url, passwdPath, tlsCAPath))
114 }
115
116 func mustWriteGroupConfig(t *testing.T, url string) {
117         mustWriteConfig(t, fmt.Sprintf(`
118 statepath = "%[1]s"
119
120 [[file]]
121 type = "group"
122 url = "%[2]s/group"
123 path = "%[3]s"
124 ca = "%[4]s"
125 `, statePath, url, groupPath, tlsCAPath))
126 }
127
128 // mustCreate creates a file, truncating it if it exists. It then changes the
129 // modification to be in the past.
130 func mustCreate(t *testing.T, path string) {
131         f, err := os.Create(path)
132         if err != nil {
133                 t.Fatal(err)
134         }
135         err = f.Close()
136         if err != nil {
137                 t.Fatal(err)
138         }
139
140         // Change modification time to the past to detect updates to the file
141         mustMakeOld(t, path)
142 }
143
144 // mustMakeOld change the modification time of all paths to be in the past.
145 func mustMakeOld(t *testing.T, paths ...string) {
146         old := time.Now().Add(-2 * time.Hour)
147         for _, p := range paths {
148                 err := os.Chtimes(p, old, old)
149                 if err != nil {
150                         t.Fatal(err)
151                 }
152         }
153 }
154
155 // mustMakeOld verifies that all paths have a modification time in the past,
156 // as set by mustMakeOld().
157 func mustBeOld(t *testing.T, paths ...string) {
158         for _, p := range paths {
159                 i, err := os.Stat(p)
160                 if err != nil {
161                         t.Fatal(err)
162                 }
163
164                 mtime := i.ModTime()
165                 now := time.Now()
166                 if now.Sub(mtime) < time.Hour {
167                         t.Errorf("%q was recently modified", p)
168                 }
169         }
170 }
171
172 // mustBeNew verifies that all paths have a modification time in the present.
173 func mustBeNew(t *testing.T, paths ...string) {
174         for _, p := range paths {
175                 i, err := os.Stat(p)
176                 if err != nil {
177                         t.Fatal(err)
178                 }
179
180                 mtime := i.ModTime()
181                 now := time.Now()
182                 if now.Sub(mtime) > time.Hour {
183                         t.Errorf("%q was not recently modified", p)
184                 }
185         }
186 }
187
188 func TestMainFetch(t *testing.T) {
189         // Suppress log messages
190         log.SetOutput(ioutil.Discard)
191         defer log.SetOutput(os.Stderr)
192
193         tests := []func(args){
194                 // Perform most tests with passwd for simplicity
195                 fetchPasswdCacheFileDoesNotExist,
196                 fetchPasswd404,
197                 fetchPasswdEmpty,
198                 fetchPasswdInvalid,
199                 fetchPasswdLimits,
200                 fetchPasswd,
201                 // Tests for plain and group
202                 fetchPlainEmpty,
203                 fetchPlain,
204                 fetchGroupEmpty,
205                 fetchGroupInvalid,
206                 fetchGroupLimits,
207                 fetchGroup,
208                 // Special tests
209                 fetchNoConfig,
210                 fetchStateCannotRead,
211                 fetchStateInvalid,
212                 fetchStateCannotWrite,
213                 fetchCannotDeploy,
214                 fetchSecondFetchFails,
215         }
216
217         // HTTP tests
218
219         for _, f := range tests {
220                 runMainTest(t, f, nil)
221         }
222
223         // HTTPS tests
224
225         tests = append(tests, fetchInvalidCA)
226
227         cert, err := tls.LoadX509KeyPair(tlsCertPath, tlsKeyPath)
228         if err != nil {
229                 t.Fatal(err)
230         }
231         tls := &tls.Config{
232                 Certificates: []tls.Certificate{cert},
233         }
234
235         for _, f := range tests {
236                 runMainTest(t, f, tls)
237         }
238 }
239
240 func runMainTest(t *testing.T, f func(args), tls *tls.Config) {
241         cleanup := []string{
242                 configPath,
243                 statePath,
244                 passwdPath,
245                 plainPath,
246                 groupPath,
247         }
248
249         // NOTE: This is not guaranteed to work according to reflect's
250         // documentation but seems to work reliable for normal functions.
251         fn := runtime.FuncForPC(reflect.ValueOf(f).Pointer())
252         name := fn.Name()
253         name = name[strings.LastIndex(name, ".")+1:]
254         if tls != nil {
255                 name = "tls" + name
256         }
257
258         t.Run(name, func(t *testing.T) {
259                 // Preparation & cleanup
260                 for _, p := range cleanup {
261                         err := os.Remove(p)
262                         if err != nil && !os.IsNotExist(err) {
263                                 t.Fatal(err)
264                         }
265                         // Remove the file at the end of this test run, if it
266                         // was created
267                         defer os.Remove(p)
268                 }
269
270                 var handler func(http.ResponseWriter, *http.Request)
271                 ts := httptest.NewUnstartedServer(http.HandlerFunc(
272                         func(w http.ResponseWriter, r *http.Request) {
273                                 handler(w, r)
274                         }))
275                 if tls == nil {
276                         ts.Start()
277                 } else {
278                         ts.TLS = tls
279                         ts.StartTLS()
280                 }
281                 defer ts.Close()
282
283                 f(args{
284                         t:       t,
285                         url:     ts.URL,
286                         handler: &handler,
287                 })
288         })
289 }
290
291 func fetchPasswdCacheFileDoesNotExist(a args) {
292         t := a.t
293         mustWritePasswdConfig(t, a.url)
294
295         err := mainFetch(configPath)
296         mustBeErrorWithSubstring(t, err,
297                 "file.path \""+passwdPath+"\" must exist")
298
299         mustNotExist(t, statePath, passwdPath, plainPath, groupPath)
300 }
301
302 func fetchPasswd404(a args) {
303         t := a.t
304         mustWritePasswdConfig(t, a.url)
305         mustCreate(t, passwdPath)
306
307         *a.handler = func(w http.ResponseWriter, r *http.Request) {
308                 // 404
309                 w.WriteHeader(http.StatusNotFound)
310         }
311
312         err := mainFetch(configPath)
313         mustBeErrorWithSubstring(t, err,
314                 "status code 404")
315
316         mustNotExist(t, statePath, plainPath, groupPath)
317         mustBeOld(a.t, passwdPath)
318 }
319
320 func fetchPasswdEmpty(a args) {
321         t := a.t
322         mustWritePasswdConfig(t, a.url)
323         mustCreate(t, passwdPath)
324
325         *a.handler = func(w http.ResponseWriter, r *http.Request) {
326                 // Empty response
327         }
328
329         err := mainFetch(configPath)
330         mustBeErrorWithSubstring(t, err,
331                 "refusing to use empty passwd file")
332
333         mustNotExist(t, statePath, plainPath, groupPath)
334         mustBeOld(t, passwdPath)
335 }
336
337 func fetchPasswdInvalid(a args) {
338         t := a.t
339         mustWritePasswdConfig(t, a.url)
340         mustCreate(t, passwdPath)
341
342         *a.handler = func(w http.ResponseWriter, r *http.Request) {
343                 if r.URL.Path != "/passwd" {
344                         return
345                 }
346
347                 fmt.Fprintln(w, "root:x:invalid:0:root:/root:/bin/bash")
348         }
349
350         err := mainFetch(configPath)
351         mustBeErrorWithSubstring(t, err,
352                 "invalid uid in line")
353
354         mustNotExist(t, statePath, plainPath, groupPath)
355         mustBeOld(t, passwdPath)
356 }
357
358 func fetchPasswdLimits(a args) {
359         t := a.t
360         mustWritePasswdConfig(t, a.url)
361         mustCreate(t, passwdPath)
362
363         *a.handler = func(w http.ResponseWriter, r *http.Request) {
364                 if r.URL.Path != "/passwd" {
365                         return
366                 }
367
368                 fmt.Fprint(w, "root:x:0:0:root:/root:/bin/bash")
369                 for i := 0; i < 65536; i++ {
370                         fmt.Fprint(w, "x")
371                 }
372                 fmt.Fprint(w, "\n")
373         }
374
375         err := mainFetch(configPath)
376         mustBeErrorWithSubstring(t, err,
377                 "passwd too large to serialize")
378
379         mustNotExist(t, statePath, plainPath, groupPath)
380         mustBeOld(t, passwdPath)
381 }
382
383 func fetchPasswd(a args) {
384         t := a.t
385         mustWritePasswdConfig(t, a.url)
386         mustCreate(t, passwdPath)
387         mustHaveHash(t, passwdPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709")
388
389         t.Log("First fetch, write files")
390
391         *a.handler = func(w http.ResponseWriter, r *http.Request) {
392                 if r.URL.Path != "/passwd" {
393                         return
394                 }
395
396                 // No "Last-Modified" header
397                 fmt.Fprintln(w, "root:x:0:0:root:/root:/bin/bash")
398                 fmt.Fprintln(w, "daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin")
399         }
400
401         err := mainFetch(configPath)
402         if err != nil {
403                 t.Error(err)
404         }
405
406         mustNotExist(t, plainPath, groupPath)
407         mustBeNew(t, passwdPath, statePath)
408         // The actual content of passwdPath is verified by the NSS tests
409         mustHaveHash(t, passwdPath, "bbb7db67469b111200400e2470346d5515d64c23")
410
411         t.Log("Fetch again, no support for Last-Modified")
412
413         mustMakeOld(t, passwdPath, statePath)
414
415         err = mainFetch(configPath)
416         if err != nil {
417                 t.Error(err)
418         }
419
420         mustNotExist(t, plainPath, groupPath)
421         mustBeNew(t, passwdPath, statePath)
422         mustHaveHash(t, passwdPath, "bbb7db67469b111200400e2470346d5515d64c23")
423
424         t.Log("Fetch again, support for Last-Modified, but not retrieved yet")
425
426         mustMakeOld(t, passwdPath, statePath)
427
428         lastChange := time.Now()
429         change := false
430         *a.handler = func(w http.ResponseWriter, r *http.Request) {
431                 if r.URL.Path != "/passwd" {
432                         return
433                 }
434
435                 modified := r.Header.Get("If-Modified-Since")
436                 if modified != "" {
437                         x, err := http.ParseTime(modified)
438                         if err != nil {
439                                 t.Fatalf("invalid If-Modified-Since %v",
440                                         modified)
441                         }
442                         if !x.Before(lastChange.Truncate(time.Second)) {
443                                 w.WriteHeader(http.StatusNotModified)
444                                 return
445                         }
446                 }
447
448                 w.Header().Add("Last-Modified",
449                         lastChange.UTC().Format(http.TimeFormat))
450                 fmt.Fprintln(w, "root:x:0:0:root:/root:/bin/bash")
451                 fmt.Fprintln(w, "daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin")
452                 if change {
453                         fmt.Fprintln(w, "bin:x:2:2:bin:/bin:/usr/sbin/nologin")
454                 }
455         }
456
457         err = mainFetch(configPath)
458         if err != nil {
459                 t.Error(err)
460         }
461
462         mustNotExist(t, plainPath, groupPath)
463         mustBeNew(t, passwdPath, statePath)
464         mustHaveHash(t, passwdPath, "bbb7db67469b111200400e2470346d5515d64c23")
465
466         t.Log("Fetch again, support for Last-Modified")
467
468         mustMakeOld(t, passwdPath, statePath)
469
470         err = mainFetch(configPath)
471         if err != nil {
472                 t.Error(err)
473         }
474
475         mustNotExist(t, plainPath, groupPath)
476         mustBeOld(t, passwdPath)
477         mustBeNew(t, statePath)
478         mustHaveHash(t, passwdPath, "bbb7db67469b111200400e2470346d5515d64c23")
479
480         t.Log("Corrupt local passwd cache, fetched again")
481
482         os.Chmod(passwdPath, 0644) // make writable again
483         mustCreate(t, passwdPath)
484         mustMakeOld(t, passwdPath, statePath)
485
486         err = mainFetch(configPath)
487         if err != nil {
488                 t.Error(err)
489         }
490
491         mustNotExist(t, plainPath, groupPath)
492         mustBeNew(t, passwdPath, statePath)
493         mustHaveHash(t, passwdPath, "bbb7db67469b111200400e2470346d5515d64c23")
494
495         t.Log("Fetch again with newer server response")
496
497         change = true
498         lastChange = time.Now().Add(time.Second)
499
500         mustMakeOld(t, passwdPath, statePath)
501
502         err = mainFetch(configPath)
503         if err != nil {
504                 t.Error(err)
505         }
506
507         mustNotExist(t, plainPath, groupPath)
508         mustBeNew(t, passwdPath, statePath)
509         mustHaveHash(t, passwdPath, "ca9c7477cb425667fc9ecbd79e8e1c2ad0e84423")
510 }
511
512 func fetchPlainEmpty(a args) {
513         t := a.t
514         mustWriteConfig(t, fmt.Sprintf(`
515 statepath = "%[1]s"
516
517 [[file]]
518 type = "plain"
519 url = "%[2]s/plain"
520 path = "%[3]s"
521 ca = "%[4]s"
522 `, statePath, a.url, plainPath, tlsCAPath))
523         mustCreate(t, plainPath)
524
525         *a.handler = func(w http.ResponseWriter, r *http.Request) {
526                 // Empty response
527         }
528
529         err := mainFetch(configPath)
530         mustBeErrorWithSubstring(t, err,
531                 "refusing to use empty response")
532
533         mustNotExist(t, statePath, passwdPath, groupPath)
534         mustBeOld(t, plainPath)
535 }
536
537 func fetchPlain(a args) {
538         t := a.t
539         mustWriteConfig(t, fmt.Sprintf(`
540 statepath = "%[1]s"
541
542 [[file]]
543 type = "plain"
544 url = "%[2]s/plain"
545 path = "%[3]s"
546 ca = "%[4]s"
547 `, statePath, a.url, plainPath, tlsCAPath))
548         mustCreate(t, plainPath)
549         mustHaveHash(t, plainPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709")
550
551         *a.handler = func(w http.ResponseWriter, r *http.Request) {
552                 if r.URL.Path != "/plain" {
553                         return
554                 }
555
556                 fmt.Fprintln(w, "some file")
557         }
558
559         err := mainFetch(configPath)
560         if err != nil {
561                 t.Error(err)
562         }
563
564         mustNotExist(t, passwdPath, groupPath)
565         mustBeNew(t, plainPath, statePath)
566         mustHaveHash(t, plainPath, "0e08b5e8c10abc3e455b75286ba4a1fbd56e18a5")
567
568         // Remaining functionality already tested in fetchPasswd()
569 }
570
571 func fetchGroupEmpty(a args) {
572         t := a.t
573         mustWriteGroupConfig(t, a.url)
574         mustCreate(t, groupPath)
575
576         *a.handler = func(w http.ResponseWriter, r *http.Request) {
577                 // Empty response
578         }
579
580         err := mainFetch(configPath)
581         mustBeErrorWithSubstring(t, err,
582                 "refusing to use empty group file")
583
584         mustNotExist(t, statePath, passwdPath, plainPath)
585         mustBeOld(t, groupPath)
586 }
587
588 func fetchGroupInvalid(a args) {
589         t := a.t
590         mustWriteGroupConfig(t, a.url)
591         mustCreate(t, groupPath)
592
593         *a.handler = func(w http.ResponseWriter, r *http.Request) {
594                 if r.URL.Path != "/group" {
595                         return
596                 }
597
598                 fmt.Fprintln(w, "root:x::")
599         }
600
601         err := mainFetch(configPath)
602         mustBeErrorWithSubstring(t, err,
603                 "invalid gid in line")
604
605         mustNotExist(t, statePath, passwdPath, plainPath)
606         mustBeOld(t, groupPath)
607 }
608
609 func fetchGroupLimits(a args) {
610         t := a.t
611         mustWriteGroupConfig(t, a.url)
612         mustCreate(t, groupPath)
613
614         *a.handler = func(w http.ResponseWriter, r *http.Request) {
615                 if r.URL.Path != "/group" {
616                         return
617                 }
618
619                 fmt.Fprint(w, "root:x:0:")
620                 for i := 0; i < 65536; i++ {
621                         fmt.Fprint(w, "x")
622                 }
623                 fmt.Fprint(w, "\n")
624         }
625
626         err := mainFetch(configPath)
627         mustBeErrorWithSubstring(t, err,
628                 "group too large to serialize")
629
630         mustNotExist(t, statePath, passwdPath, plainPath)
631         mustBeOld(t, groupPath)
632 }
633
634 func fetchGroup(a args) {
635         t := a.t
636         mustWriteGroupConfig(t, a.url)
637         mustCreate(t, groupPath)
638         mustHaveHash(t, groupPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709")
639
640         *a.handler = func(w http.ResponseWriter, r *http.Request) {
641                 if r.URL.Path != "/group" {
642                         return
643                 }
644
645                 fmt.Fprintln(w, "root:x:0:")
646                 fmt.Fprintln(w, "daemon:x:1:andariel,duriel,mephisto,diablo,baal")
647         }
648
649         err := mainFetch(configPath)
650         if err != nil {
651                 t.Error(err)
652         }
653
654         mustNotExist(t, passwdPath, plainPath)
655         mustBeNew(t, groupPath, statePath)
656         // The actual content of groupPath is verified by the NSS tests
657         mustHaveHash(t, groupPath, "8c27a8403278ba2e392b86d98d4dff1fdefcafdd")
658
659         // Remaining functionality already tested in fetchPasswd()
660 }
661
662 func fetchNoConfig(a args) {
663         t := a.t
664
665         err := mainFetch(configPath)
666         mustBeErrorWithSubstring(t, err,
667                 configPath+": no such file or directory")
668
669         mustNotExist(t, configPath, statePath, passwdPath, plainPath, groupPath)
670 }
671
672 func fetchStateCannotRead(a args) {
673         t := a.t
674         mustWritePasswdConfig(t, a.url)
675
676         mustCreate(t, statePath)
677         err := os.Chmod(statePath, 0000)
678         if err != nil {
679                 t.Fatal(err)
680         }
681
682         err = mainFetch(configPath)
683         mustBeErrorWithSubstring(t, err,
684                 statePath+": permission denied")
685
686         mustNotExist(t, passwdPath, plainPath, groupPath)
687 }
688
689 func fetchStateInvalid(a args) {
690         t := a.t
691         mustWriteGroupConfig(t, a.url)
692         mustCreate(t, statePath)
693
694         err := mainFetch(configPath)
695         mustBeErrorWithSubstring(t, err,
696                 "unexpected end of JSON input")
697
698         mustNotExist(t, groupPath, passwdPath, plainPath)
699         mustBeOld(t, statePath)
700 }
701
702 func fetchStateCannotWrite(a args) {
703         t := a.t
704         mustWriteGroupConfig(t, a.url)
705         mustCreate(t, groupPath)
706         mustHaveHash(t, groupPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709")
707
708         *a.handler = func(w http.ResponseWriter, r *http.Request) {
709                 // To prevent mainFetch() from trying to update groupPath
710                 // which will also fail
711                 w.WriteHeader(http.StatusNotModified)
712         }
713
714         err := os.Chmod("testdata", 0500)
715         if err != nil {
716                 t.Fatal(err)
717         }
718         defer os.Chmod("testdata", 0755)
719
720         err = mainFetch(configPath)
721         mustBeErrorWithSubstring(t, err,
722                 "permission denied")
723
724         mustNotExist(t, statePath, passwdPath, plainPath)
725         mustBeOld(t, groupPath)
726 }
727
728 func fetchCannotDeploy(a args) {
729         t := a.t
730         mustWriteGroupConfig(t, a.url)
731         mustCreate(t, groupPath)
732         mustHaveHash(t, groupPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709")
733
734         *a.handler = func(w http.ResponseWriter, r *http.Request) {
735                 if r.URL.Path != "/group" {
736                         return
737                 }
738
739                 fmt.Fprintln(w, "root:x:0:")
740                 fmt.Fprintln(w, "daemon:x:1:andariel,duriel,mephisto,diablo,baal")
741         }
742
743         err := os.Chmod("testdata", 0500)
744         if err != nil {
745                 t.Fatal(err)
746         }
747         defer os.Chmod("testdata", 0755)
748
749         err = mainFetch(configPath)
750         mustBeErrorWithSubstring(t, err,
751                 "permission denied")
752
753         mustNotExist(t, statePath, passwdPath, plainPath)
754         mustBeOld(t, groupPath)
755 }
756
757 func fetchSecondFetchFails(a args) {
758         t := a.t
759         mustWriteConfig(t, fmt.Sprintf(`
760 statepath = "%[1]s"
761
762 [[file]]
763 type = "passwd"
764 url = "%[2]s/passwd"
765 path = "%[3]s"
766 ca = "%[5]s"
767
768 [[file]]
769 type = "group"
770 url = "%[2]s/group"
771 path = "%[4]s"
772 ca = "%[5]s"
773 `, statePath, a.url, passwdPath, groupPath, tlsCAPath))
774         mustCreate(t, passwdPath)
775         mustCreate(t, groupPath)
776         mustHaveHash(t, passwdPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709")
777         mustHaveHash(t, groupPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709")
778
779         *a.handler = func(w http.ResponseWriter, r *http.Request) {
780                 if r.URL.Path == "/passwd" {
781                         fmt.Fprintln(w, "root:x:0:0:root:/root:/bin/bash")
782                 }
783                 if r.URL.Path == "/group" {
784                         w.WriteHeader(http.StatusNotFound)
785                 }
786         }
787
788         err := mainFetch(configPath)
789         mustBeErrorWithSubstring(t, err,
790                 "status code 404")
791
792         mustNotExist(t, statePath, plainPath)
793         // Even though passwd was successfully fetched, no files were modified
794         // because the second fetch failed
795         mustBeOld(t, passwdPath, groupPath)
796 }
797
798 func fetchInvalidCA(a args) {
799         t := a.t
800
801         // System CA
802
803         mustWriteConfig(t, fmt.Sprintf(`
804 statepath = "%[1]s"
805
806 [[file]]
807 type = "passwd"
808 url = "%[2]s/passwd"
809 path = "%[3]s"
810 `, statePath, a.url, passwdPath))
811         mustCreate(t, passwdPath)
812         mustHaveHash(t, passwdPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709")
813
814         *a.handler = func(w http.ResponseWriter, r *http.Request) {
815                 if r.URL.Path == "/passwd" {
816                         fmt.Fprintln(w, "root:x:0:0:root:/root:/bin/bash")
817                 }
818         }
819
820         err := mainFetch(configPath)
821         mustBeErrorWithSubstring(t, err,
822                 "x509: certificate signed by unknown authority")
823
824         mustNotExist(t, statePath, plainPath, groupPath)
825         mustBeOld(t, passwdPath)
826
827         // Invalid CA
828
829         mustWriteConfig(t, fmt.Sprintf(`
830 statepath = "%[1]s"
831
832 [[file]]
833 type = "passwd"
834 url = "%[2]s/passwd"
835 path = "%[3]s"
836 ca = "%[4]s"
837 `, statePath, a.url, passwdPath, tlsCA2Path))
838         mustCreate(t, passwdPath)
839         mustHaveHash(t, passwdPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709")
840
841         *a.handler = func(w http.ResponseWriter, r *http.Request) {
842                 if r.URL.Path == "/passwd" {
843                         fmt.Fprintln(w, "root:x:0:0:root:/root:/bin/bash")
844                 }
845         }
846
847         err = mainFetch(configPath)
848         mustBeErrorWithSubstring(t, err,
849                 "x509: certificate signed by unknown authority")
850
851         mustNotExist(t, statePath, plainPath, groupPath)
852         mustBeOld(t, passwdPath)
853 }