diff --git a/.copier/config.yaml b/.copier/config.yaml index d9f108f..7b8d177 100644 --- a/.copier/config.yaml +++ b/.copier/config.yaml @@ -43,6 +43,7 @@ workflows: - move: { from: "mflix/client", to: "client" } - move: { from: "mflix/server/java-spring", to: "server" } - copy: { from: "mflix/README-JAVA-SPRING.md", to: "README.md" } + - copy: { from: "mflix/check-requirements-java.sh", to: "check-requirements.sh" } - copy: { from: "mflix/.gitignore-java", to: ".gitignore" } commit_strategy: pr_title: "Update MFlix application from docs-sample-apps" @@ -67,7 +68,8 @@ workflows: transformations: - move: { from: "mflix/client", to: "client" } - move: { from: "mflix/server/js-express", to: "server" } - - copy: { from: "mflix/README-JAVASCRIPT-EXPRESS.md", to: "README.md" } + - copy: { from: "mflix/README-NODE-EXPRESS.md", to: "README.md" } + - copy: { from: "mflix/check-requirements-js.sh", to: "check-requirements.sh" } - copy: { from: "mflix/.gitignore-js", to: ".gitignore" } commit_strategy: pr_title: "Update MFlix application from docs-sample-apps" @@ -93,6 +95,7 @@ workflows: - move: { from: "mflix/client", to: "client" } - move: { from: "mflix/server/python-fastapi", to: "server" } - copy: { from: "mflix/README-PYTHON-FASTAPI.md", to: "README.md" } + - copy: { from: "mflix/check-requirements-python.sh", to: "check-requirements.sh" } - copy: { from: "mflix/.gitignore-python", to: ".gitignore" } commit_strategy: pr_title: "Update MFlix application from docs-sample-apps" diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index bf7249c..d98787f 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -5,30 +5,38 @@ ## Sample App Information **Which sample app are you using?** + + - [ ] Java (Spring Boot) -- [ ] JavaScript (Express.js) +- [ ] Node (Express.js) - [ ] Python (FastAPI) ## Environment Details **MongoDB Database Version:** + **MongoDB Driver Version:** + **Deployment Type:** + + - [ ] Local MongoDB instance - [ ] MongoDB Atlas (cloud) - [ ] Docker container - [ ] Other (please specify): **Operating System:** + **Runtime Version:** + ## Steps to Reproduce @@ -73,4 +81,3 @@ Before submitting this issue, please confirm: - [ ] I have verified my MongoDB connection string is correct - [ ] I have installed all required dependencies - [ ] I have searched existing issues to avoid duplicates - diff --git a/.github/workflows/run-python-tests.yml b/.github/workflows/run-python-tests.yml index 4a24fe6..4568fa9 100644 --- a/.github/workflows/run-python-tests.yml +++ b/.github/workflows/run-python-tests.yml @@ -107,15 +107,13 @@ jobs: working-directory: mflix/server/python-fastapi run: pytest -m unit --verbose --tb=short --junit-xml=test-results-unit.xml env: - MONGO_URI: mongodb://localhost:27017 - MONGO_DB: sample_mflix + MONGODB_URI: mongodb://localhost:27017 - name: Run integration tests working-directory: mflix/server/python-fastapi run: pytest -m integration --verbose --tb=short --junit-xml=test-results-integration.xml env: - MONGO_URI: mongodb://localhost:27017/?directConnection=true - MONGO_DB: sample_mflix + MONGODB_URI: mongodb://localhost:27017/?directConnection=true - name: Upload test results uses: actions/upload-artifact@v4 diff --git a/README.md b/README.md index 506405f..1f44dc9 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ The sample app provides a Next.js frontend in the `client` directory, with the choice of three backend stacks in the `server` directory: - Java: Spring Boot -- JavaScript: Express.js +- Node.js: Express.js - Python: FastAPI ``` @@ -34,7 +34,7 @@ choice of three backend stacks in the `server` directory: This repository serves as the source for the following artifact repositories: - Java: [mongodb/sample-app-java-mflix](https://github.com/mongodb/sample-app-java-mflix) -- JavaScript: [mongodb/sample-app-nodejs-mflix](https://github.com/mongodb/sample-app-nodejs-mflix) +- Node.js: [mongodb/sample-app-nodejs-mflix](https://github.com/mongodb/sample-app-nodejs-mflix) - Python: [mongodb/sample-app-python-mflix](https://github.com/mongodb/sample-app-python-mflix) ## Development @@ -46,9 +46,9 @@ to a target repository for each sample app. For configuration details, refer to ### Branching Model For development, work from the `development` branch. Make incremental PRs -containing new features and bug fixes to `development`, *not* `main`. +containing new features and bug fixes to `development`, _not_ `main`. -When all development work is complete, *then* create a release PR from +When all development work is complete, _then_ create a release PR from `development` to `main`. Upon merging to `main,` the copier tool runs automatically. It creates a new PR in the target repository, which must be tested and merged manually. @@ -91,7 +91,7 @@ To test and verify the PR, navigate to the target repository - see - [ ] Run the tests - [ ] Run the application and verify that it functions as expected. - [ ] Review the `deprecated_examples.json` file for any files that need to be - deleted. If files are deleted: + deleted. If files are deleted: - [ ] Add a commit to the copier PR to delete the files from the target repository. - [ ] Merge the PR. @@ -100,7 +100,7 @@ To test and verify the PR, navigate to the target repository - see If you are a developer having issues with the sample app, feel free to open an issue in this repository. Please include the following information: -- [ ] The sample app you are using (Java, JavaScript, or Python) +- [ ] The sample app you are using (Java, Node.js, or Python) - [ ] The version of the MongoDB database you are using - [ ] The version of the MongoDB driver you are using - [ ] What type of deployment you're using (local, Atlas, etc.) diff --git a/mflix/README-JAVA-SPRING.md b/mflix/README-JAVA-SPRING.md index 245afc5..d561d10 100644 --- a/mflix/README-JAVA-SPRING.md +++ b/mflix/README-JAVA-SPRING.md @@ -28,6 +28,16 @@ The `sample_mflix` dataset contains movies released up to **2016**. Searching fo - **Voyage AI API key** (For MongoDB Vector Search) - [Get a Voyage AI API key](https://www.voyageai.com/) +## Verify Requirements + +Before getting started, run the verification script to check if you have the required runtime: + +```bash +./check-requirements-java.sh --pre +``` + +This checks that Java and JAVA_HOME are configured correctly. Run with `--help` for more options. + ## Getting Started ### 1. Configure the Backend @@ -48,28 +58,19 @@ Edit the `.env` file and set your MongoDB connection string: ```env # MongoDB Connection -# Replace with your MongoDB Atlas connection string or local MongoDB URI MONGODB_URI=mongodb+srv://:@.mongodb.net/sample_mflix?retryWrites=true&w=majority -# Voyage AI Configuration -# API key for Voyage AI embedding model (required for Vector Search) +# Voyage AI Configuration (optional - required for Vector Search) VOYAGE_API_KEY=your_voyage_api_key # Server Configuration -# Port on which the Spring Boot application will run PORT=3001 # CORS Configuration -# Allowed origin for cross-origin requests (frontend URL) -# For multiple origins, separate with commas -CORS_ORIGIN=http://localhost:3000 - -# Optional: Enable MongoDB Search tests -# Uncomment the following line to enable Search tests -# ENABLE_SEARCH_TESTS=true +CORS_ORIGINS=http://localhost:3000 ``` -**Note:** Replace `username`, `password`, and `cluster` with your +**Note:** Replace ``, ``, and `` with your actual MongoDB Atlas credentials. Replace `your_voyage_api_key` with your key. @@ -190,6 +191,16 @@ cd client npm run lint ``` +## Verify Setup + +After completing the setup, run the full verification to ensure everything is configured correctly: + +```bash +./check-requirements-java.sh +``` + +This checks your Java environment, Maven dependencies, `.env` configuration, and frontend setup. + ## Issues If you have problems running the sample app, please check the following: diff --git a/mflix/README-JAVASCRIPT-EXPRESS.md b/mflix/README-NODE-EXPRESS.md similarity index 89% rename from mflix/README-JAVASCRIPT-EXPRESS.md rename to mflix/README-NODE-EXPRESS.md index f3c531c..d7c013a 100644 --- a/mflix/README-JAVASCRIPT-EXPRESS.md +++ b/mflix/README-NODE-EXPRESS.md @@ -1,4 +1,4 @@ -# JavaScript Express.js MongoDB Sample MFlix Application +# Node Express.js MongoDB Sample MFlix Application This is a full-stack movie browsing application built with Express.js and Next.js, demonstrating MongoDB operations using the `sample_mflix` dataset. The application showcases CRUD operations, aggregations, and MongoDB Search using the native MongoDB Node.js driver. @@ -27,6 +27,16 @@ The `sample_mflix` dataset contains movies released up to **2016**. Searching fo - **Voyage AI API key** (For MongoDB Vector Search) - [Get a Voyage AI API key](https://www.voyageai.com/) +## Verify Requirements + +Before getting started, run the verification script to check if you have the required runtime: + +```bash +./check-requirements-js.sh --pre +``` + +This checks that Node.js and npm are installed with the correct versions. Run with `--help` for more options. + ## Getting Started ### 1. Configure the Backend @@ -61,7 +71,7 @@ NODE_ENV=development # CORS Configuration # Allowed origin for cross-origin requests (frontend URL) # For multiple origins, separate with commas -CORS_ORIGIN=http://localhost:3000 +CORS_ORIGINS=http://localhost:3000 # Optional: Enable MongoDB Search tests # Uncomment the following line to enable Search tests @@ -89,7 +99,6 @@ From the `server` directory, run: npm run dev ``` - Or for production mode, run: ```bash @@ -98,6 +107,7 @@ npm start ``` The server will start on `http://localhost:3001`. You can verify it's running by visiting: + - API root: http://localhost:3001/ - API documentation (Swagger UI): http://localhost:3001/api-docs @@ -126,6 +136,7 @@ The Next.js application will start on `http://localhost:3000`. ### 5. Access the Application Open your browser and navigate to: + - **Frontend:** http://localhost:3000 - **Backend API:** http://localhost:3001 - **API Documentation:** http://localhost:3001/api-docs @@ -147,6 +158,7 @@ Open your browser and navigate to: ### Backend Development The Express.js backend uses: + - **Express.js 5** for REST API - **MongoDB Node.js Driver** for database operations - **TypeScript** for type safety @@ -170,6 +182,7 @@ npm run test:coverage ### Frontend Development The Next.js frontend uses: + - **React 19** with TypeScript - **Next.js 16** with App Router - **Turbopack** for fast development builds @@ -196,6 +209,7 @@ npm start # Starts production server ``` The production build: + - Minifies and optimizes JavaScript and CSS - Optimizes images and assets - Generates static pages where possible @@ -210,6 +224,16 @@ cd client npm run lint ``` +## Verify Setup + +After completing the setup, run the full verification to ensure everything is configured correctly: + +```bash +./check-requirements-js.sh +``` + +This checks your Node.js environment, npm dependencies, `.env` configuration, and frontend setup. + ## Issues If you have problems running the sample app, please check the following: diff --git a/mflix/README-PYTHON-FASTAPI.md b/mflix/README-PYTHON-FASTAPI.md index 3d79f3b..38168be 100644 --- a/mflix/README-PYTHON-FASTAPI.md +++ b/mflix/README-PYTHON-FASTAPI.md @@ -31,6 +31,16 @@ The `sample_mflix` dataset contains movies released up to **2016**. Searching fo - **Voyage AI API key** (For MongoDB Vector Search) - [Get a Voyage AI API key](https://www.voyageai.com/) +## Verify Requirements + +Before getting started, run the verification script to check if you have the required runtime: + +```bash +./check-requirements-python.sh --pre +``` + +This checks that Python and pip are installed with the correct versions. Run with `--help` for more options. + ## Getting Started ### 1. Configure the Backend @@ -50,21 +60,21 @@ cp .env.example .env Edit the `.env` file and set your MongoDB connection string: ```env -# MongoDB Configuration -MONGO_URI=mongodb+srv://:@.mongodb.net/sample_mflix?retryWrites=true&w=majority -MONGO_DB=sample_mflix +# MongoDB Connection +MONGODB_URI=mongodb+srv://:@.mongodb.net/sample_mflix?retryWrites=true&w=majority -# Voyage AI Configuration -# API key for Voyage AI embedding model (required for Vector Search) +# Voyage AI Configuration (optional - required for Vector Search) VOYAGE_API_KEY=your_voyage_api_key +# Server Configuration +PORT=3001 + # CORS Configuration -# Comma-separated list of allowed origins for CORS -CORS_ORIGINS=http://localhost:3000,http://localhost:3001 +CORS_ORIGINS=http://localhost:3000 ``` -**Note:** Replace `username`, `password`, and `cluster` with your actual MongoDB Atlas -credentials. +**Note:** Replace ``, ``, and `` with your actual MongoDB Atlas +credentials. Replace `your_voyage_api_key` with your key. Make a virtual environment: @@ -196,6 +206,16 @@ cd client npm run lint ``` +## Verify Setup + +After completing the setup, run the full verification to ensure everything is configured correctly: + +```bash +./check-requirements-python.sh +``` + +This checks your Python environment, dependencies, `.env` configuration, and frontend setup. + ## Issues If you have problems running the sample app, please check the following: diff --git a/mflix/check-requirements-java.sh b/mflix/check-requirements-java.sh new file mode 100755 index 0000000..001079a --- /dev/null +++ b/mflix/check-requirements-java.sh @@ -0,0 +1,481 @@ +#!/bin/bash +# ============================================================================= +# Requirements Verification Script for mflix Sample Application +# Java/Spring Boot Backend +# ============================================================================= +# +# This script checks that all necessary requirements are installed to run +# the mflix sample application with the Java/Spring Boot backend. +# +# Usage: +# ./check-requirements-java.sh # Check all requirements (post-setup) +# ./check-requirements-java.sh --pre # Check only runtime requirements (pre-setup) +# ./check-requirements-java.sh --setup # Check and auto-setup missing items +# ./check-requirements-java.sh --help # Show help message +# +# ============================================================================= + +# Exit on error (but handle arithmetic expressions carefully) +set -e + +# ============================================================================= +# Configuration +# ============================================================================= + +SERVER_DIR="server" +CLIENT_DIR="client" +JAVA_MIN_VERSION="21" +NODE_MIN_VERSION="18" + +# ============================================================================= +# Colors +# ============================================================================= + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# ============================================================================= +# Counters +# ============================================================================= + +CHECKS_PASSED=0 +CHECKS_FAILED=0 +CHECKS_WARNED=0 + +# ============================================================================= +# Helper Functions +# ============================================================================= + +print_header() { + echo "" + echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + echo -e "${BLUE} $1${NC}" + echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" +} + +print_section() { + echo "" + echo -e "${YELLOW}▸ $1${NC}" +} + +check_pass() { + echo -e " ${GREEN}✓${NC} $1" + CHECKS_PASSED=$((CHECKS_PASSED + 1)) +} + +check_fail() { + echo -e " ${RED}✗${NC} $1" + CHECKS_FAILED=$((CHECKS_FAILED + 1)) +} + +check_warn() { + echo -e " ${YELLOW}⚠${NC} $1" + CHECKS_WARNED=$((CHECKS_WARNED + 1)) +} + +check_info() { + echo -e " ${BLUE}→${NC} $1" +} + +command_exists() { + command -v "$1" &>/dev/null +} + +version_gte() { + # Returns 0 (true) if $1 >= $2 (numeric comparison) + [[ "$1" -ge "$2" ]] 2>/dev/null +} + +show_help() { + echo "Usage: $0 [OPTIONS]" + echo "" + echo "Options:" + echo " --pre Check only runtime requirements (use before setup)" + echo " --setup Attempt to automatically set up missing requirements" + echo " --help Show this help message" + echo "" + echo "This script checks that all necessary requirements are installed" + echo "to run the mflix sample application with the Java/Spring Boot backend." + echo "" + echo "Use --pre before starting setup to verify you have the required runtime." + echo "Use without flags after completing setup to verify everything is ready." + exit 0 +} + +# ============================================================================= +# Check Runtime Requirements (Pre-Setup) +# ============================================================================= + +check_runtime_requirements() { + print_section "Runtime Requirements" + + # Check Java version + if command_exists java; then + local java_version + java_version=$(java -version 2>&1 | head -1 | grep -oE '"[0-9]+' | tr -d '"') + if version_gte "$java_version" "$JAVA_MIN_VERSION"; then + check_pass "Java $java_version installed (>= $JAVA_MIN_VERSION required)" + else + check_fail "Java $java_version installed but >= $JAVA_MIN_VERSION required" + check_info "Install Java $JAVA_MIN_VERSION+: https://adoptium.net/" + fi + else + check_fail "Java not installed" + check_info "Install Java $JAVA_MIN_VERSION+: https://adoptium.net/" + fi + + # Check JAVA_HOME + if [[ -n "$JAVA_HOME" ]]; then + check_pass "JAVA_HOME is set: $JAVA_HOME" + else + check_warn "JAVA_HOME is not set" + check_info "Set JAVA_HOME to your Java installation directory" + fi +} + +# ============================================================================= +# Check Java/Spring Boot Backend Requirements (Full) +# ============================================================================= + +check_backend_requirements() { + print_section "Java/Spring Boot Backend Requirements" + + local server_dir="$SCRIPT_DIR/$SERVER_DIR" + + # Check Java version + if command_exists java; then + local java_version + java_version=$(java -version 2>&1 | head -n1 | sed -E 's/.*version "([0-9]+).*/\1/') + if [[ -n "$java_version" ]] && version_gte "$java_version" "$JAVA_MIN_VERSION"; then + check_pass "Java $java_version installed (>= $JAVA_MIN_VERSION required)" + else + check_fail "Java $java_version installed but >= $JAVA_MIN_VERSION required" + check_info "Install Java $JAVA_MIN_VERSION+ from https://adoptium.net/" + fi + else + check_fail "Java not installed" + check_info "Install Java $JAVA_MIN_VERSION+ from https://adoptium.net/" + return + fi + + # Check JAVA_HOME + if [[ -n "$JAVA_HOME" ]]; then + if [[ -d "$JAVA_HOME" ]]; then + check_pass "JAVA_HOME is set: $JAVA_HOME" + else + check_warn "JAVA_HOME is set but directory doesn't exist: $JAVA_HOME" + fi + else + check_warn "JAVA_HOME is not set (may cause issues with some tools)" + check_info "Set JAVA_HOME to your Java installation directory" + fi + + # Check Maven wrapper + if [[ -f "$server_dir/mvnw" ]]; then + check_pass "Maven wrapper (mvnw) found" + + # Check if mvnw is executable + if [[ -x "$server_dir/mvnw" ]]; then + check_pass "Maven wrapper is executable" + else + check_warn "Maven wrapper is not executable" + if [[ "$SETUP_MODE" == true ]]; then + chmod +x "$server_dir/mvnw" + check_pass "Made Maven wrapper executable" + else + check_info "Run: chmod +x $SERVER_DIR/mvnw" + fi + fi + + # Try to get Maven version + local maven_version + maven_version=$(cd "$server_dir" && ./mvnw --version 2>/dev/null | grep "Apache Maven" | awk '{print $3}') + if [[ -n "$maven_version" ]]; then + check_pass "Maven version: $maven_version" + fi + else + check_fail "Maven wrapper (mvnw) not found in $SERVER_DIR" + check_info "The Maven wrapper should be included in the repository" + fi + + # Check if Maven dependencies are downloaded + if [[ -d "$server_dir/target" ]]; then + check_pass "Maven target directory exists (dependencies likely downloaded)" + else + check_warn "Maven target directory not found" + if [[ "$SETUP_MODE" == true ]]; then + check_info "Downloading Maven dependencies..." + if (cd "$server_dir" && ./mvnw dependency:resolve -q); then + check_pass "Maven dependencies downloaded successfully" + else + check_fail "Failed to download Maven dependencies" + fi + else + check_info "Run: cd $SERVER_DIR && ./mvnw dependency:resolve" + fi + fi + + # Check if project compiles + if [[ -d "$server_dir/target/classes" ]]; then + check_pass "Project appears to be compiled" + else + check_warn "Project not compiled yet" + if [[ "$SETUP_MODE" == true ]]; then + check_info "Compiling project..." + if (cd "$server_dir" && ./mvnw compile -q); then + check_pass "Project compiled successfully" + else + check_fail "Failed to compile project" + fi + else + check_info "Run: cd $SERVER_DIR && ./mvnw compile" + fi + fi +} + +# ============================================================================= +# Check Environment Configuration +# ============================================================================= + +check_env_configuration() { + print_section "Environment Configuration" + + local server_dir="$SCRIPT_DIR/$SERVER_DIR" + local env_file="$server_dir/.env" + local env_example="$server_dir/.env.example" + + # Check .env file + if [[ -f "$env_file" ]]; then + check_pass ".env file exists" + + # Check MONGODB_URI + if grep -q "^MONGODB_URI=" "$env_file" 2>/dev/null; then + local mongo_uri + mongo_uri=$(grep "^MONGODB_URI=" "$env_file" | cut -d'=' -f2-) + if [[ -n "$mongo_uri" ]] && [[ "$mongo_uri" != *"<"*">"* ]]; then + check_pass "MONGODB_URI is configured" + else + check_fail "MONGODB_URI is not configured (still has placeholder value)" + check_info "Update MONGODB_URI in $SERVER_DIR/.env with your MongoDB connection string" + fi + else + check_fail "MONGODB_URI not found in .env" + check_info "Add MONGODB_URI to $SERVER_DIR/.env" + fi + + # Check VOYAGE_API_KEY (optional) + if grep -q "^VOYAGE_API_KEY=" "$env_file" 2>/dev/null; then + local voyage_key + voyage_key=$(grep "^VOYAGE_API_KEY=" "$env_file" | cut -d'=' -f2-) + if [[ -n "$voyage_key" ]] && [[ "$voyage_key" != "your_voyage_api_key" ]]; then + check_pass "VOYAGE_API_KEY is configured" + else + check_info "VOYAGE_API_KEY not configured (optional - needed for vector search)" + fi + else + check_info "VOYAGE_API_KEY not set (optional - needed for vector search)" + fi + + # Check CORS_ORIGINS (optional) + if grep -q "^CORS_ORIGINS=" "$env_file" 2>/dev/null; then + check_pass "CORS_ORIGINS is configured" + else + check_info "CORS_ORIGINS not set (will use default: http://localhost:3000)" + fi + + # Check PORT (optional) + if grep -q "^PORT=" "$env_file" 2>/dev/null; then + check_pass "PORT is configured" + else + check_info "PORT not set (will use default: 3001)" + fi + else + check_warn ".env file not found" + if [[ -f "$env_example" ]]; then + if [[ "$SETUP_MODE" == true ]]; then + check_info "Creating .env from .env.example..." + if cp "$env_example" "$env_file"; then + check_pass ".env file created from .env.example" + check_warn "Please update the placeholder values in $SERVER_DIR/.env" + else + check_fail "Failed to create .env file" + fi + else + check_info "Copy .env.example to .env: cp $SERVER_DIR/.env.example $SERVER_DIR/.env" + fi + else + check_fail "No .env.example found to use as template" + fi + fi +} + +# ============================================================================= +# Check Frontend Requirements +# ============================================================================= + +check_frontend_requirements() { + print_section "Frontend Requirements (Next.js)" + + local client_dir="$SCRIPT_DIR/$CLIENT_DIR" + + # Check Node.js + if command_exists node; then + local node_version + node_version=$(node --version | sed 's/v//') + local node_major + node_major=$(echo "$node_version" | cut -d. -f1) + if [[ "$node_major" -ge "$NODE_MIN_VERSION" ]]; then + check_pass "Node.js installed (version $node_version, >= $NODE_MIN_VERSION required)" + else + check_fail "Node.js version $node_version is below minimum required ($NODE_MIN_VERSION+)" + check_info "Install Node.js $NODE_MIN_VERSION+: https://nodejs.org/" + fi + else + check_fail "Node.js not installed" + check_info "Install Node.js $NODE_MIN_VERSION+: https://nodejs.org/" + return + fi + + # Check npm + if command_exists npm; then + local npm_version + npm_version=$(npm --version) + check_pass "npm installed (version $npm_version)" + else + check_fail "npm not installed" + check_info "npm should come with Node.js installation" + return + fi + + # Check client directory + if [[ ! -d "$client_dir" ]]; then + check_warn "Client directory not found: $CLIENT_DIR" + check_info "Frontend may be in a separate repository" + return + fi + + # Check client dependencies + if [[ -d "$client_dir/node_modules" ]]; then + check_pass "Frontend dependencies installed" + + # Check Next.js + if [[ -d "$client_dir/node_modules/next" ]]; then + check_pass "Next.js dependency installed" + else + check_warn "Next.js not found in dependencies" + fi + + # Check React + if [[ -d "$client_dir/node_modules/react" ]]; then + check_pass "React dependency installed" + else + check_warn "React not found in dependencies" + fi + else + check_warn "Frontend dependencies not installed" + if [[ "$SETUP_MODE" == true ]]; then + check_info "Installing frontend dependencies..." + if (cd "$client_dir" && npm install &>/dev/null); then + check_pass "Frontend dependencies installed successfully" + else + check_fail "Failed to install frontend dependencies" + fi + else + check_info "Run: cd $CLIENT_DIR && npm install" + fi + fi +} + +# ============================================================================= +# Print Summary +# ============================================================================= + +print_summary() { + print_header "Summary" + echo "" + echo -e " ${GREEN}Passed:${NC} $CHECKS_PASSED" + echo -e " ${RED}Failed:${NC} $CHECKS_FAILED" + echo -e " ${YELLOW}Warnings:${NC} $CHECKS_WARNED" + echo "" + + if [[ $CHECKS_FAILED -eq 0 ]]; then + echo -e "${GREEN}All required checks passed!${NC}" + if [[ $CHECKS_WARNED -gt 0 ]]; then + echo -e "${YELLOW}There are some warnings to review.${NC}" + fi + else + echo -e "${RED}Some checks failed. Please address the issues above.${NC}" + if [[ "$SETUP_MODE" != true ]]; then + echo -e "${BLUE}Tip: Run with --setup flag to auto-fix some issues${NC}" + fi + fi + echo "" +} + +# ============================================================================= +# Main Execution +# ============================================================================= + +# Get script directory and change to it +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +# Default options +SETUP_MODE=false +PRE_CHECK_MODE=false + +# Parse arguments +while [[ $# -gt 0 ]]; do + case $1 in + --pre) + PRE_CHECK_MODE=true + shift + ;; + --setup) + SETUP_MODE=true + shift + ;; + --help|-h) + show_help + ;; + *) + echo "Unknown option: $1" + echo "Use --help for usage information" + exit 1 + ;; + esac +done + +# Print banner +echo "" +echo -e "${BLUE}╔══════════════════════════════════════════════════════════════╗${NC}" +echo -e "${BLUE}║ mflix Sample Application - Requirements Check ║${NC}" +echo -e "${BLUE}║ Java/Spring Boot Backend ║${NC}" +echo -e "${BLUE}╚══════════════════════════════════════════════════════════════╝${NC}" + +if [[ "$PRE_CHECK_MODE" == true ]]; then + echo -e "${YELLOW}Pre-setup check - verifying runtime requirements only${NC}" +elif [[ "$SETUP_MODE" == true ]]; then + echo -e "${YELLOW}Running in setup mode - will attempt to fix issues${NC}" +fi + +# Run checks based on mode +if [[ "$PRE_CHECK_MODE" == true ]]; then + check_runtime_requirements +else + check_backend_requirements + check_env_configuration + check_frontend_requirements +fi + +# Print summary +print_summary + +# Exit with appropriate code +if [[ $CHECKS_FAILED -gt 0 ]]; then + exit 1 +fi +exit 0 \ No newline at end of file diff --git a/mflix/check-requirements-js.sh b/mflix/check-requirements-js.sh new file mode 100755 index 0000000..20d7f3a --- /dev/null +++ b/mflix/check-requirements-js.sh @@ -0,0 +1,476 @@ +#!/bin/bash +# ============================================================================= +# Requirements Verification Script for mflix Sample Application +# JavaScript/Express Backend +# ============================================================================= +# +# This script checks that all necessary requirements are installed to run +# the mflix sample application with the JavaScript/Express backend. +# +# Usage: +# ./check-requirements-js.sh # Check all requirements (post-setup) +# ./check-requirements-js.sh --pre # Check only runtime requirements (pre-setup) +# ./check-requirements-js.sh --setup # Check and auto-setup missing items +# ./check-requirements-js.sh --help # Show help message +# +# ============================================================================= + +# Exit on error (but handle arithmetic expressions carefully) +set -e + +# ============================================================================= +# Configuration +# ============================================================================= + +SERVER_DIR="server" +CLIENT_DIR="client" +NODE_MIN_VERSION="18" + +# ============================================================================= +# Colors +# ============================================================================= + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# ============================================================================= +# Counters +# ============================================================================= + +CHECKS_PASSED=0 +CHECKS_FAILED=0 +CHECKS_WARNED=0 + +# ============================================================================= +# Helper Functions +# ============================================================================= + +print_header() { + echo "" + echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + echo -e "${BLUE} $1${NC}" + echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" +} + +print_section() { + echo "" + echo -e "${YELLOW}▸ $1${NC}" +} + +check_pass() { + echo -e " ${GREEN}✓${NC} $1" + CHECKS_PASSED=$((CHECKS_PASSED + 1)) +} + +check_fail() { + echo -e " ${RED}✗${NC} $1" + CHECKS_FAILED=$((CHECKS_FAILED + 1)) +} + +check_warn() { + echo -e " ${YELLOW}⚠${NC} $1" + CHECKS_WARNED=$((CHECKS_WARNED + 1)) +} + +check_info() { + echo -e " ${BLUE}→${NC} $1" +} + +command_exists() { + command -v "$1" &>/dev/null +} + +version_gte() { + # Returns 0 (true) if $1 >= $2 (numeric comparison) + [[ "$1" -ge "$2" ]] 2>/dev/null +} + +show_help() { + echo "Usage: $0 [OPTIONS]" + echo "" + echo "Options:" + echo " --pre Check only runtime requirements (use before setup)" + echo " --setup Attempt to automatically set up missing requirements" + echo " --help Show this help message" + echo "" + echo "This script checks that all necessary requirements are installed" + echo "to run the mflix sample application with the JavaScript/Express backend." + echo "" + echo "Use --pre before starting setup to verify you have the required runtime." + echo "Use without flags after completing setup to verify everything is ready." + exit 0 +} + +# ============================================================================= +# Check Runtime Requirements (Pre-Setup) +# ============================================================================= + +check_runtime_requirements() { + print_section "Runtime Requirements" + + # Check Node.js version + if command_exists node; then + local node_version + node_version=$(node --version | sed 's/v//') + local node_major + node_major=$(echo "$node_version" | cut -d. -f1) + if version_gte "$node_major" "$NODE_MIN_VERSION"; then + check_pass "Node.js $node_version installed (>= $NODE_MIN_VERSION required)" + else + check_fail "Node.js $node_version installed but >= $NODE_MIN_VERSION required" + check_info "Install Node.js $NODE_MIN_VERSION+: https://nodejs.org/" + fi + else + check_fail "Node.js not installed" + check_info "Install Node.js $NODE_MIN_VERSION+: https://nodejs.org/" + fi + + # Check npm + if command_exists npm; then + local npm_version + npm_version=$(npm --version) + check_pass "npm installed (version $npm_version)" + else + check_fail "npm not installed" + check_info "npm should come with Node.js installation" + fi +} + +# ============================================================================= +# Check JavaScript/Express Backend Requirements (Full) +# ============================================================================= + +check_backend_requirements() { + print_section "JavaScript/Express Backend Requirements" + + local server_dir="$SCRIPT_DIR/$SERVER_DIR" + + # Check Node.js version + if command_exists node; then + local node_version + node_version=$(node --version | sed 's/v//') + local node_major + node_major=$(echo "$node_version" | cut -d. -f1) + if [[ "$node_major" -ge "$NODE_MIN_VERSION" ]]; then + check_pass "Node.js $node_version installed (>= $NODE_MIN_VERSION required)" + else + check_fail "Node.js $node_version installed but >= $NODE_MIN_VERSION required" + check_info "Install Node.js $NODE_MIN_VERSION+ from https://nodejs.org/" + fi + else + check_fail "Node.js not installed" + check_info "Install Node.js $NODE_MIN_VERSION+ from https://nodejs.org/" + return + fi + + # Check npm + if command_exists npm; then + local npm_version + npm_version=$(npm --version) + check_pass "npm installed (version $npm_version)" + else + check_fail "npm not installed" + check_info "npm should come with Node.js installation" + return + fi + + # Check server directory + if [[ ! -d "$server_dir" ]]; then + check_fail "Server directory not found: $SERVER_DIR" + return + fi + + # Check package.json + if [[ -f "$server_dir/package.json" ]]; then + check_pass "package.json found" + else + check_fail "package.json not found in $SERVER_DIR" + return + fi + + # Check node_modules + if [[ -d "$server_dir/node_modules" ]]; then + check_pass "node_modules directory exists" + + # Check Express.js + if [[ -d "$server_dir/node_modules/express" ]]; then + check_pass "Express.js dependency installed" + else + check_fail "Express.js dependency not installed" + fi + + # Check MongoDB driver + if [[ -d "$server_dir/node_modules/mongodb" ]]; then + check_pass "MongoDB driver dependency installed" + else + check_fail "MongoDB driver dependency not installed" + fi + + # Check TypeScript build + if [[ -d "$server_dir/dist" ]]; then + check_pass "TypeScript build output exists (dist directory)" + else + check_warn "TypeScript build output not found (dist directory)" + check_info "Run: cd $SERVER_DIR && npm run build" + fi + else + check_warn "node_modules directory not found" + if [[ "$SETUP_MODE" == true ]]; then + check_info "Installing dependencies..." + if (cd "$server_dir" && npm install &>/dev/null); then + check_pass "Dependencies installed successfully" + else + check_fail "Failed to install dependencies" + fi + else + check_info "Run: cd $SERVER_DIR && npm install" + fi + fi +} + +# ============================================================================= +# Check Environment Configuration +# ============================================================================= + +check_env_configuration() { + print_section "Environment Configuration" + + local server_dir="$SCRIPT_DIR/$SERVER_DIR" + local env_file="$server_dir/.env" + local env_example="$server_dir/.env.example" + + # Check .env file + if [[ -f "$env_file" ]]; then + check_pass ".env file exists" + + # Check MONGODB_URI + if grep -q "^MONGODB_URI=" "$env_file" 2>/dev/null; then + local mongo_uri + mongo_uri=$(grep "^MONGODB_URI=" "$env_file" | cut -d'=' -f2-) + if [[ -n "$mongo_uri" ]] && [[ "$mongo_uri" != *"<"*">"* ]]; then + check_pass "MONGODB_URI is configured" + else + check_fail "MONGODB_URI is not configured (still has placeholder value)" + check_info "Update MONGODB_URI in $SERVER_DIR/.env with your MongoDB connection string" + fi + else + check_fail "MONGODB_URI not found in .env" + check_info "Add MONGODB_URI to $SERVER_DIR/.env" + fi + + # Check VOYAGE_API_KEY (optional) + if grep -q "^VOYAGE_API_KEY=" "$env_file" 2>/dev/null; then + local voyage_key + voyage_key=$(grep "^VOYAGE_API_KEY=" "$env_file" | cut -d'=' -f2-) + if [[ -n "$voyage_key" ]] && [[ "$voyage_key" != "your_voyage_api_key" ]]; then + check_pass "VOYAGE_API_KEY is configured" + else + check_info "VOYAGE_API_KEY not configured (optional - needed for vector search)" + fi + else + check_info "VOYAGE_API_KEY not set (optional - needed for vector search)" + fi + + # Check CORS_ORIGINS (optional) + if grep -q "^CORS_ORIGINS=" "$env_file" 2>/dev/null; then + check_pass "CORS_ORIGINS is configured" + else + check_info "CORS_ORIGINS not set (will use default: http://localhost:3000)" + fi + + # Check PORT (optional) + if grep -q "^PORT=" "$env_file" 2>/dev/null; then + check_pass "PORT is configured" + else + check_info "PORT not set (will use default: 3001)" + fi + else + check_warn ".env file not found" + if [[ -f "$env_example" ]]; then + if [[ "$SETUP_MODE" == true ]]; then + check_info "Creating .env from .env.example..." + if cp "$env_example" "$env_file"; then + check_pass ".env file created from .env.example" + check_warn "Please update the placeholder values in $SERVER_DIR/.env" + else + check_fail "Failed to create .env file" + fi + else + check_info "Copy .env.example to .env: cp $SERVER_DIR/.env.example $SERVER_DIR/.env" + fi + else + check_fail "No .env.example found to use as template" + fi + fi +} + +# ============================================================================= +# Check Frontend Requirements +# ============================================================================= + +check_frontend_requirements() { + print_section "Frontend Requirements (Next.js)" + + local client_dir="$SCRIPT_DIR/$CLIENT_DIR" + + # Check Node.js + if command_exists node; then + local node_version + node_version=$(node --version | sed 's/v//') + local node_major + node_major=$(echo "$node_version" | cut -d. -f1) + if [[ "$node_major" -ge "$NODE_MIN_VERSION" ]]; then + check_pass "Node.js installed (version $node_version, >= $NODE_MIN_VERSION required)" + else + check_fail "Node.js version $node_version is below minimum required ($NODE_MIN_VERSION+)" + check_info "Install Node.js $NODE_MIN_VERSION+: https://nodejs.org/" + fi + else + check_fail "Node.js not installed" + check_info "Install Node.js $NODE_MIN_VERSION+: https://nodejs.org/" + return + fi + + # Check npm + if command_exists npm; then + local npm_version + npm_version=$(npm --version) + check_pass "npm installed (version $npm_version)" + else + check_fail "npm not installed" + check_info "npm should come with Node.js installation" + return + fi + + # Check client directory + if [[ ! -d "$client_dir" ]]; then + check_warn "Client directory not found: $CLIENT_DIR" + check_info "Frontend may be in a separate repository" + return + fi + + # Check client dependencies + if [[ -d "$client_dir/node_modules" ]]; then + check_pass "Frontend dependencies installed" + + # Check Next.js + if [[ -d "$client_dir/node_modules/next" ]]; then + check_pass "Next.js dependency installed" + else + check_warn "Next.js not found in dependencies" + fi + + # Check React + if [[ -d "$client_dir/node_modules/react" ]]; then + check_pass "React dependency installed" + else + check_warn "React not found in dependencies" + fi + else + check_warn "Frontend dependencies not installed" + if [[ "$SETUP_MODE" == true ]]; then + check_info "Installing frontend dependencies..." + if (cd "$client_dir" && npm install &>/dev/null); then + check_pass "Frontend dependencies installed successfully" + else + check_fail "Failed to install frontend dependencies" + fi + else + check_info "Run: cd $CLIENT_DIR && npm install" + fi + fi +} + +# ============================================================================= +# Print Summary +# ============================================================================= + +print_summary() { + print_header "Summary" + echo "" + echo -e " ${GREEN}Passed:${NC} $CHECKS_PASSED" + echo -e " ${RED}Failed:${NC} $CHECKS_FAILED" + echo -e " ${YELLOW}Warnings:${NC} $CHECKS_WARNED" + echo "" + + if [[ $CHECKS_FAILED -eq 0 ]]; then + echo -e "${GREEN}All required checks passed!${NC}" + if [[ $CHECKS_WARNED -gt 0 ]]; then + echo -e "${YELLOW}There are some warnings to review.${NC}" + fi + else + echo -e "${RED}Some checks failed. Please address the issues above.${NC}" + if [[ "$SETUP_MODE" != true ]]; then + echo -e "${BLUE}Tip: Run with --setup flag to auto-fix some issues${NC}" + fi + fi + echo "" +} + +# ============================================================================= +# Main Execution +# ============================================================================= + +# Get script directory and change to it +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +# Default options +SETUP_MODE=false +PRE_CHECK_MODE=false + +# Parse arguments +while [[ $# -gt 0 ]]; do + case $1 in + --pre) + PRE_CHECK_MODE=true + shift + ;; + --setup) + SETUP_MODE=true + shift + ;; + --help|-h) + show_help + ;; + *) + echo "Unknown option: $1" + echo "Use --help for usage information" + exit 1 + ;; + esac +done + +# Print banner +echo "" +echo -e "${BLUE}╔══════════════════════════════════════════════════════════════╗${NC}" +echo -e "${BLUE}║ mflix Sample Application - Requirements Check ║${NC}" +echo -e "${BLUE}║ JavaScript/Express Backend ║${NC}" +echo -e "${BLUE}╚══════════════════════════════════════════════════════════════╝${NC}" + +if [[ "$PRE_CHECK_MODE" == true ]]; then + echo -e "${YELLOW}Pre-setup check - verifying runtime requirements only${NC}" +elif [[ "$SETUP_MODE" == true ]]; then + echo -e "${YELLOW}Running in setup mode - will attempt to fix issues${NC}" +fi + +# Run checks based on mode +if [[ "$PRE_CHECK_MODE" == true ]]; then + check_runtime_requirements +else + check_backend_requirements + check_env_configuration + check_frontend_requirements +fi + +# Print summary +print_summary + +# Exit with appropriate code +if [[ $CHECKS_FAILED -gt 0 ]]; then + exit 1 +fi +exit 0 diff --git a/mflix/check-requirements-python.sh b/mflix/check-requirements-python.sh new file mode 100755 index 0000000..fd4d7f1 --- /dev/null +++ b/mflix/check-requirements-python.sh @@ -0,0 +1,478 @@ +#!/bin/bash +# ============================================================================= +# Requirements Verification Script for mflix Sample Application +# Python/FastAPI Backend +# ============================================================================= +# +# This script checks that all necessary requirements are installed to run +# the mflix sample application with the Python/FastAPI backend. +# +# Usage: +# ./check-requirements-python.sh # Check all requirements (post-setup) +# ./check-requirements-python.sh --pre # Check only runtime requirements (pre-setup) +# ./check-requirements-python.sh --setup # Check and auto-setup missing items +# ./check-requirements-python.sh --help # Show help message +# +# ============================================================================= + +# Exit on error (but handle arithmetic expressions carefully) +set -e + +# ============================================================================= +# Configuration +# ============================================================================= + +SERVER_DIR="server" +CLIENT_DIR="client" +PYTHON_MIN_VERSION="3.11" +NODE_MIN_VERSION="18" + +# ============================================================================= +# Colors +# ============================================================================= + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# ============================================================================= +# Counters +# ============================================================================= + +CHECKS_PASSED=0 +CHECKS_FAILED=0 +CHECKS_WARNED=0 + +# ============================================================================= +# Helper Functions +# ============================================================================= + +print_header() { + echo "" + echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + echo -e "${BLUE} $1${NC}" + echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" +} + +print_section() { + echo "" + echo -e "${YELLOW}▸ $1${NC}" +} + +check_pass() { + echo -e " ${GREEN}✓${NC} $1" + CHECKS_PASSED=$((CHECKS_PASSED + 1)) +} + +check_fail() { + echo -e " ${RED}✗${NC} $1" + CHECKS_FAILED=$((CHECKS_FAILED + 1)) +} + +check_warn() { + echo -e " ${YELLOW}⚠${NC} $1" + CHECKS_WARNED=$((CHECKS_WARNED + 1)) +} + +check_info() { + echo -e " ${BLUE}→${NC} $1" +} + +command_exists() { + command -v "$1" &>/dev/null +} + +version_gte() { + # Returns 0 (true) if $1 >= $2 using version sorting + [ "$(printf '%s\n' "$2" "$1" | sort -V | head -n1)" = "$2" ] +} + +show_help() { + echo "Usage: $0 [OPTIONS]" + echo "" + echo "Options:" + echo " --pre Check only runtime requirements (use before setup)" + echo " --setup Attempt to automatically set up missing requirements" + echo " --help Show this help message" + echo "" + echo "This script checks that all necessary requirements are installed" + echo "to run the mflix sample application with the Python/FastAPI backend." + echo "" + echo "Use --pre before starting setup to verify you have the required runtime." + echo "Use without flags after completing setup to verify everything is ready." + exit 0 +} + +# ============================================================================= +# Check Runtime Requirements (Pre-Setup) +# ============================================================================= + +check_runtime_requirements() { + print_section "Runtime Requirements" + + # Check Python version + if command_exists python3; then + local python_version + python_version=$(python3 --version 2>&1 | grep -oE '[0-9]+\.[0-9]+' | head -1) + if version_gte "$python_version" "$PYTHON_MIN_VERSION"; then + check_pass "Python $python_version installed (>= $PYTHON_MIN_VERSION required)" + else + check_fail "Python $python_version installed but >= $PYTHON_MIN_VERSION required" + check_info "Install Python $PYTHON_MIN_VERSION+ from https://www.python.org/downloads/" + fi + else + check_fail "Python 3 not installed" + check_info "Install Python $PYTHON_MIN_VERSION+ from https://www.python.org/downloads/" + fi + + # Check pip + if command_exists pip3 || python3 -m pip --version &>/dev/null; then + check_pass "pip installed" + else + check_fail "pip not installed" + check_info "Install pip: python3 -m ensurepip --upgrade" + fi +} + +# ============================================================================= +# Check Python/FastAPI Backend Requirements (Full) +# ============================================================================= + +check_backend_requirements() { + print_section "Python/FastAPI Backend Requirements" + + local server_dir="$SCRIPT_DIR/$SERVER_DIR" + + # Check Python version + if command_exists python3; then + local python_version + python_version=$(python3 --version 2>&1 | grep -oE '[0-9]+\.[0-9]+' | head -1) + if version_gte "$python_version" "$PYTHON_MIN_VERSION"; then + check_pass "Python $python_version installed (>= $PYTHON_MIN_VERSION required)" + else + check_fail "Python $python_version installed but >= $PYTHON_MIN_VERSION required" + check_info "Install Python $PYTHON_MIN_VERSION+ from https://www.python.org/downloads/" + fi + else + check_fail "Python 3 not installed" + check_info "Install Python $PYTHON_MIN_VERSION+ from https://www.python.org/downloads/" + return + fi + + # Check pip + if command_exists pip3 || python3 -m pip --version &>/dev/null; then + check_pass "pip installed" + else + check_fail "pip not installed" + check_info "Install pip: python3 -m ensurepip --upgrade" + fi + + # Check virtual environment + local venv_dir="$server_dir/.venv" + if [[ -d "$venv_dir" ]]; then + check_pass "Python virtual environment exists at $SERVER_DIR/.venv" + + # Check if venv activation script exists + if [[ -f "$venv_dir/bin/activate" ]]; then + check_pass "Virtual environment activation script exists" + else + check_warn "Virtual environment activation script missing" + fi + else + check_warn "Python virtual environment not found at $SERVER_DIR/.venv" + if [[ "$SETUP_MODE" == true ]]; then + check_info "Creating virtual environment..." + if python3 -m venv "$venv_dir"; then + check_pass "Virtual environment created" + else + check_fail "Failed to create virtual environment" + fi + else + check_info "Create with: cd $SERVER_DIR && python3 -m venv .venv" + fi + fi + + # Check Python dependencies + local requirements_file="$server_dir/requirements.txt" + if [[ -f "$requirements_file" ]]; then + check_pass "requirements.txt found" + + # Check if key dependencies are installed + if [[ -d "$venv_dir" ]]; then + local pip_cmd="$venv_dir/bin/pip" + if [[ -f "$pip_cmd" ]]; then + # Check FastAPI + if "$pip_cmd" show fastapi &>/dev/null; then + check_pass "FastAPI installed in virtual environment" + else + check_warn "FastAPI not installed in virtual environment" + if [[ "$SETUP_MODE" == true ]]; then + check_info "Installing dependencies..." + if "$pip_cmd" install -r "$requirements_file" &>/dev/null; then + check_pass "Dependencies installed" + else + check_fail "Failed to install dependencies" + fi + else + check_info "Install with: source $SERVER_DIR/.venv/bin/activate && pip install -r $SERVER_DIR/requirements.txt" + fi + fi + + # Check PyMongo + if "$pip_cmd" show pymongo &>/dev/null; then + check_pass "PyMongo installed in virtual environment" + else + check_warn "PyMongo not installed in virtual environment" + fi + fi + fi + else + check_fail "requirements.txt not found at $SERVER_DIR/requirements.txt" + fi +} + +# ============================================================================= +# Check Environment Configuration +# ============================================================================= + +check_env_configuration() { + print_section "Environment Configuration" + + local server_dir="$SCRIPT_DIR/$SERVER_DIR" + local env_file="$server_dir/.env" + local env_example="$server_dir/.env.example" + + # Check .env file + if [[ -f "$env_file" ]]; then + check_pass ".env file exists" + + # Check MONGODB_URI + if grep -q "^MONGODB_URI=" "$env_file" 2>/dev/null; then + local mongo_uri + mongo_uri=$(grep "^MONGODB_URI=" "$env_file" | cut -d'=' -f2-) + if [[ -n "$mongo_uri" ]] && [[ "$mongo_uri" != *"<"*">"* ]]; then + check_pass "MONGODB_URI is configured" + else + check_fail "MONGODB_URI is not configured (still has placeholder value)" + check_info "Update MONGODB_URI in $SERVER_DIR/.env with your MongoDB connection string" + fi + else + check_fail "MONGODB_URI not found in .env" + check_info "Add MONGODB_URI to $SERVER_DIR/.env" + fi + + # Check VOYAGE_API_KEY (optional) + if grep -q "^VOYAGE_API_KEY=" "$env_file" 2>/dev/null; then + local voyage_key + voyage_key=$(grep "^VOYAGE_API_KEY=" "$env_file" | cut -d'=' -f2-) + if [[ -n "$voyage_key" ]] && [[ "$voyage_key" != "your_voyage_api_key" ]]; then + check_pass "VOYAGE_API_KEY is configured" + else + check_info "VOYAGE_API_KEY not configured (optional - needed for vector search)" + fi + else + check_info "VOYAGE_API_KEY not set (optional - needed for vector search)" + fi + + # Check CORS_ORIGINS (optional) + if grep -q "^CORS_ORIGINS=" "$env_file" 2>/dev/null; then + check_pass "CORS_ORIGINS is configured" + else + check_info "CORS_ORIGINS not set (will use default: http://localhost:3000)" + fi + + # Check PORT (optional) + if grep -q "^PORT=" "$env_file" 2>/dev/null; then + check_pass "PORT is configured" + else + check_info "PORT not set (will use default: 3001)" + fi + else + check_warn ".env file not found" + if [[ -f "$env_example" ]]; then + if [[ "$SETUP_MODE" == true ]]; then + check_info "Creating .env from .env.example..." + if cp "$env_example" "$env_file"; then + check_pass ".env file created from .env.example" + check_warn "Please update the placeholder values in $SERVER_DIR/.env" + else + check_fail "Failed to create .env file" + fi + else + check_info "Copy .env.example to .env: cp $SERVER_DIR/.env.example $SERVER_DIR/.env" + fi + else + check_fail "No .env.example found to use as template" + fi + fi +} + +# ============================================================================= +# Check Frontend Requirements +# ============================================================================= + +check_frontend_requirements() { + print_section "Frontend Requirements (Next.js)" + + local client_dir="$SCRIPT_DIR/$CLIENT_DIR" + + # Check Node.js + if command_exists node; then + local node_version + node_version=$(node --version | sed 's/v//') + local node_major + node_major=$(echo "$node_version" | cut -d. -f1) + if [[ "$node_major" -ge "$NODE_MIN_VERSION" ]]; then + check_pass "Node.js installed (version $node_version, >= $NODE_MIN_VERSION required)" + else + check_fail "Node.js version $node_version is below minimum required ($NODE_MIN_VERSION+)" + check_info "Install Node.js $NODE_MIN_VERSION+: https://nodejs.org/" + fi + else + check_fail "Node.js not installed" + check_info "Install Node.js $NODE_MIN_VERSION+: https://nodejs.org/" + return + fi + + # Check npm + if command_exists npm; then + local npm_version + npm_version=$(npm --version) + check_pass "npm installed (version $npm_version)" + else + check_fail "npm not installed" + check_info "npm should come with Node.js installation" + return + fi + + # Check client directory + if [[ ! -d "$client_dir" ]]; then + check_warn "Client directory not found: $CLIENT_DIR" + check_info "Frontend may be in a separate repository" + return + fi + + # Check client dependencies + if [[ -d "$client_dir/node_modules" ]]; then + check_pass "Frontend dependencies installed" + + # Check Next.js + if [[ -d "$client_dir/node_modules/next" ]]; then + check_pass "Next.js dependency installed" + else + check_warn "Next.js not found in dependencies" + fi + + # Check React + if [[ -d "$client_dir/node_modules/react" ]]; then + check_pass "React dependency installed" + else + check_warn "React not found in dependencies" + fi + else + check_warn "Frontend dependencies not installed" + if [[ "$SETUP_MODE" == true ]]; then + check_info "Installing frontend dependencies..." + if (cd "$client_dir" && npm install &>/dev/null); then + check_pass "Frontend dependencies installed successfully" + else + check_fail "Failed to install frontend dependencies" + fi + else + check_info "Run: cd $CLIENT_DIR && npm install" + fi + fi +} + +# ============================================================================= +# Print Summary +# ============================================================================= + +print_summary() { + print_header "Summary" + echo "" + echo -e " ${GREEN}Passed:${NC} $CHECKS_PASSED" + echo -e " ${RED}Failed:${NC} $CHECKS_FAILED" + echo -e " ${YELLOW}Warnings:${NC} $CHECKS_WARNED" + echo "" + + if [[ $CHECKS_FAILED -eq 0 ]]; then + echo -e "${GREEN}All required checks passed!${NC}" + if [[ $CHECKS_WARNED -gt 0 ]]; then + echo -e "${YELLOW}There are some warnings to review.${NC}" + fi + else + echo -e "${RED}Some checks failed. Please address the issues above.${NC}" + if [[ "$SETUP_MODE" != true ]]; then + echo -e "${BLUE}Tip: Run with --setup flag to auto-fix some issues${NC}" + fi + fi + echo "" +} + +# ============================================================================= +# Main Execution +# ============================================================================= + +# Get script directory and change to it +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +# Default options +SETUP_MODE=false +PRE_CHECK_MODE=false + +# Parse arguments +while [[ $# -gt 0 ]]; do + case $1 in + --pre) + PRE_CHECK_MODE=true + shift + ;; + --setup) + SETUP_MODE=true + shift + ;; + --help|-h) + show_help + ;; + *) + echo "Unknown option: $1" + echo "Use --help for usage information" + exit 1 + ;; + esac +done + +# Print banner +echo "" +echo -e "${BLUE}╔══════════════════════════════════════════════════════════════╗${NC}" +echo -e "${BLUE}║ mflix Sample Application - Requirements Check ║${NC}" +echo -e "${BLUE}║ Python/FastAPI Backend ║${NC}" +echo -e "${BLUE}╚══════════════════════════════════════════════════════════════╝${NC}" + +if [[ "$PRE_CHECK_MODE" == true ]]; then + echo -e "${YELLOW}Pre-setup check - verifying runtime requirements only${NC}" +elif [[ "$SETUP_MODE" == true ]]; then + echo -e "${YELLOW}Running in setup mode - will attempt to fix issues${NC}" +fi + +# Run checks based on mode +if [[ "$PRE_CHECK_MODE" == true ]]; then + check_runtime_requirements +else + check_backend_requirements + check_env_configuration + check_frontend_requirements +fi + +# Print summary +print_summary + +# Exit with appropriate code +if [[ $CHECKS_FAILED -gt 0 ]]; then + exit 1 +fi +exit 0 diff --git a/mflix/server/java-spring/.env.example b/mflix/server/java-spring/.env.example index 049e6e9..96c3555 100644 --- a/mflix/server/java-spring/.env.example +++ b/mflix/server/java-spring/.env.example @@ -2,21 +2,19 @@ # Replace with your MongoDB Atlas connection string or local MongoDB URI MONGODB_URI=mongodb+srv://:@.mongodb.net/sample_mflix?retryWrites=true&w=majority -# Optional: Voyage AI Configuration -# API key for Voyage AI embedding model (required for Vector Search) +# OPTIONAL: Voyage AI Configuration (required for Vector Search) # Get your API key from https://www.voyageai.com/ -# Uncomment the following line to enable vector search -# VOYAGE_API_KEY=your-api-key +# VOYAGE_API_KEY=your_voyage_api_key # Server Configuration -# Port on which the Spring Boot application will run PORT=3001 # CORS Configuration -# Allowed origin for cross-origin requests (frontend URL) -# For multiple origins, separate with commas -CORS_ORIGIN=http://localhost:3000 +# Comma-separated list of allowed origins for cross-origin requests +CORS_ORIGINS=http://localhost:3000 -# Optional: Enable MongoDB Search tests -# Uncomment the following line to enable Search tests -# ENABLE_SEARCH_TESTS=true \ No newline at end of file +# Logging Configuration +# Log level: TRACE, DEBUG, INFO, WARN, ERROR (default: INFO) +LOG_LEVEL=INFO +# Optional: Path to log file (if not set, logs only to console) +# LOG_FILE=app.log \ No newline at end of file diff --git a/mflix/server/java-spring/src/main/resources/application.properties b/mflix/server/java-spring/src/main/resources/application.properties index ffe611f..47bd6d0 100644 --- a/mflix/server/java-spring/src/main/resources/application.properties +++ b/mflix/server/java-spring/src/main/resources/application.properties @@ -8,8 +8,8 @@ spring.data.mongodb.database=sample_mflix server.port=${PORT:3001} # CORS Configuration -# Allowed origins for cross-origin requests (typically the frontend URL) -cors.allowed.origins=${CORS_ORIGIN:http://localhost:3000} +# Comma-separated list of allowed origins for cross-origin requests +cors.allowed.origins=${CORS_ORIGINS:http://localhost:3000} # Voyage AI Configuration # API key for Voyage AI embedding model (required for vector search) diff --git a/mflix/server/js-express/.env.example b/mflix/server/js-express/.env.example index c870ee0..8b264f6 100644 --- a/mflix/server/js-express/.env.example +++ b/mflix/server/js-express/.env.example @@ -2,24 +2,19 @@ # Replace with your MongoDB Atlas connection string or local MongoDB URI MONGODB_URI=mongodb+srv://:@.mongodb.net/sample_mflix?retryWrites=true&w=majority -# Voyage AI Configuration -# API key for Voyage AI embedding model (required for Vector Search) -VOYAGE_API_KEY=your_voyage_api_key +# OPTIONAL: Voyage AI Configuration (required for Vector Search) +# Get your API key from https://www.voyageai.com/ +# VOYAGE_API_KEY=your_voyage_api_key # Server Configuration PORT=3001 NODE_ENV=development +# CORS Configuration +# Comma-separated list of allowed origins for cross-origin requests +CORS_ORIGINS=http://localhost:3000 + # Logging Configuration # Available levels: error, warn, info, http, debug # Default: debug (development), info (production), error (test) -LOG_LEVEL=debug - -# CORS Configuration -# Allowed origin for cross-origin requests (frontend URL) -# For multiple origins, separate with commas -CORS_ORIGIN=http://localhost:3000 - -# Optional: Enable MongoDB Search tests -# Uncomment the following line to enable Search tests -# ENABLE_SEARCH_TESTS=true \ No newline at end of file +LOG_LEVEL=debug \ No newline at end of file diff --git a/mflix/server/js-express/src/app.ts b/mflix/server/js-express/src/app.ts index 6e3d389..d6f2e99 100644 --- a/mflix/server/js-express/src/app.ts +++ b/mflix/server/js-express/src/app.ts @@ -32,10 +32,15 @@ const PORT = process.env.PORT || 3001; * CORS Configuration * Allows the frontend to communicate with this Express backend * In production, this should be configured to only allow specific origins + * Supports multiple origins via comma-separated CORS_ORIGINS environment variable */ +const corsOrigins = (process.env.CORS_ORIGINS || "http://localhost:3000") + .split(",") + .map((origin) => origin.trim()); + app.use( cors({ - origin: process.env.CORS_ORIGIN || "http://localhost:3000", + origin: corsOrigins.length === 1 ? corsOrigins[0] : corsOrigins, credentials: true, }) ); diff --git a/mflix/server/python-fastapi/.env.example b/mflix/server/python-fastapi/.env.example index 6dc1d0d..f0d77b2 100644 --- a/mflix/server/python-fastapi/.env.example +++ b/mflix/server/python-fastapi/.env.example @@ -1,15 +1,17 @@ # MongoDB Connection # Replace with your MongoDB Atlas connection string or local MongoDB URI -MONGO_URI="mongodb+srv://:@.mongodb.net/sample_mflix?retryWrites=true&w=majority" -MONGO_DB="sample_mflix" +MONGODB_URI=mongodb+srv://:@.mongodb.net/sample_mflix?retryWrites=true&w=majority -# Voyage AI Configuration -# API key for Voyage AI embedding model (required for Vector Search) -VOYAGE_API_KEY=your_voyage_api_key +# OPTIONAL: Voyage AI Configuration (required for Vector Search) +# Get your API key from https://www.voyageai.com/ +# VOYAGE_API_KEY=your_voyage_api_key + +# Server Configuration +PORT=3001 # CORS Configuration -# Comma-separated list of allowed origins for CORS -CORS_ORIGINS="http://localhost:3000,http://localhost:3001" +# Comma-separated list of allowed origins for cross-origin requests +CORS_ORIGINS=http://localhost:3000 # Logging Configuration # Log level: DEBUG, INFO, WARNING, ERROR, CRITICAL (default: INFO) diff --git a/mflix/server/python-fastapi/requirements.in b/mflix/server/python-fastapi/requirements.in index c50a667..baf2bf5 100644 --- a/mflix/server/python-fastapi/requirements.in +++ b/mflix/server/python-fastapi/requirements.in @@ -64,3 +64,5 @@ rich-toolkit~=0.15.1 # Extensions for the 'rich' library filelock>=3.20.3 # Transitive dep via huggingface-hub aiohttp>=3.13.3 # Transitive dep via voyageai orjson>=3.11.7 # Transitive dep via langsmith (CVE fix) +langchain-core>=1.2.11 # Transitive dep via langchain-text-splitters (CVE-2026-26013 fix) +pillow>=12.1.1 # Transitive dep via voyageai (CVE-2026-25990 fix) diff --git a/mflix/server/python-fastapi/requirements.txt b/mflix/server/python-fastapi/requirements.txt index 29e4311..9082a76 100644 --- a/mflix/server/python-fastapi/requirements.txt +++ b/mflix/server/python-fastapi/requirements.txt @@ -2,7 +2,7 @@ # This file is autogenerated by pip-compile with Python 3.13 # by the following command: # -# pip-compile requirements.in +# pip-compile --output-file=requirements.txt requirements.in # aiohappyeyeballs==2.6.1 # via aiohttp @@ -99,8 +99,10 @@ jsonpatch==1.33 # via langchain-core jsonpointer==3.0.0 # via jsonpatch -langchain-core==1.2.9 - # via langchain-text-splitters +langchain-core==1.2.11 + # via + # -r requirements.in + # langchain-text-splitters langchain-text-splitters==1.1.0 # via voyageai langsmith==0.6.9 @@ -125,8 +127,10 @@ packaging==26.0 # langchain-core # langsmith # pytest -pillow==12.1.0 - # via voyageai +pillow==12.1.1 + # via + # -r requirements.in + # voyageai pluggy==1.6.0 # via pytest propcache==0.4.1 diff --git a/mflix/server/python-fastapi/src/database/mongo_client.py b/mflix/server/python-fastapi/src/database/mongo_client.py index 37ab816..ac8e2d3 100644 --- a/mflix/server/python-fastapi/src/database/mongo_client.py +++ b/mflix/server/python-fastapi/src/database/mongo_client.py @@ -5,11 +5,13 @@ load_dotenv() -client = AsyncMongoClient(os.getenv("MONGO_URI"), +DATABASE_NAME = "sample_mflix" + +client = AsyncMongoClient(os.getenv("MONGODB_URI"), # Set application name appname="sample-app-python-mflix") -db = client[os.getenv("MONGO_DB")] +db = client[DATABASE_NAME] voyage_api_key = os.getenv("VOYAGE_API_KEY") if voyage_api_key: diff --git a/mflix/server/python-fastapi/src/routers/movies.py b/mflix/server/python-fastapi/src/routers/movies.py index 1da9b6a..50170eb 100644 --- a/mflix/server/python-fastapi/src/routers/movies.py +++ b/mflix/server/python-fastapi/src/routers/movies.py @@ -1,10 +1,18 @@ -from fastapi import APIRouter, Query, Path, Body, HTTPException +from fastapi import APIRouter, Query, Path, Body from fastapi.responses import JSONResponse from src.database.mongo_client import get_collection, voyage_ai_available from src.models.models import VectorSearchResult, CreateMovieRequest, Movie, SuccessResponse, UpdateMovieRequest, SearchMoviesResponse from typing import Any, List, Optional from src.utils.successResponse import create_success_response from src.utils.errorResponse import create_error_response +from src.utils.response_docs import ( + VECTOR_SEARCH_RESPONSES, + OBJECTID_VALIDATION_RESPONSES, + SEARCH_ENDPOINT_RESPONSES, + DATABASE_OPERATION_RESPONSES, + CRUD_OPERATION_RESPONSES, + CRUD_WITH_OBJECTID_RESPONSES +) from src.utils.exceptions import VoyageAuthError, VoyageAPIError from bson import ObjectId, errors import re @@ -114,7 +122,8 @@ "/search", response_model=SuccessResponse[SearchMoviesResponse], status_code = 200, - summary="Search movies using MongoDB Search." + summary="Search movies using MongoDB Search.", + responses=SEARCH_ENDPOINT_RESPONSES ) async def search_movies( plot: Optional[str] = None, @@ -133,9 +142,12 @@ async def search_movies( valid_operators = {"must", "should", "mustNot", "filter"} if search_operator not in valid_operators: - raise HTTPException( - status_code = 400, - detail=f"Invalid search operator '{search_operator}'. The search operator must be one of {valid_operators}." + return JSONResponse( + status_code=400, + content=create_error_response( + message=f"Invalid search operator '{search_operator}'. The search operator must be one of {valid_operators}.", + code="INVALID_SEARCH_OPERATOR" + ) ) # Build the search_phrases list based on which fields were provided by the user. @@ -207,9 +219,12 @@ async def search_movies( }) if not search_phrases: - raise HTTPException( - status_code = 400, - detail="At least one search parameter must be provided." + return JSONResponse( + status_code=400, + content=create_error_response( + message="At least one search parameter must be provided.", + code="MISSING_SEARCH_PARAMS" + ) ) # Build the aggregation pipeline for MongoDB Search. @@ -262,9 +277,12 @@ async def search_movies( try: results = await execute_aggregation(aggregation_pipeline) except Exception as e: - raise HTTPException( - status_code = 500, - detail=f"An error occurred while performing the search: {str(e)}" + return JSONResponse( + status_code=500, + content=create_error_response( + message=f"An error occurred while performing the search: {str(e)}", + code="SEARCH_ERROR" + ) ) @@ -324,7 +342,11 @@ async def search_movies( outputDimension = 2048 #Set to 2048 to match the dimensions of the collection's embeddings # Vector Search Endpoint -@router.get("/vector-search", response_model=SuccessResponse[List[VectorSearchResult]]) +@router.get( + "/vector-search", + response_model=SuccessResponse[List[VectorSearchResult]], + responses=VECTOR_SEARCH_RESPONSES +) async def vector_search_movies( q: str = Query(..., description="Search query to find similar movies by plot"), limit: int = Query(default=10, ge=1, le=50, description="Number of results to return") @@ -342,7 +364,7 @@ async def vector_search_movies( # Check if Voyage AI API key is configured if not voyage_ai_available(): return JSONResponse( - status_code=400, + status_code=503, content=create_error_response( message="Vector search unavailable: VOYAGE_API_KEY not configured. Please add your API key to the .env file", code="SERVICE_UNAVAILABLE" @@ -426,9 +448,12 @@ async def vector_search_movies( print(f"Vector search error: {str(e)}") # Handle generic errors - raise HTTPException( + return JSONResponse( status_code=500, - detail=f"Error performing vector search: {str(e)}" + content=create_error_response( + message=f"Error performing vector search: {str(e)}", + code="VECTOR_SEARCH_ERROR" + ) ) """ @@ -441,10 +466,13 @@ async def vector_search_movies( SuccessResponse[List[str]]: A response object containing the list of unique genres, sorted alphabetically. """ -@router.get("/genres", - response_model=SuccessResponse[List[str]], - status_code=200, - summary="Retrieve all distinct genres from the movies collection.") +@router.get( + "/genres", + response_model=SuccessResponse[List[str]], + status_code=200, + summary="Retrieve all distinct genres from the movies collection.", + responses=DATABASE_OPERATION_RESPONSES +) async def get_distinct_genres(): movies_collection = get_collection("movies") @@ -453,9 +481,12 @@ async def get_distinct_genres(): # MongoDB automatically flattens array fields when using distinct() genres = await movies_collection.distinct("genres") except Exception as e: - raise HTTPException( + return JSONResponse( status_code=500, - detail=f"Database error occurred: {str(e)}" + content=create_error_response( + message=f"Database error occurred: {str(e)}", + code="DATABASE_ERROR" + ) ) # Filter out null/empty values and sort alphabetically @@ -475,34 +506,46 @@ async def get_distinct_genres(): SuccessResponse[Movie]: A response object containing the movie data. """ -@router.get("/{id}", - response_model=SuccessResponse[Movie], - status_code = 200, - summary="Retrieve a single movie by its ID.") +@router.get( + "/{id}", + response_model=SuccessResponse[Movie], + status_code = 200, + summary="Retrieve a single movie by its ID.", + responses=OBJECTID_VALIDATION_RESPONSES +) async def get_movie_by_id(id: str): # Validate ObjectId format try: object_id = ObjectId(id) except errors.InvalidId: - raise HTTPException( - status_code = 400, - detail=f"The provided ID '{id}' is not a valid ObjectId" + return JSONResponse( + status_code=400, + content=create_error_response( + message=f"The provided ID '{id}' is not a valid ObjectId", + code="INVALID_OBJECT_ID" + ) ) movies_collection = get_collection("movies") try: movie = await movies_collection.find_one({"_id": object_id}) except Exception as e: - raise HTTPException( - status_code = 500, - detail=f"Database error occurred: {str(e)}" + return JSONResponse( + status_code=500, + content=create_error_response( + message=f"Database error occurred: {str(e)}", + code="DATABASE_ERROR" + ) ) if movie is None: - raise HTTPException( - status_code = 404, - detail=f"No movie found with ID: {id}" + return JSONResponse( + status_code=404, + content=create_error_response( + message=f"No movie found with ID: {id}", + code="MOVIE_NOT_FOUND" + ) ) movie["_id"] = str(movie["_id"]) # Convert ObjectId to string @@ -529,10 +572,13 @@ async def get_movie_by_id(id: str): SuccessResponse[List[Movie]]: A response object containing the list of movies and metadata. """ -@router.get("/", - response_model=SuccessResponse[List[Movie]], - status_code = 200, - summary="Retrieve a list of movies with optional filtering, sorting, and pagination.") +@router.get( + "/", + response_model=SuccessResponse[List[Movie]], + status_code = 200, + summary="Retrieve a list of movies with optional filtering, sorting, and pagination.", + responses=DATABASE_OPERATION_RESPONSES +) # Validate the query parameters using FastAPI's Query functionality. async def get_all_movies( q:str = Query(default=None), @@ -574,9 +620,12 @@ async def get_all_movies( try: result = movies_collection.find(filter_dict).sort(sort).skip(skip).limit(limit) except Exception as e: - raise HTTPException( - status_code = 500, - detail=f"An error occurred while fetching movies. {str(e)}" + return JSONResponse( + status_code=500, + content=create_error_response( + message=f"An error occurred while fetching movies. {str(e)}", + code="DATABASE_ERROR" + ) ) movies = [] @@ -608,10 +657,13 @@ async def get_all_movies( SuccessResponse[Movie]: A response object containing the created movie data. """ -@router.post("/", - response_model=SuccessResponse[Movie], - status_code = 201, - summary="Creates a new movie in the database.") +@router.post( + "/", + response_model=SuccessResponse[Movie], + status_code = 201, + summary="Creates a new movie in the database.", + responses=CRUD_OPERATION_RESPONSES +) async def create_movie(movie: CreateMovieRequest): # Pydantic automatically validates the structure movie_data = movie.model_dump(by_alias=True, exclude_none=True) @@ -620,31 +672,43 @@ async def create_movie(movie: CreateMovieRequest): try: result = await movies_collection.insert_one(movie_data) except Exception as e: - raise HTTPException( - status_code = 500, - detail=f"Database error occurred: {str(e)}" + return JSONResponse( + status_code=500, + content=create_error_response( + message=f"Database error occurred: {str(e)}", + code="DATABASE_ERROR" + ) ) # Verify that the document was created before querying it if not result.acknowledged: - raise HTTPException( - status_code = 500, - detail="Failed to create movie: The database did not acknowledge the insert operation" + return JSONResponse( + status_code=500, + content=create_error_response( + message="Failed to create movie: The database did not acknowledge the insert operation", + code="DATABASE_ERROR" + ) ) try: # Retrieve the created document to return complete data created_movie = await movies_collection.find_one({"_id": result.inserted_id}) except Exception as e: - raise HTTPException( - status_code = 500, - detail=f"Database error occurred: {str(e)}" + return JSONResponse( + status_code=500, + content=create_error_response( + message=f"Database error occurred: {str(e)}", + code="DATABASE_ERROR" + ) ) if created_movie is None: - raise HTTPException( - status_code = 500, - detail="Movie was created but could not be retrieved for verification" + return JSONResponse( + status_code=500, + content=create_error_response( + message="Movie was created but could not be retrieved for verification", + code="DATABASE_ERROR" + ) ) created_movie["_id"] = str(created_movie["_id"]) # Convert ObjectId to string @@ -678,19 +742,23 @@ async def create_movie(movie: CreateMovieRequest): """ @router.post( - "/batch", - response_model=SuccessResponse[dict], - status_code = 201, - summary = "Create multiple movies in a single request." - ) + "/batch", + response_model=SuccessResponse[dict], + status_code = 201, + summary = "Create multiple movies in a single request.", + responses=CRUD_OPERATION_RESPONSES +) async def create_movies_batch(movies: List[CreateMovieRequest]) ->SuccessResponse[dict]: movies_collection = get_collection("movies") #Verify that the movies list is not empty if not movies: - raise HTTPException( - status_code = 400, - detail="Request body must be a non-empty list of movies." + return JSONResponse( + status_code=400, + content=create_error_response( + message="Request body must be a non-empty list of movies.", + code="EMPTY_REQUEST" + ) ) movies_dicts = [] @@ -710,9 +778,12 @@ async def create_movies_batch(movies: List[CreateMovieRequest]) ->SuccessRespons f"Successfully created {len(result.inserted_ids)} movies." ) except Exception as e: - raise HTTPException( - status_code = 500, - detail=f"Database error occurred: {str(e)}" + return JSONResponse( + status_code=500, + content=create_error_response( + message=f"Database error occurred: {str(e)}", + code="DATABASE_ERROR" + ) ) """ @@ -730,10 +801,12 @@ async def create_movies_batch(movies: List[CreateMovieRequest]) ->SuccessRespons SuccessResponse: The updated movie document, the number of fields modified and a success message. """ @router.patch( - "/{id}", - response_model=SuccessResponse[Movie], - status_code = 200, - summary="Update a single movie by its ID.") + "/{id}", + response_model=SuccessResponse[Movie], + status_code = 200, + summary="Update a single movie by its ID.", + responses=CRUD_WITH_OBJECTID_RESPONSES +) async def update_movie( movie_data: UpdateMovieRequest, movie_id: str = Path(..., alias="id") @@ -745,18 +818,24 @@ async def update_movie( try: movie_id = ObjectId(movie_id) except Exception : - raise HTTPException( - status_code = 400, - detail=f"Invalid movie_id format: {movie_id}" + return JSONResponse( + status_code=400, + content=create_error_response( + message=f"Invalid movie_id format: {movie_id}", + code="INVALID_OBJECT_ID" + ) ) update_dict = movie_data.model_dump(exclude_unset=True, exclude_none=True) # Validate that the dict is not empty if not update_dict: - raise HTTPException( - status_code = 400, - detail="No valid fields provided for update." + return JSONResponse( + status_code=400, + content=create_error_response( + message="No valid fields provided for update.", + code="NO_UPDATE_DATA" + ) ) try: @@ -765,15 +844,21 @@ async def update_movie( {"$set":update_dict} ) except Exception as e: - raise HTTPException( - status_code = 500, - detail=f"An error occurred while updating the movie: {str(e)}" + return JSONResponse( + status_code=500, + content=create_error_response( + message=f"An error occurred while updating the movie: {str(e)}", + code="DATABASE_ERROR" + ) ) if result.matched_count == 0: - raise HTTPException( - status_code = 404, - detail=f"No movie with that _id was found: {movie_id}" + return JSONResponse( + status_code=404, + content=create_error_response( + message=f"No movie with that _id was found: {movie_id}", + code="MOVIE_NOT_FOUND" + ) ) updatedMovie = await movies_collection.find_one({"_id": movie_id}) @@ -793,11 +878,13 @@ async def update_movie( SuccessResponse: A response object containing the number of matched and modified movies and a success message. """ -@router.patch("/", - response_model=SuccessResponse[dict], - status_code = 200, - summary="Batch update movies matching the given filter." - ) +@router.patch( + "/", + response_model=SuccessResponse[dict], + status_code = 200, + summary="Batch update movies matching the given filter.", + responses=CRUD_OPERATION_RESPONSES +) async def update_movies_batch( request_body: dict = Body(...) ) -> SuccessResponse[dict]: @@ -808,9 +895,12 @@ async def update_movies_batch( update_data = request_body.get("update", {}) if not filter_data or not update_data: - raise HTTPException( - status_code = 400, - detail="Both filter and update objects are required" + return JSONResponse( + status_code=400, + content=create_error_response( + message="Both filter and update objects are required", + code="MISSING_FILTER" + ) ) # Convert string IDs to ObjectIds if _id filter is present @@ -820,17 +910,23 @@ async def update_movies_batch( try: filter_data["_id"]["$in"] = [ObjectId(id_str) for id_str in filter_data["_id"]["$in"]] except Exception: - raise HTTPException( - status_code = 400, - detail="Invalid ObjectId format in filter", + return JSONResponse( + status_code=400, + content=create_error_response( + message="Invalid ObjectId format in filter", + code="INVALID_OBJECT_ID" + ) ) try: result = await movies_collection.update_many(filter_data, {"$set": update_data}) except Exception as e: - raise HTTPException( - status_code = 500, - detail=f"An error occurred while updating movies: {str(e)}" + return JSONResponse( + status_code=500, + content=create_error_response( + message=f"An error occurred while updating movies: {str(e)}", + code="DATABASE_ERROR" + ) ) return create_success_response({ @@ -849,17 +945,23 @@ async def update_movies_batch( SuccessResponse[dict]: A response object containing deletion details. """ -@router.delete("/{id}", - response_model=SuccessResponse[dict], - status_code = 200, - summary="Delete a single movie by its ID.") +@router.delete( + "/{id}", + response_model=SuccessResponse[dict], + status_code = 200, + summary="Delete a single movie by its ID.", + responses=OBJECTID_VALIDATION_RESPONSES +) async def delete_movie_by_id(id: str): try: object_id = ObjectId(id) except errors.InvalidId: - raise HTTPException( - status_code = 400, - detail=f"Invalid movie ID format: The provided ID '{id}' is not a valid ObjectId" + return JSONResponse( + status_code=400, + content=create_error_response( + message=f"Invalid movie ID format: The provided ID '{id}' is not a valid ObjectId", + code="INVALID_OBJECT_ID" + ) ) movies_collection = get_collection("movies") @@ -867,15 +969,21 @@ async def delete_movie_by_id(id: str): # Use deleteOne() to remove a single document result = await movies_collection.delete_one({"_id": object_id}) except Exception as e: - raise HTTPException( - status_code = 500, - detail=f"Database error occurred: {str(e)}" + return JSONResponse( + status_code=500, + content=create_error_response( + message=f"Database error occurred: {str(e)}", + code="DATABASE_ERROR" + ) ) if result.deleted_count == 0: - raise HTTPException( - status_code = 404, - detail=f"No movie found with ID: {id}" + return JSONResponse( + status_code=404, + content=create_error_response( + message=f"No movie found with ID: {id}", + code="MOVIE_NOT_FOUND" + ) ) return create_success_response( @@ -896,10 +1004,11 @@ async def delete_movie_by_id(id: str): """ @router.delete( - "/", - response_model=SuccessResponse[dict], - status_code = 200, - summary="Delete multiple movies matching the given filter." + "/", + response_model=SuccessResponse[dict], + status_code = 200, + summary="Delete multiple movies matching the given filter.", + responses=CRUD_OPERATION_RESPONSES ) async def delete_movies_batch(request_body: dict = Body(...)) -> SuccessResponse[dict]: @@ -909,9 +1018,12 @@ async def delete_movies_batch(request_body: dict = Body(...)) -> SuccessResponse filter_data = request_body.get("filter", {}) if not filter_data: - raise HTTPException( - status_code = 400, - detail="Filter object is required and cannot be empty." + return JSONResponse( + status_code=400, + content=create_error_response( + message="Filter object is required and cannot be empty.", + code="MISSING_FILTER" + ) ) # Convert string IDs to ObjectIds if _id filter is present @@ -921,17 +1033,23 @@ async def delete_movies_batch(request_body: dict = Body(...)) -> SuccessResponse try: filter_data["_id"]["$in"] = [ObjectId(id_str) for id_str in filter_data["_id"]["$in"]] except Exception: - raise HTTPException( - status_code = 400, - detail="Invalid ObjectId format in filter." + return JSONResponse( + status_code=400, + content=create_error_response( + message="Invalid ObjectId format in filter.", + code="INVALID_OBJECT_ID" + ) ) try: result = await movies_collection.delete_many(filter_data) except Exception as e: - raise HTTPException( - status_code = 500, - detail=f"An error occurred while deleting movies: {str(e)}" + return JSONResponse( + status_code=500, + content=create_error_response( + message=f"An error occurred while deleting movies: {str(e)}", + code="DATABASE_ERROR" + ) ) return create_success_response( @@ -949,17 +1067,23 @@ async def delete_movies_batch(request_body: dict = Body(...)) -> SuccessResponse SuccessResponse[Movie]: A response object containing the deleted movie data. """ -@router.delete("/{id}/find-and-delete", - response_model=SuccessResponse[Movie], - status_code = 200, - summary="Find and delete a movie in a single operation.") +@router.delete( + "/{id}/find-and-delete", + response_model=SuccessResponse[Movie], + status_code = 200, + summary="Find and delete a movie in a single operation.", + responses=OBJECTID_VALIDATION_RESPONSES +) async def find_and_delete_movie(id: str): try: object_id = ObjectId(id) except errors.InvalidId: - raise HTTPException( - status_code = 400, - detail=f"Invalid movie ID format: The provided ID '{id}' is not a valid ObjectId" + return JSONResponse( + status_code=400, + content=create_error_response( + message=f"Invalid movie ID format: The provided ID '{id}' is not a valid ObjectId", + code="INVALID_OBJECT_ID" + ) ) movies_collection = get_collection("movies") @@ -969,15 +1093,21 @@ async def find_and_delete_movie(id: str): try: deleted_movie = await movies_collection.find_one_and_delete({"_id": object_id}) except Exception as e: - raise HTTPException( - status_code = 500, - detail=f"Database error occurred: {str(e)}" + return JSONResponse( + status_code=500, + content=create_error_response( + message=f"Database error occurred: {str(e)}", + code="DATABASE_ERROR" + ) ) if deleted_movie is None: - raise HTTPException( - status_code = 404, - detail=f"No movie found with ID: {id}" + return JSONResponse( + status_code=404, + content=create_error_response( + message=f"No movie found with ID: {id}", + code="MOVIE_NOT_FOUND" + ) ) deleted_movie["_id"] = str(deleted_movie["_id"]) # Convert ObjectId to string @@ -994,10 +1124,13 @@ async def find_and_delete_movie(id: str): SuccessResponse[List[dict]]: A response object containing movies with their most recent comments. """ -@router.get("/aggregations/reportingByComments", - response_model=SuccessResponse[List[dict]], - status_code = 200, - summary="Aggregate movies with their most recent comments.") +@router.get( + "/aggregations/reportingByComments", + response_model=SuccessResponse[List[dict]], + status_code = 200, + summary="Aggregate movies with their most recent comments.", + responses=DATABASE_OPERATION_RESPONSES +) async def aggregate_movies_recent_commented( limit: int = Query(default=10, ge=1, le=50), movie_id: str = Query(default=None) @@ -1027,9 +1160,12 @@ async def aggregate_movies_recent_commented( object_id = ObjectId(movie_id) pipeline[0]["$match"]["_id"] = object_id except Exception: - raise HTTPException( - status_code = 400, - detail="The provided movie_id is not a valid ObjectId" + return JSONResponse( + status_code=400, + content=create_error_response( + message="The provided movie_id is not a valid ObjectId", + code="INVALID_OBJECT_ID" + ) ) # Add remaining pipeline stages @@ -1116,9 +1252,12 @@ async def aggregate_movies_recent_commented( try: results = await execute_aggregation(pipeline) except Exception as e: - raise HTTPException( - status_code = 500, - detail=f"Database error occurred during aggregation: {str(e)}" + return JSONResponse( + status_code=500, + content=create_error_response( + message=f"Database error occurred during aggregation: {str(e)}", + code="DATABASE_ERROR" + ) ) # Convert ObjectId to string for response @@ -1142,10 +1281,13 @@ async def aggregate_movies_recent_commented( SuccessResponse[List[dict]]: A response object containing yearly movie statistics. """ -@router.get("/aggregations/reportingByYear", - response_model=SuccessResponse[List[dict]], - status_code = 200, - summary="Aggregate movies by year with average rating and movie count.") +@router.get( + "/aggregations/reportingByYear", + response_model=SuccessResponse[List[dict]], + status_code = 200, + summary="Aggregate movies by year with average rating and movie count.", + responses=DATABASE_OPERATION_RESPONSES +) async def aggregate_movies_by_year(): # Define aggregation pipeline to group movies by year with statistics # This pipeline demonstrates grouping, statistical calculations, and data cleaning @@ -1246,9 +1388,12 @@ async def aggregate_movies_by_year(): try: results = await execute_aggregation(pipeline) except Exception as e: - raise HTTPException( - status_code = 500, - detail=f"Database error occurred during aggregation: {str(e)}" + return JSONResponse( + status_code=500, + content=create_error_response( + message=f"Database error occurred during aggregation: {str(e)}", + code="DATABASE_ERROR" + ) ) return create_success_response( @@ -1266,10 +1411,13 @@ async def aggregate_movies_by_year(): SuccessResponse[List[dict]]: A response object containing director statistics. """ -@router.get("/aggregations/reportingByDirectors", - response_model=SuccessResponse[List[dict]], - status_code = 200, - summary="Aggregate directors with the most movies and their statistics.") +@router.get( + "/aggregations/reportingByDirectors", + response_model=SuccessResponse[List[dict]], + status_code = 200, + summary="Aggregate directors with the most movies and their statistics.", + responses=DATABASE_OPERATION_RESPONSES +) async def aggregate_directors_most_movies( limit: int = Query(default=20, ge=1, le=100) ): @@ -1344,9 +1492,12 @@ async def aggregate_directors_most_movies( try: results = await execute_aggregation(pipeline) except Exception as e: - raise HTTPException( - status_code = 500, - detail=f"Database error occurred during aggregation: {str(e)}" + return JSONResponse( + status_code=500, + content=create_error_response( + message=f"Database error occurred during aggregation: {str(e)}", + code="DATABASE_ERROR" + ) ) return create_success_response( diff --git a/mflix/server/python-fastapi/src/utils/response_docs.py b/mflix/server/python-fastapi/src/utils/response_docs.py new file mode 100644 index 0000000..7ce5135 --- /dev/null +++ b/mflix/server/python-fastapi/src/utils/response_docs.py @@ -0,0 +1,322 @@ +""" +OpenAPI Response Documentation Helpers + +This module provides reusable response documentation for FastAPI endpoints +to maintain consistent OpenAPI documentation across all movie API endpoints. +Uses the standardized error format from create_error_response() to match Express backend. +""" + + +# Helper schema for standardized error responses (matches create_error_response format) +ERROR_RESPONSE_SCHEMA = { + "type": "object", + "properties": { + "success": {"type": "boolean"}, + "message": {"type": "string"}, + "error": { + "type": "object", + "properties": { + "message": {"type": "string"}, + "code": {"type": "string"}, + "details": {"type": "string"} + } + }, + "timestamp": {"type": "string"} + } +} + + +# 400 Bad Request Responses +ERROR_400_INVALID_OBJECTID = { + "description": "Bad Request - Invalid ObjectId format", + "content": { + "application/json": { + "schema": { + **ERROR_RESPONSE_SCHEMA, + "example": { + "success": False, + "message": "The provided ID 'invalid_id' is not a valid ObjectId", + "error": { + "message": "The provided ID 'invalid_id' is not a valid ObjectId", + "code": "INVALID_OBJECT_ID", + "details": None + }, + "timestamp": "2024-01-01T12:00:00.000Z" + } + } + } + } +} + +ERROR_400_VALIDATION = { + "description": "Bad Request - Request validation failed", + "content": { + "application/json": { + "schema": { + **ERROR_RESPONSE_SCHEMA, + "example": { + "success": False, + "message": "No valid fields provided for update.", + "error": { + "message": "No valid fields provided for update.", + "code": "NO_UPDATE_DATA", + "details": None + }, + "timestamp": "2024-01-01T12:00:00.000Z" + } + } + } + } +} + +# Combined 400 response for search endpoints (covers both invalid operator and missing params) +ERROR_400_SEARCH_ERRORS = { + "description": "Bad Request - Invalid search operator or missing search parameters", + "content": { + "application/json": { + "schema": ERROR_RESPONSE_SCHEMA, + "examples": { + "invalid_operator": { + "summary": "Invalid search operator", + "value": { + "success": False, + "message": "Invalid search operator 'invalid'. The search operator must be one of {'must', 'should', 'mustNot', 'filter'}.", + "error": { + "message": "Invalid search operator 'invalid'. The search operator must be one of {'must', 'should', 'mustNot', 'filter'}.", + "code": "INVALID_SEARCH_OPERATOR", + "details": None + }, + "timestamp": "2024-01-01T12:00:00.000Z" + } + }, + "missing_params": { + "summary": "Missing search parameters", + "value": { + "success": False, + "message": "At least one search parameter must be provided.", + "error": { + "message": "At least one search parameter must be provided.", + "code": "MISSING_SEARCH_PARAMS", + "details": None + }, + "timestamp": "2024-01-01T12:00:00.000Z" + } + } + } + } + } +} + +# 401 Unauthorized Responses +ERROR_401_VOYAGE_AUTH = { + "description": "Unauthorized - Invalid Voyage AI API key", + "content": { + "application/json": { + "schema": { + **ERROR_RESPONSE_SCHEMA, + "example": { + "success": False, + "message": "Invalid Voyage AI API key", + "error": { + "message": "Invalid Voyage AI API key", + "code": "VOYAGE_AUTH_ERROR", + "details": "Please verify your VOYAGE_API_KEY is correct in the .env file" + }, + "timestamp": "2024-01-01T12:00:00.000Z" + } + } + } + } +} + +# 404 Not Found Responses +ERROR_404_MOVIE_NOT_FOUND = { + "description": "Not Found - Movie not found", + "content": { + "application/json": { + "schema": { + **ERROR_RESPONSE_SCHEMA, + "example": { + "success": False, + "message": "No movie found with ID: 507f1f77bcf86cd799439011", + "error": { + "message": "No movie found with ID: 507f1f77bcf86cd799439011", + "code": "MOVIE_NOT_FOUND", + "details": None + }, + "timestamp": "2024-01-01T12:00:00.000Z" + } + } + } + } +} + +# 422 Unprocessable Entity - FastAPI's auto-generated validation errors +FASTAPI_422_VALIDATION_ERROR = { + "description": "Unprocessable Entity - Validation error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "array", + "items": { + "type": "object", + "properties": { + "loc": {"type": "array"}, + "msg": {"type": "string"}, + "type": {"type": "string"} + } + } + } + }, + "example": { + "detail": [ + { + "loc": ["body", "title"], + "msg": "field required", + "type": "value_error.missing" + } + ] + } + } + } + } +} + +# 500 Internal Server Error Responses +ERROR_500_DATABASE = { + "description": "Internal Server Error - Database operation failed", + "content": { + "application/json": { + "schema": { + **ERROR_RESPONSE_SCHEMA, + "example": { + "success": False, + "message": "Database error occurred: Connection timeout", + "error": { + "message": "Database error occurred: Connection timeout", + "code": "DATABASE_ERROR", + "details": None + }, + "timestamp": "2024-01-01T12:00:00.000Z" + } + } + } + } +} + +ERROR_500_SEARCH = { + "description": "Internal Server Error - Search operation failed", + "content": { + "application/json": { + "schema": { + **ERROR_RESPONSE_SCHEMA, + "example": { + "success": False, + "message": "An error occurred while performing the search: Index not found", + "error": { + "message": "An error occurred while performing the search: Index not found", + "code": "SEARCH_ERROR", + "details": None + }, + "timestamp": "2024-01-01T12:00:00.000Z" + } + } + } + } +} + +ERROR_500_VECTOR_SEARCH = { + "description": "Internal Server Error - Vector search operation failed", + "content": { + "application/json": { + "schema": { + **ERROR_RESPONSE_SCHEMA, + "example": { + "success": False, + "message": "Error performing vector search: Embedding generation failed", + "error": { + "message": "Error performing vector search: Embedding generation failed", + "code": "VECTOR_SEARCH_ERROR", + "details": None + }, + "timestamp": "2024-01-01T12:00:00.000Z" + } + } + } + } +} + +# 503 Service Unavailable Responses +ERROR_503_VOYAGE = { + "description": "Service Unavailable - Vector search service unavailable", + "content": { + "application/json": { + "schema": ERROR_RESPONSE_SCHEMA, + "examples": { + "api_key_not_configured": { + "summary": "Voyage API key not configured", + "value": { + "success": False, + "message": "Vector search unavailable: VOYAGE_API_KEY not configured. Please add your API key to the .env file", + "error": { + "message": "Vector search unavailable: VOYAGE_API_KEY not configured. Please add your API key to the .env file", + "code": "SERVICE_UNAVAILABLE", + "details": None + }, + "timestamp": "2024-01-01T12:00:00.000Z" + } + }, + "voyage_api_error": { + "summary": "Voyage AI API error", + "value": { + "success": False, + "message": "Vector search service unavailable", + "error": { + "message": "Vector search service unavailable", + "code": "VOYAGE_API_ERROR", + "details": "Voyage AI service temporarily unavailable" + }, + "timestamp": "2024-01-01T12:00:00.000Z" + } + } + } + } + } +} + + +# Common response combinations for different endpoint types +OBJECTID_VALIDATION_RESPONSES = { + 400: ERROR_400_INVALID_OBJECTID, + 404: ERROR_404_MOVIE_NOT_FOUND, + 500: ERROR_500_DATABASE +} + +SEARCH_ENDPOINT_RESPONSES = { + 400: ERROR_400_SEARCH_ERRORS, + 500: ERROR_500_SEARCH +} + +VECTOR_SEARCH_RESPONSES = { + 401: ERROR_401_VOYAGE_AUTH, + 500: ERROR_500_VECTOR_SEARCH, + 503: ERROR_503_VOYAGE +} + +DATABASE_OPERATION_RESPONSES = { + 500: ERROR_500_DATABASE +} + +CRUD_OPERATION_RESPONSES = { + 400: ERROR_400_VALIDATION, + 422: FASTAPI_422_VALIDATION_ERROR, + 500: ERROR_500_DATABASE +} + +CRUD_WITH_OBJECTID_RESPONSES = { + **OBJECTID_VALIDATION_RESPONSES, + 422: FASTAPI_422_VALIDATION_ERROR +} \ No newline at end of file diff --git a/mflix/server/python-fastapi/tests/integration/test_movie_routes_integration.py b/mflix/server/python-fastapi/tests/integration/test_movie_routes_integration.py index c9c9730..30cc6c6 100644 --- a/mflix/server/python-fastapi/tests/integration/test_movie_routes_integration.py +++ b/mflix/server/python-fastapi/tests/integration/test_movie_routes_integration.py @@ -126,8 +126,8 @@ async def test_delete_movie(self, client, test_movie_data): get_response = await client.get(f"/api/movies/{movie_id}") assert get_response.status_code == 404 error_data = get_response.json() - assert "detail" in error_data - assert "no movie found" in error_data["detail"].lower() + assert error_data["success"] is False + assert error_data["error"]["code"] == "MOVIE_NOT_FOUND" # No cleanup needed - movie already deleted @@ -276,13 +276,12 @@ async def test_batch_delete_movies(self, client, multiple_test_movies): assert delete_data["data"]["deletedCount"] == 3 # Verify all movies were deleted - # Note: The API returns 200 with INTERNAL_SERVER_ERROR code, not 404 for movie_id in multiple_test_movies: get_response = await client.get(f"/api/movies/{movie_id}") assert get_response.status_code == 404 error_data = get_response.json() - assert "detail" in error_data - assert "no movie found" in error_data["detail"].lower() + assert error_data["success"] is False + assert error_data["error"]["code"] == "MOVIE_NOT_FOUND" # Note: Fixture cleanup will try to delete but movies are already gone # The fixture should handle this gracefully diff --git a/mflix/server/python-fastapi/tests/test_movie_routes.py b/mflix/server/python-fastapi/tests/test_movie_routes.py index 8bb335d..b9f12f8 100644 --- a/mflix/server/python-fastapi/tests/test_movie_routes.py +++ b/mflix/server/python-fastapi/tests/test_movie_routes.py @@ -6,10 +6,11 @@ an actual database connection or server instance. """ +import json import pytest from unittest.mock import AsyncMock, MagicMock, patch from bson import ObjectId -from fastapi import HTTPException +from fastapi.responses import JSONResponse from src.models.models import CreateMovieRequest, UpdateMovieRequest from src.utils.exceptions import VoyageAuthError, VoyageAPIError @@ -59,23 +60,27 @@ async def test_get_movie_by_id_not_found(self, mock_get_collection): # Import and call the route handler from src.routers.movies import get_movie_by_id - with pytest.raises(HTTPException) as e: - await get_movie_by_id(TEST_MOVIE_ID) + response = await get_movie_by_id(TEST_MOVIE_ID) # Assertions - assert e.value.status_code == 404 - assert "no movie found" in str(e.value.detail).lower() + assert isinstance(response, JSONResponse) + assert response.status_code == 404 + body = json.loads(response.body.decode()) + assert body["success"] is False + assert body["error"]["code"] == "MOVIE_NOT_FOUND" async def test_get_movie_by_id_invalid_id(self): """Should return error when invalid ObjectId format is provided.""" # Import and call the route handler from src.routers.movies import get_movie_by_id - with pytest.raises(HTTPException) as e: - await get_movie_by_id(INVALID_MOVIE_ID) + response = await get_movie_by_id(INVALID_MOVIE_ID) # Assertions - assert e.value.status_code == 400 - assert " not a valid" in str(e.value.detail).lower() + assert isinstance(response, JSONResponse) + assert response.status_code == 400 + body = json.loads(response.body.decode()) + assert body["success"] is False + assert body["error"]["code"] == "INVALID_OBJECT_ID" @patch('src.routers.movies.get_collection') async def test_get_movie_by_id_database_error(self, mock_get_collection): @@ -87,12 +92,14 @@ async def test_get_movie_by_id_database_error(self, mock_get_collection): # Import and call the route handler from src.routers.movies import get_movie_by_id - with pytest.raises(HTTPException) as e: - await get_movie_by_id(TEST_MOVIE_ID) + response = await get_movie_by_id(TEST_MOVIE_ID) # Assertions - assert e.value.status_code == 500 - assert "error" in str(e.value.detail).lower() + assert isinstance(response, JSONResponse) + assert response.status_code == 500 + body = json.loads(response.body.decode()) + assert body["success"] is False + assert body["error"]["code"] == "DATABASE_ERROR" @pytest.mark.unit @@ -145,12 +152,14 @@ async def test_create_movie_database_error(self, mock_get_collection): # Create request from src.routers.movies import create_movie movie_request = CreateMovieRequest(title="New Movie") - with pytest.raises(HTTPException) as e: - await create_movie(movie_request) + response = await create_movie(movie_request) # Assertions - assert e.value.status_code == 500 - assert "error" in str(e.value.detail).lower() + assert isinstance(response, JSONResponse) + assert response.status_code == 500 + body = json.loads(response.body.decode()) + assert body["success"] is False + assert body["error"]["code"] == "DATABASE_ERROR" @pytest.mark.unit @@ -201,12 +210,14 @@ async def test_update_movie_not_found(self, mock_get_collection): from src.routers.movies import update_movie update_request = UpdateMovieRequest(title="Updated Movie") - with pytest.raises(HTTPException) as e: - await update_movie(update_request, TEST_MOVIE_ID) - - #Assertions - assert e.value.status_code == 404 - assert "no movie" in str(e.value.detail.lower()) + response = await update_movie(update_request, TEST_MOVIE_ID) + + # Assertions + assert isinstance(response, JSONResponse) + assert response.status_code == 404 + body = json.loads(response.body.decode()) + assert body["success"] is False + assert body["error"]["code"] == "MOVIE_NOT_FOUND" async def test_update_movie_invalid_id(self): """Should return error when invalid ObjectId format is provided.""" @@ -214,12 +225,14 @@ async def test_update_movie_invalid_id(self): from src.routers.movies import update_movie update_request = UpdateMovieRequest(title="Updated Movie") - with pytest.raises(HTTPException) as e: - await update_movie(update_request, INVALID_MOVIE_ID) + response = await update_movie(update_request, INVALID_MOVIE_ID) - # Assertions - assert e.value.status_code == 400 - assert "invalid" in str(e.value.detail.lower()) + # Assertions + assert isinstance(response, JSONResponse) + assert response.status_code == 400 + body = json.loads(response.body.decode()) + assert body["success"] is False + assert body["error"]["code"] == "INVALID_OBJECT_ID" @pytest.mark.unit @@ -258,23 +271,27 @@ async def test_delete_movie_not_found(self, mock_get_collection): # Call the route handler from src.routers.movies import delete_movie_by_id - with pytest.raises(HTTPException) as e: - await delete_movie_by_id(TEST_MOVIE_ID) + response = await delete_movie_by_id(TEST_MOVIE_ID) - # Assertions - assert e.value.status_code == 404 - assert "no movie" in str(e.value.detail.lower()) + # Assertions + assert isinstance(response, JSONResponse) + assert response.status_code == 404 + body = json.loads(response.body.decode()) + assert body["success"] is False + assert body["error"]["code"] == "MOVIE_NOT_FOUND" async def test_delete_movie_invalid_id(self): """Should return error when invalid ObjectId format is provided.""" # Call the route handler from src.routers.movies import delete_movie_by_id - with pytest.raises(HTTPException) as e: - await delete_movie_by_id(INVALID_MOVIE_ID) + response = await delete_movie_by_id(INVALID_MOVIE_ID) # Assertions - assert e.value.status_code == 400 - assert "invalid movie id" in str(e.value.detail.lower()) + assert isinstance(response, JSONResponse) + assert response.status_code == 400 + body = json.loads(response.body.decode()) + assert body["success"] is False + assert body["error"]["code"] == "INVALID_OBJECT_ID" @patch('src.routers.movies.get_collection') async def test_delete_movie_database_error(self, mock_get_collection): @@ -286,12 +303,14 @@ async def test_delete_movie_database_error(self, mock_get_collection): # Call the route handler from src.routers.movies import delete_movie_by_id - with pytest.raises(HTTPException) as e: - await delete_movie_by_id(TEST_MOVIE_ID) + response = await delete_movie_by_id(TEST_MOVIE_ID) - # Assertions - assert e.value.status_code == 500 - assert "error" in str(e.value.detail.lower()) + # Assertions + assert isinstance(response, JSONResponse) + assert response.status_code == 500 + body = json.loads(response.body.decode()) + assert body["success"] is False + assert body["error"]["code"] == "DATABASE_ERROR" @pytest.mark.unit @pytest.mark.asyncio @@ -394,12 +413,14 @@ async def test_get_all_movies_database_error(self, mock_get_collection): # Call the route handler from src.routers.movies import get_all_movies - with pytest.raises(HTTPException) as e: - await get_all_movies() + response = await get_all_movies() # Assertions - assert e.value.status_code == 500 - assert "error" in str(e.value.detail.lower()) + assert isinstance(response, JSONResponse) + assert response.status_code == 500 + body = json.loads(response.body.decode()) + assert body["success"] is False + assert body["error"]["code"] == "DATABASE_ERROR" @pytest.mark.unit @@ -441,12 +462,14 @@ async def test_create_movies_batch_empty_list(self, mock_get_collection): # Create request with empty list from src.routers.movies import create_movies_batch - with pytest.raises(HTTPException) as e: - await create_movies_batch([]) + response = await create_movies_batch([]) # Assertions - assert e.value.status_code == 400 - assert "empty" in str(e.value.detail.lower()) + assert isinstance(response, JSONResponse) + assert response.status_code == 400 + body = json.loads(response.body.decode()) + assert body["success"] is False + assert body["error"]["code"] == "EMPTY_REQUEST" @patch('src.routers.movies.get_collection') async def test_delete_movies_batch_success(self, mock_get_collection): @@ -476,12 +499,14 @@ async def test_delete_movies_batch_missing_filter(self, mock_get_collection): # Create request without filter from src.routers.movies import delete_movies_batch request_body = {} - with pytest.raises(HTTPException) as e: - await delete_movies_batch(request_body) + response = await delete_movies_batch(request_body) # Assertions - assert e.value.status_code == 400 - assert "filter" in e.value.detail.lower() + assert isinstance(response, JSONResponse) + assert response.status_code == 400 + body = json.loads(response.body.decode()) + assert body["success"] is False + assert body["error"]["code"] == "MISSING_FILTER" @@ -523,23 +548,27 @@ async def test_find_and_delete_not_found(self, mock_get_collection): # Call the route handler from src.routers.movies import find_and_delete_movie - with pytest.raises(HTTPException) as e: - await find_and_delete_movie(TEST_MOVIE_ID) + response = await find_and_delete_movie(TEST_MOVIE_ID) # Assertions - assert e.value.status_code == 404 - assert "no movie" in str(e.value.detail.lower()) + assert isinstance(response, JSONResponse) + assert response.status_code == 404 + body = json.loads(response.body.decode()) + assert body["success"] is False + assert body["error"]["code"] == "MOVIE_NOT_FOUND" async def test_find_and_delete_invalid_id(self): """Should return error when invalid ObjectId format is provided.""" # Call the route handler from src.routers.movies import find_and_delete_movie - with pytest.raises(HTTPException) as e: - await find_and_delete_movie(INVALID_MOVIE_ID) + response = await find_and_delete_movie(INVALID_MOVIE_ID) # Assertions - assert e.value.status_code == 400 - assert "invalid" in str(e.value.detail.lower()) + assert isinstance(response, JSONResponse) + assert response.status_code == 400 + body = json.loads(response.body.decode()) + assert body["success"] is False + assert body["error"]["code"] == "INVALID_OBJECT_ID" @pytest.mark.unit @@ -580,12 +609,14 @@ async def test_update_movies_batch_missing_filter(self, mock_get_collection): # Create request without filter from src.routers.movies import update_movies_batch request_body = {"update": {"$set": {"rated": "PG-13"}}} - with pytest.raises(HTTPException) as e: - await update_movies_batch(request_body) + response = await update_movies_batch(request_body) # Assertions - assert e.value.status_code == 400 - assert "filter" in str(e.value.detail).lower() + assert isinstance(response, JSONResponse) + assert response.status_code == 400 + body = json.loads(response.body.decode()) + assert body["success"] is False + assert body["error"]["code"] == "MISSING_FILTER" @patch('src.routers.movies.get_collection') async def test_update_movies_batch_missing_update(self, mock_get_collection): @@ -595,12 +626,14 @@ async def test_update_movies_batch_missing_update(self, mock_get_collection): # Create request without update from src.routers.movies import update_movies_batch request_body = {"filter": {"year": 2020}} - with pytest.raises(HTTPException) as e: - await update_movies_batch(request_body) + response = await update_movies_batch(request_body) - # Assertions - assert e.value.status_code == 400 - assert "update" in str(e.value.detail).lower() + # Assertions - code returns MISSING_FILTER for both missing filter and missing update + assert isinstance(response, JSONResponse) + assert response.status_code == 400 + body = json.loads(response.body.decode()) + assert body["success"] is False + assert body["error"]["code"] == "MISSING_FILTER" @patch('src.routers.movies.get_collection') async def test_update_movies_batch_no_matches(self, mock_get_collection): @@ -700,22 +733,26 @@ async def test_search_movies_with_pagination(self, mock_execute_aggregation): async def test_search_movies_no_parameters(self): """Should return error when no search parameters provided.""" from src.routers.movies import search_movies - with pytest.raises(HTTPException) as e: - await search_movies(search_operator="must") + response = await search_movies(search_operator="must") # Assertions - assert e.value.status_code == 400 - assert "one search parameter" in str(e.value.detail).lower() + assert isinstance(response, JSONResponse) + assert response.status_code == 400 + body = json.loads(response.body.decode()) + assert body["success"] is False + assert body["error"]["code"] == "MISSING_SEARCH_PARAMS" async def test_search_movies_invalid_operator(self): """Should return error for invalid search operator.""" from src.routers.movies import search_movies - with pytest.raises(HTTPException) as e: - await search_movies(plot="test", search_operator="invalid") + response = await search_movies(plot="test", search_operator="invalid") # Assertions - assert e.value.status_code == 400 - assert "invalid search operator" in str(e.value.detail).lower() + assert isinstance(response, JSONResponse) + assert response.status_code == 400 + body = json.loads(response.body.decode()) + assert body["success"] is False + assert body["error"]["code"] == "INVALID_SEARCH_OPERATOR" @patch('src.routers.movies.execute_aggregation') async def test_search_movies_database_error(self, mock_execute_aggregation): @@ -725,12 +762,14 @@ async def test_search_movies_database_error(self, mock_execute_aggregation): # Call the route handler from src.routers.movies import search_movies - with pytest.raises(HTTPException) as e: - await search_movies(plot="test", search_operator="must") + response = await search_movies(plot="test", search_operator="must") # Assertions - assert e.value.status_code == 500 - assert "error" in str(e.value.detail).lower() + assert isinstance(response, JSONResponse) + assert response.status_code == 500 + body = json.loads(response.body.decode()) + assert body["success"] is False + assert body["error"]["code"] == "SEARCH_ERROR" @patch('src.routers.movies.execute_aggregation') async def test_search_movies_empty_results(self, mock_execute_aggregation): @@ -770,7 +809,7 @@ async def test_vector_search_unavailable(self, mock_voyage_available): # Assertions assert isinstance(response, JSONResponse) - assert response.status_code == 400 + assert response.status_code == 503 # Parse the response body import json @@ -827,12 +866,14 @@ async def test_vector_search_embedding_error(self, mock_get_embedding, mock_voya # Call the route handler from src.routers.movies import vector_search_movies - with pytest.raises(HTTPException) as e: - await vector_search_movies(q="action movie") + response = await vector_search_movies(q="action movie") # Assertions - assert e.value.status_code == 500 - assert "error" in str(e.value.detail).lower() + assert isinstance(response, JSONResponse) + assert response.status_code == 500 + body = json.loads(response.body.decode()) + assert body["success"] is False + assert body["error"]["code"] == "VECTOR_SEARCH_ERROR" @patch('src.routers.movies.voyage_ai_available') @patch('src.routers.movies.voyageai.Client') @@ -926,12 +967,14 @@ async def test_aggregate_movies_by_movie_id(self, mock_execute_aggregation): async def test_aggregate_movies_invalid_movie_id(self): """Should return error for invalid movie ID format.""" from src.routers.movies import aggregate_movies_recent_commented - with pytest.raises(HTTPException) as e: - await aggregate_movies_recent_commented(movie_id="invalid_id") + response = await aggregate_movies_recent_commented(movie_id="invalid_id") # Assertions - assert e.value.status_code == 400 - assert "movie_id is not" in str(e.value.detail).lower() + assert isinstance(response, JSONResponse) + assert response.status_code == 400 + body = json.loads(response.body.decode()) + assert body["success"] is False + assert body["error"]["code"] == "INVALID_OBJECT_ID" @patch('src.routers.movies.execute_aggregation') async def test_aggregate_movies_database_error(self, mock_execute_aggregation): @@ -941,12 +984,14 @@ async def test_aggregate_movies_database_error(self, mock_execute_aggregation): # Call the route handler from src.routers.movies import aggregate_movies_recent_commented - with pytest.raises(HTTPException) as e: - await aggregate_movies_recent_commented(limit=10, movie_id=None) + response = await aggregate_movies_recent_commented(limit=10, movie_id=None) # Assertions - assert e.value.status_code == 500 - assert "error" in str(e.value.detail).lower() + assert isinstance(response, JSONResponse) + assert response.status_code == 500 + body = json.loads(response.body.decode()) + assert body["success"] is False + assert body["error"]["code"] == "DATABASE_ERROR" @patch('src.routers.movies.execute_aggregation') async def test_aggregate_movies_empty_results(self, mock_execute_aggregation): @@ -997,12 +1042,14 @@ async def test_aggregate_movies_by_year_database_error(self, mock_execute_aggreg # Call the route handler from src.routers.movies import aggregate_movies_by_year - with pytest.raises(HTTPException) as e: - await aggregate_movies_by_year() + response = await aggregate_movies_by_year() # Assertions - assert e.value.status_code == 500 - assert "error" in str(e.value.detail).lower() + assert isinstance(response, JSONResponse) + assert response.status_code == 500 + body = json.loads(response.body.decode()) + assert body["success"] is False + assert body["error"]["code"] == "DATABASE_ERROR" @patch('src.routers.movies.execute_aggregation') async def test_aggregate_movies_by_year_empty_results(self, mock_execute_aggregation): @@ -1071,12 +1118,14 @@ async def test_aggregate_directors_database_error(self, mock_execute_aggregation # Call the route handler from src.routers.movies import aggregate_directors_most_movies - with pytest.raises(HTTPException) as e: - await aggregate_directors_most_movies() + response = await aggregate_directors_most_movies() # Assertions - assert e.value.status_code == 500 - assert "error" in str(e.value.detail).lower() + assert isinstance(response, JSONResponse) + assert response.status_code == 500 + body = json.loads(response.body.decode()) + assert body["success"] is False + assert body["error"]["code"] == "DATABASE_ERROR" @patch('src.routers.movies.execute_aggregation') async def test_aggregate_directors_empty_results(self, mock_execute_aggregation): @@ -1164,9 +1213,11 @@ async def test_get_distinct_genres_database_error(self, mock_get_collection): # Call the route handler from src.routers.movies import get_distinct_genres - with pytest.raises(HTTPException) as exc_info: - await get_distinct_genres() + response = await get_distinct_genres() # Assertions - assert exc_info.value.status_code == 500 - assert "Database error" in str(exc_info.value.detail) + assert isinstance(response, JSONResponse) + assert response.status_code == 500 + body = json.loads(response.body.decode()) + assert body["success"] is False + assert body["error"]["code"] == "DATABASE_ERROR"