package api import ( "encoding/json" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" "time" "gardomatic.kleiax.de/internal/mailer" "gardomatic.kleiax.de/internal/storage" ) type applicationSettingsTestModel struct { settings storage.ApplicationSettings calls int asOf time.Time } func (m *applicationSettingsTestModel) Get() (storage.ApplicationSettings, error) { return m.settings, nil } func (m *applicationSettingsTestModel) Update(value storage.ApplicationSettings) (storage.ApplicationSettings, error) { m.settings = value return value, nil } func (m *applicationSettingsTestModel) RemoveExpiredPlants(asOf time.Time, _, _ int) (int, error) { m.calls++ m.asOf = asOf return 2, nil } func TestLifecycleMaintenanceHonorsEnabledSettingAndCutoff(t *testing.T) { app, _, _ := newGardenTestApplication() model := &applicationSettingsTestModel{settings: storage.ApplicationSettings{LifecycleStatusEnabled: true, LifecycleRemovalMonth: 12, LifecycleRemovalDay: 1, Timezone: "Europe/Berlin"}} app.models.ApplicationSettings = model if err := app.runLifecycleMaintenance(time.Date(2026, 11, 30, 12, 0, 0, 0, time.UTC)); err != nil { t.Fatal(err) } if model.calls != 0 { t.Fatalf("maintenance ran before cutoff") } if err := app.runLifecycleMaintenance(time.Date(2026, 12, 1, 12, 0, 0, 0, time.UTC)); err != nil { t.Fatal(err) } if model.calls != 1 || model.asOf.Format("2006-01-02") != "2026-12-01" { t.Fatalf("maintenance calls=%d cutoff=%v", model.calls, model.asOf) } } func TestShowAdminEnvironmentMasksSecrets(t *testing.T) { app, _, _ := newGardenTestApplication() app.config = Config{ DB: DatabaseConfig{Dsn: "postgres://user:secret@db/gardomatic"}, Mail: mailer.Config{Password: "smtp-secret"}, } response := httptest.NewRecorder() request := httptest.NewRequest(http.MethodGet, "/v1/admin/environment", nil) app.showAdminEnvironmentHandler(response, request) if response.Code != http.StatusOK { t.Fatalf("status: got %d, want %d", response.Code, http.StatusOK) } body := response.Body.String() if strings.Contains(body, "secret") { t.Fatalf("environment response exposes a secret: %s", body) } var result struct { Variables []environmentVariable `json:"variables"` } if err := json.Unmarshal(response.Body.Bytes(), &result); err != nil { t.Fatal(err) } for _, name := range []string{"GARDOMATIC_DB_DSN", "GARDOMATIC_SMTP_PASSWORD", "GARDOMATIC_SMTP_SENDER"} { found := false for _, variable := range result.Variables { found = found || variable.Name == name } if !found { t.Errorf("missing environment variable %s", name) } } } func TestSendAdminTestMailValidatesAndDelivers(t *testing.T) { app, _, _ := newGardenTestApplication() mailPath := filepath.Join(t.TempDir(), "mail.log") configuredMailer, err := mailer.New(mailer.Config{Mode: mailer.ModeFile, Sender: "gardomatic@example.com", FilePath: mailPath}) if err != nil { t.Fatal(err) } app.mailer = configuredMailer invalidResponse := httptest.NewRecorder() invalidRequest := httptest.NewRequest(http.MethodPost, "/v1/admin/test-mail", strings.NewReader(`{"email":"not-an-email"}`)) app.sendAdminTestMailHandler(invalidResponse, invalidRequest) if invalidResponse.Code != http.StatusUnprocessableEntity { t.Fatalf("invalid status: got %d, want %d; body: %s", invalidResponse.Code, http.StatusUnprocessableEntity, invalidResponse.Body.String()) } response := httptest.NewRecorder() request := httptest.NewRequest(http.MethodPost, "/v1/admin/test-mail", strings.NewReader(`{"email":" Test@Example.com "}`)) app.sendAdminTestMailHandler(response, request) if response.Code != http.StatusAccepted { t.Fatalf("status: got %d, want %d; body: %s", response.Code, http.StatusAccepted, response.Body.String()) } content, err := os.ReadFile(mailPath) if err != nil { t.Fatal(err) } for _, want := range []string{"test@example.com", "Gardomatic Testmail", "Gardomatic funktion"} { if !strings.Contains(string(content), want) { t.Errorf("test mail is missing %q: %s", want, content) } } }