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,
206 for _, f := range tests {
207 // NOTE: This is not guaranteed to work according to reflect's
208 // documentation but seems to work reliable for normal
210 fn := runtime.FuncForPC(reflect.ValueOf(f).Pointer())
212 name = name[strings.LastIndex(name, ".")+1:]
214 t.Run(name, func(t *testing.T) {
215 // Preparation & cleanup
216 for _, p := range cleanup {
218 if err != nil && !os.IsNotExist(err) {
221 // Remove the file at the end of this test
222 // run, if it was created
226 var handler func(http.ResponseWriter, *http.Request)
227 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
241 func fetchPasswdCacheFileDoesNotExist(a args) {
243 mustWritePasswdConfig(t, a.url)
245 err := mainFetch(configPath)
246 mustBeErrorWithSubstring(t, err,
247 "file.path \""+passwdPath+"\" must exist")
249 mustNotExist(t, statePath, passwdPath, plainPath, groupPath)
252 func fetchPasswd404(a args) {
254 mustWritePasswdConfig(t, a.url)
255 mustCreate(t, passwdPath)
257 *a.handler = func(w http.ResponseWriter, r *http.Request) {
259 w.WriteHeader(http.StatusNotFound)
262 err := mainFetch(configPath)
263 mustBeErrorWithSubstring(t, err,
266 mustNotExist(t, statePath, plainPath, groupPath)
267 mustBeOld(a.t, passwdPath)
270 func fetchPasswdEmpty(a args) {
272 mustWritePasswdConfig(t, a.url)
273 mustCreate(t, passwdPath)
275 *a.handler = func(w http.ResponseWriter, r *http.Request) {
279 err := mainFetch(configPath)
280 mustBeErrorWithSubstring(t, err,
281 "refusing to use empty passwd file")
283 mustNotExist(t, statePath, plainPath, groupPath)
284 mustBeOld(t, passwdPath)
287 func fetchPasswdInvalid(a args) {
289 mustWritePasswdConfig(t, a.url)
290 mustCreate(t, passwdPath)
292 *a.handler = func(w http.ResponseWriter, r *http.Request) {
293 if r.URL.Path != "/passwd" {
297 fmt.Fprintln(w, "root:x:invalid:0:root:/root:/bin/bash")
300 err := mainFetch(configPath)
301 mustBeErrorWithSubstring(t, err,
302 "invalid uid in line")
304 mustNotExist(t, statePath, plainPath, groupPath)
305 mustBeOld(t, passwdPath)
308 func fetchPasswdLimits(a args) {
310 mustWritePasswdConfig(t, a.url)
311 mustCreate(t, passwdPath)
313 *a.handler = func(w http.ResponseWriter, r *http.Request) {
314 if r.URL.Path != "/passwd" {
318 fmt.Fprint(w, "root:x:0:0:root:/root:/bin/bash")
319 for i := 0; i < 65536; i++ {
325 err := mainFetch(configPath)
326 mustBeErrorWithSubstring(t, err,
327 "passwd too large to serialize")
329 mustNotExist(t, statePath, plainPath, groupPath)
330 mustBeOld(t, passwdPath)
333 func fetchPasswd(a args) {
335 mustWritePasswdConfig(t, a.url)
336 mustCreate(t, passwdPath)
337 mustHaveHash(t, passwdPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709")
339 t.Log("First fetch, write files")
341 *a.handler = func(w http.ResponseWriter, r *http.Request) {
342 if r.URL.Path != "/passwd" {
346 // No "Last-Modified" header
347 fmt.Fprintln(w, "root:x:0:0:root:/root:/bin/bash")
348 fmt.Fprintln(w, "daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin")
351 err := mainFetch(configPath)
356 mustNotExist(t, plainPath, groupPath)
357 mustBeNew(t, passwdPath, statePath)
358 // The actual content of passwdPath is verified by the NSS tests
359 mustHaveHash(t, passwdPath, "bbb7db67469b111200400e2470346d5515d64c23")
361 t.Log("Fetch again, no support for Last-Modified")
363 mustMakeOld(t, passwdPath, statePath)
365 err = mainFetch(configPath)
370 mustNotExist(t, plainPath, groupPath)
371 mustBeNew(t, passwdPath, statePath)
372 mustHaveHash(t, passwdPath, "bbb7db67469b111200400e2470346d5515d64c23")
374 t.Log("Fetch again, support for Last-Modified, but not retrieved yet")
376 mustMakeOld(t, passwdPath, statePath)
378 lastChange := time.Now()
379 *a.handler = func(w http.ResponseWriter, r *http.Request) {
380 if r.URL.Path != "/passwd" {
384 modified := r.Header.Get("If-Modified-Since")
386 x, err := http.ParseTime(modified)
388 t.Fatalf("invalid If-Modified-Since %v",
391 if !x.Before(lastChange) {
392 w.WriteHeader(http.StatusNotModified)
397 w.Header().Add("Last-Modified",
398 lastChange.Format(http.TimeFormat))
399 fmt.Fprintln(w, "root:x:0:0:root:/root:/bin/bash")
400 fmt.Fprintln(w, "daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin")
403 err = mainFetch(configPath)
408 mustNotExist(t, plainPath, groupPath)
409 mustBeNew(t, passwdPath, statePath)
410 mustHaveHash(t, passwdPath, "bbb7db67469b111200400e2470346d5515d64c23")
412 t.Log("Fetch again, support for Last-Modified")
414 mustMakeOld(t, passwdPath, statePath)
416 err = mainFetch(configPath)
421 mustNotExist(t, plainPath, groupPath)
422 mustBeOld(t, passwdPath)
423 mustBeNew(t, statePath)
424 mustHaveHash(t, passwdPath, "bbb7db67469b111200400e2470346d5515d64c23")
426 t.Log("Corrupt local passwd cache, fetched again")
428 os.Chmod(passwdPath, 0644) // make writable again
429 mustCreate(t, passwdPath)
430 mustMakeOld(t, passwdPath, statePath)
432 err = mainFetch(configPath)
437 mustNotExist(t, plainPath, groupPath)
438 mustBeNew(t, passwdPath, statePath)
439 mustHaveHash(t, passwdPath, "bbb7db67469b111200400e2470346d5515d64c23")
442 func fetchNoConfig(a args) {
445 err := mainFetch(configPath)
446 mustBeErrorWithSubstring(t, err,
447 configPath+": no such file or directory")
449 mustNotExist(t, configPath, statePath, passwdPath, plainPath, groupPath)