Skip to content

Commit

Permalink
test(functional): log in, change password (pypi#16724)
Browse files Browse the repository at this point in the history
* test: prerequisites for running functional webtests

Signed-off-by: Mike Fiedler <miketheman@gmail.com>

* test: log in, change password

Signed-off-by: Mike Fiedler <miketheman@gmail.com>

* refactor: replace static secret with factory one

Signed-off-by: Mike Fiedler <miketheman@gmail.com>

* refactor: externalize password generation

Signed-off-by: Mike Fiedler <miketheman@gmail.com>

* Update tests/functional/manage/test_views.py

* tests(ci): tell gha to also run redis

Signed-off-by: Mike Fiedler <miketheman@gmail.com>

* remove unneeded fixture

Signed-off-by: Mike Fiedler <miketheman@gmail.com>

---------

Signed-off-by: Mike Fiedler <miketheman@gmail.com>
  • Loading branch information
miketheman committed Sep 17, 2024
1 parent 35c0f16 commit a36ae29
Show file tree
Hide file tree
Showing 5 changed files with 93 additions and 3 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ jobs:
POSTGRES_INITDB_ARGS: '--no-sync --set fsync=off --set full_page_writes=off'
# Set health checks to wait until postgres has started
options: --health-cmd "pg_isready --username=postgres --dbname=postgres" --health-interval 10s --health-timeout 5s --health-retries 5
redis:
image: ${{ (matrix.name == 'Tests') && 'redis:7.0' || '' }}
ports:
- 6379:6379
stripe:
image: ${{ (matrix.name == 'Tests') && 'stripe/stripe-mock:v0.162.0' || '' }}
ports:
Expand Down
2 changes: 2 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@ services:
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
stripe:
condition: service_started

Expand Down
20 changes: 19 additions & 1 deletion tests/common/db/accounts.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

import factory

from argon2 import PasswordHasher

from warehouse.accounts.models import Email, ProhibitedUserName, User

from .base import WarehouseFactory
Expand All @@ -33,10 +35,26 @@ class Params:
verified=True,
)
)
# Allow passing a cleartext password to the factory
# This will be hashed before saving the user.
# Usage: UserFactory(clear_pwd="password")
clear_pwd = None

username = factory.Faker("pystr", max_chars=12)
name = factory.Faker("word")
password = "!"
password = factory.LazyAttribute(
# Note: argon2 is used directly here, since it's our "best" hashing algorithm
# instead of using `passlib`, since we may wish to replace it.
lambda obj: (
PasswordHasher(
memory_cost=1024,
parallelism=6,
time_cost=6,
).hash(obj.clear_pwd)
if obj.clear_pwd
else "!"
)
)
is_active = True
is_superuser = False
is_moderator = False
Expand Down
9 changes: 7 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,11 @@ def app_config(database):
@pytest.fixture(scope="session")
def app_config_dbsession_from_env(database):
nondefaults = {
"warehouse.db_create_session": lambda r: r.environ.get("warehouse.db_session")
"warehouse.db_create_session": lambda r: r.environ.get("warehouse.db_session"),
"breached_passwords.backend": "warehouse.accounts.services.NullPasswordBreachedService", # noqa: E501
"token.two_factor.secret": "insecure token",
# A running redis service is required for functional web sessions
"sessions.url": "redis://redis:0/",
}

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


@pytest.fixture
def webtest(app_config_dbsession_from_env):
def webtest(app_config_dbsession_from_env, remote_addr):
"""
This fixture yields a test app with an alternative Pyramid configuration,
injecting the database session and transaction manager into the app.
Expand Down Expand Up @@ -727,6 +731,7 @@ def webtest(app_config_dbsession_from_env):
"warehouse.db_session": _db_session,
"tm.active": True, # disable pyramid_tm
"tm.manager": tm, # pass in our own tm for the app to use
"REMOTE_ADDR": remote_addr, # set the same address for all requests
},
)
yield testapp
Expand Down
61 changes: 61 additions & 0 deletions tests/functional/manage/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import time

from http import HTTPStatus

import faker
import pretend
import pytest

Expand All @@ -20,6 +24,7 @@
from warehouse.manage.views import organizations as org_views
from warehouse.organizations.interfaces import IOrganizationService
from warehouse.organizations.models import OrganizationType
from warehouse.utils.otp import _get_totp

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

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

def test_changing_password_succeeds(self, webtest, socket_enabled):
"""A user can log in, and change their password."""
# create a User
user = UserFactory.create(
with_verified_primary_email=True, clear_pwd="password"
)

# visit login page
login_page = webtest.get("/account/login/", status=HTTPStatus.OK)

# Fill & submit the login form
login_form = login_page.forms[2] # TODO: form should have an ID, doesn't yet
anonymous_csrf_token = login_form["csrf_token"].value
login_form["username"] = user.username
login_form["password"] = "password"
login_form["csrf_token"] = anonymous_csrf_token

two_factor_page = login_form.submit().follow(status=HTTPStatus.OK)

# TODO: form doesn't have an ID yet
two_factor_form = two_factor_page.forms[2]
two_factor_form["csrf_token"] = anonymous_csrf_token

# Generate the correct TOTP value from the known secret
two_factor_form["totp_value"] = (
_get_totp(user.totp_secret).generate(time.time()).decode()
)

logged_in = two_factor_form.submit().follow(status=HTTPStatus.OK)
assert logged_in.html.find("title", text="Warehouse · The Python Package Index")

# Now visit the change password page
change_password_page = logged_in.goto("/manage/account/", status=HTTPStatus.OK)

# Ensure that the CSRF token changes once logged in and a session is established
logged_in_csrf_token = change_password_page.html.find(
"input", {"name": "csrf_token"}
)["value"]
assert anonymous_csrf_token != logged_in_csrf_token

# Fill & submit the change password form
# TODO: form doesn't have an ID yet
new_password = faker.Faker().password() # a secure-enough password for testing
change_password_form = change_password_page.forms[3]
change_password_form["csrf_token"] = logged_in_csrf_token
change_password_form["password"] = "password"
change_password_form["new_password"] = new_password
change_password_form["password_confirm"] = new_password

change_password_form.submit().follow(status=HTTPStatus.OK)

# Request the JavaScript-enabled flash messages directly to get the message
resp = webtest.get("/_includes/flash-messages/", status=HTTPStatus.OK)
success_message = resp.html.find("span", {"class": "notification-bar__message"})
assert success_message.text == "Password updated"


class TestManageOrganizations:
@pytest.mark.usefixtures("_enable_organizations")
Expand Down

0 comments on commit a36ae29

Please sign in to comment.