diff --git a/android/scripts/dev_setup.sh b/android/scripts/dev_setup.sh index 4a88374..65a2c88 100755 --- a/android/scripts/dev_setup.sh +++ b/android/scripts/dev_setup.sh @@ -2,6 +2,10 @@ set -e +# Set Java version for the session +export JAVA_HOME=$(/Users/garotconklin/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home) +export PATH="$JAVA_HOME/bin:$PATH" + echo "๐Ÿš€ Setting up development environment..." # Function to check if a command exists diff --git a/android/scripts/fix_formatting.sh b/android/scripts/fix_formatting.sh index fab3073..b3d6d6d 100755 --- a/android/scripts/fix_formatting.sh +++ b/android/scripts/fix_formatting.sh @@ -3,6 +3,10 @@ # Exit on error set -e +# Set Java version for the session +export JAVA_HOME=$(/Users/garotconklin/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home) +export PATH="$JAVA_HOME/bin:$PATH" + echo "๐Ÿ”ง Fixing file formatting..." # Find all Kotlin files diff --git a/android/scripts/format_and_lint.sh b/android/scripts/format_and_lint.sh index 2810381..4e20ff8 100755 --- a/android/scripts/format_and_lint.sh +++ b/android/scripts/format_and_lint.sh @@ -2,6 +2,10 @@ set -e +# Set Java version for the session +export JAVA_HOME=$(/Users/garotconklin/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home) +export PATH="$JAVA_HOME/bin:$PATH" + # Change to the android directory (parent of scripts directory) cd "$(dirname "$0")/.." diff --git a/android/scripts/run_local.sh b/android/scripts/run_local.sh new file mode 100644 index 0000000..abeb425 --- /dev/null +++ b/android/scripts/run_local.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +# Set Java version for the session +export JAVA_HOME=$(/Users/garotconklin/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home) +export PATH="$JAVA_HOME/bin:$PATH" + +# Run ktlint +echo "๐Ÿงน Running code formatting..." +echo "Running ktlint..." +./gradlew ktlintFormat + +# Run the build +echo "๐Ÿ—๏ธ Building Android project..." +./gradlew build \ No newline at end of file diff --git a/backend/__init__.py b/backend/__init__.py new file mode 100644 index 0000000..3e87812 --- /dev/null +++ b/backend/__init__.py @@ -0,0 +1 @@ +"""Backend package initialization.""" diff --git a/backend/config/__init__.py b/backend/config/__init__.py index 2e36b25..bba9c6a 100644 --- a/backend/config/__init__.py +++ b/backend/config/__init__.py @@ -1,4 +1,4 @@ -"""Configuration package.""" +"""Config package initialization.""" from .environment import Environment diff --git a/backend/docs/ANDROID_INTEGRATION.md b/backend/docs/ANDROID_INTEGRATION.md new file mode 100644 index 0000000..807f870 --- /dev/null +++ b/backend/docs/ANDROID_INTEGRATION.md @@ -0,0 +1,261 @@ +# Android Integration Guide + +This guide outlines the steps required to integrate the RunOn backend with the Android application. + +## 1. Backend API Documentation + +### Generate OpenAPI Specification + +First, we need to generate OpenAPI/Swagger documentation for the Android team: + +```bash +# In backend directory +uvicorn main:app --reload --host 0.0.0.0 --port 8000 +``` + +Access the API documentation at: + +- Swagger UI: `http://localhost:8000/docs` +- ReDoc: `http://localhost:8000/redoc` +- OpenAPI JSON: `http://localhost:8000/openapi.json` + +### Current Endpoints + +#### Authentication + +```http +POST /auth/google +Content-Type: application/json +Authorization: Bearer {GOOGLE_ID_TOKEN} + +Response: { + "access_token": "string", + "token_type": "bearer" +} +``` + +#### Event Search + +```http +POST /events/search +Content-Type: application/json +Authorization: Bearer {ACCESS_TOKEN} + +Query Parameters: +- query: string (required) + +Response: [ + "event1", + "event2" +] +``` + +## 2. Android Setup Requirements + +### 1. Dependencies + +Add to `app/build.gradle`: + +```gradle +dependencies { + // Retrofit for API calls + implementation 'com.squareup.retrofit2:retrofit:2.9.0' + implementation 'com.squareup.retrofit2:converter-gson:2.9.0' + + // Google Auth + implementation 'com.google.android.gms:play-services-auth:20.7.0' + + // Coroutines for async operations + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.7.3' +} +``` + +### 2. API Interface + +Create `app/src/main/java/com/flexrpl/runon/api/RunOnApi.kt`: + +```kotlin +interface RunOnApi { + @POST("events/search") + suspend fun searchEvents( + @Query("query") query: String, + @Header("Authorization") auth: String + ): List + + @POST("auth/google") + suspend fun authenticateGoogle( + @Header("Authorization") idToken: String + ): AuthResponse + + data class AuthResponse( + val access_token: String, + val token_type: String + ) +} +``` + +### 3. API Client + +Create `app/src/main/java/com/flexrpl/runon/api/ApiClient.kt`: + +```kotlin +object ApiClient { + private const val BASE_URL = "http://your-backend-url:8000/" + + private val retrofit = Retrofit.Builder() + .baseUrl(BASE_URL) + .addConverterFactory(GsonConverterFactory.create()) + .build() + + val api: RunOnApi = retrofit.create(RunOnApi::class.java) +} +``` + +## 3. Backend Deployment for Android Development + +### 1. Local Testing + +For local Android development: + +```bash +# In backend directory +./scripts/run_local.sh +``` + +Update Android `BASE_URL` to: + +```kotlin +private const val BASE_URL = "http://10.0.2.2:8000/" // Android Emulator +// or +private const val BASE_URL = "http://your-computer-ip:8000/" // Physical Device +``` + +### 2. Production Deployment + +For production: + +1. Deploy backend to cloud provider (e.g., Google Cloud Run) +2. Update Android `BASE_URL` to production URL +3. Enable HTTPS +4. Update CORS settings in backend + +## 4. Security Considerations + +### 1. Backend Updates Required + +Add to `main.py`: + +```python +from fastapi.middleware.cors import CORSMiddleware + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # Update with specific Android app scheme + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) +``` + +### 2. Android Security + +1. Store sensitive data in Android Keystore +2. Implement certificate pinning +3. Implement token refresh mechanism +4. Handle expired tokens gracefully + +## 5. Testing Integration + +### 1. Backend Integration Tests + +Create `backend/tests/integration/test_android_integration.py`: + +```python +def test_android_auth_flow(): + # Test complete Android authentication flow + pass + +def test_android_search_flow(): + # Test Android search functionality + pass +``` + +### 2. Android Integration Tests + +Create appropriate tests in Android project: + +```kotlin +@Test +fun testBackendIntegration() { + // Test API calls to backend +} +``` + +## 6. Debugging Tips + +### Backend Debugging + +1. Enable detailed logging: + + ```python + logging.basicConfig(level=logging.DEBUG) + ``` + +2. Add Android-specific debug endpoints: + + ```python + @app.get("/debug/android") + async def debug_android(): + return {"status": "ok", "version": "1.0"} + ``` + +### Android Debugging + +1. Use OkHttp logging interceptor: + + ```kotlin + implementation 'com.squareup.okhttp3:logging-interceptor:4.9.1' + ``` + +2. Add debug logging: + + ```kotlin + if (BuildConfig.DEBUG) { + // Add logging interceptor + } + ``` + +## 7. Next Steps + +1. Deploy backend to staging environment +2. Update Android app configuration +3. Implement error handling +4. Add monitoring and analytics +5. Set up CI/CD pipeline for both components +6. Plan for scalability and performance testing + +## Common Integration Issues + +1. **CORS Issues** + - Verify CORS middleware configuration + - Check Android network security config + +2. **Authentication Flow** + - Test token exchange process + - Verify Google Sign-In configuration + +3. **Network Issues** + - Configure Android network security + - Handle various network states + +4. **Version Compatibility** + - Maintain API version compatibility + - Plan for backward compatibility + +## Resources + +- [FastAPI Documentation](https://fastapi.tiangolo.com/) +- [Android Network Security Config](https://developer.android.com/training/articles/security-config) +- [Google Sign-In for Android](https://developers.google.com/identity/sign-in/android) +- [Retrofit Documentation](https://square.github.io/retrofit/) diff --git a/backend/docs/LOCAL_ENV_TESTING.md b/backend/docs/LOCAL_ENV_TESTING.md new file mode 100644 index 0000000..b8d80cc --- /dev/null +++ b/backend/docs/LOCAL_ENV_TESTING.md @@ -0,0 +1,233 @@ +# Local Environment Testing Guide + +This guide provides detailed instructions for setting up and testing the RunOn backend in a local development environment. + +## Prerequisites + +- Python 3.9 or higher +- Google Cloud Platform account with the following APIs enabled: + - Google Calendar API + - Google Custom Search API + - Google OAuth 2.0 +- macOS, Linux, or WSL for Windows users + +## Environment Setup + +### 1. Google Cloud Configuration + +1. **Access Google Cloud Console** + - Navigate to [Google Cloud Console](https://console.cloud.google.com) + - Create a new project or select an existing one + +2. **Enable Required APIs** + - In the left sidebar, go to "APIs & Services" > "Library" + - Search for and enable: + - Google Calendar API + - Google Custom Search API + +3. **Create OAuth Credentials** + - Go to "APIs & Services" > "Credentials" + - Click "Create Credentials" > "OAuth client ID" + - If prompted, configure the OAuth consent screen: + - User Type: External + - App name: RunOn + - User support email: your email + - Developer contact information: your email + - For application type, select "Web application" + - Add authorized redirect URIs: + - `http://localhost:8000/auth/callback` + - Save your Client ID and Client Secret + +### 2. Local Environment Configuration + +1. **Environment Variables** + Create a `.env` file in the project root: + + ```bash + # Required OAuth credentials + RUNON_CLIENT_ID=your_client_id_here + RUNON_API_KEY=your_api_key_here + RUNON_SEARCH_ENGINE_ID=your_search_engine_id_here + ``` + +2. **Verify File Permissions** + + ```bash + chmod 600 .env # Restrict access to owner only + ``` + +## Running the Backend + +### 1. Initial Setup + +```bash +# Navigate to backend directory +cd backend + +# Make the run script executable +chmod +x scripts/run_local.sh +``` + +### 2. Start the Server + +```bash +./scripts/run_local.sh +``` + +This script performs the following: + +- Removes existing virtual environment (if any) +- Creates a new Python virtual environment +- Installs all required dependencies +- Sets up environment variables +- Starts the FastAPI server on port 8000 + +### 3. Testing Endpoints + +#### Health Check + +```bash +curl http://localhost:8000/health +``` +Expected response: + +```json +{"status": "healthy"} +``` + +#### Search Events + +```bash +# In a new terminal: +cd /path/to/RunOn && \ +while IFS= read -r line; do \ + [[ $line =~ ^#.*$ ]] && continue; \ + [[ -z $line ]] && continue; \ + export "$line"; \ +done < <(grep -v '^#' .env | grep -v '^$') && \ +curl -X POST "http://localhost:8000/events/search?query=Boston%20Marathon" \ +-H "Authorization: Bearer $RUNON_CLIENT_ID" \ +-H "Content-Type: application/json" +``` + +Expected response (development): + +```json +["event1", "event2"] +``` + +## Troubleshooting + +### Common Issues and Solutions + +1. **Virtual Environment Issues** + + ```bash + # Remove existing venv + rm -rf backend/venv + # Try running setup again + ./scripts/run_local.sh + ``` + +2. **Permission Denied for run_local.sh** + + ```bash + chmod +x scripts/run_local.sh + ``` + +3. **Environment Variables Not Loading** + + ```bash + # Check .env file exists + ls -la .env + + # Verify file contents (safely) + grep -v '^#' .env | grep -v '^$' + ``` + +4. **Authentication Errors** + - Verify `RUNON_CLIENT_ID` matches Google Cloud Console + - Check API enablement status in Google Cloud Console + - Verify OAuth consent screen configuration + +5. **Dependencies Installation Fails** + - Check Python version: + + ```bash + python3 --version # Should be 3.9+ + ``` + + - On macOS, ensure Xcode Command Line Tools: + + ```bash + xcode-select --install + ``` + +### Debug Mode + +For more detailed logging: + +1. **Enable Debug Logging** + Add to `.env`: + + ```bash + DEBUG=True + ``` + +2. **View Logs** + + ```bash + tail -f backend/logs/app.log + ``` + +## Development Tips + +### Testing Changes + +1. **Auto-reload** + The server automatically reloads when you modify Python files. + +2. **Manual Restart** + If needed: + + ```bash + # Stop the server (Ctrl+C) + # Restart + ./scripts/run_local.sh + ``` + +### Code Quality + +Before committing changes: + +```bash + +# Run formatting and linting +bash scripts/format_and_lint.sh + +# Run tests +python -m pytest +``` + +## Security Notes + +- Never commit `.env` file +- Keep API keys secure +- Use environment variables for sensitive data +- Regularly update dependencies +- Monitor Google Cloud Console for suspicious activity + +## Getting Help + +- Check [GitHub Issues](https://github.com/fleXRPL/RunOn/issues) +- Review [Project Wiki](https://github.com/fleXRPL/RunOn/wiki) +- Join [Discussions](https://github.com/fleXRPL/RunOn/discussions) + +## Next Steps + +After successful local testing: + +1. Review the main README.md for full project context +2. Check the Android app setup guide +3. Review contribution guidelines +4. Consider setting up CI/CD integration \ No newline at end of file diff --git a/backend/functions/__init__.py b/backend/functions/__init__.py index e69de29..f4d283b 100644 --- a/backend/functions/__init__.py +++ b/backend/functions/__init__.py @@ -0,0 +1 @@ +"""Functions package initialization.""" diff --git a/backend/functions/auth/__init__.py b/backend/functions/auth/__init__.py new file mode 100644 index 0000000..b853202 --- /dev/null +++ b/backend/functions/auth/__init__.py @@ -0,0 +1,5 @@ +"""Auth package initialization.""" + +from .auth import verify_google_id_token + +__all__ = ["verify_google_id_token"] diff --git a/backend/functions/auth/auth.py b/backend/functions/auth/auth.py new file mode 100644 index 0000000..f859864 --- /dev/null +++ b/backend/functions/auth/auth.py @@ -0,0 +1,39 @@ +"""Authentication functionality.""" + +import logging + +from google.auth.transport import requests +from google.oauth2 import id_token + +from config.environment import Environment + +logging.basicConfig(level=logging.DEBUG) +logger = logging.getLogger(__name__) + + +def verify_google_id_token(token: str) -> dict: + """Verify Google ID token and return payload. + + Args: + token: The Google ID token to verify + + Returns: + dict: The decoded token payload + + Raises: + ValueError: If token is invalid + """ + try: + client_id = Environment.get_required("RUNON_CLIENT_ID") + logger.debug(f"Using client_id: {client_id[:10]}...") + logger.debug(f"Token received: {token[:10]}...") + + request = requests.Request() + idinfo = id_token.verify_oauth2_token(token, request, client_id) + logger.debug(f"Token verification successful: {str(idinfo)[:100]}...") + return idinfo + except Exception as e: + logger.error(f"Token verification failed: {str(e)}") + logger.error(f"Token: {token[:10]}...") + logger.error(f"Client ID: {client_id[:10]}...") + raise ValueError(f"Invalid token: {str(e)}") diff --git a/backend/functions/calendar_sync/__init__.py b/backend/functions/calendar_sync/__init__.py index 3039d4a..32d9aad 100644 --- a/backend/functions/calendar_sync/__init__.py +++ b/backend/functions/calendar_sync/__init__.py @@ -1 +1 @@ -"""Calendar synchronization package.""" +"""Calendar sync package initialization.""" diff --git a/backend/functions/calendar_sync/calendar.py b/backend/functions/calendar_sync/calendar.py index e6e1ebe..15a8b86 100644 --- a/backend/functions/calendar_sync/calendar.py +++ b/backend/functions/calendar_sync/calendar.py @@ -1,10 +1,11 @@ """Calendar sync functionality.""" -from typing import Optional +from typing import List, Optional from google.oauth2.credentials import Credentials from googleapiclient.discovery import build +from functions.event_discovery.search import search_running_events from models.event import Event @@ -12,7 +13,15 @@ def add_event_to_calendar( event: Event, credentials: Credentials, ) -> Optional[str]: - """Add event to user's Google Calendar.""" + """Add event to user's Google Calendar. + + Args: + event: Event to add to calendar + credentials: Google OAuth credentials + + Returns: + Optional[str]: Event ID if successful, None otherwise + """ service = build( "calendar", "v3", @@ -34,3 +43,27 @@ def add_event_to_calendar( except Exception as e: print(f"Error adding event to calendar: {e}") return None + + +def create_events_from_search( + search_query: str, + credentials: Credentials, +) -> List[str]: + """Create calendar events from search results. + + Args: + search_query: Location or search query + credentials: Google OAuth credentials + + Returns: + List[str]: List of created event IDs + """ + events = search_running_events(search_query) + event_ids = [] + + for event in events: + event_id = add_event_to_calendar(event, credentials) + if event_id: + event_ids.append(event_id) + + return event_ids diff --git a/backend/functions/event_discovery/__init__.py b/backend/functions/event_discovery/__init__.py index 637aca1..55d164a 100644 --- a/backend/functions/event_discovery/__init__.py +++ b/backend/functions/event_discovery/__init__.py @@ -1 +1 @@ -"""Event discovery package.""" +"""Event discovery package initialization.""" diff --git a/backend/functions/event_discovery/search.py b/backend/functions/event_discovery/search.py index 293b83a..6a05448 100644 --- a/backend/functions/event_discovery/search.py +++ b/backend/functions/event_discovery/search.py @@ -3,23 +3,46 @@ from datetime import datetime from typing import List +import requests + +from config.environment import Environment from models.event import Event def search_running_events(location: str) -> List[Event]: - """Search for running events in a given location.""" - # Simplified implementation - events = [] - - # Process results into Event objects - events.append( - Event( - name="Sample Running Event", - date=datetime.now(), - location=location, - description="A running event near you", - url="https://example.com", - ) - ) - - return events + """Search for running events in a given location using Google Custom Search. + + Args: + location: Location to search for events + + Returns: + List[Event]: List of running events found + """ + api_key = Environment.get_required("RUNON_API_KEY") + search_engine_id = Environment.get_required("RUNON_SEARCH_ENGINE_ID") + + base_url = "https://www.googleapis.com/customsearch/v1" + query = f"running events races {location}" + + params = {"key": api_key, "cx": search_engine_id, "q": query, "num": 10} # Number of results + + try: + response = requests.get(base_url, params=params) + response.raise_for_status() + search_results = response.json() + + events = [] + for item in search_results.get("items", []): + event = Event( + name=item.get("title", "Unknown Event"), + date=datetime.now(), # Default to now, would parse from result in production + location=location, + description=item.get("snippet", ""), + url=item.get("link", ""), + ) + events.append(event) + + return events + except Exception as e: + print(f"Search error: {str(e)}") + return [] diff --git a/backend/main.py b/backend/main.py new file mode 100644 index 0000000..6bad43c --- /dev/null +++ b/backend/main.py @@ -0,0 +1,52 @@ +"""Main FastAPI application.""" + +from typing import List, Optional + +from fastapi import Depends, FastAPI, Header, HTTPException + +from config.environment import Environment + +app = FastAPI(title="RunOn API") + + +async def verify_token(authorization: Optional[str] = Header(None)): + """Verify the authorization token.""" + if not authorization: + raise HTTPException(status_code=401, detail="Authorization header required") + + # Remove 'Bearer ' prefix if present + token = authorization.replace("Bearer ", "") + + # For development/testing, if the token matches our client ID, we'll allow it + try: + client_id = Environment.get_required("RUNON_CLIENT_ID") + except Exception: + raise HTTPException(status_code=500, detail="Server configuration error") + + if token == client_id: + return True + + raise HTTPException(status_code=401, detail="Invalid credentials") + + +def get_mock_events() -> List[str]: + """Get mock events for testing.""" + return ["event1", "event2"] + + +@app.post("/events/search") +async def search_and_create_events( + query: str, authorized: bool = Depends(verify_token) +) -> List[str]: + """Search for events and create them in calendar.""" + try: + # For now, return a mock response + return get_mock_events() + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@app.get("/health") +async def health_check(): + """Health check endpoint.""" + return {"status": "healthy"} diff --git a/backend/models/__init__.py b/backend/models/__init__.py index 238d1c6..50a4a97 100644 --- a/backend/models/__init__.py +++ b/backend/models/__init__.py @@ -3,3 +3,5 @@ from .event import Event __all__ = ["Event"] + +"""Models package initialization.""" diff --git a/backend/pytest.ini b/backend/pytest.ini index 558aa12..040c006 100644 --- a/backend/pytest.ini +++ b/backend/pytest.ini @@ -1,4 +1,6 @@ [pytest] testpaths = tests +python_paths = . +asyncio_mode = strict python_files = test_*.py addopts = --cov=. --cov-report=term-missing --cov-fail-under=80 \ No newline at end of file diff --git a/backend/requirements-dev.txt b/backend/requirements-dev.txt index ab93d5a..ea87e0b 100644 --- a/backend/requirements-dev.txt +++ b/backend/requirements-dev.txt @@ -1,6 +1,8 @@ # Testing -pytest>=7.0.0 -pytest-cov>=3.0.0 +pytest==7.4.3 +pytest-cov==4.1.0 +pytest-asyncio==0.21.1 +httpx==0.24.1 # Formatting black>=22.0.0 diff --git a/backend/requirements.txt b/backend/requirements.txt index 7391e45..9b5e433 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -10,6 +10,7 @@ pyjwt==2.10.* requests==2.32.* python-dateutil==2.9.* fastapi==0.104.1 +starlette==0.27.0 pydantic==2.4.2 python-jose[cryptography]==3.3.0 python-multipart==0.0.18 diff --git a/backend/scripts/format_and_lint.sh b/backend/scripts/format_and_lint.sh index d7e6fa9..94ee86c 100755 --- a/backend/scripts/format_and_lint.sh +++ b/backend/scripts/format_and_lint.sh @@ -16,4 +16,4 @@ echo "Running flake8 linter..." flake8 . echo "Running pytest with coverage..." -pytest --cov=. --cov-report=xml --cov-report=term-missing +PYTHONPATH=$PYTHONPATH:$(pwd) pytest --cov=. --cov-report=xml --cov-report=term-missing diff --git a/backend/scripts/run_local.sh b/backend/scripts/run_local.sh new file mode 100644 index 0000000..423ebae --- /dev/null +++ b/backend/scripts/run_local.sh @@ -0,0 +1,88 @@ +#!/bin/bash + +# Set Java version for the session +export JAVA_HOME=$(/Users/garotconklin/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home) +export PATH="$JAVA_HOME/bin:$PATH" + +# Exit on any error +set -e + +# Change to the backend directory +cd "$(dirname "$0")/.." + +# Get the project root directory (one level up from backend) +PROJECT_ROOT="$(dirname "$(pwd)")" + +# Check if .env file exists in project root +if [ ! -f "${PROJECT_ROOT}/.env" ]; then + echo "โŒ Error: .env file not found in project root!" + echo "Please create a .env file in ${PROJECT_ROOT} with the following variables:" + echo "RUNON_CLIENT_ID=your_client_id" + echo "RUNON_API_KEY=your_api_key" + echo "RUNON_SEARCH_ENGINE_ID=your_search_engine_id" + exit 1 +fi + +# Create symlink to .env file if it doesn't exist in backend +if [ ! -f ".env" ]; then + echo "๐Ÿ”— Creating symlink to .env file..." + ln -sf "${PROJECT_ROOT}/.env" .env +fi + +# Load environment variables (ignoring comments and empty lines) +echo "๐Ÿ“š Loading environment variables..." +while IFS= read -r line; do + # Skip comments and empty lines + [[ $line =~ ^#.*$ ]] && continue + [[ -z $line ]] && continue + + # Export the variable + export "$line" +done < <(grep -v '^#' "${PROJECT_ROOT}/.env" | grep -v '^$') + +echo "๐Ÿ” Environment variables loaded. To test, run in a new terminal:" +echo "cd ${PROJECT_ROOT} && while IFS= read -r line; do [[ \$line =~ ^#.*$ ]] && continue; [[ -z \$line ]] && continue; export \"\$line\"; done < <(grep -v '^#' .env | grep -v '^$') && curl -X POST \"http://localhost:8000/events/search?query=Boston%20Marathon\" -H \"Authorization: Bearer \$RUNON_CLIENT_ID\" -H \"Content-Type: application/json\"" + +# Clean up existing virtual environment +if [ -d "venv" ]; then + echo "๐Ÿงน Removing existing virtual environment..." + rm -rf venv + echo "โœ… Existing virtual environment removed" +fi + +# Create new virtual environment +echo "๐Ÿ”จ Creating new virtual environment..." +python3.9 -m venv venv +echo "โœ… Virtual environment created" + +# Activate virtual environment +echo "๐Ÿ”Œ Activating virtual environment..." +source venv/bin/activate +echo "โœ… Virtual environment activated" + +# Install dependencies +echo "๐Ÿ“ฆ Installing dependencies..." +pip install -r requirements.txt +pip install -r requirements-dev.txt +echo "โœ… Dependencies installed" + +# Export environment variables +echo "๐Ÿ”‘ Exporting environment variables..." +while IFS= read -r line; do + [[ $line =~ ^#.*$ ]] && continue + [[ -z $line ]] && continue + export "$line" + # Print masked value for debugging + key=$(echo "$line" | cut -d'=' -f1) + echo "Exported $key=********" +done < <(grep -v '^#' ../.env | grep -v '^$') +echo "โœ… Environment variables exported" + +# Kill any existing uvicorn processes +echo "๐Ÿงน Cleaning up any existing server processes..." +pkill -f "uvicorn main:app" || true +sleep 2 + +# Run the server with environment variables +echo "๐Ÿš€ Starting local server..." +PYTHONPATH=$PYTHONPATH:$(pwd) uvicorn main:app --reload --host 0.0.0.0 --port 8000 \ No newline at end of file diff --git a/backend/scripts/setup.sh b/backend/scripts/setup.sh index 0837c8e..c1a8387 100644 --- a/backend/scripts/setup.sh +++ b/backend/scripts/setup.sh @@ -36,6 +36,6 @@ pip install -r requirements-dev.txt # Run format and lint echo "๐Ÿ” Running format and lint checks..." -bash scripts/format_and_lint.sh +PYTHONPATH=$PYTHONPATH:$(pwd) bash scripts/format_and_lint.sh echo "โœ… Setup complete!" \ No newline at end of file diff --git a/backend/tests/calendar_sync/test_calendar.py b/backend/tests/calendar_sync/test_calendar.py index ff39c66..a198ab6 100644 --- a/backend/tests/calendar_sync/test_calendar.py +++ b/backend/tests/calendar_sync/test_calendar.py @@ -5,7 +5,7 @@ import pytest -from functions.calendar_sync.calendar import add_event_to_calendar +from functions.calendar_sync.calendar import add_event_to_calendar, create_events_from_search from models.event import Event @@ -18,6 +18,21 @@ def mock_calendar_service(): yield service +@pytest.fixture +def mock_search(): + """Mock search functionality.""" + with patch("functions.calendar_sync.calendar.search_running_events") as mock: + mock.return_value = [ + Event( + name="Test Run", + date=datetime.now(), + location="Test Location", + description="Test Description", + ) + ] + yield mock + + def test_add_event_to_calendar_success(mock_calendar_service): """Test successfully adding event to calendar.""" event = Event( @@ -45,8 +60,31 @@ def test_add_event_to_calendar_failure(mock_calendar_service): ) credentials = MagicMock() - # Simulate API error mock_calendar_service.events().insert().execute.side_effect = Exception("API Error") result = add_event_to_calendar(event, credentials) assert result is None + + +def test_create_events_from_search_success(mock_calendar_service, mock_search): + """Test creating events from search results.""" + mock_calendar_service.events().insert().execute.return_value = { + "id": "test123", + } + + credentials = MagicMock() + event_ids = create_events_from_search("New York", credentials) + + assert len(event_ids) == 1 + assert event_ids[0] == "test123" + mock_search.assert_called_once_with("New York") + + +def test_create_events_from_search_no_results(mock_calendar_service, mock_search): + """Test handling no search results.""" + mock_search.return_value = [] + + credentials = MagicMock() + event_ids = create_events_from_search("Remote Location", credentials) + + assert len(event_ids) == 0 diff --git a/backend/tests/event_discovery/test_search.py b/backend/tests/event_discovery/test_search.py deleted file mode 100644 index 392458f..0000000 --- a/backend/tests/event_discovery/test_search.py +++ /dev/null @@ -1,28 +0,0 @@ -"""Tests for event discovery search.""" - -from datetime import datetime - -from functions.event_discovery.search import search_running_events -from models.event import Event - - -def test_search_running_events(): - """Test searching for running events.""" - location = "New York" - - events = search_running_events(location) - - assert isinstance(events, list) - assert len(events) > 0 - assert isinstance(events[0], Event) - assert events[0].location == location - - -def test_search_running_events_returns_valid_event(): - """Test that returned event has required fields.""" - events = search_running_events("Boston") - - event = events[0] - assert event.name - assert isinstance(event.date, datetime) - assert event.location diff --git a/backend/tests/functions/auth/test_auth.py b/backend/tests/functions/auth/test_auth.py new file mode 100644 index 0000000..2e8064a --- /dev/null +++ b/backend/tests/functions/auth/test_auth.py @@ -0,0 +1,43 @@ +"""Tests for authentication functionality.""" + +from unittest.mock import patch + +import pytest + +from functions.auth import verify_google_id_token + + +@pytest.fixture +def mock_environment(): + """Mock environment variables.""" + with patch("config.environment.Environment.get_required") as mock: + mock.return_value = "test-client-id" + yield mock + + +@pytest.fixture +def mock_id_token(): + """Mock Google ID token verification.""" + with patch("google.oauth2.id_token.verify_oauth2_token") as mock: + mock.return_value = {"sub": "123", "email": "test@example.com"} + yield mock + + +def test_verify_google_id_token_success(mock_environment, mock_id_token): + """Test successful token verification.""" + token = "valid-token" + result = verify_google_id_token(token) + + assert result["sub"] == "123" + assert result["email"] == "test@example.com" + mock_id_token.assert_called_once() + + +def test_verify_google_id_token_invalid(mock_environment, mock_id_token): + """Test invalid token handling.""" + mock_id_token.side_effect = ValueError("Invalid token") + + with pytest.raises(ValueError) as exc: + verify_google_id_token("invalid-token") + + assert "Invalid token" in str(exc.value) diff --git a/backend/tests/functions/event_discovery/test_search.py b/backend/tests/functions/event_discovery/test_search.py new file mode 100644 index 0000000..a57013b --- /dev/null +++ b/backend/tests/functions/event_discovery/test_search.py @@ -0,0 +1,63 @@ +"""Tests for event discovery search.""" + +from unittest.mock import MagicMock, patch + +import pytest + +from functions.event_discovery.search import search_running_events +from models.event import Event + + +@pytest.fixture +def mock_environment(): + """Mock environment variables.""" + with patch("config.environment.Environment.get_required") as mock: + mock.side_effect = ["test-api-key", "test-search-engine-id"] + yield mock + + +@pytest.fixture +def mock_requests(): + """Mock requests library.""" + with patch("requests.get") as mock: + mock.return_value = MagicMock( + json=lambda: { + "items": [ + { + "title": "Test Run Event", + "snippet": "A test running event", + "link": "https://example.com/event", + } + ] + } + ) + yield mock + + +def test_search_running_events_success(mock_environment, mock_requests): + """Test successful event search.""" + location = "New York" + events = search_running_events(location) + + assert len(events) == 1 + assert isinstance(events[0], Event) + assert events[0].name == "Test Run Event" + assert events[0].location == location + assert events[0].description == "A test running event" + assert events[0].url == "https://example.com/event" + + +def test_search_running_events_api_error(mock_environment, mock_requests): + """Test handling of API errors.""" + mock_requests.side_effect = Exception("API Error") + + events = search_running_events("Boston") + assert len(events) == 0 + + +def test_search_running_events_no_results(mock_environment, mock_requests): + """Test handling of no search results.""" + mock_requests.return_value = MagicMock(json=lambda: {"items": []}) + + events = search_running_events("Remote Location") + assert len(events) == 0 diff --git a/backend/tests/test_main.py b/backend/tests/test_main.py new file mode 100644 index 0000000..a2361b8 --- /dev/null +++ b/backend/tests/test_main.py @@ -0,0 +1,141 @@ +"""Tests for main FastAPI application.""" + +import pytest +from fastapi import HTTPException +from fastapi.testclient import TestClient + + +@pytest.fixture +def client(): + """Create a test client.""" + from main import app + + client = TestClient(app) + return client + + +@pytest.fixture +def mock_env(monkeypatch): + """Mock environment variables.""" + + def mock_get_required(key: str) -> str: + return "test_client_id" + + from config.environment import Environment + + monkeypatch.setattr(Environment, "get_required", mock_get_required) + return mock_get_required + + +@pytest.fixture +def mock_env_error(monkeypatch): + """Mock environment variables with error.""" + + def mock_get_required(key: str) -> str: + raise Exception("Configuration error") + + from config.environment import Environment + + monkeypatch.setattr(Environment, "get_required", mock_get_required) + return mock_get_required + + +def test_health_check(client): + """Test health check endpoint.""" + response = client.get("/health") + assert response.status_code == 200 + assert response.json() == {"status": "healthy"} + + +def test_search_events_no_auth(client): + """Test search events endpoint without authentication.""" + response = client.post("/events/search?query=test") + assert response.status_code == 401 + assert response.json() == {"detail": "Authorization header required"} + + +def test_search_events_invalid_auth(client, mock_env): + """Test search events endpoint with invalid authentication.""" + response = client.post( + "/events/search?query=test", + headers={"Authorization": "Bearer invalid_token"}, + ) + assert response.status_code == 401 + assert response.json()["detail"] == "Invalid credentials" + + +def test_search_events_valid_auth(client, mock_env): + """Test search events endpoint with valid authentication.""" + response = client.post( + "/events/search?query=test", + headers={"Authorization": "Bearer test_client_id"}, + ) + assert response.status_code == 200 + assert response.json() == ["event1", "event2"] + + +def test_search_events_missing_query(client, mock_env): + """Test search events endpoint without query parameter.""" + response = client.post( + "/events/search", + headers={"Authorization": "Bearer test_client_id"}, + ) + assert response.status_code == 422 # Validation error + + +@pytest.mark.asyncio +async def test_verify_token_missing(): + """Test token verification with missing token.""" + from main import verify_token + + with pytest.raises(HTTPException) as exc: + await verify_token(None) + assert exc.value.detail == "Authorization header required" + assert exc.value.status_code == 401 + + +@pytest.mark.asyncio +async def test_verify_token_invalid(mock_env): + """Test token verification with invalid token.""" + from main import verify_token + + with pytest.raises(HTTPException) as exc: + await verify_token("Bearer invalid_token") + assert exc.value.detail == "Invalid credentials" + assert exc.value.status_code == 401 + + +@pytest.mark.asyncio +async def test_verify_token_valid(mock_env): + """Test token verification with valid token.""" + from main import verify_token + + result = await verify_token("Bearer test_client_id") + assert result is True + + +@pytest.mark.asyncio +async def test_verify_token_config_error(mock_env_error): + """Test token verification with configuration error.""" + from main import verify_token + + with pytest.raises(HTTPException) as exc: + await verify_token("Bearer any_token") + assert exc.value.detail == "Server configuration error" + assert exc.value.status_code == 500 + + +def test_search_events_server_error(client, mock_env, monkeypatch): + """Test search events endpoint with server error.""" + + def mock_get_events(): + raise Exception("Search failed") + + monkeypatch.setattr("main.get_mock_events", mock_get_events) + + response = client.post( + "/events/search?query=test", + headers={"Authorization": "Bearer test_client_id"}, + ) + assert response.status_code == 500 + assert response.json()["detail"] == "Search failed"