Skip to content

Commit 0135535

Browse files
committed
add unit test
1 parent d6984b1 commit 0135535

File tree

8 files changed

+621
-2
lines changed

8 files changed

+621
-2
lines changed

.claude/settings.local.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@
2323
"Bash(npm run build:*)",
2424
"Bash(uv add:*)",
2525
"Bash(dir:*)",
26-
"Bash(mkdir:*)"
26+
"Bash(mkdir:*)",
27+
"Bash(uv run pytest:*)"
2728
],
2829
"deny": [],
2930
"ask": []

backend/app/crud.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# Backwards compatibility layer for tests
2+
# CRUD operations are now in app/modules/*/repository.py
3+
4+
import uuid
5+
from typing import Any
6+
7+
from sqlmodel import Session
8+
9+
from app.modules.users.repository import UserRepository
10+
from app.modules.users.models import User
11+
from app.modules.users.schemas import UserCreate, UserCreateOAuth, UserUpdate
12+
from app.modules.items.repository import ItemRepository
13+
from app.modules.items.models import Item
14+
from app.modules.items.schemas import ItemCreate
15+
16+
17+
def create_user(*, session: Session, user_create: UserCreate) -> User:
18+
"""Create a new user with password."""
19+
repo = UserRepository(session)
20+
return repo.create(user_create)
21+
22+
23+
def create_user_oauth(*, session: Session, user_create: UserCreateOAuth) -> User:
24+
"""Create a new user via OAuth."""
25+
repo = UserRepository(session)
26+
return repo.create_oauth(user_create)
27+
28+
29+
def update_user(*, session: Session, db_user: User, user_in: UserUpdate) -> Any:
30+
"""Update an existing user."""
31+
repo = UserRepository(session)
32+
return repo.update(db_user, user_in)
33+
34+
35+
def get_user_by_email(*, session: Session, email: str) -> User | None:
36+
"""Get user by email."""
37+
repo = UserRepository(session)
38+
return repo.get_by_email(email)
39+
40+
41+
def authenticate(*, session: Session, email: str, password: str) -> User | None:
42+
"""Authenticate user by email and password."""
43+
repo = UserRepository(session)
44+
return repo.authenticate(email, password)
45+
46+
47+
def create_item(*, session: Session, item_in: ItemCreate, owner_id: uuid.UUID) -> Item:
48+
"""Create a new item."""
49+
repo = ItemRepository(session)
50+
return repo.create(item_in, owner_id)

