Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • inject/backend
1 result
Show changes
Showing
with 538 additions and 221 deletions
from django import setup
setup()
from user.models import User
from common_lib.logger import logger
if User.objects.count() == 0:
User.objects.create_superuser("admin@admin.com", "test")
logger.info("created user: admin@admin.com, password: test")
User.objects.create_staffuser("instructor@instructor.com", "test")
logger.info("created user: instructor@instructor.com, password: test")
User.objects.create_user("user1@user.com", "test")
logger.info("created user: user1@admin.com, password: test")
User.objects.create_user("user2@user.com", "test")
logger.info("created user: user2@user.com, password: test")
User.objects.create_user("user3@user.com", "test")
logger.info("created user: user3@user.com, password: test")
import os
import shutil
from django.conf import settings
from django.test import override_settings
from common_lib.graphql import GraphQLApiTestCase
from common_lib.test_utils import (
internal_upload_definition,
internal_create_exercise,
)
from exercise.models import Exercise
from exercise_definition.models import Definition
from user.models import User
TEST_DATA_STORAGE = "RENAME_ME"
@override_settings(
DATA_STORAGE=TEST_DATA_STORAGE,
FILE_STORAGE=os.path.join(TEST_DATA_STORAGE, "files"),
)
class RENAME_ME(GraphQLApiTestCase):
base_definition: Definition
email_definition: Definition
roles_definition: Definition
base_exercise: Exercise
email_exercise: Exercise
roles_exercise: Exercise
@classmethod
def setUpTestData(cls):
cls.base_definition = internal_upload_definition("base_definition")
cls.email_definition = internal_upload_definition("emails_definition")
cls.roles_definition = internal_upload_definition("roles_definition")
cls.base_exercise = internal_create_exercise(cls.base_definition.id, 2)
cls.email_exercise = internal_create_exercise(
cls.email_definition.id, 2
)
cls.roles_exercise = internal_create_exercise(
cls.roles_definition.id, 3
)
cls.instructor = User.objects.create_staffuser(
"instructor@instructor.com", "instructor"
)
cls.user = User.objects.create_user("user@user.com", "user")
cls.instructor.definitions.add(cls.base_definition)
cls.instructor.definitions.add(cls.email_definition)
cls.instructor.definitions.add(cls.roles_definition)
cls.instructor.exercises.add(cls.base_exercise)
cls.instructor.exercises.add(cls.email_exercise)
cls.instructor.exercises.add(cls.roles_exercise)
cls.user.teams.add(cls.base_exercise.teams.first())
cls.user.teams.add(cls.email_exercise.teams.first())
cls.user.teams.add(cls.roles_exercise.teams.first())
@classmethod
def tearDownClass(cls):
shutil.rmtree(settings.DATA_STORAGE)
super().tearDownClass()
import os
import shutil
from django.conf import settings
from django.test import TestCase, override_settings
from common_lib.test_utils import (
internal_upload_definition,
internal_create_exercise,
)
from exercise.models import Exercise
from exercise_definition.models import Definition
TEST_DATA_STORAGE = "RENAME_ME"
@override_settings(
DATA_STORAGE=TEST_DATA_STORAGE,
FILE_STORAGE=os.path.join(TEST_DATA_STORAGE, "files"),
)
class RENAME_ME(TestCase):
base_definition: Definition
email_definition: Definition
roles_definition: Definition
base_exercise: Exercise
email_exercise: Exercise
roles_exercise: Exercise
@classmethod
def setUpTestData(cls):
cls.base_definition = internal_upload_definition("base_definition")
cls.email_definition = internal_upload_definition("emails_definition")
cls.roles_definition = internal_upload_definition("roles_definition")
cls.base_exercise = internal_create_exercise(cls.base_definition.id, 2)
cls.email_exercise = internal_create_exercise(
cls.email_definition.id, 2
)
cls.roles_exercise = internal_create_exercise(
cls.roles_definition.id, 3
)
@classmethod
def tearDownClass(cls):
shutil.rmtree(settings.DATA_STORAGE)
super().tearDownClass()
......@@ -7,7 +7,7 @@ from graphene_django.utils import camelize
from locust import HttpUser, between, events, task, tag
from locust.env import Environment
from locust.runners import MasterRunner
from requests import Response
from requests import Response, get
from common_lib.graphql import (
TeamActionLogsAction,
......@@ -17,6 +17,7 @@ from common_lib.graphql import (
EmailThreadsAction,
EmailContactsAction,
Fragments,
LoginAction,
)
from common_lib.graphql.api import APIAction
from dev.testing.utils import TeamData, ToolData
......@@ -66,14 +67,20 @@ class GraphQLUser(HttpUser):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.client.headers["Accept"] = "application/json"
self.client.headers["Content-Type"] = "application/json"
# this request will always fail, however we need it for retrieving the initial csrf token
# doing the request through the client would add it to the stats which we do not want
resp = get(self.host)
self.client.cookies = resp.cookies
self.update_csrf_header()
def execute_query(self, action: APIAction) -> Response:
response = self.client.post(
url=self.host,
name=action.query_name,
headers={
"Accept": "application/json",
"Content-Type": "application/json",
"Authorization": "<Authorization-Token>",
"Content-type": "application/json",
},
json={
"query": action.query_string(),
......@@ -88,6 +95,11 @@ class GraphQLUser(HttpUser):
action.result = response_data["data"][action.query_name]
return response
def update_csrf_header(self):
self.client.headers["X-CSRFTOKEN"] = self.client.cookies.get(
"csrftoken"
)
class InjectTeam(GraphQLUser):
team: TeamData
......@@ -112,9 +124,17 @@ class InjectTeam(GraphQLUser):
email_addresses,
)
)
self.login(self.team.user.username, self.team.user.password)
# create at least one thread
self.create_thread()
def login(self, username: str, password: str):
action = LoginAction({"username": username, "password": password})
resp = self.execute_query(action)
if resp.status_code != 200:
raise Exception(f"Login failed: {resp.content}")
self.update_csrf_header()
@task(10)
def action_logs(self):
action = TeamActionLogsAction(
......
......@@ -2,8 +2,8 @@ import argparse
import os
from dataclasses import dataclass
from typing import List, Dict, Any
import django
import django
import gevent
from gevent import monkey
from locust.html import get_html_report
......@@ -32,10 +32,13 @@ from common_lib.graphql import (
EmailContactsAction,
ExtendedTeamToolsAction,
ExerciseConfigAction,
LoginAction,
UsersAction,
AssignUsersToTeamAction,
)
from common_lib.graphql.api import APIAction
from dev.testing.locustfile import InjectTeam
from dev.testing.utils import TeamData, ToolData
from dev.testing.utils import TeamData, ToolData, User
setup_logging("INFO", None)
......@@ -49,6 +52,8 @@ class Arguments:
growth: int
preserve: bool
output: str
login: str
team_logins: str
@dataclass
......@@ -60,76 +65,130 @@ class PreparedExercise:
config: Dict[str, Any]
def send_action(action: APIAction, throw: bool = True) -> requests.Response:
response = requests.post(
url=GRAPHQL_URL,
headers={
"Accept": "application/json",
"Content-Type": "application/json",
},
json={
"query": action.query_string(),
"operationName": action.query_name,
"variables": camelize(action.get_variables()),
},
)
if response.status_code == 404:
raise Exception("Resource not found")
PREFIX: str = f"inject/api/v1"
class Client:
url: str
session: requests.Session
def __init__(self, host: str, credentials: str):
self.session = requests.session()
self.url = f"{host}/{PREFIX}"
self.session.headers["Accept"] = "application/json"
self.session.get(f"{self.url}/graphql/")
self.update_csrf_header()
if len(credentials) != 0:
username, password = credentials.split(":")
self.login(username, password)
def login(self, username: str, password: str):
action = LoginAction({"username": username, "password": password})
resp = self.send_action(action)
if resp.status_code != 200:
raise Exception(f"Login failed: {resp.content}")
self.update_csrf_header()
def send_action(
self, action: APIAction, throw: bool = True
) -> requests.Response:
response = self.session.post(
url=f"{self.url}/graphql/",
headers={
"Content-type": "application/json",
},
json={
"query": action.query_string(),
"operationName": action.query_name,
"variables": camelize(action.get_variables()),
},
)
if response.status_code == 404:
raise Exception("Resource not found")
if response.status_code == 403:
raise Exception("Unauthorized")
response_data = response.json()
if response.status_code != 200:
if throw:
raise Exception(f"{action.query_name}: {response_data}")
action.result = response_data
else:
action.result = response_data["data"][action.query_name]
return response
def update_csrf_header(self):
self.session.headers["X-CSRFTOKEN"] = self.session.cookies.get(
"csrftoken"
)
response_data = response.json()
if response.status_code != 200:
if throw:
raise Exception(f"{action.query_name}: {response_data}")
action.result = response_data
else:
action.result = response_data["data"][action.query_name]
return response
CLIENT: Client
def upload_definition(definition_path: str) -> int:
global CLIENT
if not os.path.exists(definition_path):
raise Exception(f"ERROR: {definition_path=} does not exist.")
files = {"file": ("file", open(definition_path, "rb"), "application/zip")}
values = {"definition_name": "perf-test-definition"}
response = requests.post(
DEFINITION_UPLOAD_URL,
response = CLIENT.session.post(
f"{CLIENT.url}/exercise_definition/upload-definition",
files=files,
data=values,
)
if response.status_code != 200:
raise Exception(f"Failed to upload definition: {response.content}")
id = response.json()["detail"].split(":")[1]
return int(id)
def get_teams(exercise_id: int, team_dicts: List[dict]) -> List[TeamData]:
def get_teams(
exercise_id: int, team_dicts: List[dict], team_logins: List[str]
) -> List[TeamData]:
return [
TeamData(
exercise_id,
int(team["id"]),
str(team["emailAddress"]["address"]),
[],
User("", *login.split(":")) if len(login) != 0 else None,
)
for team in team_dicts
for team, login in zip(team_dicts, team_logins)
]
def get_exercise_config(exercise_id: int) -> Dict[str, Any]:
global CLIENT
action = ExerciseConfigAction({"exercise_id": exercise_id})
send_action(action)
CLIENT.send_action(action)
return action.result
def create_exercise(definition_id: int, team_count: int) -> PreparedExercise:
def create_exercise(
definition_id: int, arguments: Arguments
) -> PreparedExercise:
global CLIENT
action = CreateExerciseAction(
{
"definition_id": definition_id,
"team_count": team_count,
"team_count": arguments.team_count,
},
{},
)
send_action(action)
logins = arguments.team_logins.split(",")
if len(logins) != arguments.team_count:
raise Exception(
f"Number of credentials ({len(logins)})"
f"does not match number of teams ({arguments.team_count})"
)
CLIENT.send_action(action)
exercise_id = int(action.result["exercise"]["id"])
exercise_config = get_exercise_config(exercise_id)
return PreparedExercise(
......@@ -138,6 +197,7 @@ def create_exercise(definition_id: int, team_count: int) -> PreparedExercise:
get_teams(
int(action.result["exercise"]["id"]),
action.result["exercise"]["teams"],
logins,
),
email_addresses=[],
config=exercise_config,
......@@ -145,8 +205,9 @@ def create_exercise(definition_id: int, team_count: int) -> PreparedExercise:
def get_email_contacts() -> List[str]:
global CLIENT
action = EmailContactsAction({"visible_only": True})
send_action(action)
CLIENT.send_action(action)
return [str(participant["address"]) for participant in action.result]
......@@ -160,13 +221,14 @@ def create_tools(tools: List[Dict[str, Any]]) -> List[ToolData]:
def get_team_tools(exercise: PreparedExercise):
global CLIENT
action = ExtendedTeamToolsAction({"team_id": None})
# if roles are not enabled, all teams have access to the same tools,
# therefore it is not necessary to send a query for each team
if not exercise.config["enableRoles"]:
action.variables = {"team_id": exercise.teams[0].team_id}
send_action(action)
CLIENT.send_action(action)
tools = create_tools(action.result)
for team in exercise.teams:
team.tools = tools
......@@ -174,43 +236,60 @@ def get_team_tools(exercise: PreparedExercise):
for team in exercise.teams:
action.variables = {"team_id": team.team_id}
send_action(action)
CLIENT.send_action(action)
team.tools = create_tools(action.result)
def assign_users(exercise: PreparedExercise):
global CLIENT
action = UsersAction({"active": True})
CLIENT.send_action(action)
emails = {user["username"]: user["id"] for user in action.result}
action = AssignUsersToTeamAction({})
for team in exercise.teams:
team.user.id = emails[team.user.username]
action.variables = {"team_id": team.team_id, "user_ids": [team.user.id]}
CLIENT.send_action(action)
def prepare_exercise(arguments: Arguments) -> PreparedExercise:
global CLIENT
definition_id = upload_definition(arguments.definition)
exercise = create_exercise(definition_id, arguments.team_count)
if arguments.team_count > len(exercise.teams):
send_action(DeleteDefinitionAction({"definition_id": definition_id}))
raise Exception(
f"Requested team count: {arguments.team_count} is higher than the amount of teams specified in the definition: {len(exercise.teams)}."
)
exercise = create_exercise(definition_id, arguments)
assign_users(exercise)
get_team_tools(exercise)
send_action(StartExerciseAction({"exercise_id": exercise.exercise_id}))
CLIENT.send_action(
StartExerciseAction({"exercise_id": exercise.exercise_id})
)
exercise.email_addresses = get_email_contacts()
return exercise
def cleanup(exercise: PreparedExercise, should_preserve: bool):
send_action(StopExerciseAction({"exercise_id": exercise.exercise_id}))
global CLIENT
CLIENT.send_action(
StopExerciseAction({"exercise_id": exercise.exercise_id})
)
if should_preserve:
return
send_action(
CLIENT.send_action(
DeleteDefinitionAction({"definition_id": exercise.definition_id})
)
def run_load_test(arguments: Arguments, exercise: PreparedExercise):
global CLIENT
if len(exercise.teams) > arguments.team_count:
logging.warning(
f"Starting load test with less teams: {arguments.team_count} than specified in the definition: {len(exercise.teams)}"
)
env = Environment(
host=GRAPHQL_URL, user_classes=[InjectTeam], events=events
host=f"{CLIENT.url}/graphql/", user_classes=[InjectTeam], events=events
)
runner = env.create_local_runner()
env.events.init.fire(
......@@ -274,11 +353,26 @@ def init_argument_parser() -> argparse.ArgumentParser:
p.add_argument(
"--output",
type=str,
help="generate a html file for viewing stats with the given name, if no name is provided, the report will not be generated",
help="generate a html file for viewing stats with the given name,"
" if no name is provided, the report will not be generated",
required=False,
default="",
)
p.add_argument(
"--login",
type=str,
help="login details for an account with necessary permissions"
" in the format <username>:<password>",
required=False,
default="",
)
p.add_argument(
"--team_logins",
type=str,
help="login details for user accounts, comma-separated list of <username>:<password>",
required=False,
default="",
)
return p
......@@ -286,6 +380,9 @@ def parse_arguments(p: argparse.ArgumentParser) -> Arguments:
import sys
parsed_args = p.parse_args(sys.argv[1:])
if parsed_args.login == "" or parsed_args.team_logins == "":
raise Exception("Must provide logins when authorization is enabled")
return Arguments(
host=parsed_args.host,
team_count=parsed_args.team_count,
......@@ -294,6 +391,8 @@ def parse_arguments(p: argparse.ArgumentParser) -> Arguments:
growth=parsed_args.growth,
preserve=parsed_args.preserve,
output=parsed_args.output,
login=parsed_args.login,
team_logins=parsed_args.team_logins,
)
......@@ -304,11 +403,7 @@ if __name__ == "__main__":
VERSION = "v1"
URL_PREFIX = f"inject/api/{VERSION}/"
BASE_URL = parsed_arguments.host
DEFINITION_UPLOAD_URL = (
f"{BASE_URL}/{URL_PREFIX}exercise_definition/upload-definition"
)
GRAPHQL_URL = f"{BASE_URL}/{URL_PREFIX}graphql/"
CLIENT = Client(parsed_arguments.host, parsed_arguments.login)
run_load_test(
parsed_arguments,
......
......@@ -8,9 +8,17 @@ class ToolData:
responses: List[str]
@dataclass
class User:
id: str
username: str
password: str
@dataclass
class TeamData:
exercise_id: int
team_id: int
team_address: str
tools: List[ToolData]
user: User
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.