Skip to content

Commit a36ae29

Browse files
authored
test(functional): log in, change password (#16724)
* test: prerequisites for running functional webtests Signed-off-by: Mike Fiedler <[email protected]> * test: log in, change password Signed-off-by: Mike Fiedler <[email protected]> * refactor: replace static secret with factory one Signed-off-by: Mike Fiedler <[email protected]> * refactor: externalize password generation Signed-off-by: Mike Fiedler <[email protected]> * Update tests/functional/manage/test_views.py * tests(ci): tell gha to also run redis Signed-off-by: Mike Fiedler <[email protected]> * remove unneeded fixture Signed-off-by: Mike Fiedler <[email protected]> --------- Signed-off-by: Mike Fiedler <[email protected]>
1 parent 35c0f16 commit a36ae29

File tree

5 files changed

+93
-3
lines changed

5 files changed

+93
-3
lines changed

.github/workflows/ci.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@ jobs:
6969
POSTGRES_INITDB_ARGS: '--no-sync --set fsync=off --set full_page_writes=off'
7070
# Set health checks to wait until postgres has started
7171
options: --health-cmd "pg_isready --username=postgres --dbname=postgres" --health-interval 10s --health-timeout 5s --health-retries 5
72+
redis:
73+
image: ${{ (matrix.name == 'Tests') && 'redis:7.0' || '' }}
74+
ports:
75+
- 6379:6379
7276
stripe:
7377
image: ${{ (matrix.name == 'Tests') && 'stripe/stripe-mock:v0.162.0' || '' }}
7478
ports:

docker-compose.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,8 @@ services:
137137
depends_on:
138138
db:
139139
condition: service_healthy
140+
redis:
141+
condition: service_started
140142
stripe:
141143
condition: service_started
142144

tests/common/db/accounts.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414

1515
import factory
1616

17+
from argon2 import PasswordHasher
18+
1719
from warehouse.accounts.models import Email, ProhibitedUserName, User
1820

1921
from .base import WarehouseFactory
@@ -33,10 +35,26 @@ class Params:
3335
verified=True,
3436
)
3537
)
38+
# Allow passing a cleartext password to the factory
39+
# This will be hashed before saving the user.
40+
# Usage: UserFactory(clear_pwd="password")
41+
clear_pwd = None
3642

3743
username = factory.Faker("pystr", max_chars=12)
3844
name = factory.Faker("word")
39-
password = "!"
45+
password = factory.LazyAttribute(
46+
# Note: argon2 is used directly here, since it's our "best" hashing algorithm
47+
# instead of using `passlib`, since we may wish to replace it.
48+
lambda obj: (
49+
PasswordHasher(
50+
memory_cost=1024,
51+
parallelism=6,
52+
time_cost=6,
53+
).hash(obj.clear_pwd)
54+
if obj.clear_pwd
55+
else "!"
56+
)
57+
)
4058
is_active = True
4159
is_superuser = False
4260
is_moderator = False

tests/conftest.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -394,7 +394,11 @@ def app_config(database):
394394
@pytest.fixture(scope="session")
395395
def app_config_dbsession_from_env(database):
396396
nondefaults = {
397-
"warehouse.db_create_session": lambda r: r.environ.get("warehouse.db_session")
397+
"warehouse.db_create_session": lambda r: r.environ.get("warehouse.db_session"),
398+
"breached_passwords.backend": "warehouse.accounts.services.NullPasswordBreachedService", # noqa: E501
399+
"token.two_factor.secret": "insecure token",
400+
# A running redis service is required for functional web sessions
401+
"sessions.url": "redis://redis:0/",
398402
}
399403

400404
return get_app_config(database, nondefaults)
@@ -697,7 +701,7 @@ def xmlrpc(self, path, method, *args):
697701

698702

