package api import ( "context" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" "time" "gardomatic.kleiax.de/internal/mailer" "gardomatic.kleiax.de/internal/storage" "github.com/julienschmidt/httprouter" ) type gardenInviteTestModel struct { invite storage.GardenInvite } func (m *gardenInviteTestModel) Upsert(invite storage.GardenInvite) (storage.GardenInvite, error) { invite.ID = 23 invite.Token = "garden-invite-token" m.invite = invite return invite, nil } func (m *gardenInviteTestModel) GetByToken(string) (storage.GardenInvite, error) { return storage.GardenInvite{}, storage.ErrRecordNotFound } func (m *gardenInviteTestModel) GetAllForGarden(int) ([]storage.GardenInvite, error) { return nil, nil } func (m *gardenInviteTestModel) Delete(int, int) error { return nil } func (m *gardenInviteTestModel) Accept(string, storage.User) (storage.GardenMember, error) { return storage.GardenMember{}, storage.ErrRecordNotFound } type gardenInviteRoleTestModel struct{} func (gardenInviteRoleTestModel) List(storage.RoleScope) ([]storage.Role, error) { return nil, nil } func (gardenInviteRoleTestModel) ListForGarden(int) ([]storage.Role, error) { return nil, nil } func (gardenInviteRoleTestModel) Get(string) (storage.Role, error) { return storage.Role{}, storage.ErrRecordNotFound } func (gardenInviteRoleTestModel) GetForGarden(gardenID int, name string) (storage.Role, error) { return storage.Role{Name: name, Scope: storage.RoleScopeGarden, GardenID: &gardenID}, nil } func (gardenInviteRoleTestModel) Create(role storage.Role) (storage.Role, error) { return role, nil } func (gardenInviteRoleTestModel) Update(role storage.Role) (storage.Role, error) { return role, nil } func (gardenInviteRoleTestModel) Delete(string) error { return nil } func (gardenInviteRoleTestModel) ListGardenOverrides(int) ([]storage.GardenRolePermissionOverride, error) { return nil, nil } func (gardenInviteRoleTestModel) ReplaceGardenOverrides(int, string, []storage.GardenRolePermissionOverride) error { return nil } func TestCreateGardenInviteMailContainsToken(t *testing.T) { invites := new(gardenInviteTestModel) 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, _, _ := newGardenTestApplication() app.config.WebBaseURL = "https://garden.example.com/" app.models.GardenInvites = invites app.models.Roles = gardenInviteRoleTestModel{} app.mailer = configuredMailer request := httptest.NewRequest(http.MethodPost, "/v1/gardens/17/invites", strings.NewReader(`{"email":" ADA@EXAMPLE.COM ","role":"member"}`)) request = request.WithContext(context.WithValue(request.Context(), httprouter.ParamsKey, httprouter.Params{{Key: "gardenID", Value: "17"}})) request = app.contextSetGardenMember(request, storage.GardenMember{GardenID: 17, UserID: 9, Role: storage.GardenRoleOwner}) response := httptest.NewRecorder() app.createGardenInviteHandler(response, request) app.wg.Wait() if response.Code != http.StatusCreated { t.Fatalf("status: got %d, want %d; body: %s", response.Code, http.StatusCreated, response.Body.String()) } if strings.Contains(response.Body.String(), "garden-invite-token") { t.Fatalf("API response exposes invitation token: %s", response.Body.String()) } if invites.invite.Email != "ada@example.com" || invites.invite.ExpiresAt.Before(time.Now().Add(6*24*time.Hour)) { t.Fatalf("unexpected stored invitation: %+v", invites.invite) } mailContent, err := os.ReadFile(mailPath) if err != nil { t.Fatal(err) } want := "token=3Dgarden-invite-token" if !strings.Contains(string(mailContent), want) { t.Errorf("invitation mail is missing %q: %s", want, mailContent) } }