mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2026-07-12 20:48:40 +00:00
Fixes #12564. Fixes #8951. This introduces a new setting, `ADD_MEMBERS_BY_INVITATIONS`, which is turned off by default. When turned on, adding a user to a team issues an invitation instead of adding them directly to the team. A prerequisite for this work was to be able to link invitations to existing users (so far, they were only associated to an email address, since those invitations were meant to be issued to users who didn't have an account yet). --- I plan to work on the following improvements, which I propose to do in separate PRs given that this one is already a bit big: * generate an in-app notification for the invited user * advertise the invitation to the invited user from the org page as well (#12120) * show the list of invited users in the list of organization members (not just on the team page) and various other improvements to invitations (#12570, #12716). Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/12845 Reviewed-by: Gusted <gusted@noreply.codeberg.org>
448 lines
15 KiB
Go
448 lines
15 KiB
Go
// Copyright 2022 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package integration
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"testing"
|
|
|
|
"forgejo.org/models/auth"
|
|
"forgejo.org/models/db"
|
|
"forgejo.org/models/organization"
|
|
"forgejo.org/models/unittest"
|
|
user_model "forgejo.org/models/user"
|
|
"forgejo.org/modules/setting"
|
|
"forgejo.org/modules/test"
|
|
"forgejo.org/tests"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestOrgTeamEmailInvite(t *testing.T) {
|
|
if setting.MailService == nil {
|
|
t.Skip()
|
|
return
|
|
}
|
|
|
|
defer tests.PrepareTestEnv(t)()
|
|
|
|
org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
|
|
team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 2})
|
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
|
|
|
|
isMember, err := organization.IsTeamMember(db.DefaultContext, team.OrgID, team.ID, user.ID)
|
|
require.NoError(t, err)
|
|
assert.False(t, isMember)
|
|
|
|
session := loginUser(t, "user1")
|
|
|
|
teamURL := fmt.Sprintf("/org/%s/teams/%s", org.Name, team.Name)
|
|
req := NewRequestWithValues(t, "POST", teamURL+"/action/add", map[string]string{
|
|
"uid": "1",
|
|
"uname": user.Email,
|
|
})
|
|
resp := session.MakeRequest(t, req, http.StatusSeeOther)
|
|
req = NewRequest(t, "GET", test.RedirectURL(resp))
|
|
session.MakeRequest(t, req, http.StatusOK)
|
|
|
|
// get the invite token
|
|
invites, err := organization.GetInvitesByTeamID(db.DefaultContext, team.ID)
|
|
require.NoError(t, err)
|
|
assert.Len(t, invites, 1)
|
|
|
|
session = loginUser(t, user.Name)
|
|
|
|
// get the invite page
|
|
inviteURL := fmt.Sprintf("/org/invite/%s", invites[0].Token)
|
|
req = NewRequest(t, "GET", inviteURL)
|
|
resp = session.MakeRequest(t, req, http.StatusOK)
|
|
doc := NewHTMLParser(t, resp.Body)
|
|
|
|
// check the button exists
|
|
submitButton := doc.Find(`button:contains('Join')`).Length()
|
|
assert.Equal(t, 1, submitButton)
|
|
|
|
// join the team
|
|
req = NewRequest(t, "POST", inviteURL)
|
|
resp = session.MakeRequest(t, req, http.StatusSeeOther)
|
|
req = NewRequest(t, "GET", test.RedirectURL(resp))
|
|
session.MakeRequest(t, req, http.StatusOK)
|
|
|
|
isMember, err = organization.IsTeamMember(db.DefaultContext, team.OrgID, team.ID, user.ID)
|
|
require.NoError(t, err)
|
|
assert.True(t, isMember)
|
|
}
|
|
|
|
// Check that users are redirected to accept the invitation correctly after login
|
|
func TestOrgTeamEmailInviteRedirectsExistingUser(t *testing.T) {
|
|
if setting.MailService == nil {
|
|
t.Skip()
|
|
return
|
|
}
|
|
|
|
defer tests.PrepareTestEnv(t)()
|
|
|
|
org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
|
|
team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 2})
|
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
|
|
|
|
isMember, err := organization.IsTeamMember(db.DefaultContext, team.OrgID, team.ID, user.ID)
|
|
require.NoError(t, err)
|
|
assert.False(t, isMember)
|
|
|
|
// create the invite
|
|
session := loginUser(t, "user1")
|
|
|
|
teamURL := fmt.Sprintf("/org/%s/teams/%s", org.Name, team.Name)
|
|
req := NewRequestWithValues(t, "POST", teamURL+"/action/add", map[string]string{
|
|
"uid": "1",
|
|
"uname": user.Email,
|
|
})
|
|
resp := session.MakeRequest(t, req, http.StatusSeeOther)
|
|
req = NewRequest(t, "GET", test.RedirectURL(resp))
|
|
session.MakeRequest(t, req, http.StatusOK)
|
|
|
|
// get the invite token
|
|
invites, err := organization.GetInvitesByTeamID(db.DefaultContext, team.ID)
|
|
require.NoError(t, err)
|
|
assert.Len(t, invites, 1)
|
|
|
|
// accept the invite
|
|
inviteURL := fmt.Sprintf("/org/invite/%s", invites[0].Token)
|
|
req = NewRequest(t, "GET", fmt.Sprintf("/user/login?redirect_to=%s", url.QueryEscape(inviteURL)))
|
|
resp = MakeRequest(t, req, http.StatusOK)
|
|
|
|
req = NewRequestWithValues(t, "POST", "/user/login", map[string]string{
|
|
"user_name": "user5",
|
|
"password": "password",
|
|
})
|
|
for _, c := range resp.Result().Cookies() {
|
|
req.AddCookie(c)
|
|
}
|
|
|
|
resp = MakeRequest(t, req, http.StatusSeeOther)
|
|
assert.Equal(t, inviteURL, test.RedirectURL(resp))
|
|
|
|
// complete the login process
|
|
ch := http.Header{}
|
|
ch.Add("Cookie", strings.Join(resp.Header()["Set-Cookie"], ";"))
|
|
cr := http.Request{Header: ch}
|
|
|
|
session = emptyTestSession(t)
|
|
baseURL, err := url.Parse(setting.AppURL)
|
|
require.NoError(t, err)
|
|
session.jar.SetCookies(baseURL, cr.Cookies())
|
|
|
|
// make the request
|
|
req = NewRequest(t, "POST", test.RedirectURL(resp))
|
|
resp = session.MakeRequest(t, req, http.StatusSeeOther)
|
|
req = NewRequest(t, "GET", test.RedirectURL(resp))
|
|
session.MakeRequest(t, req, http.StatusOK)
|
|
|
|
isMember, err = organization.IsTeamMember(db.DefaultContext, team.OrgID, team.ID, user.ID)
|
|
require.NoError(t, err)
|
|
assert.True(t, isMember)
|
|
}
|
|
|
|
// Check that newly signed up users are redirected to accept the invitation correctly
|
|
func TestOrgTeamEmailInviteRedirectsNewUser(t *testing.T) {
|
|
if setting.MailService == nil {
|
|
t.Skip()
|
|
return
|
|
}
|
|
|
|
defer tests.PrepareTestEnv(t)()
|
|
|
|
org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
|
|
team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 2})
|
|
|
|
// create the invite
|
|
session := loginUser(t, "user1")
|
|
|
|
teamURL := fmt.Sprintf("/org/%s/teams/%s", org.Name, team.Name)
|
|
req := NewRequestWithValues(t, "POST", teamURL+"/action/add", map[string]string{
|
|
"uid": "1",
|
|
"uname": "doesnotexist@example.com",
|
|
})
|
|
resp := session.MakeRequest(t, req, http.StatusSeeOther)
|
|
req = NewRequest(t, "GET", test.RedirectURL(resp))
|
|
session.MakeRequest(t, req, http.StatusOK)
|
|
|
|
// get the invite token
|
|
invites, err := organization.GetInvitesByTeamID(db.DefaultContext, team.ID)
|
|
require.NoError(t, err)
|
|
assert.Len(t, invites, 1)
|
|
|
|
// accept the invite
|
|
inviteURL := fmt.Sprintf("/org/invite/%s", invites[0].Token)
|
|
req = NewRequest(t, "GET", fmt.Sprintf("/user/sign_up?redirect_to=%s", url.QueryEscape(inviteURL)))
|
|
resp = MakeRequest(t, req, http.StatusOK)
|
|
|
|
req = NewRequestWithValues(t, "POST", "/user/sign_up", map[string]string{
|
|
"user_name": "doesnotexist",
|
|
"email": "doesnotexist@example.com",
|
|
"password": "examplePassword!1",
|
|
"retype": "examplePassword!1",
|
|
})
|
|
for _, c := range resp.Result().Cookies() {
|
|
req.AddCookie(c)
|
|
}
|
|
|
|
resp = MakeRequest(t, req, http.StatusSeeOther)
|
|
assert.Equal(t, inviteURL, test.RedirectURL(resp))
|
|
|
|
// complete the signup process
|
|
ch := http.Header{}
|
|
ch.Add("Cookie", strings.Join(resp.Header()["Set-Cookie"], ";"))
|
|
cr := http.Request{Header: ch}
|
|
|
|
session = emptyTestSession(t)
|
|
baseURL, err := url.Parse(setting.AppURL)
|
|
require.NoError(t, err)
|
|
session.jar.SetCookies(baseURL, cr.Cookies())
|
|
|
|
// make the redirected request
|
|
req = NewRequest(t, "POST", test.RedirectURL(resp))
|
|
resp = session.MakeRequest(t, req, http.StatusSeeOther)
|
|
req = NewRequest(t, "GET", test.RedirectURL(resp))
|
|
session.MakeRequest(t, req, http.StatusOK)
|
|
|
|
// get the new user
|
|
newUser, err := user_model.GetUserByName(db.DefaultContext, "doesnotexist")
|
|
require.NoError(t, err)
|
|
|
|
isMember, err := organization.IsTeamMember(db.DefaultContext, team.OrgID, team.ID, newUser.ID)
|
|
require.NoError(t, err)
|
|
assert.True(t, isMember)
|
|
}
|
|
|
|
// Check that users are redirected correctly after confirming their email
|
|
func TestOrgTeamEmailInviteRedirectsNewUserWithActivation(t *testing.T) {
|
|
if setting.MailService == nil {
|
|
t.Skip()
|
|
return
|
|
}
|
|
defer test.MockVariableValue(&setting.Service.RegisterEmailConfirm, true)()
|
|
defer tests.PrepareTestEnv(t)()
|
|
|
|
org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
|
|
team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 2})
|
|
|
|
// create the invite
|
|
session := loginUser(t, "user1")
|
|
|
|
teamURL := fmt.Sprintf("/org/%s/teams/%s", org.Name, team.Name)
|
|
req := NewRequestWithValues(t, "POST", teamURL+"/action/add", map[string]string{
|
|
"uid": "1",
|
|
"uname": "doesnotexist@example.com",
|
|
})
|
|
resp := session.MakeRequest(t, req, http.StatusSeeOther)
|
|
req = NewRequest(t, "GET", test.RedirectURL(resp))
|
|
session.MakeRequest(t, req, http.StatusOK)
|
|
|
|
// get the invite token
|
|
invites, err := organization.GetInvitesByTeamID(db.DefaultContext, team.ID)
|
|
require.NoError(t, err)
|
|
assert.Len(t, invites, 1)
|
|
|
|
// accept the invite
|
|
inviteURL := fmt.Sprintf("/org/invite/%s", invites[0].Token)
|
|
req = NewRequest(t, "GET", fmt.Sprintf("/user/sign_up?redirect_to=%s", url.QueryEscape(inviteURL)))
|
|
inviteResp := MakeRequest(t, req, http.StatusOK)
|
|
|
|
req = NewRequestWithValues(t, "POST", "/user/sign_up", map[string]string{
|
|
"user_name": "doesnotexist",
|
|
"email": "doesnotexist@example.com",
|
|
"password": "examplePassword!1",
|
|
"retype": "examplePassword!1",
|
|
})
|
|
for _, c := range inviteResp.Result().Cookies() {
|
|
req.AddCookie(c)
|
|
}
|
|
|
|
resp = MakeRequest(t, req, http.StatusOK)
|
|
|
|
user, err := user_model.GetUserByName(db.DefaultContext, "doesnotexist")
|
|
require.NoError(t, err)
|
|
|
|
ch := http.Header{}
|
|
ch.Add("Cookie", strings.Join(resp.Header()["Set-Cookie"], ";"))
|
|
cr := http.Request{Header: ch}
|
|
|
|
session = emptyTestSession(t)
|
|
baseURL, err := url.Parse(setting.AppURL)
|
|
require.NoError(t, err)
|
|
session.jar.SetCookies(baseURL, cr.Cookies())
|
|
|
|
code, err := user.GenerateEmailAuthorizationCode(db.DefaultContext, auth.UserActivation)
|
|
require.NoError(t, err)
|
|
|
|
req = NewRequestWithValues(t, "POST", "/user/activate?code="+url.QueryEscape(code), map[string]string{
|
|
"password": "examplePassword!1",
|
|
})
|
|
|
|
// use the cookies set by the signup request
|
|
for _, c := range inviteResp.Result().Cookies() {
|
|
req.AddCookie(c)
|
|
}
|
|
|
|
resp = session.MakeRequest(t, req, http.StatusSeeOther)
|
|
// should be redirected to accept the invite
|
|
assert.Equal(t, inviteURL, test.RedirectURL(resp))
|
|
|
|
req = NewRequest(t, "POST", test.RedirectURL(resp))
|
|
resp = session.MakeRequest(t, req, http.StatusSeeOther)
|
|
req = NewRequest(t, "GET", test.RedirectURL(resp))
|
|
session.MakeRequest(t, req, http.StatusOK)
|
|
|
|
isMember, err := organization.IsTeamMember(db.DefaultContext, team.OrgID, team.ID, user.ID)
|
|
require.NoError(t, err)
|
|
assert.True(t, isMember)
|
|
}
|
|
|
|
// Test that a logged-in user who navigates to the sign-up link is then redirected using redirect_to
|
|
// For example: an invite may have been created before the user account was created, but they may be
|
|
// accepting the invite after having created an account separately
|
|
func TestOrgTeamEmailInviteRedirectsExistingUserWithLogin(t *testing.T) {
|
|
if setting.MailService == nil {
|
|
t.Skip()
|
|
return
|
|
}
|
|
|
|
defer tests.PrepareTestEnv(t)()
|
|
|
|
org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
|
|
team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 2})
|
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
|
|
|
|
isMember, err := organization.IsTeamMember(db.DefaultContext, team.OrgID, team.ID, user.ID)
|
|
require.NoError(t, err)
|
|
assert.False(t, isMember)
|
|
|
|
// create the invite
|
|
session := loginUser(t, "user1")
|
|
|
|
teamURL := fmt.Sprintf("/org/%s/teams/%s", org.Name, team.Name)
|
|
req := NewRequestWithValues(t, "POST", teamURL+"/action/add", map[string]string{
|
|
"uid": "1",
|
|
"uname": user.Email,
|
|
})
|
|
resp := session.MakeRequest(t, req, http.StatusSeeOther)
|
|
req = NewRequest(t, "GET", test.RedirectURL(resp))
|
|
session.MakeRequest(t, req, http.StatusOK)
|
|
|
|
// get the invite token
|
|
invites, err := organization.GetInvitesByTeamID(db.DefaultContext, team.ID)
|
|
require.NoError(t, err)
|
|
assert.Len(t, invites, 1)
|
|
|
|
// note: the invited user has logged in
|
|
session = loginUser(t, "user5")
|
|
|
|
// accept the invite (note: this uses the sign_up url)
|
|
inviteURL := fmt.Sprintf("/org/invite/%s", invites[0].Token)
|
|
req = NewRequest(t, "GET", fmt.Sprintf("/user/sign_up?redirect_to=%s", url.QueryEscape(inviteURL)))
|
|
resp = session.MakeRequest(t, req, http.StatusSeeOther)
|
|
assert.Equal(t, inviteURL, test.RedirectURL(resp))
|
|
|
|
// make the request
|
|
req = NewRequest(t, "POST", test.RedirectURL(resp))
|
|
resp = session.MakeRequest(t, req, http.StatusSeeOther)
|
|
req = NewRequest(t, "GET", test.RedirectURL(resp))
|
|
session.MakeRequest(t, req, http.StatusOK)
|
|
|
|
isMember, err = organization.IsTeamMember(db.DefaultContext, team.OrgID, team.ID, user.ID)
|
|
require.NoError(t, err)
|
|
assert.True(t, isMember)
|
|
}
|
|
|
|
// Test that a user can accept a team invite linked to their existing account
|
|
func TestOrgTeamEmailInviteExistingUser(t *testing.T) {
|
|
if setting.MailService == nil {
|
|
t.Skip()
|
|
return
|
|
}
|
|
|
|
defer tests.PrepareTestEnv(t)()
|
|
|
|
team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 2})
|
|
inviter := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
|
|
|
|
isMember, err := organization.IsTeamMember(db.DefaultContext, team.OrgID, team.ID, user.ID)
|
|
require.NoError(t, err)
|
|
assert.False(t, isMember)
|
|
|
|
// create the invite
|
|
invite, err := organization.CreateTeamInviteForUser(db.DefaultContext, inviter, user, team)
|
|
require.NoError(t, err)
|
|
|
|
// log in the invited user
|
|
session := loginUser(t, "user5")
|
|
|
|
// view the invite
|
|
inviteURL := fmt.Sprintf("/org/invite/%s", invite.Token)
|
|
req := NewRequest(t, "GET", inviteURL)
|
|
session.MakeRequest(t, req, http.StatusOK)
|
|
|
|
// accept the invite
|
|
req = NewRequest(t, "POST", inviteURL)
|
|
resp := session.MakeRequest(t, req, http.StatusSeeOther)
|
|
req = NewRequest(t, "GET", test.RedirectURL(resp))
|
|
session.MakeRequest(t, req, http.StatusOK)
|
|
|
|
isMember, err = organization.IsTeamMember(db.DefaultContext, team.OrgID, team.ID, user.ID)
|
|
require.NoError(t, err)
|
|
assert.True(t, isMember)
|
|
}
|
|
|
|
// Test that a user cannot accept an invite if it was meant for another user
|
|
func TestOrgTeamEmailInviteCannotBeAcceptedByOtherUser(t *testing.T) {
|
|
if setting.MailService == nil {
|
|
t.Skip()
|
|
return
|
|
}
|
|
|
|
defer tests.PrepareTestEnv(t)()
|
|
|
|
team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 2})
|
|
inviter := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
|
invited := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
|
|
attacker := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 11})
|
|
|
|
isMember, err := organization.IsTeamMember(db.DefaultContext, team.OrgID, team.ID, invited.ID)
|
|
require.NoError(t, err)
|
|
assert.False(t, isMember)
|
|
isMember, err = organization.IsTeamMember(db.DefaultContext, team.OrgID, team.ID, attacker.ID)
|
|
require.NoError(t, err)
|
|
assert.False(t, isMember)
|
|
|
|
// create the invite for the invited user
|
|
invite, err := organization.CreateTeamInviteForUser(db.DefaultContext, inviter, invited, team)
|
|
require.NoError(t, err)
|
|
|
|
// log in as the attacker
|
|
session := loginUser(t, attacker.Name)
|
|
|
|
// viewing the invite doesn't work
|
|
inviteURL := fmt.Sprintf("/org/invite/%s", invite.Token)
|
|
req := NewRequest(t, "GET", inviteURL)
|
|
session.MakeRequest(t, req, http.StatusNotFound)
|
|
|
|
// accepting the invite doesn't either
|
|
req = NewRequest(t, "POST", inviteURL)
|
|
session.MakeRequest(t, req, http.StatusNotFound)
|
|
|
|
// neither the invited user nor the attacker are part of the team
|
|
isMember, err = organization.IsTeamMember(db.DefaultContext, team.OrgID, team.ID, invited.ID)
|
|
require.NoError(t, err)
|
|
assert.False(t, isMember)
|
|
isMember, err = organization.IsTeamMember(db.DefaultContext, team.OrgID, team.ID, attacker.ID)
|
|
require.NoError(t, err)
|
|
assert.False(t, isMember)
|
|
}
|