699703
@pytest.fixture
700-
def webtest(app_config_dbsession_from_env):
704+
def webtest(app_config_dbsession_from_env, remote_addr):
701705
"""
702706
This fixture yields a test app with an alternative Pyramid configuration,
703707
injecting the database session and transaction manager into the app.
@@ -727,6 +731,7 @@ def webtest(app_config_dbsession_from_env):
727731
"warehouse.db_session": _db_session,
728732
"tm.active": True, # disable pyramid_tm
729733
"tm.manager": tm, # pass in our own tm for the app to use
734+
"REMOTE_ADDR": remote_addr, # set the same address for all requests
730735
},
731736
)
732737
yield testapp

tests/functional/manage/test_views.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@
99
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1010
# See the License for the specific language governing permissions and
1111
# limitations under the License.
12+
import time
1213

14+
from http import HTTPStatus
15+
16+
import faker
1317
import pretend
1418
import pytest
1519

@@ -20,6 +24,7 @@
2024
from warehouse.manage.views import organizations as org_views
2125
from warehouse.organizations.interfaces import IOrganizationService
2226
from warehouse.organizations.models import OrganizationType
27+
from warehouse.utils.otp import _get_totp
2328

2429
from ...common.db.accounts import EmailFactory, UserFactory
2530

@@ -48,6 +53,62 @@ def test_save_account(self, pyramid_services, user_service, db_request):
4853
assert user.name == "new name"
4954
assert user.public_email is None
5055

56+
def test_changing_password_succeeds(self, webtest, socket_enabled):
57+
"""A user can log in, and change their password."""
58+
# create a User
59+
user = UserFactory.create(
60+
with_verified_primary_email=True, clear_pwd="password"
61+
)
62+
63+
# visit login page
64+
login_page = webtest.get("/account/login/", status=HTTPStatus.OK)
65+
66+
# Fill & submit the login form
67+
login_form = login_page.forms[2] # TODO: form should have an ID, doesn't yet
68+
anonymous_csrf_token = login_form["csrf_token"].value
69+
login_form["username"] = user.username
70+
login_form["password"] = "password"
71+
login_form["csrf_token"] = anonymous_csrf_token
72+
73+
two_factor_page = login_form.submit().follow(status=HTTPStatus.OK)
74+
75+
# TODO: form doesn't have an ID yet
76+
two_factor_form = two_factor_page.forms[2]
77+
two_factor_form["csrf_token"] = anonymous_csrf_token
78+
79+
# Generate the correct TOTP value from the known secret
80+
two_factor_form["totp_value"] = (
81+
_get_totp(user.totp_secret).generate(time.time()).decode()
82+
)
83+
84+
logged_in = two_factor_form.submit().follow(status=HTTPStatus.OK)
85+
assert logged_in.html.find("title", text="Warehouse · The Python Package Index")
86+
87+
# Now visit the change password page
88+
change_password_page = logged_in.goto("/manage/account/", status=HTTPStatus.OK)
89+
90+
# Ensure that the CSRF token changes once logged in and a session is established
91+
logged_in_csrf_token = change_password_page.html.find(
92+
"input", {"name": "csrf_token"}
93+
)["value"]
94+
assert anonymous_csrf_token != logged_in_csrf_token
95+
96+
# Fill & submit the change password form
97+
# TODO: form doesn't have an ID yet
98+
new_password = faker.Faker().password() # a secure-enough password for testing
99+
change_password_form = change_password_page.forms[3]
100+
change_password_form["csrf_token"] = logged_in_csrf_token
101+
change_password_form["password"] = "password"
102+
change_password_form["new_password"] = new_password
103+
change_password_form["password_confirm"] = new_password
104+
105+
change_password_form.submit().follow(status=HTTPStatus.OK)
106+
107+
# Request the JavaScript-enabled flash messages directly to get the message
108+
resp = webtest.get("/_includes/flash-messages/", status=HTTPStatus.OK)
109+
success_message = resp.html.find("span", {"class": "notification-bar__message"})
110+
assert success_message.text == "Password updated"
111+
51112

52113
class TestManageOrganizations:
53114
@pytest.mark.usefixtures("_enable_organizations")

0 commit comments

Comments
 (0)