From 9d7e3ce82b003366412855798a5b047b9238716c Mon Sep 17 00:00:00 2001 From: Cevin Neubauer Date: Tue, 25 Mar 2025 18:58:47 +0100 Subject: [PATCH] Adds stress test --- stress_test/.gitignore | 1 + stress_test/README.md | 75 ++++++++++++++++++++++ stress_test/locustfile.py | 116 +++++++++++++++++++++++++++++++++++ stress_test/requirements.txt | 2 + stress_test/user_setup.py | 83 +++++++++++++++++++++++++ stress_test/workout_setup.py | 74 ++++++++++++++++++++++ 6 files changed, 351 insertions(+) create mode 100644 stress_test/.gitignore create mode 100644 stress_test/README.md create mode 100644 stress_test/locustfile.py create mode 100644 stress_test/requirements.txt create mode 100644 stress_test/user_setup.py create mode 100644 stress_test/workout_setup.py diff --git a/stress_test/.gitignore b/stress_test/.gitignore new file mode 100644 index 0000000..ba0430d --- /dev/null +++ b/stress_test/.gitignore @@ -0,0 +1 @@ +__pycache__/ \ No newline at end of file diff --git a/stress_test/README.md b/stress_test/README.md new file mode 100644 index 0000000..63e1f47 --- /dev/null +++ b/stress_test/README.md @@ -0,0 +1,75 @@ +# SecFit Application Stress Testing + +This directory contains scripts for stress testing the SecFit application under load conditions. The tests are designed to simulate 10 users sending requests every 10 seconds for a duration of 2 minutes, adhering to the limit of not overloading the NTNU server. + +## Prerequisites + +- Python 3.x installed +- Locust installed (`pip install locust`) +- Running SecFit application instance (local or remote) + +## Running the Tests + +### Local Testing + +1. Make sure your SecFit application is running (either locally or on a server). +2. Navigate to this directory (`stress_test/`). +3. Run the Locust server: + +```bash +locust -f locustfile.py --host=http://localhost:8000 +``` + +### Testing Against External Server + +To test against an external server, provide the host URL as a command-line argument: + +```bash +locust -f locustfile.py --host=https://your-external-server.example.com +``` + +Alternatively, you can set the host via an environment variable: + +```bash +SECFIT_HOST=https://your-external-server.example.com locust -f locustfile.py +``` + +### Configuring the Test + +1. Open a web browser and go to `http://localhost:8089/` to access the Locust web interface. +2. In the web interface, specify: + - Number of users: 10 + - Spawn rate: 10 (users per second) + - Host: (should be pre-filled from the command line) +3. Click "Start swarming" to begin the test. + +The test will automatically: +- Create test users if they don't already exist in the application +- Run the test for exactly 2 minutes +- Stop automatically after the specified duration + +## Test Design Overview + +The test script simulates realistic user behavior by: + +1. Logging in to obtain authentication tokens +2. Browsing workouts and exercises (high-frequency tasks) +3. Viewing specific workout details +5. Adding comments to workouts + +Tasks are weighted to simulate realistic user patterns, with browse operations occurring more frequently than write operations. + +## Analyzing Results + +Locust provides real-time statistics during the test run, including: +- Response times (min, max, average) +- Requests per second +- Failure rates +- Response time distribution + +After the test completes, you can download a CSV report for further analysis. + +## Notes + +- Test users (user1, user2, etc.) will be created automatically if they don't exist in the application. +- Adjust the workout and user IDs in the script to match actual IDs in your database if needed. \ No newline at end of file diff --git a/stress_test/locustfile.py b/stress_test/locustfile.py new file mode 100644 index 0000000..b2693ba --- /dev/null +++ b/stress_test/locustfile.py @@ -0,0 +1,116 @@ +import time +import random +import json +import logging +import os +import sys +from locust import HttpUser, task, between, tag, events +from user_setup import UserSetup +from user_setup import TEST_USERS +from workout_setup import WorkoutSetup + +# Configure logging to be more verbose +# Set log level to DEBUG for detailed logging +logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s') +logger = logging.getLogger("secfit_stress_test") +logger.setLevel(logging.DEBUG) + +# Enable detailed logging for requests and urllib3 +logging.getLogger("requests").setLevel(logging.DEBUG) +logging.getLogger("urllib3").setLevel(logging.DEBUG) + +# Enable detailed logging for Locust +logging.getLogger("locust").setLevel(logging.DEBUG) + +# Quiet message for terminal +print("SecFit Stress Test: Starting...", file=sys.stderr) +print("Access the Locust web interface at http://localhost:8089", file=sys.stderr) +print("User setup in progress - please wait...", file=sys.stderr) + +# Get host configuration from environment variable if available +DEFAULT_HOST = "http://localhost:8000" + +class SecFitUser(HttpUser): + # Wait time between tasks - setting to 10 seconds per requirements + wait_time = between(9.5, 10.5) # Slight variation around 10s + + # Store authorization token after login + token = None + username = None + + def on_start(self): + """User logs in when starting""" + # Randomly choose a username from our test users + user_data = random.choice(TEST_USERS) + self.username = user_data["username"] + self.password = user_data["password"] + + # Login to get authentication token + response = self.client.post( + "/api/token/", + json={"username": self.username, "password": self.password}, + name="/api/token/ (Login)" + ) + + if response.status_code == 200: + self.token = response.json()["access"] + logger.info(f"User {self.username} logged in successfully") + else: + logger.error(f"Login failed for {self.username}: {response.text}") + + def _authenticated_request(self, method, endpoint, json_data=None, name=None): + """Helper method to make authenticated requests""" + headers = {"Authorization": f"Bearer {self.token}"} if self.token else {} + + if method == "get": + return self.client.get(endpoint, headers=headers, name=name) + elif method == "post": + return self.client.post(endpoint, headers=headers, json=json_data, name=name) + elif method == "put": + return self.client.put(endpoint, headers=headers, json=json_data, name=name) + elif method == "delete": + return self.client.delete(endpoint, headers=headers, name=name) + + @tag('browse') + @task(3) + def browse_workouts(self): + """Browse workout listings - high frequency task""" + logger.info(f"{self.username} is browsing workouts") + self._authenticated_request("get", "/api/workouts/", name="/api/workouts/ (Browse)") + + @tag('browse') + @task(2) + def view_workout_details(self): + """View a specific workout's details""" + workout_id = random.randint(1, 10) # Assuming workout IDs 1-10 exist + logger.info(f"{self.username} is viewing workout details for workout {workout_id}") + self._authenticated_request("get", f"/api/workouts/{workout_id}/", name="/api/workouts/[id]/ (View Details)") + + @tag('interaction') + @task(1) + def add_comment(self): + """Add a comment to a workout""" + workout_id = random.randint(1, 10) + workout_url = f"/api/workouts/{workout_id}/" + comment_data = { + "content": f"Test comment from load testing {time.time()}", + "workout": workout_url + } + logger.info(f"{self.username} is adding a comment to workout {workout_id}") + self._authenticated_request("post", "/api/comments/", json_data=comment_data, name="/api/comments/ (Add Comment)") + +# Register event handlers to control test duration and user setup +@events.init.add_listener +def on_locust_init(environment, **kwargs): + """Initialize the test with a time limit and create test users""" + # Setup test users before starting the test + if environment.host: + host = environment.host + else: + host = os.environ.get("SECFIT_HOST", DEFAULT_HOST) + environment.host = host + + # Create the test users + UserSetup.ensure_users_exist(host) + # Create the test workouts + WorkoutSetup.ensure_workouts_exist(host) \ No newline at end of file diff --git a/stress_test/requirements.txt b/stress_test/requirements.txt new file mode 100644 index 0000000..be49add --- /dev/null +++ b/stress_test/requirements.txt @@ -0,0 +1,2 @@ +requests +locust \ No newline at end of file diff --git a/stress_test/user_setup.py b/stress_test/user_setup.py new file mode 100644 index 0000000..2bf937d --- /dev/null +++ b/stress_test/user_setup.py @@ -0,0 +1,83 @@ +import requests +from requests.exceptions import RequestException +import random +import json +import logging +import sys + +# Configure logging +logger = logging.getLogger("secfit_stress_test") + +# Test user configuration with stronger passwords and additional required fields +TEST_USERS = [ + {"username": "locust_user1", "password": "StrongP@ssw0rd123!A", "email": "locust_user1@test.com"}, + {"username": "locust_user2", "password": "StrongP@ssw0rd123!B", "email": "locust_user2@test.com"}, + {"username": "locust_user3", "password": "StrongP@ssw0rd123!C", "email": "locust_user3@test.com"}, + {"username": "locust_user4", "password": "StrongP@ssw0rd123!D", "email": "locust_user4@test.com"}, + {"username": "locust_user5", "password": "StrongP@ssw0rd123!E", "email": "locust_user5@test.com"} +] + +class UserSetup: + """Helper class to set up test users""" + @staticmethod + def ensure_users_exist(host): + """Ensure all test users exist in the application""" + + # Suppress excessive logging during setup + user_created_count = 0 + user_existed_count = 0 + + for user_data in TEST_USERS: + username = user_data["username"] + password = user_data["password"] + + # First try to authenticate to check if user exists + try: + login_response = requests.post( + f"{host}/api/token/", + json={"username": username, "password": password} + ) + + # Log the full request and response for debugging + logger.info(f"Login request for {username}: {json.dumps({'username': username, 'password': password})}") + logger.info(f"Login response for {username}: {login_response.status_code} - {login_response.text}") + + # If login successful, user exists + if login_response.status_code == 200: + user_existed_count += 1 + continue + + # User doesn't exist, register them + # Add all required fields for registration based on the error messages + registration_data = { + "username": username, + "password": password, + "password1": password, # Additional password confirmation field + "email": user_data["email"], + "phone_number": f"+471234{random.randint(1000, 9999)}", + "isCoach": False, # Assuming all test users are athletes + "athletes": [], # Empty list as required by the API + "workouts": [], # Empty list as required by the API + "coach_files": [], # Empty list as required by the API + "athlete_files": [] # Empty list as required by the API + } + + # Register the user + register_response = requests.post( + f"{host}/api/users/", + json=registration_data + ) + + # Log the full request and response for debugging + logger.info(f"Registration request for {username}: {json.dumps(registration_data)}") + logger.info(f"Registration response for {username}: {register_response.status_code} - {register_response.text}") + + if register_response.status_code == 201: + user_created_count += 1 + + except RequestException as e: + # Log the exception for debugging + logger.error(f"Error during user setup for {username}: {str(e)}") + pass + + print(f"User setup complete: {user_created_count} users created, {user_existed_count} users already exist", file=sys.stderr) \ No newline at end of file diff --git a/stress_test/workout_setup.py b/stress_test/workout_setup.py new file mode 100644 index 0000000..ca395eb --- /dev/null +++ b/stress_test/workout_setup.py @@ -0,0 +1,74 @@ +import requests +from requests.exceptions import RequestException +import time +import json +import logging +import sys +from user_setup import TEST_USERS + +# Configure logging +logger = logging.getLogger("secfit_stress_test") + +class WorkoutSetup: + """Helper class to set up workouts""" + @staticmethod + def ensure_workouts_exist(host): + """Ensure all test workouts exist in the application""" + + # Suppress excessive logging during setup + workout_created_count = 0 + workout_existed_count = 0 + + # Use a separate user for creating workouts + workout_creator = TEST_USERS[0] + username = workout_creator["username"] + password = workout_creator["password"] + + try: + login_response = requests.post( + f"{host}/api/token/", + json={"username": username, "password": password} + ) + + # Log the full request and response for debugging + logger.info(f"Login request for {username}: {json.dumps({'username': username, 'password': password})}") + logger.info(f"Login response for {username}: {login_response.status_code} - {login_response.text}") + + # If login successful, user exists + if login_response.status_code == 200: + token = login_response.json()["access"] + + for workout_id in range(1, 11): # Assuming workout IDs 1-10 + # Create a workout for the user + workout_data = { + "name": f"Test Workout {workout_id}", + "description": "This is a test workout.", + "visibility": "PU", # Public visibility + "owner": username, + "exercise_instances": [], # Empty list as required by the API + "date" : time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime()), # Current date and time, + "notes": "Test notes for the workout.", + } + + # Register the workout + create_workout_response = requests.post( + f"{host}/api/workouts/", + headers={"Authorization": f"Bearer {token}"}, + json=workout_data + ) + + # Log the full request and response for debugging + logger.info(f"Create workout request for {username}: {json.dumps(workout_data)}") + logger.info(f"Create workout response for {username}: {create_workout_response.status_code} - {create_workout_response.text}") + + if create_workout_response.status_code == 201: + workout_created_count += 1 + else: + workout_existed_count += 1 + + except RequestException as e: + # Log the exception for debugging + logger.error(f"Error during workout setup for {username}: {str(e)}") + pass + + print(f"Workout setup complete: {workout_created_count} workouts created, {workout_existed_count} workouts already exist", file=sys.stderr) \ No newline at end of file