Merge pull request #1 from katariina-jarvenmaki/adding-functionality
Adding functionality
This commit is contained in:
+2
-1
@@ -1,2 +1,3 @@
|
||||
app/__pycache__
|
||||
venv/
|
||||
venv/
|
||||
test.txt
|
||||
@@ -12,11 +12,11 @@ source venv/bin/activate
|
||||
```bash
|
||||
uvicorn app.main:app --reload
|
||||
```
|
||||
Then check:
|
||||
http://127.0.0.1:8000
|
||||
Then check:<br>
|
||||
http://127.0.0.1:8000<br>
|
||||
http://127.0.0.1:8000/docs
|
||||
|
||||
Test Short URL Endpoint
|
||||
Test Short URL Endpoint:<br>
|
||||
http://127.0.0.1:8000/shorten
|
||||
|
||||
## Running a test
|
||||
|
||||
+72
-24
@@ -1,13 +1,18 @@
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi.responses import RedirectResponse
|
||||
from contextlib import asynccontextmanager
|
||||
from pydantic import BaseModel
|
||||
from app.utils import generate_short_code
|
||||
from dotenv import load_dotenv
|
||||
import os
|
||||
from pydantic import BaseModel, HttpUrl
|
||||
from pydantic_settings import BaseSettings
|
||||
import sqlite3
|
||||
import logging
|
||||
|
||||
class AppSettings(BaseSettings):
|
||||
base_url: str = "http://localhost:8000"
|
||||
database_url: str = "urls.db"
|
||||
|
||||
settings = AppSettings() # Load environment variables
|
||||
|
||||
# Set up logging configuration
|
||||
logging.basicConfig(level=logging.INFO,
|
||||
format="%(asctime)s - %(levelname)s - %(message)s")
|
||||
@@ -16,9 +21,18 @@ logger = logging.getLogger(__name__)
|
||||
# Database connection setup
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
conn = sqlite3.connect('urls.db')
|
||||
conn.execute('''CREATE TABLE IF NOT EXISTS urls
|
||||
(id INTEGER PRIMARY KEY, short_code TEXT, original_url TEXT, clicks INTEGER)''')
|
||||
conn = sqlite3.connect(settings.database_url)
|
||||
conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS urls (
|
||||
id INTEGER PRIMARY KEY,
|
||||
short_code TEXT UNIQUE,
|
||||
original_url TEXT NOT NULL,
|
||||
clicks INTEGER DEFAULT 0
|
||||
);
|
||||
""")
|
||||
conn.execute("""
|
||||
CREATE INDEX IF NOT EXISTS idx_short_code ON urls(short_code);
|
||||
""")
|
||||
conn.commit()
|
||||
logger.info("Database initialized or already exists.")
|
||||
yield
|
||||
@@ -28,11 +42,10 @@ async def lifespan(app: FastAPI):
|
||||
app = FastAPI(lifespan=lifespan)
|
||||
|
||||
def get_db_connection():
|
||||
conn = sqlite3.connect('urls.db')
|
||||
return conn
|
||||
return sqlite3.connect(settings.database_url, check_same_thread=False)
|
||||
|
||||
class URLRequest(BaseModel):
|
||||
url: str
|
||||
url: HttpUrl
|
||||
|
||||
@app.get("/")
|
||||
def home():
|
||||
@@ -41,36 +54,71 @@ def home():
|
||||
|
||||
@app.post("/shorten")
|
||||
def shorten_url(request: URLRequest):
|
||||
logger.info(f"Request received to shorten URL: {request.url}")
|
||||
short_code = generate_short_code()
|
||||
conn = get_db_connection()
|
||||
|
||||
while conn.execute("SELECT 1 FROM urls WHERE short_code = ?", (short_code,)).fetchone():
|
||||
existing = conn.execute(
|
||||
"SELECT short_code FROM urls WHERE original_url = ?",
|
||||
(str(request.url),)
|
||||
).fetchone()
|
||||
|
||||
if existing:
|
||||
conn.close()
|
||||
return {"short_url": f"{settings.base_url}/{existing[0]}"}
|
||||
|
||||
short_code = generate_short_code()
|
||||
while conn.execute(
|
||||
"SELECT 1 FROM urls WHERE short_code = ?",
|
||||
(short_code,)
|
||||
).fetchone():
|
||||
short_code = generate_short_code()
|
||||
|
||||
conn.execute("INSERT INTO urls (short_code, original_url, clicks) VALUES (?, ?, ?)",
|
||||
(short_code, request.url, 0))
|
||||
conn.execute(
|
||||
"INSERT INTO urls (short_code, original_url, clicks) VALUES (?, ?, 0)",
|
||||
(short_code, str(request.url))
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
short_url = f"{os.getenv('BASE_URL', 'http://localhost:8000')}/{short_code}"
|
||||
logger.info(f"Shortened URL created: {short_url}")
|
||||
return {"short_url": short_url}
|
||||
return {"short_url": f"{settings.base_url}/{short_code}"}
|
||||
|
||||
@app.get("/{short_code}")
|
||||
def redirect_to_url(short_code: str):
|
||||
logger.info(f"Redirect request received for short_code: {short_code}")
|
||||
conn = get_db_connection()
|
||||
url_data = conn.execute("SELECT original_url, clicks FROM urls WHERE short_code = ?", (short_code,)).fetchone()
|
||||
url_data = conn.execute(
|
||||
"SELECT original_url FROM urls WHERE short_code = ?",
|
||||
(short_code,)
|
||||
).fetchone()
|
||||
|
||||
if url_data is None:
|
||||
logger.warning(f"Shortened URL for {short_code} not found.")
|
||||
conn.close()
|
||||
raise HTTPException(status_code=404, detail="Shortened URL not found")
|
||||
|
||||
original_url, clicks = url_data
|
||||
conn.execute("UPDATE urls SET clicks = ? WHERE short_code = ?", (clicks + 1, short_code))
|
||||
original_url = url_data[0]
|
||||
|
||||
conn.execute(
|
||||
"UPDATE urls SET clicks = clicks + 1 WHERE short_code = ?",
|
||||
(short_code,)
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
logger.info(f"Redirecting to {original_url}. Total clicks: {clicks + 1}")
|
||||
return RedirectResponse(url=original_url)
|
||||
return RedirectResponse(url=original_url)
|
||||
|
||||
@app.get("/stats/{short_code}")
|
||||
def get_url_stats(short_code: str):
|
||||
logger.info(f"Stats request received for short_code: {short_code}")
|
||||
conn = get_db_connection()
|
||||
url_data = conn.execute("SELECT original_url, clicks FROM urls WHERE short_code = ?", (short_code,)).fetchone()
|
||||
|
||||
if url_data is None:
|
||||
logger.warning(f"Stats not found for {short_code}.")
|
||||
conn.close()
|
||||
raise HTTPException(status_code=404, detail="URL stats not found")
|
||||
|
||||
original_url, clicks = url_data
|
||||
|
||||
conn.close()
|
||||
|
||||
logger.info(f"Returning stats for {original_url}. Total clicks: {clicks}")
|
||||
|
||||
return {"original_url": original_url, "clicks": clicks}
|
||||
@@ -0,0 +1,7 @@
|
||||
# app/settings.py
|
||||
|
||||
import os
|
||||
|
||||
# Example configuration
|
||||
database_url = os.getenv("DATABASE_URL", "sqlite:///default.db")
|
||||
debug = os.getenv("DEBUG", "True") == "True"
|
||||
+51
-7
@@ -3,20 +3,64 @@ import sys
|
||||
import os
|
||||
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
||||
|
||||
from app.main import app
|
||||
from app import settings
|
||||
import tempfile
|
||||
import pytest
|
||||
|
||||
client = TestClient(app)
|
||||
@pytest.fixture
|
||||
def test_client():
|
||||
with tempfile.NamedTemporaryFile() as tmp:
|
||||
settings.database_url = tmp.name
|
||||
with TestClient(app) as c:
|
||||
yield c
|
||||
|
||||
def test_home():
|
||||
response = client.get("/")
|
||||
@pytest.fixture
|
||||
def use_temp_db():
|
||||
with tempfile.NamedTemporaryFile() as tmp:
|
||||
settings.database_url = tmp.name
|
||||
yield
|
||||
|
||||
def test_home(test_client):
|
||||
response = test_client.get("/")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"message": "URL Shortener API"}
|
||||
|
||||
def test_shorten_url():
|
||||
response = client.post("/shorten", json={"url": "https://google.com"})
|
||||
def test_shorten_url(test_client):
|
||||
response = test_client.post("/shorten", json={"url": "https://google.com"})
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "short_url" in data
|
||||
short_url = data["short_url"]
|
||||
assert short_url.startswith("http://localhost:8000/")
|
||||
assert short_url.startswith("http://localhost:8000/")
|
||||
|
||||
def test_redirect(test_client):
|
||||
response = test_client.post("/shorten", json={"url": "https://google.com"})
|
||||
short_url = response.json()["short_url"]
|
||||
code = short_url.split("/")[-1]
|
||||
|
||||
redirect = test_client.get(f"/{code}", follow_redirects=False)
|
||||
assert redirect.status_code == 307
|
||||
|
||||
def test_duplicate_url_returns_same_code(test_client):
|
||||
r1 = test_client.post("/shorten", json={"url": "https://example.com"})
|
||||
r2 = test_client.post("/shorten", json={"url": "https://example.com"})
|
||||
|
||||
assert r1.status_code == 200
|
||||
assert r2.status_code == 200
|
||||
assert r1.json()["short_url"] == r2.json()["short_url"]
|
||||
|
||||
def test_stats_endpoint(test_client):
|
||||
response = test_client.post("/shorten", json={"url": "https://stats-test.com"})
|
||||
code = response.json()["short_url"].split("/")[-1]
|
||||
|
||||
test_client.get(f"/{code}", follow_redirects=False)
|
||||
|
||||
stats = test_client.get(f"/stats/{code}")
|
||||
assert stats.status_code == 200
|
||||
data = stats.json()
|
||||
assert data["clicks"] == 1
|
||||
|
||||
def test_redirect_404(test_client):
|
||||
response = test_client.get("/nonexistent", follow_redirects=False)
|
||||
assert response.status_code == 404
|
||||
Reference in New Issue
Block a user