package server import ( "context" "net/http" "testing" "time" "jarl/internal/api" "jarl/internal/store" ) // header sends a GET with a token and returns a response header. func (e *apiEnv) header(path, token, name string) string { e.t.Helper() r, _ := http.NewRequest("GET", e.ts.URL+api.Prefix+path, nil) r.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(r) if err != nil { e.t.Fatal(err) } resp.Body.Close() if resp.StatusCode != 200 { e.t.Fatalf("GET %s = %d", path, resp.StatusCode) } return resp.Header.Get(name) } func TestTokenExpiryWarnings(t *testing.T) { e := newAPIEnv(t) e.do("POST", "/users", e.token, map[string]any{"username": "certbot", "password": "", "grant_strings": []string{"records:write"}}, 201) var uid int64 for _, u := range e.list("/users", e.token) { if u["username"] == "certbot" { uid = int64(u["id"].(float64)) } } soon := e.do("POST", "/users/"+itoa(uid)+"/tokens", e.token, map[string]any{"name": "soon", "expires_in": "5d"}, 201) later := e.do("POST", "/users/"+itoa(uid)+"/tokens", e.token, map[string]any{"name": "later", "expires_in": "90d"}, 201) revoked := e.do("POST", "/users/"+itoa(uid)+"/tokens", e.token, map[string]any{"name": "gone", "expires_in": "1d"}, 201) e.do("DELETE", "/tokens/"+itoa(int64(revoked["id"].(float64))), e.token, nil, 204) // The dashboard lists the expiring token for the administrator... names := func(token string) []string { var out []string for _, t := range e.do("GET", "/status", token, nil, 200)["expiring_tokens"].([]any) { out = append(out, t.(map[string]any)["name"].(string)) } return out } if got := names(e.token); len(got) != 1 || got[0] != "soon" { t.Errorf("admin's expiring tokens = %v", got) } // ...and for its own user. if got := names(soon["token"].(string)); len(got) != 1 || got[0] != "soon" { t.Errorf("certbot's expiring tokens = %v", got) } // Requests with the expiring token say so (the CLI warns); others don't. if h := e.header("/auth/whoami", soon["token"].(string), api.TokenExpiresHeader); h == "" { t.Error("no expiry header for a token that expires in 5 days") } else if exp, err := time.Parse(time.RFC3339, h); err != nil || time.Until(exp) < 4*24*time.Hour { t.Errorf("expiry header %q", h) } if h := e.header("/auth/whoami", later["token"].(string), api.TokenExpiresHeader); h != "" { t.Errorf("expiry header for a 90-day token: %q", h) } if h := e.header("/auth/whoami", e.token, api.TokenExpiresHeader); h != "" { t.Errorf("expiry header for a token without expiry: %q", h) } } func TestBackupStatus(t *testing.T) { e := newAPIEnvConfig(t, Config{Version: "test", BackupDir: "/var/backups/jarl", BackupWarnAfter: 48 * time.Hour}) backup := func() map[string]any { b, _ := e.do("GET", "/status", e.token, nil, 200)["backup"].(map[string]any) return b } if b := backup(); b == nil || b["last_at"] != nil || b["stale"] != false || b["dir"] != "/var/backups/jarl" { t.Errorf("before any backup: %v", b) } ctx := context.Background() now := time.Now() e.st.RecordBackup(ctx, store.BackupRecord{At: now.Add(-3 * time.Hour), File: "/var/backups/jarl/jarl-x.db", Size: 1234}) if b := backup(); b["last_file"] != "/var/backups/jarl/jarl-x.db" || b["stale"] != false || b["last_error"] != nil { t.Errorf("fresh backup: %v", b) } // A failure newer than the last success is reported; an older one isn't. e.st.RecordBackup(ctx, store.BackupRecord{At: now.Add(-time.Hour), Error: "disk full"}) if b := backup(); b["last_error"] != "disk full" { t.Errorf("failed backup: %v", b) } e.st.RecordBackup(ctx, store.BackupRecord{At: now.Add(-72 * time.Hour), File: "/var/backups/jarl/jarl-y.db"}) e.st.RecordBackup(ctx, store.BackupRecord{At: now.Add(-80 * time.Hour), Error: "old"}) if b := backup(); b["stale"] != true || b["last_error"] != nil { t.Errorf("old backup: %v", b) } // Only administrators see it. e.do("POST", "/users", e.token, map[string]any{"username": "viewer", "password": "", "role": "readonly"}, 201) var uid int64 for _, u := range e.list("/users", e.token) { if u["username"] == "viewer" { uid = int64(u["id"].(float64)) } } tok := e.do("POST", "/users/"+itoa(uid)+"/tokens", e.token, map[string]any{"name": "t"}, 201)["token"].(string) if st := e.do("GET", "/status", tok, nil, 200); st["backup"] != nil { t.Errorf("non-admin sees backups: %v", st["backup"]) } } func TestServerHeader(t *testing.T) { e := newAPIEnv(t) if h := e.header("/auth/whoami", e.token, "Server"); h != "Jarl/test" { t.Errorf("Server: %q", h) } }