1 // Copyright (C) 2019 Simon Ruderich
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.
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.
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/>.
35 configPath = "testdata/config.toml"
36 statePath = "testdata/state.json"
37 passwdPath = "testdata/passwd.nsscash"
38 plainPath = "testdata/plain"
39 groupPath = "testdata/group.nsscash"
45 handler *func(http.ResponseWriter, *http.Request)
48 // mustNotExist verifies that all given paths don't exist in the file system.
49 func mustNotExist(t *testing.T, paths ...string) {
50 for _, p := range paths {
53 if !os.IsNotExist(err) {
54 t.Errorf("path %q: unexpected error: %v",
58 t.Errorf("path %q exists", p)
64 // mustHaveHash checks if the given path content has the given SHA-1 string
66 func mustHaveHash(t *testing.T, path string, hash string) {
67 x, err := ioutil.ReadFile(path)
74 y := hex.EncodeToString(h.Sum(nil))
77 t.Errorf("%q has unexpected hash %q", path, y)
81 // mustBeErrorWithSubstring checks if the given error, represented as string,
82 // contains the given substring. This is somewhat ugly but the simplest way to
83 // check for proper errors.
84 func mustBeErrorWithSubstring(t *testing.T, err error, substring string) {
86 t.Errorf("err is nil")
87 } else if !strings.Contains(err.Error(), substring) {
88 t.Errorf("err %q does not contain string %q", err, substring)
92 func mustWriteConfig(t *testing.T, config string) {
93 err := ioutil.WriteFile(configPath, []byte(config), 0644)
99 func mustWritePasswdConfig(t *testing.T, url string) {
100 mustWriteConfig(t, fmt.Sprintf(`
107 `, statePath, url, passwdPath))
110 func mustWriteGroupConfig(t *testing.T, url string) {
111 mustWriteConfig(t, fmt.Sprintf(`
118 `, statePath, url, groupPath))
121 // mustCreate creates a file, truncating it if it exists. It then changes the
122 // modification to be in the past.
123 func mustCreate(t *testing.T, path string) {
124 f, err := os.Create(path)
133 // Change modification time to the past to detect updates to the file
137 // mustMakeOld change the modification time of all paths to be in the past.
138 func mustMakeOld(t *testing.T, paths ...string) {
139 old := time.Now().Add(-2 * time.Hour)
140 for _, p := range paths {
141 err := os.Chtimes(p, old, old)
148 // mustMakeOld verifies that all paths have a modification time in the past,
149 // as set by mustMakeOld().
150 func mustBeOld(t *testing.T, paths ...string) {
151 for _, p := range paths {
159 if now.Sub(mtime) < time.Hour {
160 t.Errorf("%q was recently modified", p)
165 // mustBeNew verifies that all paths have a modification time in the present.
166 func mustBeNew(t *testing.T, paths ...string) {
167 for _, p := range paths {
175 if now.Sub(mtime) > time.Hour {
176 t.Errorf("%q was not recently modified", p)
181 func TestMainFetch(t *testing.T) {
182 // Suppress log messages
183 log.SetOutput(ioutil.Discard)
184 defer log.SetOutput(os.Stderr)
186 tests := []func(args){
187 // Perform most tests with passwd for simplicity
188 fetchPasswdCacheFileDoesNotExist,
194 // Tests for plain and group
203 fetchStateCannotRead,
205 fetchStateCannotWrite,
207 fetchSecondFetchFails,
218 for _, f := range tests {
219 // NOTE: This is not guaranteed to work according to reflect's
220 // documentation but seems to work reliable for normal
222 fn := runtime.FuncForPC(reflect.ValueOf(f).Pointer())
224 name = name[strings.LastIndex(name, ".")+1:]
226 t.Run(name, func(t *testing.T) {
227 // Preparation & cleanup
228 for _, p := range cleanup {
230 if err != nil && !os.IsNotExist(err) {
233 // Remove the file at the end of this test
234 // run, if it was created
238 var handler func(http.ResponseWriter, *http.Request)
239 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
253 func fetchPasswdCacheFileDoesNotExist(a args) {
255 mustWritePasswdConfig(t, a.url)
257 err := mainFetch(configPath)
258 mustBeErrorWithSubstring(t, err,
259 "file.path \""+passwdPath+"\" must exist")
261 mustNotExist(t, statePath, passwdPath, plainPath, groupPath)
264 func fetchPasswd404(a args) {
266 mustWritePasswdConfig(t, a.url)
267 mustCreate(t, passwdPath)
269 *a.handler = func(w http.ResponseWriter, r *http.Request) {
271 w.WriteHeader(http.StatusNotFound)
274 err := mainFetch(configPath)
275 mustBeErrorWithSubstring(t, err,
278 mustNotExist(t, statePath, plainPath, groupPath)
279 mustBeOld(a.t, passwdPath)
282 func fetchPasswdEmpty(a args) {
284 mustWritePasswdConfig(t, a.url)
285 mustCreate(t, passwdPath)
287 *a.handler = func(w http.ResponseWriter, r *http.Request) {
291 err := mainFetch(configPath)
292 mustBeErrorWithSubstring(t, err,
293 "refusing to use empty passwd file")
295 mustNotExist(t, statePath, plainPath, groupPath)
296 mustBeOld(t, passwdPath)
299 func fetchPasswdInvalid(a args) {
301 mustWritePasswdConfig(t, a.url)
302 mustCreate(t, passwdPath)
304 *a.handler = func(w http.ResponseWriter, r *http.Request) {
305 if r.URL.Path != "/passwd" {
309 fmt.Fprintln(w, "root:x:invalid:0:root:/root:/bin/bash")
312 err := mainFetch(configPath)
313 mustBeErrorWithSubstring(t, err,
314 "invalid uid in line")
316 mustNotExist(t, statePath, plainPath, groupPath)
317 mustBeOld(t, passwdPath)
320 func fetchPasswdLimits(a args) {
322 mustWritePasswdConfig(t, a.url)
323 mustCreate(t, passwdPath)
325 *a.handler = func(w http.ResponseWriter, r *http.Request) {
326 if r.URL.Path != "/passwd" {
330 fmt.Fprint(w, "root:x:0:0:root:/root:/bin/bash")
331 for i := 0; i < 65536; i++ {
337 err := mainFetch(configPath)
338 mustBeErrorWithSubstring(t, err,
339 "passwd too large to serialize")
341 mustNotExist(t, statePath, plainPath, groupPath)
342 mustBeOld(t, passwdPath)
345 func fetchPasswd(a args) {
347 mustWritePasswdConfig(t, a.url)
348 mustCreate(t, passwdPath)
349 mustHaveHash(t, passwdPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709")
351 t.Log("First fetch, write files")
353 *a.handler = func(w http.ResponseWriter, r *http.Request) {
354 if r.URL.Path != "/passwd" {
358 // No "Last-Modified" header
359 fmt.Fprintln(w, "root:x:0:0:root:/root:/bin/bash")
360 fmt.Fprintln(w, "daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin")
363 err := mainFetch(configPath)
368 mustNotExist(t, plainPath, groupPath)
369 mustBeNew(t, passwdPath, statePath)
370 // The actual content of passwdPath is verified by the NSS tests
371 mustHaveHash(t, passwdPath, "bbb7db67469b111200400e2470346d5515d64c23")
373 t.Log("Fetch again, no support for Last-Modified")
375 mustMakeOld(t, passwdPath, statePath)
377 err = mainFetch(configPath)
382 mustNotExist(t, plainPath, groupPath)
383 mustBeNew(t, passwdPath, statePath)
384 mustHaveHash(t, passwdPath, "bbb7db67469b111200400e2470346d5515d64c23")
386 t.Log("Fetch again, support for Last-Modified, but not retrieved yet")
388 mustMakeOld(t, passwdPath, statePath)
390 lastChange := time.Now()
391 *a.handler = func(w http.ResponseWriter, r *http.Request) {
392 if r.URL.Path != "/passwd" {
396 modified := r.Header.Get("If-Modified-Since")
398 x, err := http.ParseTime(modified)
400 t.Fatalf("invalid If-Modified-Since %v",
403 if !x.Before(lastChange) {
404 w.WriteHeader(http.StatusNotModified)
409 w.Header().Add("Last-Modified",
410 lastChange.Format(http.TimeFormat))
411 fmt.Fprintln(w, "root:x:0:0:root:/root:/bin/bash")
412 fmt.Fprintln(w, "daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin")
415 err = mainFetch(configPath)
420 mustNotExist(t, plainPath, groupPath)
421 mustBeNew(t, passwdPath, statePath)
422 mustHaveHash(t, passwdPath, "bbb7db67469b111200400e2470346d5515d64c23")
424 t.Log("Fetch again, support for Last-Modified")
426 mustMakeOld(t, passwdPath, statePath)
428 err = mainFetch(configPath)
433 mustNotExist(t, plainPath, groupPath)
434 mustBeOld(t, passwdPath)
435 mustBeNew(t, statePath)
436 mustHaveHash(t, passwdPath, "bbb7db67469b111200400e2470346d5515d64c23")
438 t.Log("Corrupt local passwd cache, fetched again")
440 os.Chmod(passwdPath, 0644) // make writable again
441 mustCreate(t, passwdPath)
442 mustMakeOld(t, passwdPath, statePath)
444 err = mainFetch(configPath)
449 mustNotExist(t, plainPath, groupPath)
450 mustBeNew(t, passwdPath, statePath)
451 mustHaveHash(t, passwdPath, "bbb7db67469b111200400e2470346d5515d64c23")
454 func fetchPlainEmpty(a args) {
456 mustWriteConfig(t, fmt.Sprintf(`
463 `, statePath, a.url, plainPath))
464 mustCreate(t, plainPath)
466 *a.handler = func(w http.ResponseWriter, r *http.Request) {
470 err := mainFetch(configPath)
471 mustBeErrorWithSubstring(t, err,
472 "refusing to use empty response")
474 mustNotExist(t, statePath, passwdPath, groupPath)
475 mustBeOld(t, plainPath)
478 func fetchPlain(a args) {
480 mustWriteConfig(t, fmt.Sprintf(`
487 `, statePath, a.url, plainPath))
488 mustCreate(t, plainPath)
489 mustHaveHash(t, plainPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709")
491 *a.handler = func(w http.ResponseWriter, r *http.Request) {
492 if r.URL.Path != "/plain" {
496 fmt.Fprintln(w, "some file")
499 err := mainFetch(configPath)
504 mustNotExist(t, passwdPath, groupPath)
505 mustBeNew(t, plainPath, statePath)
506 mustHaveHash(t, plainPath, "0e08b5e8c10abc3e455b75286ba4a1fbd56e18a5")
508 // Remaining functionality already tested in fetchPasswd()
511 func fetchGroupEmpty(a args) {
513 mustWriteGroupConfig(t, a.url)
514 mustCreate(t, groupPath)
516 *a.handler = func(w http.ResponseWriter, r *http.Request) {
520 err := mainFetch(configPath)
521 mustBeErrorWithSubstring(t, err,
522 "refusing to use empty group file")
524 mustNotExist(t, statePath, passwdPath, plainPath)
525 mustBeOld(t, groupPath)
528 func fetchGroupInvalid(a args) {
530 mustWriteGroupConfig(t, a.url)
531 mustCreate(t, groupPath)
533 *a.handler = func(w http.ResponseWriter, r *http.Request) {
534 if r.URL.Path != "/group" {
538 fmt.Fprintln(w, "root:x::")
541 err := mainFetch(configPath)
542 mustBeErrorWithSubstring(t, err,
543 "invalid gid in line")
545 mustNotExist(t, statePath, passwdPath, plainPath)
546 mustBeOld(t, groupPath)
549 func fetchGroupLimits(a args) {
551 mustWriteGroupConfig(t, a.url)
552 mustCreate(t, groupPath)
554 *a.handler = func(w http.ResponseWriter, r *http.Request) {
555 if r.URL.Path != "/group" {
559 fmt.Fprint(w, "root:x:0:")
560 for i := 0; i < 65536; i++ {
566 err := mainFetch(configPath)
567 mustBeErrorWithSubstring(t, err,
568 "group too large to serialize")
570 mustNotExist(t, statePath, passwdPath, plainPath)
571 mustBeOld(t, groupPath)
574 func fetchGroup(a args) {
576 mustWriteGroupConfig(t, a.url)
577 mustCreate(t, groupPath)
578 mustHaveHash(t, groupPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709")
580 *a.handler = func(w http.ResponseWriter, r *http.Request) {
581 if r.URL.Path != "/group" {
585 fmt.Fprintln(w, "root:x:0:")
586 fmt.Fprintln(w, "daemon:x:1:andariel,duriel,mephisto,diablo,baal")
589 err := mainFetch(configPath)
594 mustNotExist(t, passwdPath, plainPath)
595 mustBeNew(t, groupPath, statePath)
596 // The actual content of groupPath is verified by the NSS tests
597 mustHaveHash(t, groupPath, "8c27a8403278ba2e392b86d98d4dff1fdefcafdd")
599 // Remaining functionality already tested in fetchPasswd()
602 func fetchNoConfig(a args) {
605 err := mainFetch(configPath)
606 mustBeErrorWithSubstring(t, err,
607 configPath+": no such file or directory")
609 mustNotExist(t, configPath, statePath, passwdPath, plainPath, groupPath)
612 func fetchStateCannotRead(a args) {
614 mustWritePasswdConfig(t, a.url)
616 mustCreate(t, statePath)
617 err := os.Chmod(statePath, 0000)
622 err = mainFetch(configPath)
623 mustBeErrorWithSubstring(t, err,
624 statePath+": permission denied")
626 mustNotExist(t, passwdPath, plainPath, groupPath)
629 func fetchStateInvalid(a args) {
631 mustWriteGroupConfig(t, a.url)
632 mustCreate(t, statePath)
634 err := mainFetch(configPath)
635 mustBeErrorWithSubstring(t, err,
636 "unexpected end of JSON input")
638 mustNotExist(t, groupPath, passwdPath, plainPath)
639 mustBeOld(t, statePath)
642 func fetchStateCannotWrite(a args) {
644 mustWriteGroupConfig(t, a.url)
645 mustCreate(t, groupPath)
646 mustHaveHash(t, groupPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709")
648 *a.handler = func(w http.ResponseWriter, r *http.Request) {
649 // To prevent mainFetch() from trying to update groupPath
650 // which will also fail
651 w.WriteHeader(http.StatusNotModified)
654 err := os.Chmod("testdata", 0500)
658 defer os.Chmod("testdata", 0755)
660 err = mainFetch(configPath)
661 mustBeErrorWithSubstring(t, err,
664 mustNotExist(t, statePath, passwdPath, plainPath)
665 mustBeOld(t, groupPath)
668 func fetchCannotDeploy(a args) {
670 mustWriteGroupConfig(t, a.url)
671 mustCreate(t, groupPath)
672 mustHaveHash(t, groupPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709")
674 *a.handler = func(w http.ResponseWriter, r *http.Request) {
675 if r.URL.Path != "/group" {
679 fmt.Fprintln(w, "root:x:0:")
680 fmt.Fprintln(w, "daemon:x:1:andariel,duriel,mephisto,diablo,baal")
683 err := os.Chmod("testdata", 0500)
687 defer os.Chmod("testdata", 0755)
689 err = mainFetch(configPath)
690 mustBeErrorWithSubstring(t, err,
693 mustNotExist(t, statePath, passwdPath, plainPath)
694 mustBeOld(t, groupPath)
697 func fetchSecondFetchFails(a args) {
699 mustWriteConfig(t, fmt.Sprintf(`
711 `, statePath, a.url, passwdPath, groupPath))
712 mustCreate(t, passwdPath)
713 mustCreate(t, groupPath)
714 mustHaveHash(t, passwdPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709")
715 mustHaveHash(t, groupPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709")
717 *a.handler = func(w http.ResponseWriter, r *http.Request) {
718 if r.URL.Path == "/passwd" {
719 fmt.Fprintln(w, "root:x:0:0:root:/root:/bin/bash")
721 if r.URL.Path == "/group" {
722 w.WriteHeader(http.StatusNotFound)
726 err := mainFetch(configPath)
727 mustBeErrorWithSubstring(t, err,
730 mustNotExist(t, statePath, plainPath)
731 // Even though passwd was successfully fetched, no files were modified
732 // because the second fetch failed
733 mustBeOld(t, passwdPath, groupPath)