2017-04-25 10:24:51 +03:00
// Copyright 2017 The Gitea Authors. All rights reserved.
2022-11-27 21:20:29 +03:00
// SPDX-License-Identifier: MIT
2017-04-25 10:24:51 +03:00
2022-09-02 22:18:23 +03:00
package integration
2017-04-25 10:24:51 +03:00
import (
"net/http"
"testing"
2023-01-18 00:46:03 +03:00
auth_model "code.gitea.io/gitea/models/auth"
2022-06-13 12:37:59 +03:00
issues_model "code.gitea.io/gitea/models/issues"
2022-04-07 21:59:56 +03:00
repo_model "code.gitea.io/gitea/models/repo"
2021-11-16 11:53:21 +03:00
"code.gitea.io/gitea/models/unittest"
2021-11-24 12:49:20 +03:00
user_model "code.gitea.io/gitea/models/user"
2023-07-31 11:44:45 +03:00
"code.gitea.io/gitea/modules/setting"
2022-04-07 21:59:56 +03:00
api "code.gitea.io/gitea/modules/structs"
2017-12-16 00:11:02 +03:00
"code.gitea.io/gitea/modules/test"
2022-06-26 17:19:22 +03:00
"code.gitea.io/gitea/modules/translation"
2022-09-02 22:18:23 +03:00
"code.gitea.io/gitea/tests"
2017-07-01 22:48:29 +03:00
2017-04-25 10:24:51 +03:00
"github.com/stretchr/testify/assert"
)
func TestViewUser ( t * testing . T ) {
2022-09-02 22:18:23 +03:00
defer tests . PrepareTestEnv ( t ) ( )
2017-04-25 10:24:51 +03:00
2017-06-10 03:41:36 +03:00
req := NewRequest ( t , "GET" , "/user2" )
2017-07-07 22:36:47 +03:00
MakeRequest ( t , req , http . StatusOK )
2017-04-25 10:24:51 +03:00
}
2017-07-01 22:48:29 +03:00
func TestRenameUsername ( t * testing . T ) {
2022-09-02 22:18:23 +03:00
defer tests . PrepareTestEnv ( t ) ( )
2017-07-01 22:48:29 +03:00
session := loginUser ( t , "user2" )
2017-07-07 22:36:47 +03:00
req := NewRequestWithValues ( t , "POST" , "/user/settings" , map [ string ] string {
2018-05-05 03:28:30 +03:00
"_csrf" : GetCSRF ( t , session , "/user/settings" ) ,
"name" : "newUsername" ,
"email" : "user2@example.com" ,
2020-12-04 09:20:30 +03:00
"language" : "en-US" ,
2017-07-01 22:48:29 +03:00
} )
2022-03-23 07:54:07 +03:00
session . MakeRequest ( t , req , http . StatusSeeOther )
2017-07-01 22:48:29 +03:00
2021-11-24 12:49:20 +03:00
unittest . AssertExistsAndLoadBean ( t , & user_model . User { Name : "newUsername" } )
unittest . AssertNotExistsBean ( t , & user_model . User { Name : "user2" } )
2017-07-01 22:48:29 +03:00
}
func TestRenameInvalidUsername ( t * testing . T ) {
2022-09-02 22:18:23 +03:00
defer tests . PrepareTestEnv ( t ) ( )
2017-07-01 22:48:29 +03:00
invalidUsernames := [ ] string {
"%2f*" ,
"%2f." ,
"%2f.." ,
"%00" ,
"thisHas ASpace" ,
2019-01-24 17:12:17 +03:00
"p<A>tho>lo<gical" ,
2022-11-04 12:04:08 +03:00
"." ,
".." ,
".well-known" ,
".abc" ,
"abc." ,
"a..bc" ,
"a...bc" ,
"a.-bc" ,
"a._bc" ,
"a_-bc" ,
"a/bc" ,
"☁️" ,
"-" ,
"--diff" ,
"-im-here" ,
"a space" ,
2017-07-01 22:48:29 +03:00
}
session := loginUser ( t , "user2" )
for _ , invalidUsername := range invalidUsernames {
t . Logf ( "Testing username %s" , invalidUsername )
2017-07-07 22:36:47 +03:00
req := NewRequestWithValues ( t , "POST" , "/user/settings" , map [ string ] string {
"_csrf" : GetCSRF ( t , session , "/user/settings" ) ,
2017-07-01 22:48:29 +03:00
"name" : invalidUsername ,
"email" : "user2@example.com" ,
} )
2017-07-07 22:36:47 +03:00
resp := session . MakeRequest ( t , req , http . StatusOK )
htmlDoc := NewHTMLParser ( t , resp . Body )
2017-07-01 22:48:29 +03:00
assert . Contains ( t ,
htmlDoc . doc . Find ( ".ui.negative.message" ) . Text ( ) ,
2022-11-04 12:04:08 +03:00
translation . NewLocale ( "en-US" ) . Tr ( "form.username_error" ) ,
2017-07-01 22:48:29 +03:00
)
2021-11-24 12:49:20 +03:00
unittest . AssertNotExistsBean ( t , & user_model . User { Name : invalidUsername } )
2017-07-01 22:48:29 +03:00
}
}
func TestRenameReservedUsername ( t * testing . T ) {
2022-09-02 22:18:23 +03:00
defer tests . PrepareTestEnv ( t ) ( )
2017-07-01 22:48:29 +03:00
reservedUsernames := [ ] string {
2022-11-04 12:04:08 +03:00
// ".", "..", ".well-known", // The names are not only reserved but also invalid
2019-09-24 20:12:56 +03:00
"admin" ,
"api" ,
2022-03-31 07:02:13 +03:00
"assets" ,
2019-09-24 20:12:56 +03:00
"attachments" ,
2022-03-31 07:02:13 +03:00
"avatar" ,
2019-09-24 20:12:56 +03:00
"avatars" ,
2022-03-31 07:02:13 +03:00
"captcha" ,
"commits" ,
"debug" ,
"error" ,
2019-09-24 20:12:56 +03:00
"explore" ,
2022-03-31 07:02:13 +03:00
"favicon.ico" ,
"ghost" ,
2019-09-24 20:12:56 +03:00
"issues" ,
"login" ,
2022-03-31 07:02:13 +03:00
"manifest.json" ,
2019-09-24 20:12:56 +03:00
"metrics" ,
2022-03-31 07:02:13 +03:00
"milestones" ,
"new" ,
2019-09-24 20:12:56 +03:00
"notifications" ,
"org" ,
"pulls" ,
2022-03-31 07:02:13 +03:00
"raw" ,
2019-09-24 20:12:56 +03:00
"repo" ,
2022-03-31 07:02:13 +03:00
"repo-avatars" ,
"robots.txt" ,
2019-11-18 23:03:25 +03:00
"search" ,
2022-03-31 07:02:13 +03:00
"serviceworker.js" ,
"ssh_info" ,
"swagger.v1.json" ,
"user" ,
"v2" ,
2017-07-01 22:48:29 +03:00
}
session := loginUser ( t , "user2" )
for _ , reservedUsername := range reservedUsernames {
t . Logf ( "Testing username %s" , reservedUsername )
2017-07-07 22:36:47 +03:00
req := NewRequestWithValues ( t , "POST" , "/user/settings" , map [ string ] string {
2018-05-05 03:28:30 +03:00
"_csrf" : GetCSRF ( t , session , "/user/settings" ) ,
"name" : reservedUsername ,
"email" : "user2@example.com" ,
2020-12-04 09:20:30 +03:00
"language" : "en-US" ,
2017-07-01 22:48:29 +03:00
} )
2022-03-23 07:54:07 +03:00
resp := session . MakeRequest ( t , req , http . StatusSeeOther )
2017-07-01 22:48:29 +03:00
2017-12-16 00:11:02 +03:00
req = NewRequest ( t , "GET" , test . RedirectURL ( resp ) )
2017-07-07 22:36:47 +03:00
resp = session . MakeRequest ( t , req , http . StatusOK )
htmlDoc := NewHTMLParser ( t , resp . Body )
2017-07-01 22:48:29 +03:00
assert . Contains ( t ,
htmlDoc . doc . Find ( ".ui.negative.message" ) . Text ( ) ,
2022-06-26 17:19:22 +03:00
translation . NewLocale ( "en-US" ) . Tr ( "user.form.name_reserved" , reservedUsername ) ,
2017-07-01 22:48:29 +03:00
)
2021-11-24 12:49:20 +03:00
unittest . AssertNotExistsBean ( t , & user_model . User { Name : reservedUsername } )
2017-07-01 22:48:29 +03:00
}
}
2019-04-14 19:43:56 +03:00
func TestExportUserGPGKeys ( t * testing . T ) {
2022-09-02 22:18:23 +03:00
defer tests . PrepareTestEnv ( t ) ( )
2022-01-20 20:46:10 +03:00
// Export empty key list
2019-04-14 19:43:56 +03:00
testExportUserGPGKeys ( t , "user1" , ` -- -- - BEGIN PGP PUBLIC KEY BLOCK -- -- -
2022-03-02 19:32:18 +03:00
Note : This user hasn ' t uploaded any GPG keys .
2019-04-14 19:43:56 +03:00
= twTO
-- -- - END PGP PUBLIC KEY BLOCK -- -- -
` )
2022-01-20 20:46:10 +03:00
// Import key
// User1 <user1@example.com>
2019-04-14 19:43:56 +03:00
session := loginUser ( t , "user1" )
Redesign Scoped Access Tokens (#24767)
## Changes
- Adds the following high level access scopes, each with `read` and
`write` levels:
- `activitypub`
- `admin` (hidden if user is not a site admin)
- `misc`
- `notification`
- `organization`
- `package`
- `issue`
- `repository`
- `user`
- Adds new middleware function `tokenRequiresScopes()` in addition to
`reqToken()`
- `tokenRequiresScopes()` is used for each high-level api section
- _if_ a scoped token is present, checks that the required scope is
included based on the section and HTTP method
- `reqToken()` is used for individual routes
- checks that required authentication is present (but does not check
scope levels as this will already have been handled by
`tokenRequiresScopes()`
- Adds migration to convert old scoped access tokens to the new set of
scopes
- Updates the user interface for scope selection
### User interface example
<img width="903" alt="Screen Shot 2023-05-31 at 1 56 55 PM"
src="https://github.com/go-gitea/gitea/assets/23248839/654766ec-2143-4f59-9037-3b51600e32f3">
<img width="917" alt="Screen Shot 2023-05-31 at 1 56 43 PM"
src="https://github.com/go-gitea/gitea/assets/23248839/1ad64081-012c-4a73-b393-66b30352654c">
## tokenRequiresScopes Design Decision
- `tokenRequiresScopes()` was added to more reliably cover api routes.
For an incoming request, this function uses the given scope category
(say `AccessTokenScopeCategoryOrganization`) and the HTTP method (say
`DELETE`) and verifies that any scoped tokens in use include
`delete:organization`.
- `reqToken()` is used to enforce auth for individual routes that
require it. If a scoped token is not present for a request,
`tokenRequiresScopes()` will not return an error
## TODO
- [x] Alphabetize scope categories
- [x] Change 'public repos only' to a radio button (private vs public).
Also expand this to organizations
- [X] Disable token creation if no scopes selected. Alternatively, show
warning
- [x] `reqToken()` is missing from many `POST/DELETE` routes in the api.
`tokenRequiresScopes()` only checks that a given token has the correct
scope, `reqToken()` must be used to check that a token (or some other
auth) is present.
- _This should be addressed in this PR_
- [x] The migration should be reviewed very carefully in order to
minimize access changes to existing user tokens.
- _This should be addressed in this PR_
- [x] Link to api to swagger documentation, clarify what
read/write/delete levels correspond to
- [x] Review cases where more than one scope is needed as this directly
deviates from the api definition.
- _This should be addressed in this PR_
- For example:
```go
m.Group("/users/{username}/orgs", func() {
m.Get("", reqToken(), org.ListUserOrgs)
m.Get("/{org}/permissions", reqToken(), org.GetUserOrgsPermissions)
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser,
auth_model.AccessTokenScopeCategoryOrganization),
context_service.UserAssignmentAPI())
```
## Future improvements
- [ ] Add required scopes to swagger documentation
- [ ] Redesign `reqToken()` to be opt-out rather than opt-in
- [ ] Subdivide scopes like `repository`
- [ ] Once a token is created, if it has no scopes, we should display
text instead of an empty bullet point
- [ ] If the 'public repos only' option is selected, should read
categories be selected by default
Closes #24501
Closes #24799
Co-authored-by: Jonathan Tran <jon@allspice.io>
Co-authored-by: Kyle D <kdumontnu@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
2023-06-04 21:57:16 +03:00
token := getTokenForLoggedInUser ( t , session , auth_model . AccessTokenScopeWriteUser )
2019-04-14 19:43:56 +03:00
testCreateGPGKey ( t , session . MakeRequest , token , http . StatusCreated , ` -- -- - BEGIN PGP PUBLIC KEY BLOCK -- -- -
mQENBFyy / VUBCADJ7zbM20Z1RWmFoVgp5WkQfI2rU1Vj9cQHes9i42wVLLtcbPeo
QzubgzvMPITDy7nfWxgSf83E23DoHQ1ACFbQh / 6 eFSRrjsusp3YQ / 0 8 NSfPPbcu8
0 M5G + VGwSfzS5uEcwBVQmHyKdcOZIERTNMtYZx1C3bjLD1XVJHvWz9D72Uq4qeO3
8 SR + lzp5n6ppUakcmRnxt3nGRBj1 + hEGkdgzyPo93iy + WioegY2lwCA9xMEo5dah
BmYxWx51zyiXYlReTaxlyb3 / nuSUt8IcW3Q8zjdtJj4Nu8U1SpV8EdaA1I9IPbHW
510 OSLmD3XhqHH5m6mIxL1YoWxk3V7gpDROtABEBAAG0GVVzZXIxIDx1c2VyMUBl
eGFtcGxlLmNvbT6JAU4EEwEIADgWIQTQEbrYxmXsp1z3j7z9 + v0I6RSEHwUCXLL9
VQIbAwULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRD9 + v0I6RSEH22YCACFqL5 +
6 M0m18AMC / pumcpnnmvAS1GrrKTF8nOROA1augZwp1WCNuKw2R6uOJIHANrYECSn
u7 + j6GBP2gbIW8mSAzS6HWCs7GGiPpVtT4wcu8wljUI6BxjpyZtoEkriyBjt6HfK
rkegbkuySoJvjq4IcO5D1LB1JWgsUjMYQJj / ZpBIzVtjG9QtFSOiT1Hct4PoZHdC
nsdSgyCkwRZXG + u3kT / wP9F663ba4o16vYlz3dCGo66lF2tyoG3qcyZ1OUzUrnuv
96 ytAzT6XIhrE0nVoBprMxFF5zExotJD3bHjcGBFNLf944bhjKee3U6t9 + OsfJVC
l7N5xxIawCuTQdbfuQENBFyy / VUBCADe61yGEoTwKfsOKIhxLaNoRmD883O0tiWt
soO / HPj9dPQLTOiwXgSgSCd8C + LNxGKct87wgFozpah4tDLC6c0nALuHJ0SLbkfz
55 aRhLeOOcrAydatDp72GroXzqpZ0xZBk5wjIWdgEol2GmVRM8QGbeuakU / HVz5y
lPzxUUocgdbSi3GE3zbzijQzVJdyL / kw / KP7pKT / PPKKJ2C5NQDLy0XGKEHddXGR
EWKkVlRalxq / TjfaMR0bi3MpezBsQmp99ATPO / d7trayZUxQHRtXzGFiOXfDHATr
qN730sODjqvU + mpc / SHCRwh9qWDjZRHSuKU5YDBjb5jIQJivZsQ / ABEBAAGJATYE
GAEIACAWIQTQEbrYxmXsp1z3j7z9 + v0I6RSEHwUCXLL9VQIbDAAKCRD9 + v0I6RSE
H7WoB / 4 tXl + 97 rQ6owPCGSVp1Xbwt2521V7COgsOFRVTRTryEWxRW8mm0S7wQvax
C0TLXKur6NVYQMn01iyL + FZzRpEWNuYF3f9QeeLJ / + l2DafESNhNTy17 + RPmacK6
21 dccpqchByVw / UMDeHSyjQLiG2lxzt8Gfx2gHmSbrq3aWovTGyz6JTffZvfy / n2
0 Hm437OBPazO0gZyXhdV2PE5RSUfvAgm44235tcV5EV0d32TJDfv61 + Vr2GUbah6
7 XhJ1v6JYuh8kaYaEz8OpZDeh7f6Ho6PzJrsy / TKTKhGgZNINj1iaPFyOkQgKR5M
GrE0MHOxUbc9tbtyk0F1SuzREUBH
= DDXw
-- -- - END PGP PUBLIC KEY BLOCK -- -- -
` )
2022-01-20 20:46:10 +03:00
// Export new key
2019-04-14 19:43:56 +03:00
testExportUserGPGKeys ( t , "user1" , ` -- -- - BEGIN PGP PUBLIC KEY BLOCK -- -- -
xsBNBFyy / VUBCADJ7zbM20Z1RWmFoVgp5WkQfI2rU1Vj9cQHes9i42wVLLtcbPeo
QzubgzvMPITDy7nfWxgSf83E23DoHQ1ACFbQh / 6 eFSRrjsusp3YQ / 0 8 NSfPPbcu8
0 M5G + VGwSfzS5uEcwBVQmHyKdcOZIERTNMtYZx1C3bjLD1XVJHvWz9D72Uq4qeO3
8 SR + lzp5n6ppUakcmRnxt3nGRBj1 + hEGkdgzyPo93iy + WioegY2lwCA9xMEo5dah
BmYxWx51zyiXYlReTaxlyb3 / nuSUt8IcW3Q8zjdtJj4Nu8U1SpV8EdaA1I9IPbHW
510 OSLmD3XhqHH5m6mIxL1YoWxk3V7gpDROtABEBAAHNGVVzZXIxIDx1c2VyMUBl
eGFtcGxlLmNvbT7CwI4EEwEIADgWIQTQEbrYxmXsp1z3j7z9 + v0I6RSEHwUCXLL9
VQIbAwULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRD9 + v0I6RSEH22YCACFqL5 +
6 M0m18AMC / pumcpnnmvAS1GrrKTF8nOROA1augZwp1WCNuKw2R6uOJIHANrYECSn
u7 + j6GBP2gbIW8mSAzS6HWCs7GGiPpVtT4wcu8wljUI6BxjpyZtoEkriyBjt6HfK
rkegbkuySoJvjq4IcO5D1LB1JWgsUjMYQJj / ZpBIzVtjG9QtFSOiT1Hct4PoZHdC
nsdSgyCkwRZXG + u3kT / wP9F663ba4o16vYlz3dCGo66lF2tyoG3qcyZ1OUzUrnuv
96 ytAzT6XIhrE0nVoBprMxFF5zExotJD3bHjcGBFNLf944bhjKee3U6t9 + OsfJVC
l7N5xxIawCuTQdbfzsBNBFyy / VUBCADe61yGEoTwKfsOKIhxLaNoRmD883O0tiWt
soO / HPj9dPQLTOiwXgSgSCd8C + LNxGKct87wgFozpah4tDLC6c0nALuHJ0SLbkfz
55 aRhLeOOcrAydatDp72GroXzqpZ0xZBk5wjIWdgEol2GmVRM8QGbeuakU / HVz5y
lPzxUUocgdbSi3GE3zbzijQzVJdyL / kw / KP7pKT / PPKKJ2C5NQDLy0XGKEHddXGR
EWKkVlRalxq / TjfaMR0bi3MpezBsQmp99ATPO / d7trayZUxQHRtXzGFiOXfDHATr
qN730sODjqvU + mpc / SHCRwh9qWDjZRHSuKU5YDBjb5jIQJivZsQ / ABEBAAHCwHYE
GAEIACAWIQTQEbrYxmXsp1z3j7z9 + v0I6RSEHwUCXLL9VQIbDAAKCRD9 + v0I6RSE
H7WoB / 4 tXl + 97 rQ6owPCGSVp1Xbwt2521V7COgsOFRVTRTryEWxRW8mm0S7wQvax
C0TLXKur6NVYQMn01iyL + FZzRpEWNuYF3f9QeeLJ / + l2DafESNhNTy17 + RPmacK6
21 dccpqchByVw / UMDeHSyjQLiG2lxzt8Gfx2gHmSbrq3aWovTGyz6JTffZvfy / n2
0 Hm437OBPazO0gZyXhdV2PE5RSUfvAgm44235tcV5EV0d32TJDfv61 + Vr2GUbah6
7 XhJ1v6JYuh8kaYaEz8OpZDeh7f6Ho6PzJrsy / TKTKhGgZNINj1iaPFyOkQgKR5M
GrE0MHOxUbc9tbtyk0F1SuzREUBH
= WFf5
-- -- - END PGP PUBLIC KEY BLOCK -- -- -
` )
}
func testExportUserGPGKeys ( t * testing . T , user , expected string ) {
session := loginUser ( t , user )
t . Logf ( "Testing username %s export gpg keys" , user )
req := NewRequest ( t , "GET" , "/" + user + ".gpg" )
resp := session . MakeRequest ( t , req , http . StatusOK )
2022-01-20 20:46:10 +03:00
// t.Log(resp.Body.String())
2019-04-14 19:43:56 +03:00
assert . Equal ( t , expected , resp . Body . String ( ) )
}
2022-04-07 21:59:56 +03:00
2023-04-07 13:08:36 +03:00
func TestGetUserRss ( t * testing . T ) {
user34 := "the_34-user.with.all.allowedChars"
req := NewRequestf ( t , "GET" , "/%s.rss" , user34 )
resp := MakeRequest ( t , req , http . StatusOK )
if assert . EqualValues ( t , "application/rss+xml;charset=utf-8" , resp . Header ( ) . Get ( "Content-Type" ) ) {
rssDoc := NewHTMLParser ( t , resp . Body ) . Find ( "channel" )
title , _ := rssDoc . ChildrenFiltered ( "title" ) . Html ( )
assert . EqualValues ( t , "Feed of "the_1-user.with.all.allowedChars"" , title )
description , _ := rssDoc . ChildrenFiltered ( "description" ) . Html ( )
2023-05-21 00:02:52 +03:00
assert . EqualValues ( t , "<p dir="auto">some <a href="https://commonmark.org/" rel="nofollow">commonmark</a>!</p>\n" , description )
2023-04-07 13:08:36 +03:00
}
}
2022-04-07 21:59:56 +03:00
func TestListStopWatches ( t * testing . T ) {
2022-09-02 22:18:23 +03:00
defer tests . PrepareTestEnv ( t ) ( )
2022-04-07 21:59:56 +03:00
2022-08-16 05:22:25 +03:00
repo := unittest . AssertExistsAndLoadBean ( t , & repo_model . Repository { ID : 1 } )
owner := unittest . AssertExistsAndLoadBean ( t , & user_model . User { ID : repo . OwnerID } )
2022-04-07 21:59:56 +03:00
session := loginUser ( t , owner . Name )
req := NewRequestf ( t , "GET" , "/user/stopwatches" )
resp := session . MakeRequest ( t , req , http . StatusOK )
var apiWatches [ ] * api . StopWatch
DecodeJSON ( t , resp , & apiWatches )
2022-08-16 05:22:25 +03:00
stopwatch := unittest . AssertExistsAndLoadBean ( t , & issues_model . Stopwatch { UserID : owner . ID } )
issue := unittest . AssertExistsAndLoadBean ( t , & issues_model . Issue { ID : stopwatch . IssueID } )
2022-04-07 21:59:56 +03:00
if assert . Len ( t , apiWatches , 1 ) {
assert . EqualValues ( t , stopwatch . CreatedUnix . AsTime ( ) . Unix ( ) , apiWatches [ 0 ] . Created . Unix ( ) )
assert . EqualValues ( t , issue . Index , apiWatches [ 0 ] . IssueIndex )
assert . EqualValues ( t , issue . Title , apiWatches [ 0 ] . IssueTitle )
assert . EqualValues ( t , repo . Name , apiWatches [ 0 ] . RepoName )
assert . EqualValues ( t , repo . OwnerName , apiWatches [ 0 ] . RepoOwnerName )
2022-06-20 13:02:49 +03:00
assert . Greater ( t , apiWatches [ 0 ] . Seconds , int64 ( 0 ) )
2022-04-07 21:59:56 +03:00
}
}
2023-07-31 11:44:45 +03:00
func TestUserLocationMapLink ( t * testing . T ) {
setting . Service . UserLocationMapURL = "https://example/foo/"
defer tests . PrepareTestEnv ( t ) ( )
session := loginUser ( t , "user2" )
req := NewRequestWithValues ( t , "POST" , "/user/settings" , map [ string ] string {
"_csrf" : GetCSRF ( t , session , "/user/settings" ) ,
"name" : "user2" ,
"email" : "user@example.com" ,
"language" : "en-US" ,
"location" : "A/b" ,
} )
session . MakeRequest ( t , req , http . StatusSeeOther )
req = NewRequest ( t , "GET" , "/user2/" )
resp := session . MakeRequest ( t , req , http . StatusOK )
htmlDoc := NewHTMLParser ( t , resp . Body )
htmlDoc . AssertElement ( t , ` a[href="https://example/foo/A%2Fb"] ` , true )
}