Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: CI - Tests

on: [push]

jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.11"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install -r requirements.txt
python -m pip install pytest pytest-cov
- name: Running tests with pytest
run: |
python -m pytest --cov=./website --cov-report=term-missing
python -m pytest --cov=./website --cov-branch --cov-report=term-missing
4 changes: 1 addition & 3 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

"""Pytest fixtures for the test suite.

These fixtures are intentionally simple and used only in tests; test
Expand All @@ -21,7 +20,6 @@
flask_app = create_app()



@pytest.fixture(scope="module")
def app():
"""Provide Flask app object for tests (module scope)."""
Expand All @@ -36,7 +34,7 @@ def app_ctx(app):


@pytest.fixture(autouse=True)
def cleanup_db(_app_ctx):
def cleanup_db(app_ctx):
"""Automatically clean up database before and after each test."""
table_names = set(db.metadata.tables.keys())

Expand Down
137 changes: 137 additions & 0 deletions tests/test_views_functional.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
# pylint: disable=missing-module-docstring,missing-function-docstring,missing-class-docstring,import-outside-toplevel,too-many-lines,unused-argument
import json
from types import SimpleNamespace

import pytest

from website import db
from website.models.tracking import User as TrackingUser, Action, Visit, Feedback
from website.models.user import User, WishlistItem


def login_user_in_client(client, user):
with client.session_transaction() as sess:
sess["_user_id"] = str(user.id)
sess["_fresh"] = True


class TestLandingAndRoadmapFunctional:
def test_homepage_sets_tracking_cookie_and_creates_visit(self, app, app_ctx):
with app.test_client() as client:
rv = client.get("/")
assert rv.status_code == 200
set_cookie = rv.headers.get("Set-Cookie", "")
assert "tracking_id=" in set_cookie

visits = Visit.query.filter_by(page="homepage.html").all()
assert visits, "Expected at least one Visit row for homepage"

def test_onboarding_assigns_ab_cookie_and_redirects(self, app, app_ctx):
with app.test_client() as client:
rv = client.get("/onboarding")
assert rv.status_code in (302, 301)
set_cookie = rv.headers.get("Set-Cookie", "")
assert "onboarding_ab=" in set_cookie

def test_onboarding_variant_pages_return_200(self, app, app_ctx):
with app.test_client() as client:
rv_a = client.get("/onboarding/variantA")
assert rv_a.status_code == 200
rv_b = client.get("/onboarding/variantB")
assert rv_b.status_code == 200

def test_track_action_requires_json(self, app, app_ctx):
with app.test_client() as client:
rv = client.post("/track-action", data="notjson", content_type="text/plain")
assert rv.status_code == 400

def test_track_action_records_action_when_user_cookie(self, app, app_ctx):
tuser = TrackingUser(uuid="track-test-1")
db.session.add(tuser)
db.session.commit()

with app.test_client() as client:
client.set_cookie(key="tracking_id", value="track-test-1")

payload = {"atype": "roadmap_link_click", "detail": {"foo": "bar"}}
rv = client.post("/track-action", json=payload)

assert rv.status_code == 200

a = Action.query.filter_by(atype="roadmap_link_click").first()
assert a is not None
assert a.detail and a.detail.get("foo") == "bar"

def test_submit_info_creates_core_action_and_redirects(self, app, app_ctx):
tuser = TrackingUser(uuid="track-test-2")
db.session.add(tuser)
db.session.commit()

with app.test_client() as client:
client.set_cookie(key="tracking_id", value="track-test-2")

data = {
"class_year": "2026",
"major": "cs",
"onboarding_variant": "full",
"career_goal": "research",
"career_stage": "early",
"priority": "high",
}

rv = client.post("/submit-info", data=data, follow_redirects=False)
assert rv.status_code in (302, 301)

a = Action.query.filter_by(atype="roadmap_submit").first()
assert a is not None

def test_feedback_post_creates_feedback(self, app, app_ctx):
tuser = TrackingUser(uuid="track-test-3")
db.session.add(tuser)
db.session.commit()

with app.test_client() as client:
client.set_cookie(key="tracking_id", value="track-test-3")

rv = client.post("/feedback", data={"feedback_content": "Great site!"})
assert rv.status_code in (302, 301)

f = Feedback.query.filter_by(content="Great site!").first()
assert f is not None

def test_wishlist_save_requires_auth(self, app, app_ctx):
with app.test_client() as client:
rv = client.post("/wishlist/items/from-roadmap", json={})
assert rv.status_code == 401

def test_wishlist_save_creates_item_when_logged_in(self, app, app_ctx):
app_user = User(first_name="Test", last_name="User", email="tuser@example.com")
db.session.add(app_user)
db.session.commit()

with app.test_client() as client:
login_user_in_client(client, app_user)

payload = {
"roadmap_item_id": "item-123",
"title": "Do something",
"checked": True,
}

rv = client.post("/wishlist/items/from-roadmap", json=payload)
assert rv.status_code == 200

item = WishlistItem.query.filter_by(
roadmap_item_id="item-123", user_id=app_user.id
).first()

assert item is not None

def test_export_roadmap_metrics_csv_empty(self, app, app_ctx):
with app.test_client() as client:
rv = client.get("/dashboard/export-roadmap-metrics.csv")
assert rv.status_code == 200
assert "text/csv" in rv.headers.get("Content-Type", "")

data = rv.get_data(as_text=True)
assert "user_id,variant_letter" in data
93 changes: 93 additions & 0 deletions tests/test_views_unit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import pytest
from types import SimpleNamespace

from website.views import auth_views, roadmap_views, landing_views, dashboard_views


# ---------------------------
# FIX: alias missing fixture
# ---------------------------
@pytest.fixture
def _app_ctx(app_ctx):
return app_ctx


# ---------------------------
# roadmap cache key
# ---------------------------
def test_cache_key_is_deterministic():
p = {
"major": "cs",
"year": "senior",
"career_goal": "",
"career_stage": "",
"priority": "",
}
k1 = roadmap_views._cache_key(p)
k2 = roadmap_views._cache_key(p.copy())
assert k1.startswith("roadmap_")
assert k1 == k2


# ---------------------------
# admin password validation
# ---------------------------
def test_admin_password_is_valid_true_and_false():
good = "secretpw"
bad = "wrong"

assert landing_views._admin_password_is_valid(provided=good, expected=good) is True
assert landing_views._admin_password_is_valid(provided=bad, expected=good) is False
assert landing_views._admin_password_is_valid(provided="", expected=good) is False
assert landing_views._admin_password_is_valid(provided=good, expected="") is False


# ---------------------------
# mentor profile
# ---------------------------
def test_mentor_profile_context_and_llm_profile():
ctx = landing_views._mentor_profile_context(None)
assert ctx["profile_class_year"] == "Not set yet"

llm = landing_views._mentor_llm_profile(None)
assert llm["class_year"] == ""

tu = SimpleNamespace(
class_year="2026",
major="cs",
career_goal="software_engineer",
career_stage="no_internships",
onboarding_variant="full",
)

ctx2 = landing_views._mentor_profile_context(tu)
assert ctx2["profile_class_year"] == "2026"

llm2 = landing_views._mentor_llm_profile(tu)
assert llm2["class_year"] == "2026"
assert llm2["major"] == "cs"


# ---------------------------
# dashboard helpers
# ---------------------------
def test_sum_roadmap_time_seconds_from_details_handles_various_values():
good = SimpleNamespace(atype="roadmap_time_on_page", detail={"seconds": "30"})
int_sec = SimpleNamespace(atype="roadmap_time_on_page", detail={"seconds": 45})
missing = SimpleNamespace(atype="roadmap_time_on_page", detail={"foo": "bar"})
not_dict = SimpleNamespace(atype="roadmap_time_on_page", detail="string")
other_action = SimpleNamespace(atype="roadmap_link_click", detail={"seconds": 100})

total = dashboard_views._sum_roadmap_time_seconds_from_details(
[good, int_sec, missing, not_dict, other_action]
)
assert total == 75


def test_label_for_value_with_mappings():
mapping = {"x": "Label X"}

assert dashboard_views._label_for_value(mapping, "x") == "Label X"
assert dashboard_views._label_for_value(mapping, None) == "Not answered"
assert dashboard_views._label_for_value(mapping, "") == "Not answered"
assert dashboard_views._label_for_value(mapping, "unknown") == "unknown"
Loading