Add member profile questions, admin tooling, legal pages, and fast tests
- Add configurable profile questions with conditional visibility, admin-only fields, user answers, and seeded onboarding/volunteer questions
- Add admin UI for managing profile questions and member profile answers
- Add volunteer level/profile data support across backend schemas, models, API, and migration
- Update dashboard/profile UI, super admin menu, membership service types, and related styling
- Add privacy policy, terms of service, cookie notice, and footer links
- Add frontend Vitest coverage for profile question logic
- Add backend pytest coverage for profile answer normalization and validation
- Update restart.sh to build, run frontend/backend unit tests, and restart only after tests pass
- Refresh README, quickstart, project structure, instructions, and Square docs to match current app features
- Protect feature flag reload behind super-admin access
- Restrict admin-triggered password resets so admins can only reset member accounts
- Replace email template HTML preview rendering with escaped text preview
- Update docs for feature flag reload access, password reset scope, and email template preview safety
-- test user questions are also made by AI and not very useful. but i didn't know what to put there so its good enough for a test
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
from datetime import date, datetime
|
||||
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
|
||||
from app.api.v1.users import (
|
||||
_deserialize_answer_value,
|
||||
_normalize_answer_value,
|
||||
_normalize_volunteer_level,
|
||||
_parse_options,
|
||||
_serialize_options,
|
||||
)
|
||||
from app.models.models import ProfileQuestion
|
||||
from app.schemas import QuestionOption
|
||||
|
||||
|
||||
def make_question(input_type: str, options_json: str | None = None) -> ProfileQuestion:
|
||||
return ProfileQuestion(
|
||||
key=f"{input_type}_question",
|
||||
label=f"{input_type.title()} Question",
|
||||
input_type=input_type,
|
||||
options_json=options_json,
|
||||
)
|
||||
|
||||
|
||||
def test_option_parsing_and_serialization_filters_invalid_items() -> None:
|
||||
assert _parse_options('[{"label":" Yes ","value":" yes "}, {"label":"","value":"no"}, "bad"]') == [
|
||||
{"label": "Yes", "value": "yes"}
|
||||
]
|
||||
assert _parse_options("not-json") == []
|
||||
|
||||
serialized = _serialize_options([QuestionOption(label="Private Pilot", value="ppl")])
|
||||
|
||||
assert _parse_options(serialized) == [{"label": "Private Pilot", "value": "ppl"}]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("value", "expected"),
|
||||
[
|
||||
(True, "true"),
|
||||
("yes", "true"),
|
||||
("0", "false"),
|
||||
(False, "false"),
|
||||
],
|
||||
)
|
||||
def test_boolean_answers_are_normalized(value: object, expected: str) -> None:
|
||||
assert _normalize_answer_value(make_question("boolean"), value) == expected
|
||||
|
||||
|
||||
def test_invalid_boolean_answer_raises_400() -> None:
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
_normalize_answer_value(make_question("boolean"), "maybe")
|
||||
|
||||
assert exc.value.status_code == 400
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("value", "expected"),
|
||||
[
|
||||
(3, "3"),
|
||||
("3.50", "3.5"),
|
||||
(date(2026, 5, 4), "2026-05-04"),
|
||||
(datetime(2026, 5, 4, 12, 30), "2026-05-04"),
|
||||
],
|
||||
)
|
||||
def test_number_and_date_answers_are_normalized(value: object, expected: str) -> None:
|
||||
input_type = "date" if isinstance(value, (date, datetime)) else "number"
|
||||
|
||||
assert _normalize_answer_value(make_question(input_type), value) == expected
|
||||
|
||||
|
||||
def test_select_answers_must_match_configured_options() -> None:
|
||||
question = make_question("select", '[{"label":"Private Pilot","value":"ppl"}]')
|
||||
|
||||
assert _normalize_answer_value(question, "ppl") == "ppl"
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
_normalize_answer_value(question, "cpl")
|
||||
|
||||
assert exc.value.status_code == 400
|
||||
|
||||
|
||||
def test_empty_answers_clear_existing_values() -> None:
|
||||
assert _normalize_answer_value(make_question("text"), "") is None
|
||||
assert _normalize_answer_value(make_question("text"), None) is None
|
||||
|
||||
|
||||
def test_answer_deserialization_restores_frontend_types() -> None:
|
||||
assert _deserialize_answer_value(make_question("boolean"), "true") is True
|
||||
assert _deserialize_answer_value(make_question("boolean"), "false") is False
|
||||
assert _deserialize_answer_value(make_question("number"), "10") == 10
|
||||
assert _deserialize_answer_value(make_question("number"), "10.5") == 10.5
|
||||
assert _deserialize_answer_value(make_question("text"), "SASA") == "SASA"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("value", "expected"),
|
||||
[
|
||||
("yes", "yes"),
|
||||
("true", "yes"),
|
||||
("0", "no"),
|
||||
("", None),
|
||||
(None, None),
|
||||
],
|
||||
)
|
||||
def test_volunteer_level_accepts_boolean_like_values(value: str | None, expected: str | None) -> None:
|
||||
assert _normalize_volunteer_level(value) == expected
|
||||
|
||||
|
||||
def test_invalid_volunteer_level_raises_400() -> None:
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
_normalize_volunteer_level("sometimes")
|
||||
|
||||
assert exc.value.status_code == 400
|
||||
Reference in New Issue
Block a user