2026-10-27 07:10:27 -04:00
|
|
|
import re
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
from django.core import mail
|
|
|
|
|
from django.core.exceptions import ValidationError
|
2026-10-27 08:42:19 -04:00
|
|
|
from django.urls import NoReverseMatch, reverse
|
2026-10-27 07:10:27 -04:00
|
|
|
|
|
|
|
|
from intent.accounts.validators import username_validators
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _sign_in_with_code(client, email):
|
|
|
|
|
client.post(reverse("account_request_login_code"), {"email": email})
|
|
|
|
|
code = re.search(r"^\s*([A-Z0-9-]{6,})\s*$", mail.outbox[-1].body, re.MULTILINE).group(1)
|
|
|
|
|
return client.post(reverse("account_confirm_login_code"), {"code": code})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_login_page_offers_code_not_password(client, db):
|
|
|
|
|
html = client.get(reverse("account_login")).content.decode()
|
|
|
|
|
assert 'type="password"' not in html
|
|
|
|
|
assert reverse("account_request_login_code") in html
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_signup_has_no_password(client, db):
|
|
|
|
|
html = client.get(reverse("account_signup")).content.decode()
|
|
|
|
|
assert 'type="password"' not in html
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_sign_in_with_emailed_code(client, make_user):
|
|
|
|
|
user = make_user("ada")
|
|
|
|
|
response = _sign_in_with_code(client, "ada@example.com")
|
|
|
|
|
assert response.status_code == 302
|
|
|
|
|
assert client.session["_auth_user_id"] == user.pk
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_staff_cannot_sign_in_through_allauth(client, make_user):
|
|
|
|
|
make_user("root", is_staff=True)
|
|
|
|
|
response = _sign_in_with_code(client, "root@example.com")
|
|
|
|
|
assert response["Location"].startswith(reverse("admin:login"))
|
|
|
|
|
assert "_auth_user_id" not in client.session
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize("value", ["ada", "ada-lovelace", "a_1"])
|
|
|
|
|
def test_valid_usernames(value):
|
|
|
|
|
for validator in username_validators:
|
|
|
|
|
validator(value)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize("value", ["ada@example.com", "-ada", "ada.l", "x" * 40])
|
|
|
|
|
def test_invalid_usernames(value):
|
|
|
|
|
with pytest.raises(ValidationError):
|
|
|
|
|
for validator in username_validators:
|
|
|
|
|
validator(value)
|
2026-10-27 08:42:19 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
|
|
|
"name", ["account_set_password", "account_change_password", "account_reset_password"]
|
|
|
|
|
)
|
|
|
|
|
def test_password_urls_do_not_exist(name):
|
|
|
|
|
with pytest.raises(NoReverseMatch):
|
|
|
|
|
reverse(name)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_signed_in_pages_without_passwords_render(client, owner):
|
|
|
|
|
client.force_login(owner)
|
|
|
|
|
assert client.get(reverse("account_email")).status_code == 200
|
|
|
|
|
assert client.get("/accounts/password/set/").status_code == 404
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_staff_cannot_grant_staff_without_superuser(client, make_user):
|
|
|
|
|
from django.contrib.auth.models import Permission
|
|
|
|
|
|
|
|
|
|
staff = make_user("stan", is_staff=True)
|
|
|
|
|
staff.user_permissions.add(*Permission.objects.filter(content_type__app_label="accounts"))
|
|
|
|
|
client.force_login(staff)
|
|
|
|
|
target = make_user("tina")
|
|
|
|
|
page = client.get(reverse("admin:accounts_user_change", args=[target.pk])).content.decode()
|
|
|
|
|
assert 'name="is_superuser"' not in page and 'name="is_staff"' not in page
|
Add issues and comment threads (Phase 3)
Issues: per-project numbers, statuses (open, accepted, rejected,
withdrawn, duplicate), feature links, edits, and a timeline of comments
and events. From an issue, collaborators create or update a feature
with the revision tied to the issue and an option to accept it.
Comments: general and line-anchored threads on features, one
discussion thread per issue, edit history, tombstone deletes, resolve,
and promoting a feature thread to an issue. Line threads attach to
top-level Markdown blocks and go outdated only when the text changes.
HTMX swaps threads in place with focus management; every action also
works as a plain form post. Markdown headings are re-levelled so user
text never skips heading levels on the page.
The example flow from concept.md (steps 1-7) is covered end to end in
tests/test_example_flow.py.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 09:18:45 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_admin_pages_for_issues_and_threads_render(admin_client, db):
|
|
|
|
|
for name in ["admin:issues_issue_changelist", "admin:comments_commentthread_changelist"]:
|
|
|
|
|
assert admin_client.get(reverse(name)).status_code == 200
|