backend/app/models.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Re-export models for backwards compatibility with tests
2+
# Models are now organized in app/modules/*/models.py
3+
4+
from app.modules.users.models import User
5+
from app.modules.users.schemas import (
6+
UserCreate,
7+
UserPublic,
8+
UserRegister,
9+
UserUpdate,
10+
UserUpdateMe,
11+
UpdatePassword,
12+
)
13+
from app.modules.items.models import Item
14+
from app.modules.items.schemas import ItemCreate, ItemPublic, ItemUpdate
15+
from app.modules.shortener.models import ShortUrl
16+
17+
__all__ = [
18+
# Users
19+
"User",
20+
"UserCreate",
21+
"UserPublic",
22+
"UserRegister",
23+
"UserUpdate",
24+
"UserUpdateMe",
25+
"UpdatePassword",
26+
# Items
27+
"Item",
28+
"ItemCreate",
29+
"ItemPublic",
30+
"ItemUpdate",
31+
# Shortener
32+
"ShortUrl",
33+
]
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
from fastapi.testclient import TestClient
2+
from sqlmodel import Session
3+
4+
from app.modules.shortener.schemas import ShortUrlCreate, ShortUrlUpdate
5+
from app.modules.shortener.service import ShortUrlService
6+
from tests.utils.shortener import create_random_short_url
7+
from tests.utils.utils import random_lower_string
8+
9+
10+
def test_redirect_to_url_success(client: TestClient, db: Session) -> None:
11+
"""Test successful redirect to original URL."""
12+
short_url = create_random_short_url(db)
13+
14+
response = client.get(
15+
f"/s/{short_url.short_code}",
16+
follow_redirects=False,
17+
)
18+
assert response.status_code == 307
19+
assert response.headers["location"] == short_url.original_url
20+
21+
22+
def test_redirect_not_found(client: TestClient) -> None:
23+
"""Test redirect returns 404 for non-existent short code."""
24+
response = client.get(
25+
f"/s/nonexistent{random_lower_string()[:6]}",
26+
follow_redirects=False,
27+
)
28+
assert response.status_code == 404
29+
# Should return HTML error page
30+
assert "Link Not Found" in response.text
31+
32+
33+
def test_redirect_inactive_url(client: TestClient, db: Session) -> None:
34+
"""Test redirect returns 410 for deactivated short URL."""
35+
short_url = create_random_short_url(db)
36+
37+
# Deactivate the short URL
38+
service = ShortUrlService(db)
39+
service.repository.update(
40+
db_short_url=short_url,
41+
short_url_in=ShortUrlUpdate(is_active=False),
42+
)
43+
44+
response = client.get(
45+
f"/s/{short_url.short_code}",
46+
follow_redirects=False,
47+
)
48+
assert response.status_code == 410
49+
# Should return HTML error page
50+
assert "Link Deactivated" in response.text
51+
52+
53+
def test_redirect_increments_click_count(client: TestClient, db: Session) -> None:
54+
"""Test that redirect increments the click count."""
55+
short_url = create_random_short_url(db)
56+
initial_count = short_url.click_count
57+
58+
# Make a redirect request
59+
response = client.get(
60+
f"/s/{short_url.short_code}",
61+
follow_redirects=False,
62+
)
63+
assert response.status_code == 307
64+
65+
# Refresh and check click count
66+
db.refresh(short_url)
67+
assert short_url.click_count == initial_count + 1
68+
69+
70+
def test_redirect_multiple_clicks(client: TestClient, db: Session) -> None:
71+
"""Test that multiple redirects properly increment click count."""
72+
short_url = create_random_short_url(db)
73+
initial_count = short_url.click_count
74+
75+
# Make multiple redirect requests
76+
for i in range(3):
77+
response = client.get(
78+
f"/s/{short_url.short_code}",
79+
follow_redirects=False,
80+
)
81+
assert response.status_code == 307
82+
83+
# Refresh and check click count
84+
db.refresh(short_url)
85+
assert short_url.click_count == initial_count + 3
86+
87+
88+
def test_redirect_no_cache_headers(client: TestClient, db: Session) -> None:
89+
"""Test that redirect response includes no-cache headers."""
90+
short_url = create_random_short_url(db)
91+
92+
response = client.get(
93+
f"/s/{short_url.short_code}",
94+
follow_redirects=False,
95+
)
96+
assert response.status_code == 307
97+
assert "no-store" in response.headers.get("cache-control", "")
98+
assert response.headers.get("pragma") == "no-cache"
99+
100+
101+
def test_redirect_inactive_no_click_increment(client: TestClient, db: Session) -> None:
102+
"""Test that inactive URLs don't increment click count."""
103+
short_url = create_random_short_url(db)
104+
initial_count = short_url.click_count
105+
106+
# Deactivate the short URL
107+
service = ShortUrlService(db)
108+
service.repository.update(
109+
db_short_url=short_url,
110+
short_url_in=ShortUrlUpdate(is_active=False),
111+
)
112+
113+
# Try to access
114+
response = client.get(
115+
f"/s/{short_url.short_code}",
116+
follow_redirects=False,
117+
)
118+
assert response.status_code == 410
119+
120+
# Click count should not have changed
121+
db.refresh(short_url)
122+
assert short_url.click_count == initial_count

0 commit comments

Comments
 (0)