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,
210 for _, f := range tests {
215 func runMainTest(t *testing.T, f func(args)) {
224 // NOTE: This is not guaranteed to work according to reflect's
225 // documentation but seems to work reliable for normal functions.
226 fn := runtime.FuncForPC(reflect.ValueOf(f).Pointer())
228 name = name[strings.LastIndex(name, ".")+1:]
230 t.Run(name, func(t *testing.T) {
231 // Preparation & cleanup
232 for _, p := range cleanup {
234 if err != nil && !os.IsNotExist(err) {
237 // Remove the file at the end of this test run, if it
242 var handler func(http.ResponseWriter, *http.Request)
243 ts := httptest.NewServer(http.HandlerFunc(
244 func(w http.ResponseWriter, r *http.Request) {
257 func fetchPasswdCacheFileDoesNotExist(a args) {
259 mustWritePasswdConfig(t, a.url)
261 err := mainFetch(configPath)
262 mustBeErrorWithSubstring(t, err,
263 "file.path \""+passwdPath+"\" must exist")
265 mustNotExist(t, statePath, passwdPath, plainPath, groupPath)
268 func fetchPasswd404(a args) {
270 mustWritePasswdConfig(t, a.url)
271 mustCreate(t, passwdPath)
273 *a.handler = func(w http.ResponseWriter, r *http.Request) {
275 w.WriteHeader(http.StatusNotFound)
278 err := mainFetch(configPath)
279 mustBeErrorWithSubstring(t, err,
282 mustNotExist(t, statePath, plainPath, groupPath)
283 mustBeOld(a.t, passwdPath)
286 func fetchPasswdEmpty(a args) {
288 mustWritePasswdConfig(t, a.url)
289 mustCreate(t, passwdPath)
291 *a.handler = func(w http.ResponseWriter, r *http.Request) {
295 err := mainFetch(configPath)
296 mustBeErrorWithSubstring(t, err,
297 "refusing to use empty passwd file")
299 mustNotExist(t, statePath, plainPath, groupPath)
300 mustBeOld(t, passwdPath)
303 func fetchPasswdInvalid(a args) {
305 mustWritePasswdConfig(t, a.url)
306 mustCreate(t, passwdPath)
308 *a.handler = func(w http.ResponseWriter, r *http.Request) {
309 if r.URL.Path != "/passwd" {
313 fmt.Fprintln(w, "root:x:invalid:0:root:/root:/bin/bash")
316 err := mainFetch(configPath)
317 mustBeErrorWithSubstring(t, err,
318 "invalid uid in line")
320 mustNotExist(t, statePath, plainPath, groupPath)
321 mustBeOld(t, passwdPath)
324 func fetchPasswdLimits(a args) {
326 mustWritePasswdConfig(t, a.url)
327 mustCreate(t, passwdPath)
329 *a.handler = func(w http.ResponseWriter, r *http.Request) {
330 if r.URL.Path != "/passwd" {
334 fmt.Fprint(w, "root:x:0:0:root:/root:/bin/bash")
335 for i := 0; i < 65536; i++ {
341 err := mainFetch(configPath)
342 mustBeErrorWithSubstring(t, err,
343 "passwd too large to serialize")
345 mustNotExist(t, statePath, plainPath, groupPath)
346 mustBeOld(t, passwdPath)
349 func fetchPasswd(a args) {
351 mustWritePasswdConfig(t, a.url)
352 mustCreate(t, passwdPath)
353 mustHaveHash(t, passwdPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709")
355 t.Log("First fetch, write files")
357 *a.handler = func(w http.ResponseWriter, r *http.Request) {
358 if r.URL.Path != "/passwd" {
362 // No "Last-Modified" header
363 fmt.Fprintln(w, "root:x:0:0:root:/root:/bin/bash")
364 fmt.Fprintln(w, "daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin")
367 err := mainFetch(configPath)
372 mustNotExist(t, plainPath, groupPath)
373 mustBeNew(t, passwdPath, statePath)
374 // The actual content of passwdPath is verified by the NSS tests
375 mustHaveHash(t, passwdPath, "bbb7db67469b111200400e2470346d5515d64c23")
377 t.Log("Fetch again, no support for Last-Modified")
379 mustMakeOld(t, passwdPath, statePath)
381 err = mainFetch(configPath)
386 mustNotExist(t, plainPath, groupPath)
387 mustBeNew(t, passwdPath, statePath)
388 mustHaveHash(t, passwdPath, "bbb7db67469b111200400e2470346d5515d64c23")
390 t.Log("Fetch again, support for Last-Modified, but not retrieved yet")
392 mustMakeOld(t, passwdPath, statePath)
394 lastChange := time.Now()
395 *a.handler = func(w http.ResponseWriter, r *http.Request) {
396 if r.URL.Path != "/passwd" {
400 modified := r.Header.Get("If-Modified-Since")
402 x, err := http.ParseTime(modified)
404 t.Fatalf("invalid If-Modified-Since %v",
407 if !x.Before(lastChange) {
408 w.WriteHeader(http.StatusNotModified)
413 w.Header().Add("Last-Modified",
414 lastChange.Format(http.TimeFormat))
415 fmt.Fprintln(w, "root:x:0:0:root:/root:/bin/bash")
416 fmt.Fprintln(w, "daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin")
419 err = mainFetch(configPath)
424 mustNotExist(t, plainPath, groupPath)
425 mustBeNew(t, passwdPath, statePath)
426 mustHaveHash(t, passwdPath, "bbb7db67469b111200400e2470346d5515d64c23")
428 t.Log("Fetch again, support for Last-Modified")
430 mustMakeOld(t, passwdPath, statePath)
432 err = mainFetch(configPath)
437 mustNotExist(t, plainPath, groupPath)
438 mustBeOld(t, passwdPath)
439 mustBeNew(t, statePath)
440 mustHaveHash(t, passwdPath, "bbb7db67469b111200400e2470346d5515d64c23")
442 t.Log("Corrupt local passwd cache, fetched again")
444 os.Chmod(passwdPath, 0644) // make writable again
445 mustCreate(t, passwdPath)
446 mustMakeOld(t, passwdPath, statePath)
448 err = mainFetch(configPath)
453 mustNotExist(t, plainPath, groupPath)
454 mustBeNew(t, passwdPath, statePath)
455 mustHaveHash(t, passwdPath, "bbb7db67469b111200400e2470346d5515d64c23")
458 func fetchPlainEmpty(a args) {
460 mustWriteConfig(t, fmt.Sprintf(`
467 `, statePath, a.url, plainPath))
468 mustCreate(t, plainPath)
470 *a.handler = func(w http.ResponseWriter, r *http.Request) {
474 err := mainFetch(configPath)
475 mustBeErrorWithSubstring(t, err,
476 "refusing to use empty response")
478 mustNotExist(t, statePath, passwdPath, groupPath)
479 mustBeOld(t, plainPath)
482 func fetchPlain(a args) {
484 mustWriteConfig(t, fmt.Sprintf(`
491 `, statePath, a.url, plainPath))
492 mustCreate(t, plainPath)
493 mustHaveHash(t, plainPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709")
495 *a.handler = func(w http.ResponseWriter, r *http.Request) {
496 if r.URL.Path != "/plain" {
500 fmt.Fprintln(w, "some file")
503 err := mainFetch(configPath)
508 mustNotExist(t, passwdPath, groupPath)
509 mustBeNew(t, plainPath, statePath)
510 mustHaveHash(t, plainPath, "0e08b5e8c10abc3e455b75286ba4a1fbd56e18a5")
512 // Remaining functionality already tested in fetchPasswd()
515 func fetchGroupEmpty(a args) {
517 mustWriteGroupConfig(t, a.url)
518 mustCreate(t, groupPath)
520 *a.handler = func(w http.ResponseWriter, r *http.Request) {
524 err := mainFetch(configPath)
525 mustBeErrorWithSubstring(t, err,
526 "refusing to use empty group file")
528 mustNotExist(t, statePath, passwdPath, plainPath)
529 mustBeOld(t, groupPath)
532 func fetchGroupInvalid(a args) {
534 mustWriteGroupConfig(t, a.url)
535 mustCreate(t, groupPath)
537 *a.handler = func(w http.ResponseWriter, r *http.Request) {
538 if r.URL.Path != "/group" {
542 fmt.Fprintln(w, "root:x::")
545 err := mainFetch(configPath)
546 mustBeErrorWithSubstring(t, err,
547 "invalid gid in line")
549 mustNotExist(t, statePath, passwdPath, plainPath)
550 mustBeOld(t, groupPath)
553 func fetchGroupLimits(a args) {
555 mustWriteGroupConfig(t, a.url)
556 mustCreate(t, groupPath)
558 *a.handler = func(w http.ResponseWriter, r *http.Request) {
559 if r.URL.Path != "/group" {
563 fmt.Fprint(w, "root:x:0:")
564 for i := 0; i < 65536; i++ {
570 err := mainFetch(configPath)
571 mustBeErrorWithSubstring(t, err,
572 "group too large to serialize")
574 mustNotExist(t, statePath, passwdPath, plainPath)
575 mustBeOld(t, groupPath)
578 func fetchGroup(a args) {
580 mustWriteGroupConfig(t, a.url)
581 mustCreate(t, groupPath)
582 mustHaveHash(t, groupPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709")
584 *a.handler = func(w http.ResponseWriter, r *http.Request) {
585 if r.URL.Path != "/group" {
589 fmt.Fprintln(w, "root:x:0:")
590 fmt.Fprintln(w, "daemon:x:1:andariel,duriel,mephisto,diablo,baal")
593 err := mainFetch(configPath)
598 mustNotExist(t, passwdPath, plainPath)
599 mustBeNew(t, groupPath, statePath)
600 // The actual content of groupPath is verified by the NSS tests
601 mustHaveHash(t, groupPath, "8c27a8403278ba2e392b86d98d4dff1fdefcafdd")
603 // Remaining functionality already tested in fetchPasswd()
606 func fetchNoConfig(a args) {
609 err := mainFetch(configPath)
610 mustBeErrorWithSubstring(t, err,
611 configPath+": no such file or directory")
613 mustNotExist(t, configPath, statePath, passwdPath, plainPath, groupPath)
616 func fetchStateCannotRead(a args) {
618 mustWritePasswdConfig(t, a.url)
620 mustCreate(t, statePath)
621 err := os.Chmod(statePath, 0000)
626 err = mainFetch(configPath)
627 mustBeErrorWithSubstring(t, err,
628 statePath+": permission denied")
630 mustNotExist(t, passwdPath, plainPath, groupPath)
633 func fetchStateInvalid(a args) {
635 mustWriteGroupConfig(t, a.url)
636 mustCreate(t, statePath)
638 err := mainFetch(configPath)
639 mustBeErrorWithSubstring(t, err,
640 "unexpected end of JSON input")
642 mustNotExist(t, groupPath, passwdPath, plainPath)
643 mustBeOld(t, statePath)
646 func fetchStateCannotWrite(a args) {
648 mustWriteGroupConfig(t, a.url)
649 mustCreate(t, groupPath)
650 mustHaveHash(t, groupPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709")
652 *a.handler = func(w http.ResponseWriter, r *http.Request) {
653 // To prevent mainFetch() from trying to update groupPath
654 // which will also fail
655 w.WriteHeader(http.StatusNotModified)
658 err := os.Chmod("testdata", 0500)
662 defer os.Chmod("testdata", 0755)
664 err = mainFetch(configPath)
665 mustBeErrorWithSubstring(t, err,
668 mustNotExist(t, statePath, passwdPath, plainPath)
669 mustBeOld(t, groupPath)
672 func fetchCannotDeploy(a args) {
674 mustWriteGroupConfig(t, a.url)
675 mustCreate(t, groupPath)
676 mustHaveHash(t, groupPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709")
678 *a.handler = func(w http.ResponseWriter, r *http.Request) {
679 if r.URL.Path != "/group" {
683 fmt.Fprintln(w, "root:x:0:")
684 fmt.Fprintln(w, "daemon:x:1:andariel,duriel,mephisto,diablo,baal")
687 err := os.Chmod("testdata", 0500)
691 defer os.Chmod("testdata", 0755)
693 err = mainFetch(configPath)
694 mustBeErrorWithSubstring(t, err,
697 mustNotExist(t, statePath, passwdPath, plainPath)
698 mustBeOld(t, groupPath)
701 func fetchSecondFetchFails(a args) {
703 mustWriteConfig(t, fmt.Sprintf(`
715 `, statePath, a.url, passwdPath, groupPath))
716 mustCreate(t, passwdPath)
717 mustCreate(t, groupPath)
718 mustHaveHash(t, passwdPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709")
719 mustHaveHash(t, groupPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709")
721 *a.handler = func(w http.ResponseWriter, r *http.Request) {
722 if r.URL.Path == "/passwd" {
723 fmt.Fprintln(w, "root:x:0:0:root:/root:/bin/bash")
725 if r.URL.Path == "/group" {
726 w.WriteHeader(http.StatusNotFound)
730 err := mainFetch(configPath)
731 mustBeErrorWithSubstring(t, err,
734 mustNotExist(t, statePath, plainPath)
735 // Even though passwd was successfully fetched, no files were modified
736 // because the second fetch failed
737 mustBeOld(t, passwdPath, groupPath)