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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ This repo contains the samples for [Keploy's](https://keploy.io) integration wit

5. [Flask-Redis](https://github.com/keploy/samples-python/tree/main/flask-redis) - This Flask-based application provides a book management system utilizing Redis for caching and storage. It supports adding, retrieving, updating, and deleting book records, with optimized search functionality and cache management for improved performance. The API endpoints ensure efficient data handling and quick access to book information.

6. [FastAPI-Mongo](www.github.com/keploy/samples-python/tree/main/fastapi-mongo) - This application is for a Quiz Platform that generated Quizzes using LLMs. This project demonstrates how to use Keploy API Test Generator to automatically generate API tests for a FastAPI application that interacts with a MongoDB database.

## Community Support ❤️

### 🤔 Questions?
Expand Down
4 changes: 4 additions & 0 deletions fastapi-mongo/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
**/__pycache__/**
**/__init__.py/**
__init__.py
Comment on lines +2 to +3
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The .gitignore pattern **/__init__.py/** on line 2 is incorrect. __init__.py is a file, not a directory, so the trailing /** doesn't make sense. If the intention is to ignore all __init__.py files (which is unusual and likely incorrect), it should be **/__init__.py. However, typically __init__.py files should be tracked in version control as they're needed for Python packages.

Suggested change
**/__init__.py/**
__init__.py

Copilot uses AI. Check for mistakes.
.DS_Store
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing environment file in .gitignore. The application uses a .env file for configuration (as seen in core/config.py), but .env is not listed in .gitignore. This could lead to accidental commits of sensitive credentials like SECRET_KEY, MONGODB_URI, and API keys.

Suggested change
.DS_Store
.DS_Store
.env

Copilot uses AI. Check for mistakes.
32 changes: 32 additions & 0 deletions fastapi-mongo/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# 1. Use a lightweight Python 3.10 image
FROM python:3.10-slim

# 2. Set environment variables
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1

# 3. Set working directory
WORKDIR /app

# 4. Install system dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
gcc \
libpq-dev \
curl \
&& rm -rf /var/lib/apt/lists/*

# 5. Copy requirements.txt from the root folder
COPY ../requirements.txt .
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The COPY instruction path is incorrect. The path ../requirements.txt assumes the requirements.txt is in the parent directory, but Docker COPY doesn't support parent directory references with .. in the same way. This will likely fail during the Docker build. The COPY command should reference the file relative to the build context.

Suggested change
COPY ../requirements.txt .
COPY requirements.txt .

Copilot uses AI. Check for mistakes.

# 6. Install Python dependencies
RUN pip install --upgrade pip && pip install -r requirements.txt

# 7. Copy everything from quizzly folder (backend code)
COPY . .

# 8. Expose port
EXPOSE 8000

# 9. Run FastAPI app with uvicorn
CMD ["uvicorn", "quizzly.main:app", "--host", "0.0.0.0", "--port", "8000"]
20 changes: 20 additions & 0 deletions fastapi-mongo/auth/jwt_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from jose import jwt
from datetime import datetime, timedelta
from dotenv import load_dotenv
import os

load_dotenv()

SECRET_KEY = os.getenv("SECRET_KEY")
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Security issue: This file loads environment variables directly using os.getenv(), while the rest of the application uses pydantic_settings for configuration management (as seen in core/config.py). Using os.getenv() bypasses validation and could result in the SECRET_KEY being None if not set, which would cause runtime errors. This inconsistency also makes the codebase harder to maintain.

Suggested change
SECRET_KEY = os.getenv("SECRET_KEY")
SECRET_KEY = os.getenv("SECRET_KEY")
if not SECRET_KEY:
raise RuntimeError("SECRET_KEY environment variable must be set and non-empty")

Copilot uses AI. Check for mistakes.
ALGORITHM = "HS256"


def create_access_token(data: dict, expires_delta: timedelta = timedelta(days=1)):
to_encode = data.copy()
expire = datetime.utcnow() + expires_delta
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The datetime.utcnow() method is deprecated as of Python 3.12. Replace with datetime.now(timezone.utc) for timezone-aware datetime handling.

Copilot uses AI. Check for mistakes.
to_encode.update({"exp": expire})
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)


def decode_access_token(token: str):
return jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
Comment on lines +1 to +20
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file appears to be duplicate JWT handling code. The utils/jwt.py file already provides JWT token creation and verification functionality. Having two implementations of the same functionality can lead to inconsistencies and maintenance issues. Consider removing this file and using the utils/jwt.py implementation throughout the application.

Suggested change
from jose import jwt
from datetime import datetime, timedelta
from dotenv import load_dotenv
import os
load_dotenv()
SECRET_KEY = os.getenv("SECRET_KEY")
ALGORITHM = "HS256"
def create_access_token(data: dict, expires_delta: timedelta = timedelta(days=1)):
to_encode = data.copy()
expire = datetime.utcnow() + expires_delta
to_encode.update({"exp": expire})
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
def decode_access_token(token: str):
return jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
from datetime import timedelta
from utils.jwt import (
create_access_token as _create_access_token,
decode_access_token as _decode_access_token,
)
def create_access_token(data: dict, expires_delta: timedelta = timedelta(days=1)):
"""
Delegate access token creation to the shared utils.jwt implementation.
"""
return _create_access_token(data, expires_delta)
def decode_access_token(token: str):
"""
Delegate access token decoding to the shared utils.jwt implementation.
"""
return _decode_access_token(token)

Copilot uses AI. Check for mistakes.
17 changes: 17 additions & 0 deletions fastapi-mongo/core/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# app/core/config.py
from pydantic_settings import BaseSettings


class Settings(BaseSettings):
MONGODB_URI: str
SECRET_KEY: str
FRONTEND_URL: str
GEMINI_API_KEY: str
YOUTUBE_API_KEY: str
CORS_ORIGINS: str

class Config:
env_file = ".env"


settings = Settings()
6 changes: 6 additions & 0 deletions fastapi-mongo/db/mongodb.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# app/db/mongodb.py
from motor.motor_asyncio import AsyncIOMotorClient
from quizzly.core.config import settings

client = AsyncIOMotorClient(settings.MONGODB_URI)
db = client.quiz_app
9 changes: 9 additions & 0 deletions fastapi-mongo/keploy-api-test-generator/README.md
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you please move the relevant content from this folder to the new readme file to be created at the project root ?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AkashKumar7902 Under this root README?

Screenshot 2026-01-27 at 8 36 34 PM

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
### Keploy API Test Generator for FastAPI with MongoDB

This project demonstrates how to use Keploy to automatically generate API tests for a FastAPI application that interacts with a MongoDB database. Keploy captures the API requests and responses, allowing you to create comprehensive test cases effortlessly.

Keploy API Test Generator: https://keploy.io/docs/running-keploy/api-test-generator/

### About the Project

This is FastAPI + MongoDB backend written for a Quiz Application. The application allows users to perform CRUD operations on quiz data stored in a MongoDB database.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
28 changes: 28 additions & 0 deletions fastapi-mongo/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# app/main.py
from fastapi import FastAPI
from quizzly.routes import parent, quiz, content
from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.cors import CORSMiddleware
from quizzly.routes import parent, quiz
from quizzly.core.config import settings


app = FastAPI()
# Read and parse CORS origins
origins_str = settings.CORS_ORIGINS
origins = [origin.strip() for origin in origins_str.split(",") if origin.strip()]

print(origins)

# Enable CORS
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)

app.include_router(parent.router, prefix="/parent", tags=["Parent"])
app.include_router(quiz.router, prefix="/quiz", tags=["Quiz"])
app.include_router(content.router, prefix="/content", tags=["Content"])
17 changes: 17 additions & 0 deletions fastapi-mongo/models/parent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# app/models/parent.py
from pydantic import BaseModel, Field
from typing import Optional
from bson import ObjectId


class ParentCreate(BaseModel):
username: str
password: str
name: Optional[str]


class Parent(BaseModel):
id: Optional[str] = Field(default_factory=lambda: str(ObjectId()), alias="_id")
name: str
username: str
password: str
82 changes: 82 additions & 0 deletions fastapi-mongo/models/quiz.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# app/models/quiz.py

from pydantic import BaseModel, Field, HttpUrl
from typing import List, Optional
from bson import ObjectId
from datetime import datetime
import pytz

IST = pytz.timezone("Asia/Kolkata")


class EndQuizRequest(BaseModel):
name: str


class QuizStart(BaseModel):
password: int
name: str


class AnswerSubmission(BaseModel):
name: str
answers: List[str]


# Question model to represent individual question details
class Question(BaseModel):
question: str
choice_A: str
choice_B: str
choice_C: str
choice_D: str
answer: str # Should be one of "A", "B", "C", or "D"
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The answer field lacks validation. It should be constrained to only accept values "A", "B", "C", or "D" to prevent invalid data. Consider using a Literal type or Field with regex validation.

Copilot uses AI. Check for mistakes.
is_correct: Optional[bool] = False


# QuizCreate model – input from client
class QuizCreate(BaseModel):
name: str
subject: str
num_questions: int
# trigger_link: HttpUrl
# questions: List[Question]
topic: str
difficulty_level: int
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing validation for the difficulty_level field. Based on line 32 in utils/gemini.py, difficulty_level should be between 1 and 10, but there's no validation to enforce this constraint in the model.

Copilot uses AI. Check for mistakes.


class QuestionUpdate(BaseModel):
question: str
choice_A: str
choice_B: str
choice_C: str
choice_D: str
answer: str
is_correct: bool


# Main Quiz model – includes fields auto-populated by backend
class Quiz(BaseModel):
id: str = Field(default_factory=lambda: str(ObjectId()), alias="_id")
name: str
subject: str
created_by: str
created_at: datetime = Field(default_factory=datetime.now(IST))
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The created_at field uses datetime.now(IST) as a default factory, but this is evaluated once at module load time, not at instance creation time. This should be Field(default_factory=lambda: datetime.now(IST)) to ensure each Quiz instance gets the current timestamp.

Suggested change
created_at: datetime = Field(default_factory=datetime.now(IST))
created_at: datetime = Field(default_factory=lambda: datetime.now(IST))

Copilot uses AI. Check for mistakes.
metadata_fields: Optional[dict] = Field(default_factory=dict)
trigger_link: HttpUrl
taken_by: List[str]
num_questions: int
questions: List[Question]
user_responses: List[dict]
is_started: bool = False
start_time: Optional[datetime] = None
end_time: Optional[datetime] = None
exec_time: Optional[float] = None # in seconds
topic: str
difficulty_level: int
password: int
is_executed: bool = False

class Config:
json_encoders = {ObjectId: str, datetime: lambda v: v.isoformat()}
populate_by_name = True
73 changes: 73 additions & 0 deletions fastapi-mongo/routes/content.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
from fastapi import APIRouter, HTTPException
from typing import List
import json
from googleapiclient.discovery import build
from duckduckgo_search import DDGS
from quizzly.core.config import settings
import os
from pydantic import BaseModel

router = APIRouter()

class TopicsRequest(BaseModel):
topics: List[str]
num_results: int = 5

@router.post("/articles")
async def get_articles(request: TopicsRequest):
"""
Get articles based on provided topics using DuckDuckGo search
"""
try:
results = {}
with DDGS() as ddgs:
for topic in request.topics:
search_results = ddgs.text(topic, max_results=request.num_results)
articles = []
for res in search_results:
title = res.get('title')
url = res.get('href')
if title and url:
articles.append({'title': title, 'url': url})
results[topic] = articles
return results
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))

@router.post("/youtube")
async def get_youtube_videos(request: TopicsRequest):
"""
Get YouTube videos with thumbnails based on provided topics
"""
if not settings.YOUTUBE_API_KEY:
raise HTTPException(status_code=500, detail="YouTube API key not configured")

try:
youtube = build('youtube', 'v3', developerKey=settings.YOUTUBE_API_KEY)
results = {}

for topic in request.topics:
search_response = youtube.search().list(
q=topic,
part='snippet',
type='video',
maxResults=request.num_results
).execute()

youtube_data = []
for item in search_response['items']:
video_title = item['snippet']['title']
video_url = f"https://www.youtube.com/watch?v={item['id']['videoId']}"
video_id = item['id']['videoId']
thumbnail_url = item['snippet']['thumbnails']['high']['url']
youtube_data.append({
'title': video_title,
'video_url': video_url,
'thumbnail_url': thumbnail_url
})

results[topic] = youtube_data

return results
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
36 changes: 36 additions & 0 deletions fastapi-mongo/routes/parent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# app/routes/parent.py
import bcrypt
from fastapi import APIRouter, HTTPException
from quizzly.models.parent import Parent, ParentCreate
from quizzly.db.mongodb import db
from quizzly.utils.jwt import create_token
from pydantic import BaseModel


class LoginRequest(BaseModel):
username: str
password: str


router = APIRouter()


@router.post("/register")
async def register_parent(data: ParentCreate):
parent_data = Parent(**data.dict())
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing duplicate username check before registration. If a user tries to register with an existing username, the insert_one operation will fail without a proper error message. Add a check to verify the username doesn't already exist and return a meaningful error message.

Suggested change
parent_data = Parent(**data.dict())
parent_data = Parent(**data.dict())
# Check for existing user with the same username to provide a clear error
existing_parent = await db.parents.find_one({"username": parent_data.username})
if existing_parent:
raise HTTPException(status_code=400, detail="Username already exists")

Copilot uses AI. Check for mistakes.
parent_data.password = bcrypt.hashpw(parent_data.password.encode(), bcrypt.gensalt()).decode()
Comment on lines +19 to +21
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing input validation for the password field. The password is hashed but there's no validation for minimum length, complexity, or other security requirements before registration.

Copilot uses AI. Check for mistakes.
await db.parents.insert_one(parent_data.model_dump(by_alias=True))
parent = await db.parents.find_one({"username": parent_data.username})
token = create_token({"id": str(parent["_id"])})
return {"message": "Parent registered successfully", "access_token": token}


@router.post("/login")
async def login(data: LoginRequest):
parent = await db.parents.find_one({"username": data.username})
if not parent:
raise HTTPException(status_code=404, detail="User not found")
if not bcrypt.checkpw(data.password.encode(), parent["password"].encode()):
raise HTTPException(status_code=401, detail="Invalid password")
token = create_token({"id": str(parent["_id"])})
return {"access_token": token}
Loading