forked from mathialm/secfit
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Cevin Neubauer
committed
Mar 25, 2025
1 parent
a0b2073
commit 9d7e3ce
Showing
6 changed files
with
351 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| __pycache__/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| requests | ||
| locust |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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) |