From 7618ad9cdd6eb73cf369aa42fb57028e5dbfc8c3 Mon Sep 17 00:00:00 2001
From: Josef Olsson <josol381@student.liu.se>
Date: Mon, 12 Apr 2021 15:48:18 +0000
Subject: [PATCH] Resolve "Add more api calls"

---
 server/app/apis/__init__.py          |  10 +-
 server/app/apis/auth.py              |  13 +-
 server/app/apis/competitions.py      |  40 +--
 server/app/apis/questions.py         |  74 +++++
 server/app/apis/slides.py            |   5 +-
 server/app/apis/teams.py             |  28 +-
 server/app/apis/users.py             |  18 +-
 server/app/core/controller/add.py    |   8 +-
 server/app/core/controller/delete.py |  51 +++-
 server/app/core/controller/edit.py   |  31 +++
 server/app/core/controller/get.py    |  62 ++++-
 server/app/core/dto.py               |   6 +
 server/app/core/http_codes.py        |   1 +
 server/app/core/parsers.py           |  34 ++-
 server/app/core/rich_schemas.py      |  13 +-
 server/app/core/schemas.py           |  11 +
 server/app/core/sockets.py           |  36 +++
 server/tests/test_app.py             | 399 ++++++++++++++++++++++++---
 server/tests/test_db.py              |  63 ++++-
 server/tests/test_helpers.py         |  82 +++++-
 20 files changed, 845 insertions(+), 140 deletions(-)
 create mode 100644 server/app/apis/questions.py
 create mode 100644 server/app/core/sockets.py

diff --git a/server/app/apis/__init__.py b/server/app/apis/__init__.py
index 493a1a94..061a4519 100644
--- a/server/app/apis/__init__.py
+++ b/server/app/apis/__init__.py
@@ -22,11 +22,11 @@ def admin_required():
     return wrapper
 
 
-def text_response(message, code=200):
-    return {"message": message}, 200
+def text_response(message, code=codes.OK):
+    return {"message": message}, codes.OK
 
 
-def list_response(items, total=None, code=200):
+def list_response(items, total=None, code=codes.OK):
     if type(items) is not list:
         abort(codes.INTERNAL_SERVER_ERROR)
     if not total:
@@ -34,7 +34,7 @@ def list_response(items, total=None, code=200):
     return {"items": items, "count": len(items), "total_count": total}, code
 
 
-def item_response(item, code=200):
+def item_response(item, code=codes.OK):
     if isinstance(item, list):
         abort(codes.INTERNAL_SERVER_ERROR)
     return item, code
@@ -45,6 +45,7 @@ from flask_restx import Api
 from .auth import api as auth_ns
 from .competitions import api as comp_ns
 from .misc import api as misc_ns
+from .questions import api as question_ns
 from .slides import api as slide_ns
 from .teams import api as team_ns
 from .users import api as user_ns
@@ -56,3 +57,4 @@ flask_api.add_namespace(auth_ns, path="/api/auth")
 flask_api.add_namespace(comp_ns, path="/api/competitions")
 flask_api.add_namespace(slide_ns, path="/api/competitions/<CID>/slides")
 flask_api.add_namespace(team_ns, path="/api/competitions/<CID>/teams")
+flask_api.add_namespace(question_ns, path="/api/competitions/<CID>/questions")
diff --git a/server/app/apis/auth.py b/server/app/apis/auth.py
index 4fa5624a..df3a8e5c 100644
--- a/server/app/apis/auth.py
+++ b/server/app/apis/auth.py
@@ -29,15 +29,12 @@ class AuthSignup(Resource):
     def post(self):
         args = create_user_parser.parse_args(strict=True)
         email = args.get("email")
-        password = args.get("password")
-        role_id = args.get("role_id")
-        city_id = args.get("city_id")
-        name = args.get("name")
 
         if User.query.filter(User.email == email).count() > 0:
             api.abort(codes.BAD_REQUEST, "User already exists")
 
-        item_user = dbc.add.user(email, password, role_id, city_id, name)
+        item_user = dbc.add.user(**args)
+        # TODO: Clarify when this case is needed or add it to a test
         if not item_user:
             api.abort(codes.BAD_REQUEST, "User could not be created")
 
@@ -50,8 +47,12 @@ class AuthDelete(Resource):
     @jwt_required
     def delete(self, ID):
         item_user = User.query.filter(User.id == ID).first()
+
+        if not item_user:
+            api.abort(codes.NOT_FOUND, f"Could not find user with id {ID}.")
+
         dbc.delete.default(item_user)
-        if ID == get_jwt_identity():
+        if int(ID) == get_jwt_identity():
             jti = get_raw_jwt()["jti"]
             dbc.add.blacklist(jti)
         return text_response(f"User {ID} deleted")
diff --git a/server/app/apis/competitions.py b/server/app/apis/competitions.py
index 9f17a932..93f03227 100644
--- a/server/app/apis/competitions.py
+++ b/server/app/apis/competitions.py
@@ -21,42 +21,32 @@ class CompetitionsList(Resource):
     def post(self):
         args = competition_parser.parse_args(strict=True)
 
-        name = args.get("name")
-        city_id = args.get("city_id")
-        year = args.get("year")
-
         # Add competition
-        item = dbc.add.competition(name, year, city_id)
+        item = dbc.add.competition(**args)
 
         # Add default slide
         dbc.add.slide(item)
-
-        dbc.refresh(item)
         return item_response(schema.dump(item))
 
 
-@api.route("/<ID>")
-@api.param("ID")
+@api.route("/<CID>")
+@api.param("CID")
 class Competitions(Resource):
     @jwt_required
-    def get(self, ID):
-        item = get_comp(ID)
+    def get(self, CID):
+        item = get_comp(CID)
         return item_response(schema.dump(item))
 
     @jwt_required
-    def put(self, ID):
+    def put(self, CID):
         args = competition_parser.parse_args(strict=True)
-
-        item = get_comp(ID)
-        name = args.get("name")
-        year = args.get("year")
-        city_id = args.get("city_id")
-        item = dbc.edit.competition(item, name, year, city_id)
+        item = get_comp(CID)
+        item = dbc.edit.competition(item, **args)
         return item_response(schema.dump(item))
 
     @jwt_required
-    def delete(self, ID):
-        item = get_comp(ID)
+    def delete(self, CID):
+        item = get_comp(CID)
         dbc.delete.competition(item)
         return "deleted"
 
@@ -66,13 +56,5 @@ class CompetitionSearch(Resource):
     @jwt_required
     def get(self):
         args = competition_search_parser.parse_args(strict=True)
-        name = args.get("name")
-        year = args.get("year")
-        city_id = args.get("city_id")
-        page = args.get("page", 0)
-        page_size = args.get("page_size", 15)
-        order = args.get("order", 1)
-        order_by = args.get("order_by")
-
-        items, total = dbc.get.search_competitions(name, year, city_id, page, page_size, order, order_by)
+        items, total = dbc.get.search_competitions(**args)
         return list_response(list_schema.dump(items), total)
diff --git a/server/app/apis/questions.py b/server/app/apis/questions.py
new file mode 100644
index 00000000..76ca53f9
--- /dev/null
+++ b/server/app/apis/questions.py
@@ -0,0 +1,74 @@
+import app.core.controller as dbc
+import app.core.http_codes as codes
+from app.apis import admin_required, item_response, list_response
+from app.core.controller.add import competition
+from app.core.dto import QuestionDTO
+from app.core.models import Question
+from app.core.parsers import question_parser
+from flask_jwt_extended import get_jwt_identity, jwt_required
+from flask_restx import Namespace, Resource
+
+api = QuestionDTO.api
+schema = QuestionDTO.schema
+list_schema = QuestionDTO.list_schema
+
+
+@api.route("/")
+@api.param("CID")
+class QuestionsList(Resource):
+    @jwt_required
+    def get(self, CID):
+        items, total = dbc.get.search_questions(competition_id=CID)
+        return list_response(list_schema.dump(items), total)
+
+    @jwt_required
+    def post(self, CID):
+        args = question_parser.parse_args(strict=True)
+
+        name = args.get("name")
+        total_score = args.get("total_score")
+        type_id = args.get("type_id")
+        slide_id = args.get("slide_id")
+
+        item_slide = dbc.get.slide(CID, slide_id)
+        item = dbc.add.question(name, total_score, type_id, item_slide)
+
+        return item_response(schema.dump(item))
+
+
+@api.route("/<QID>")
+@api.param("CID,QID")
+class Questions(Resource):
+    @jwt_required
+    def get(self, CID, QID):
+        item_question = Question.query.filter(Question.id == QID).first()
+
+        if item_question is None:
+            api.abort(codes.NOT_FOUND, f"Could not find question with id {QID}.")
+
+        if item_question.slide.competition.id != int(CID):
+            api.abort(codes.NOT_FOUND, f"Could not find question with id {QID} in competition with id {CID}.")
+
+        return item_response(schema.dump(item_question))
+
+    @jwt_required
+    def put(self, CID, QID):
+        args = question_parser.parse_args(strict=True)
+        print(f"questions 54: {args=}")
+
+        item_question = Question.query.filter(Question.id == QID).first()
+        if item_question.slide.competition.id != int(CID):
+            api.abort(codes.NOT_FOUND, f"Could not find question with id {QID} in competition with id {CID}.")
+
+        item_question = dbc.edit.question(item_question, **args)
+
+        return item_response(schema.dump(item_question))
+
+    @jwt_required
+    def delete(self, CID, QID):
+        item_question = dbc.get.question(CID, QID)
+        if not item_question:
+            return {"response": "No content found"}, codes.NOT_FOUND
+
+        dbc.delete.question(item_question)
+        return {}, codes.NO_CONTENT
diff --git a/server/app/apis/slides.py b/server/app/apis/slides.py
index 6d9465e2..ab7caa59 100644
--- a/server/app/apis/slides.py
+++ b/server/app/apis/slides.py
@@ -54,8 +54,11 @@ class Slides(Resource):
     @jwt_required
     def delete(self, CID, SID):
         item_slide = dbc.get.slide(CID, SID)
+        if not item_slide:
+            return {"response": "No content found"}, codes.NOT_FOUND
+
         dbc.delete.slide(item_slide)
-        return "deleted"
+        return {}, codes.NO_CONTENT
 
 
 @api.route("/<SID>/order")
diff --git a/server/app/apis/teams.py b/server/app/apis/teams.py
index bec6cb6a..7f7fdf0e 100644
--- a/server/app/apis/teams.py
+++ b/server/app/apis/teams.py
@@ -3,6 +3,7 @@ import app.core.http_codes as codes
 from app.apis import admin_required, item_response, list_response
 from app.core.dto import TeamDTO
 from app.core.models import Competition, Team
+from app.core.parsers import team_parser
 from flask_jwt_extended import get_jwt_identity, jwt_required
 from flask_restx import Namespace, Resource, reqparse
 
@@ -25,14 +26,10 @@ class TeamsList(Resource):
 
     @jwt_required
     def post(self, CID):
-        parser = reqparse.RequestParser()
-        parser.add_argument("name", type=str, location="json")
-        args = parser.parse_args(strict=True)
-
+        args = team_parser.parse_args(strict=True)
         item_comp = get_comp(CID)
-        dbc.add.team(args["name"], item_comp)
-        dbc.refresh(item_comp)
-        return list_response(list_schema.dump(item_comp.teams))
+        item_team = dbc.add.team(args["name"], item_comp)
+        return item_response(schema.dump(item_team))
 
 
 @api.route("/<TID>")
@@ -46,5 +43,20 @@ class Teams(Resource):
     @jwt_required
     def delete(self, CID, TID):
         item_team = dbc.get.team(CID, TID)
+        if not item_team:
+            api.abort(codes.NOT_FOUND, f"Could not find team with id {TID} in competition with id {CID}.")
+
         dbc.delete.team(item_team)
-        return "deleted"
+        return {}, codes.NO_CONTENT
+
+    @jwt_required
+    def put(self, CID, TID):
+        args = team_parser.parse_args(strict=True)
+        name = args.get("name")
+
+        item_team = dbc.get.team(CID, TID)
+        if not item_team:
+            api.abort(codes.NOT_FOUND, f"Could not find team with id {TID} in competition with id {CID}.")
+
+        item_team = dbc.edit.team(item_team, name=name, competition_id=CID)
+        return item_response(schema.dump(item_team))
diff --git a/server/app/apis/users.py b/server/app/apis/users.py
index 4aaf372b..07e2484d 100644
--- a/server/app/apis/users.py
+++ b/server/app/apis/users.py
@@ -4,6 +4,7 @@ from app.apis import admin_required, item_response, list_response
 from app.core.dto import UserDTO
 from app.core.models import User
 from app.core.parsers import user_parser, user_search_parser
+from flask import request
 from flask_jwt_extended import get_jwt_identity, jwt_required
 from flask_restx import Namespace, Resource
 
@@ -14,15 +15,11 @@ list_schema = UserDTO.list_schema
 
 def edit_user(item_user, args):
     email = args.get("email")
-    name = args.get("name")
-    city_id = args.get("city_id")
-    role_id = args.get("role_id")
-
     if email:
         if User.query.filter(User.email == args["email"]).count() > 0:
             api.abort(codes.BAD_REQUEST, "Email is already in use")
 
-    return dbc.edit.user(item_user, name, email, city_id, role_id)
+    return dbc.edit.user(item_user, **args)
 
 
 @api.route("/")
@@ -61,14 +58,5 @@ class UserSearch(Resource):
     @jwt_required
     def get(self):
         args = user_search_parser.parse_args(strict=True)
-        name = args.get("name")
-        email = args.get("email")
-        role_id = args.get("role_id")
-        city_id = args.get("city_id")
-        page = args.get("page", 0)
-        page_size = args.get("page_size", 15)
-        order = args.get("order", 1)
-        order_by = args.get("order_by")
-
-        items, total = dbc.get.search_user(email, name, city_id, role_id, page, page_size, order, order_by)
+        items, total = dbc.get.search_user(**args)
         return list_response(list_schema.dump(items), total)
diff --git a/server/app/core/controller/add.py b/server/app/core/controller/add.py
index 2352ec0d..38c2a283 100644
--- a/server/app/core/controller/add.py
+++ b/server/app/core/controller/add.py
@@ -25,13 +25,13 @@ def slide(item_competition):
 
 
 @db_add
-def user(email, plaintext_password, role_id, city_id, name=None):
-    return User(email, plaintext_password, role_id, city_id, name)
+def user(email, password, role_id, city_id, name=None):
+    return User(email, password, role_id, city_id, name)
 
 
 @db_add
-def question(name, order, type_id, item_slide):
-    return Question(name, order, type_id, item_slide.id)
+def question(name, total_score, type_id, item_slide):
+    return Question(name, total_score, type_id, item_slide.id)
 
 
 @db_add
diff --git a/server/app/core/controller/delete.py b/server/app/core/controller/delete.py
index 037373e4..ac6ddf70 100644
--- a/server/app/core/controller/delete.py
+++ b/server/app/core/controller/delete.py
@@ -1,3 +1,4 @@
+import app.core.controller as dbc
 from app.core import db
 from app.core.models import Blacklist, City, Competition, Role, Slide, User
 
@@ -7,20 +8,48 @@ def default(item):
     db.session.commit()
 
 
-def slide(item):
-    default(item)
+def slide(item_slide):
+    for item_question in item_slide.questions:
+        question(item_question)
 
+    deleted_slide_competition_id = item_slide.competition_id
+    deleted_slide_order = item_slide.order
+    default(item_slide)
 
-def team(item):
-    default(item)
+    # Update slide order for all slides after the deleted slide
+    slides_in_same_competition, _ = dbc.get.search_slide(competition_id=deleted_slide_competition_id)
+    for other_slide in slides_in_same_competition:
+        if other_slide.order > deleted_slide_order:
+            other_slide.order -= 1
+
+    db.session.commit()
+
+
+def team(item_team):
+    for item_question_answer in item_team.question_answers:
+        question_answers(item_question_answer)
+    default(item_team)
+
+
+def question(item_question):
+    for item_question_answer in item_question.question_answers:
+        question_answers(item_question_answer)
+    for item_alternative in item_question.alternatives:
+        alternatives(item_alternative)
+    default(item_question)
 
 
-def competition(item):
-    # Remove all slides from competition
-    for item_slide in item.slides:
+def alternatives(item_alternatives):
+    default(item_alternatives)
+
+
+def question_answers(item_question_answers):
+    default(item_question_answers)
+
+
+def competition(item_competition):
+    for item_slide in item_competition.slides:
         slide(item_slide)
-    # Remove all teams from competition
-    for item_team in item.teams:
+    for item_team in item_competition.teams:
         team(item_team)
-
-    default(item)
+    default(item_competition)
diff --git a/server/app/core/controller/edit.py b/server/app/core/controller/edit.py
index c8b1633b..0a1b190f 100644
--- a/server/app/core/controller/edit.py
+++ b/server/app/core/controller/edit.py
@@ -31,6 +31,17 @@ def slide(item, title=None, timer=None):
     return item
 
 
+def team(item_team, name=None, competition_id=None):
+    if name:
+        item_team.name = name
+    if competition_id:
+        item_team.competition_id = competition_id
+
+    db.session.commit()
+    db.session.refresh(item_team)
+    return item_team
+
+
 def competition(item, name=None, year=None, city_id=None):
     if name:
         item.name = name
@@ -61,3 +72,23 @@ def user(item, name=None, email=None, city_id=None, role_id=None):
     db.session.commit()
     db.session.refresh(item)
     return item
+
+
+def question(item_question, name=None, total_score=None, type_id=None, slide_id=None):
+
+    if name:
+        item_question.name = name
+
+    if total_score:
+        item_question.total_score = total_score
+
+    if type_id:
+        item_question.type_id = type_id
+
+    if slide_id:
+        item_question.slide_id = slide_id
+
+    db.session.commit()
+    db.session.refresh(item_question)
+
+    return item_question
diff --git a/server/app/core/controller/get.py b/server/app/core/controller/get.py
index 7c3842d9..f695deee 100644
--- a/server/app/core/controller/get.py
+++ b/server/app/core/controller/get.py
@@ -1,4 +1,4 @@
-from app.core.models import Competition, Slide, Team, User
+from app.core.models import Competition, Question, Slide, Team, User
 
 
 def slide_by_order(CID, order):
@@ -13,6 +13,13 @@ def team(CID, TID):
     return Team.query.filter((Team.competition_id == CID) & (Team.id == TID)).first()
 
 
+def question(CID, QID):
+    slide_ids = set(
+        [x.id for x in Slide.query.filter(Slide.competition_id == CID).all()]
+    )  # TODO: Filter using database instead of creating a set of slide_ids
+    return Question.query.filter(Question.slide_id.in_(slide_ids) & (Question.id == QID)).first()
+
+
 def _search(query, order_column, page=0, page_size=15, order=1):
     if order == 1:
         query = query.order_by(order_column)
@@ -43,6 +50,59 @@ def search_user(email=None, name=None, city_id=None, role_id=None, page=0, page_
     return _search(query, order_column, page, page_size, order)
 
 
+def search_slide(
+    slide_order=None, title=None, body=None, competition_id=None, page=0, page_size=15, order=1, order_by=None
+):
+    query = Slide.query
+    if slide_order:
+        query = query.filter(Slide.order == slide_order)
+    if title:
+        query = query.filter(Slide.title.like(f"%{title}%"))
+    if body:
+        query = query.filter(Slide.body.like(f"%{body}%"))
+    if competition_id:
+        query = query.filter(Slide.competition_id == competition_id)
+
+    order_column = Slide.id  # Default order_by
+    if order_by:
+        order_column = getattr(Slide.__table__.c, order_by)
+
+    return _search(query, order_column, page, page_size, order)
+
+
+def search_questions(
+    name=None,
+    total_score=None,
+    type_id=None,
+    slide_id=None,
+    competition_id=None,
+    page=0,
+    page_size=15,
+    order=1,
+    order_by=None,
+):
+    query = Question.query
+    if name:
+        query = query.filter(Question.name.like(f"%{name}%"))
+    if total_score:
+        query = query.filter(Question.total_score == total_score)
+    if type_id:
+        query = query.filter(Question.type_id == type_id)
+    if slide_id:
+        query = query.filter(Question.slide_id == slide_id)
+    if competition_id:
+        slide_ids = set(
+            [x.id for x in Slide.query.filter(Slide.competition_id == competition_id).all()]
+        )  # TODO: Filter using database instead of creating a set of slide_ids
+        query = query.filter(Question.slide_id.in_(slide_ids))
+
+    order_column = Question.id  # Default order_by
+    if order_by:
+        order_column = getattr(Question.__table__.c, order_by)
+
+    return _search(query, order_column, page, page_size, order)
+
+
 def search_competitions(name=None, year=None, city_id=None, page=0, page_size=15, order=1, order_by=None):
     query = Competition.query
     if name:
diff --git a/server/app/core/dto.py b/server/app/core/dto.py
index 5eca7963..e2f36bbb 100644
--- a/server/app/core/dto.py
+++ b/server/app/core/dto.py
@@ -40,3 +40,9 @@ class MiscDTO:
     question_type_schema = schemas.QuestionTypeSchema(many=True)
     media_type_schema = schemas.MediaTypeSchema(many=True)
     city_schema = schemas.CitySchema(many=True)
+
+
+class QuestionDTO:
+    api = Namespace("questions")
+    schema = rich_schemas.QuestionSchemaRich(many=False)
+    list_schema = schemas.QuestionSchema(many=True)
diff --git a/server/app/core/http_codes.py b/server/app/core/http_codes.py
index cfa3a2b1..95a99175 100644
--- a/server/app/core/http_codes.py
+++ b/server/app/core/http_codes.py
@@ -1,4 +1,5 @@
 OK = 200
+NO_CONTENT = 204
 BAD_REQUEST = 400
 UNAUTHORIZED = 401
 FORBIDDEN = 403
diff --git a/server/app/core/parsers.py b/server/app/core/parsers.py
index d4aaf0ec..f05adc10 100644
--- a/server/app/core/parsers.py
+++ b/server/app/core/parsers.py
@@ -5,7 +5,7 @@ search_parser = reqparse.RequestParser()
 search_parser.add_argument("page", type=int, default=0, location="args")
 search_parser.add_argument("page_size", type=int, default=15, location="args")
 search_parser.add_argument("order", type=int, default=1, location="args")
-search_parser.add_argument("order_by", type=str, location="args")
+search_parser.add_argument("order_by", type=str, default=None, location="args")
 
 ###LOGIN####
 login_parser = reqparse.RequestParser()
@@ -14,7 +14,6 @@ login_parser.add_argument("password", required=True, location="json")
 
 ###CREATE_USER####
 create_user_parser = login_parser.copy()
-create_user_parser.add_argument("email", type=inputs.email(), required=True, location="json")
 create_user_parser.add_argument("city_id", type=int, required=True, location="json")
 create_user_parser.add_argument("role_id", type=int, required=True, location="json")
 
@@ -33,22 +32,35 @@ user_search_parser.add_argument("city_id", type=int, default=None, location="arg
 user_search_parser.add_argument("role_id", type=int, default=None, location="args")
 
 
-###COMPETIION####
+###COMPETITION####
 competition_parser = reqparse.RequestParser()
-competition_parser.add_argument("name", type=str)
-competition_parser.add_argument("year", type=int)
-competition_parser.add_argument("city_id", type=int)
+competition_parser.add_argument("name", type=str, location="json")
+competition_parser.add_argument("year", type=int, location="json")
+competition_parser.add_argument("city_id", type=int, location="json")
 
 
-###SEARCH_COMPETITOIN####
+###SEARCH_COMPETITION####
 competition_search_parser = search_parser.copy()
 competition_search_parser.add_argument("name", type=str, default=None, location="args")
-competition_search_parser.add_argument("year", type=str, default=None, location="args")
+competition_search_parser.add_argument("year", type=int, default=None, location="args")
 competition_search_parser.add_argument("city_id", type=int, default=None, location="args")
 
 
 ###SLIDER_PARSER####
 slide_parser = reqparse.RequestParser()
-slide_parser.add_argument("order", type=int, default=None)
-slide_parser.add_argument("title", type=str, default=None)
-slide_parser.add_argument("timer", type=int, default=None)
+slide_parser.add_argument("order", type=int, default=None, location="json")
+slide_parser.add_argument("title", type=str, default=None, location="json")
+slide_parser.add_argument("timer", type=int, default=None, location="json")
+
+
+###QUESTION####
+question_parser = reqparse.RequestParser()
+question_parser.add_argument("name", type=str, default=None, location="json")
+question_parser.add_argument("total_score", type=int, default=None, location="json")
+question_parser.add_argument("slide_id", type=int, default=None, location="json")
+question_parser.add_argument("type_id", type=int, default=None, location="json")
+
+
+###TEAM####
+team_parser = reqparse.RequestParser()
+team_parser.add_argument("name", type=str, location="json")
diff --git a/server/app/core/rich_schemas.py b/server/app/core/rich_schemas.py
index e4dc4a84..d714d76d 100644
--- a/server/app/core/rich_schemas.py
+++ b/server/app/core/rich_schemas.py
@@ -22,6 +22,17 @@ class UserSchemaRich(RichSchema):
     city = fields.Nested(schemas.CitySchema, many=False)
 
 
+class QuestionSchemaRich(RichSchema):
+    class Meta(RichSchema.Meta):
+        model = models.Question
+
+    id = ma.auto_field()
+    name = ma.auto_field()
+    total_score = ma.auto_field()
+    type = fields.Nested(schemas.QuestionTypeSchema, many=False)
+    slide = fields.Nested(schemas.SlideSchema, many=False)
+
+
 class CompetitionSchemaRich(RichSchema):
     class Meta(RichSchema.Meta):
         model = models.Competition
@@ -31,5 +42,3 @@ class CompetitionSchemaRich(RichSchema):
     year = ma.auto_field()
     slides = fields.Nested(schemas.SlideSchema, many=True)
     city = fields.Nested(schemas.CitySchema, many=False)
-
-
diff --git a/server/app/core/schemas.py b/server/app/core/schemas.py
index 26fba469..c1d91c0d 100644
--- a/server/app/core/schemas.py
+++ b/server/app/core/schemas.py
@@ -18,6 +18,17 @@ class QuestionTypeSchema(BaseSchema):
     name = ma.auto_field()
 
 
+class QuestionSchema(BaseSchema):
+    class Meta(BaseSchema.Meta):
+        model = models.Question
+
+    id = ma.auto_field()
+    name = ma.auto_field()
+    total_score = ma.auto_field()
+    type_id = ma.auto_field()
+    slide_id = ma.auto_field()
+
+
 class MediaTypeSchema(BaseSchema):
     class Meta(BaseSchema.Meta):
         model = models.MediaType
diff --git a/server/app/core/sockets.py b/server/app/core/sockets.py
new file mode 100644
index 00000000..d9407a69
--- /dev/null
+++ b/server/app/core/sockets.py
@@ -0,0 +1,36 @@
+from flask.globals import request
+from flask_socketio import SocketIO, emit, join_room
+
+sio = SocketIO(cors_allowed_origins="http://localhost:3000")
+
+
+@sio.on("connect")
+def connect():
+    print(f"[Connected]: {request.sid}")
+
+
+@sio.on("disconnect")
+def disconnect():
+    print(f"[Disconnected]: {request.sid}")
+
+
+@sio.on("join_competition")
+def join_competition(data):
+    competitionID = data["competitionID"]
+    join_room(data["competitionID"])
+    print(f"[Join room]: {request.sid} -> {competitionID}")
+
+
+@sio.on("sync_slide")
+def sync_slide(data):
+    slide, competitionID = data["slide"], data["competitionID"]
+    emit("sync_slide", {"slide": slide}, room=competitionID, include_self=False)
+    print(f"[Sync slide]: {slide} -> {competitionID}")
+
+
+@sio.on("sync_timer")
+def sync_timer(data):
+    competitionID = data["competitionID"]
+    timer = data["timer"]
+    emit("sync_timer", {"timer": timer}, room=competitionID, include_self=False)
+    print(f"[Sync timer]: {competitionID=} {timer=}")
diff --git a/server/tests/test_app.py b/server/tests/test_app.py
index e4e0f290..38a103b9 100644
--- a/server/tests/test_app.py
+++ b/server/tests/test_app.py
@@ -1,119 +1,446 @@
+import app.core.http_codes as codes
+from app.core.models import Slide
+
 from tests import app, client, db
-from tests.test_helpers import add_default_values, delete, get, post, put
+from tests.test_helpers import add_default_values, change_order_test, delete, get, post, put
 
 
-def test_misc(client):
+def test_misc_api(client):
     add_default_values()
 
     # Login in with default user
     response, body = post(client, "/api/auth/login", {"email": "test@test.se", "password": "password"})
-    assert response.status_code == 200
+    assert response.status_code == codes.OK
     headers = {"Authorization": "Bearer " + body["access_token"]}
 
     ## Get misc
     response, body = get(client, "/api/misc/roles", headers=headers)
+    assert response.status_code == codes.OK
     assert body["count"] >= 2
 
     response, body = get(client, "/api/misc/cities", headers=headers)
-    assert body["count"] >= 1
+    assert response.status_code == codes.OK
+    assert body["count"] == 2
     assert body["items"][0]["name"] == "Linköping"
+    assert body["items"][1]["name"] == "Testköping"
 
     response, body = get(client, "/api/misc/media_types", headers=headers)
+    assert response.status_code == codes.OK
     assert body["count"] >= 2
 
     response, body = get(client, "/api/misc/question_types", headers=headers)
+    assert response.status_code == codes.OK
     assert body["count"] >= 3
 
     ## Cities
     response, body = post(client, "/api/misc/cities", {"name": "Göteborg"}, headers=headers)
+    assert response.status_code == codes.OK
     assert body["count"] >= 2
-    assert body["items"][1]["name"] == "Göteborg"
+    assert body["items"][2]["name"] == "Göteborg"
 
-    response, body = put(client, "/api/misc/cities/2", {"name": "Gbg"}, headers=headers)
+    # Rename city
+    response, body = put(client, "/api/misc/cities/3", {"name": "Gbg"}, headers=headers)
+    assert response.status_code == codes.OK
     assert body["count"] >= 2
-    assert body["items"][1]["name"] == "Gbg"
+    assert body["items"][2]["name"] == "Gbg"
+
+    # Delete city
+    # First checks current cities
+    response, body = get(client, "/api/misc/cities", headers=headers)
+    assert response.status_code == codes.OK
+    assert body["count"] == 3
+    assert body["items"][0]["name"] == "Linköping"
+    assert body["items"][1]["name"] == "Testköping"
+    assert body["items"][2]["name"] == "Gbg"
+    # Deletes city
+    response, body = delete(client, "/api/misc/cities/3", headers=headers)
+    assert response.status_code == codes.OK
+    assert body["count"] == 2
+    assert body["items"][0]["name"] == "Linköping"
+    assert body["items"][1]["name"] == "Testköping"
 
 
-def test_competition(client):
+def test_competition_api(client):
     add_default_values()
 
     # Login in with default user
     response, body = post(client, "/api/auth/login", {"email": "test@test.se", "password": "password"})
-    assert response.status_code == 200
+    assert response.status_code == codes.OK
     headers = {"Authorization": "Bearer " + body["access_token"]}
 
     # Create competition
     data = {"name": "c1", "year": 2020, "city_id": 1}
     response, body = post(client, "/api/competitions", data, headers=headers)
-    assert response.status_code == 200
+    assert response.status_code == codes.OK
     assert body["name"] == "c1"
+    competition_id = body["id"]
+
+    # Save number of slides
+    num_slides = len(Slide.query.all())
 
     # Get competition
-    response, body = get(client, "/api/competitions/1", headers=headers)
-    assert response.status_code == 200
+    response, body = get(client, f"/api/competitions/{competition_id}", headers=headers)
+    assert response.status_code == codes.OK
     assert body["name"] == "c1"
 
-    response, body = post(client, "/api/competitions/1/slides", {}, headers=headers)
-    assert response.status_code == 200
+    response, body = post(client, f"/api/competitions/{competition_id}/slides", headers=headers)
+    assert response.status_code == codes.OK
 
-    response, body = get(client, "/api/competitions/1/slides", headers=headers)
-    assert response.status_code == 200
+    response, body = get(client, f"/api/competitions/{competition_id}/slides", headers=headers)
+    assert response.status_code == codes.OK
     assert len(body["items"]) == 2
 
-    response, body = put(client, "/api/competitions/1/slides/1/order", {"order": 1}, headers=headers)
-    assert response.status_code == 200
+    response, body = put(
+        client, f"/api/competitions/{competition_id}/slides/{num_slides}/order", {"order": 1}, headers=headers
+    )
+    assert response.status_code == codes.OK
 
-    response, body = post(client, "/api/competitions/1/teams", {"name": "t1"}, headers=headers)
-    assert response.status_code == 200
+    response, body = post(client, f"/api/competitions/{competition_id}/teams", {"name": "t1"}, headers=headers)
+    assert response.status_code == codes.OK
 
-    response, body = get(client, "/api/competitions/1/teams", headers=headers)
-    assert response.status_code == 200
+    response, body = get(client, f"/api/competitions/{competition_id}/teams", headers=headers)
+    assert response.status_code == codes.OK
     assert len(body["items"]) == 1
     assert body["items"][0]["name"] == "t1"
 
-    response, body = delete(client, "/api/competitions/1", {}, headers=headers)
-    assert response.status_code == 200
+    response, body = delete(client, f"/api/competitions/{competition_id}", headers=headers)
+    assert response.status_code == codes.OK
 
 
-def test_app(client):
+def test_auth_and_user_api(client):
     add_default_values()
 
     # Login in with default user
     response, body = post(client, "/api/auth/login", {"email": "test@test.se", "password": "password"})
-    assert response.status_code == 200
+    assert response.status_code == codes.OK
     headers = {"Authorization": "Bearer " + body["access_token"]}
 
     # Create user
     register_data = {"email": "test1@test.se", "password": "abc123", "role_id": 2, "city_id": 1}
     response, body = post(client, "/api/auth/signup", register_data, headers)
-    assert response.status_code == 200
+    assert response.status_code == codes.OK
     assert body["id"] == 2
     assert "password" not in body
     assert "_password" not in body
 
+    # Try to create user with same email
+    register_data = {"email": "test1@test.se", "password": "354213", "role_id": 1, "city_id": 1}
+    response, body = post(client, "/api/auth/signup", register_data, headers)
+    assert response.status_code == codes.BAD_REQUEST
+
     # Try loggin with wrong PASSWORD
     response, body = post(client, "/api/auth/login", {"email": "test1@test.se", "password": "abc1234"})
-    assert response.status_code == 401
+    assert response.status_code == codes.UNAUTHORIZED
 
     # Try loggin with wrong Email
     response, body = post(client, "/api/auth/login", {"email": "testx@test.se", "password": "abc1234"})
-    assert response.status_code == 401
+    assert response.status_code == codes.UNAUTHORIZED
 
     # Try loggin with right PASSWORD
     response, body = post(client, "/api/auth/login", {"email": "test1@test.se", "password": "abc123"})
-    assert response.status_code == 200
+    assert response.status_code == codes.OK
+    refresh_token = body["refresh_token"]
     headers = {"Authorization": "Bearer " + body["access_token"]}
 
     # Get the current user
     response, body = get(client, "/api/users", headers=headers)
-    assert response.status_code == 200
+    assert response.status_code == codes.OK
     assert body["email"] == "test1@test.se"
 
     # Edit current user name
-    response, body = put(client, "/api/users", {"name": "carl carlsson"}, headers=headers)
-    assert response.status_code == 200
+    response, body = put(client, "/api/users", {"name": "carl carlsson", "city_id": 2, "role_id": 1}, headers=headers)
+    assert response.status_code == codes.OK
     assert body["name"] == "Carl Carlsson"
+    assert body["city"]["id"] == 2
+    assert body["role"]["id"] == 1
+
+    # Find other user
+    response, body = get(
+        client,
+        "/api/users/search",
+        query_string={"name": "Olle Olsson", "email": "test@test.se", "role_id": 1, "city_id": 1},
+        headers=headers,
+    )
+    assert response.status_code == codes.OK
+    assert body["count"] == 1
+
+    # Get user from ID
+    searched_user = body["items"][0]
+    user_id = searched_user["id"]
+    response, body = get(client, f"/api/users/{user_id}", headers=headers)
+    assert response.status_code == codes.OK
+    assert searched_user["name"] == body["name"]
+    assert searched_user["email"] == body["email"]
+    assert searched_user["role_id"] == body["role"]["id"]
+    assert searched_user["city_id"] == body["city"]["id"]
+    assert searched_user["id"] == body["id"]
+
+    # Edit user from ID
+    response, body = put(client, f"/api/users/{user_id}", {"email": "carl@carlsson.test"}, headers=headers)
+    assert response.status_code == codes.OK
+    assert body["email"] == "carl@carlsson.test"
+
+    # Edit user from ID but add the same email as other user
+    response, body = put(client, f"/api/users/{user_id}", {"email": "test1@test.se"}, headers=headers)
+    assert response.status_code == codes.BAD_REQUEST
+
+    # Delete other user
+    response, body = delete(client, f"/api/auth/delete/{user_id}", headers=headers)
+    assert response.status_code == codes.OK
+
+    # Try to delete other user again
+    response, body = delete(client, f"/api/auth/delete/{user_id}", headers=headers)
+    assert response.status_code == codes.NOT_FOUND
+
+    # Logout and try to access current user
+    response, body = post(client, f"/api/auth/logout", headers=headers)
+    assert response.status_code == codes.OK
+
+    # TODO: Check if current users jwt (jti) is in blacklist after logging out
+
+    response, body = get(client, "/api/users", headers=headers)
+    assert response.status_code == codes.UNAUTHORIZED
+
+    # Login in again with default user
+    response, body = post(client, "/api/auth/login", {"email": "test1@test.se", "password": "abc123"})
+    assert response.status_code == codes.OK
+    headers = {"Authorization": "Bearer " + body["access_token"]}
+
+    # TODO: Add test for refresh api for current user
+    # response, body = post(client, "/api/auth/refresh", headers={**headers, "refresh_token": refresh_token})
+    # assert response.status_code == codes.OK
+
+    # Find current user
+    response, body = get(client, "/api/users", headers=headers)
+    assert response.status_code == codes.OK
+    assert body["email"] == "test1@test.se"
+    assert body["city"]["id"] == 2
+    assert body["role"]["id"] == 1
+
+    # Delete current user
+    user_id = body["id"]
+    response, body = delete(client, f"/api/auth/delete/{user_id}", headers=headers)
+    assert response.status_code == codes.OK
+
+    # TODO: Check that user was blacklisted
+    # Look for current users jwt in blacklist
+    # Blacklist.query.filter(Blacklist.jti == )
+
+
+def test_slide_api(client):
+    add_default_values()
+
+    # Login in with default user
+    response, body = post(client, "/api/auth/login", {"email": "test@test.se", "password": "password"})
+    assert response.status_code == codes.OK
+    headers = {"Authorization": "Bearer " + body["access_token"]}
+
+    # Get slides from empty competition
+    CID = 1
+    response, body = get(client, f"/api/competitions/{CID}/slides", headers=headers)
+    assert response.status_code == codes.OK
+    assert body["count"] == 0
+
+    # Get slides
+    CID = 2
+    num_slides = 3
+    response, body = get(client, f"/api/competitions/{CID}/slides", headers=headers)
+    assert response.status_code == codes.OK
+    assert body["count"] == num_slides
+
+    # Add slide
+    response, body = post(client, f"/api/competitions/{CID}/slides", headers=headers)
+    num_slides += 1
+    assert response.status_code == codes.OK
+    assert body["count"] == num_slides
+
+    # Get slide
+    SID = 1
+    response, item_slide = get(client, f"/api/competitions/{CID}/slides/{SID}", headers=headers)
+    assert response.status_code == codes.OK
+    assert item_slide["id"] == SID
+
+    # Edit slide
+    order = 6
+    title = "Ny titel"
+    body = "Ny body"
+    timer = 43
+    assert item_slide["order"] != order
+    assert item_slide["title"] != title
+    # assert item_slide["body"] != body
+    assert item_slide["timer"] != timer
+    response, item_slide = put(
+        client,
+        f"/api/competitions/{CID}/slides/{SID}",
+        # TODO: Implement so these commented lines can be edited
+        # {"order": order, "title": title, "body": body, "timer": timer},
+        {"title": title, "timer": timer},
+        headers=headers,
+    )
+    assert response.status_code == codes.OK
+    # assert item_slide["order"] == order
+    assert item_slide["title"] == title
+    # assert item_slide["body"] == body
+    assert item_slide["timer"] == timer
+
+    # Delete slide
+    response, _ = delete(client, f"/api/competitions/{CID}/slides/{SID}", headers=headers)
+    num_slides -= 1
+    assert response.status_code == codes.NO_CONTENT
+    # Checks that there are fewer slides
+    response, body = get(client, f"/api/competitions/{CID}/slides", headers=headers)
+    assert response.status_code == codes.OK
+    assert body["count"] == num_slides
+
+    # Tries to delete slide again
+    response, _ = delete(client, f"/api/competitions/{CID}/slides/{SID}", headers=headers)
+    assert response.status_code == codes.NOT_FOUND
+
+    # Changes the order to the same order
+    i = 0
+    SID = body["items"][i]["id"]
+    order = body["items"][i]["order"]
+    response, _ = put(client, f"/api/competitions/{CID}/slides/{SID}/order", {"order": order}, headers=headers)
+    assert response.status_code == codes.BAD_REQUEST
+
+    # Changes the order
+    change_order_test(client, CID, SID, order + 1, headers)
+
+    # Changes order to 0
+    SID = 7
+    change_order_test(client, CID, SID, -1, headers)
+
+
+def test_question_api(client):
+    add_default_values()
+
+    # Login in with default user
+    response, body = post(client, "/api/auth/login", {"email": "test@test.se", "password": "password"})
+    assert response.status_code == codes.OK
+    headers = {"Authorization": "Bearer " + body["access_token"]}
+
+    # Get questions from empty competition
+    CID = 1  # TODO: Fix api-calls so that the ones not using CID don't require one
+    response, body = get(client, f"/api/competitions/{CID}/questions", headers=headers)
+    assert response.status_code == codes.OK
+    assert body["count"] == 0
+
+    # Get questions from another competition that should have some questions
+    CID = 3
+    num_questions = 3
+    response, body = get(client, f"/api/competitions/{CID}/questions", headers=headers)
+    assert response.status_code == codes.OK
+    assert body["count"] == num_questions
+
+    # # Get specific question
+    # name = "Q2"
+    # # total_score = 2
+    # type_id = 5
+    # slide_id = 5
+    # response, body = get(
+    #     client,
+    #     f"/api/competitions/{CID}/questions/",
+    #     headers=headers,
+    # )
+    # # print(f"357: {body['items']}")
+    # assert response.status_code == codes.OK
+    # assert body["count"] == 1
+    # item_question = body["items"][0]
+    # # print(f"338: {item_question}")
+    # assert item_question["name"] == name
+    # # assert item_question["total_score"] == total_score
+    # assert item_question["type_id"] == type_id
+    # assert item_question["slide_id"] == slide_id
+
+    # Add question
+    name = "Nytt namn"
+    # total_score = 2
+    type_id = 2
+    slide_id = 5
+    response, item_question = post(
+        client,
+        f"/api/competitions/{CID}/questions",
+        {"name": name, "type_id": type_id, "slide_id": slide_id},
+        headers=headers,
+    )
+    num_questions += 1
+    assert response.status_code == codes.OK
+    assert item_question["name"] == name
+    # # assert item_question["total_score"] == total_score
+    assert item_question["type"]["id"] == type_id
+    assert item_question["slide"]["id"] == slide_id
+    # Checks number of questions
+    response, body = get(client, f"/api/competitions/{CID}/questions", headers=headers)
+    assert response.status_code == codes.OK
+    assert body["count"] == num_questions
+
+    # Try to get question in another competition
+    QID = 1
+    response, item_question = get(client, f"/api/competitions/{CID}/questions/{QID}", headers=headers)
+    assert response.status_code == codes.NOT_FOUND
+
+    # Get question
+    QID = 4
+    response, item_question = get(client, f"/api/competitions/{CID}/questions/{QID}", headers=headers)
+    assert response.status_code == codes.OK
+    assert item_question["id"] == QID
+
+    # Try to edit question in another competition
+    name = "Nyare namn"
+    # total_score = 2
+    type_id = 3
+    slide_id = 1
+    QID = 1
+    response, _ = put(
+        client,
+        f"/api/competitions/{CID}/questions/{QID}",
+        # {"name": name, "total_score": total_score, "type_id": type_id, "slide_id": slide_id},
+        {"name": name, "type_id": type_id, "slide_id": slide_id},
+        headers=headers,
+    )
+    assert response.status_code == codes.NOT_FOUND
+    # Checks number of questions
+    response, body = get(client, f"/api/competitions/{CID}/questions", headers=headers)
+    assert response.status_code == codes.OK
+    assert body["count"] == num_questions
+
+    # Edit question
+    name = "Nyare namn"
+    # total_score = 2
+    type_id = 3
+    slide_id = 5
+    QID = 4
+    assert item_question["name"] != name
+    # assert item_question["total_score"] != total_score
+    assert item_question["type"]["id"] != type_id
+    assert item_question["slide"]["id"] != slide_id
+    response, item_question = put(
+        client,
+        f"/api/competitions/{CID}/questions/{QID}",
+        # {"name": name, "total_score": total_score, "type_id": type_id, "slide_id": slide_id},
+        {"name": name, "type_id": type_id, "slide_id": slide_id},
+        headers=headers,
+    )
+    assert response.status_code == codes.OK
+    assert item_question["name"] == name
+    # # assert item_question["total_score"] == total_score
+    assert item_question["type"]["id"] == type_id
+    assert item_question["slide"]["id"] == slide_id
+    # Checks number of questions
+    response, body = get(client, f"/api/competitions/{CID}/questions", headers=headers)
+    assert response.status_code == codes.OK
+    assert body["count"] == num_questions
+
+    # Delete question
+    response, _ = delete(client, f"/api/competitions/{CID}/questions/{QID}", headers=headers)
+    num_questions -= 1
+    assert response.status_code == codes.NO_CONTENT
+
+    # Checks that there are fewer questions
+    response, body = get(client, f"/api/competitions/{CID}/questions", headers=headers)
+    assert response.status_code == codes.OK
+    assert body["count"] == num_questions
 
-    # Delete created user
-    response, body = delete(client, "/api/auth/delete/1", {}, headers=headers)
-    assert response.status_code == 200
+    # Tries to delete question again
+    response, _ = delete(client, f"/api/competitions/{CID}/questions/{QID}", headers=headers)
+    assert response.status_code == codes.NOT_FOUND
diff --git a/server/tests/test_db.py b/server/tests/test_db.py
index 49ca53ad..68f49d5a 100644
--- a/server/tests/test_db.py
+++ b/server/tests/test_db.py
@@ -60,7 +60,7 @@ def test_question(client):
     item_competition_2 = Competition.query.filter_by(name="teknik9").first()
 
     assert item_competition is not None
-    assert item_competition.id == 1
+    assert item_competition.id == 4
     assert item_competition.city.name == "Linköping"
 
     # Add teams
@@ -85,7 +85,7 @@ def test_question(client):
 
     # Try add slide with same order
     assert_insert_fail(Slide, 1, item_competition.id)
-    assert_exists(Slide, 1, order=1)
+    assert_exists(Slide, 3, order=1)
 
     item_slide1 = Slide.query.filter_by(order=0).first()
     item_slide2 = Slide.query.filter_by(order=1).first()
@@ -109,3 +109,62 @@ def test_question(client):
     item_q2 = Question.query.filter_by(name="Fråga2").first()
     assert item_q1.type.name == "Boolean"
     assert item_q2.type.name == "Multiple"
+
+    # Get question
+    CID = 3
+    QID = 4
+    item_q1 = dbc.get.question(CID, QID)
+    assert item_q1.id == QID
+    item_slide = dbc.get.slide(CID, item_q1.slide_id)
+    assert item_q1.slide_id == item_slide.id
+
+    # Edit question
+    print(item_q1.type_id)
+    print(item_q1.slide_id)
+    name = "Nytt namn"
+    total_score = 44
+    type_id = 2
+    slide_id = 4
+    dbc.edit.question(item_q1, name=name, total_score=total_score, type_id=type_id, slide_id=slide_id)
+    item_q1 = Question.query.filter_by(name=name).first()
+    assert item_q1.name == name
+    assert item_q1.total_score == total_score
+    assert item_q1.type_id == type_id
+    assert item_q1.slide_id == slide_id
+
+    # Search for question
+    item_q2, _ = dbc.get.search_questions(
+        name=name, total_score=total_score, type_id=type_id, slide_id=slide_id, competition_id=CID
+    )
+    assert item_q1 == item_q2[0]
+
+
+def test_slide(client):
+    add_default_values()
+
+    # Get all slides
+    slides = Slide.query.all()
+    item_slides = dbc.get.search_slide()
+    assert slides == item_slides[0]
+
+    # Search using all parameters
+    item_comp = Competition.query.filter(Competition.name == "Tävling 1").first()
+    aux = dbc.get.search_slide(slide_order=1, title="Title 1", body="Body 1", competition_id=item_comp.id)
+    item_slide = aux[0][0]
+    assert item_comp.slides[1] == item_slide
+
+    # Edit all parameters of a slide
+    title = "Ändrad titel"
+    timer = 42
+    slide_id = item_slide.id
+    dbc.edit.slide(item_slide, title=title, timer=timer)
+    aux = dbc.get.search_slide(slide_order=1, title=title, body="Body 1", competition_id=item_comp.id)
+    item_slide = aux[0][0]
+    assert item_slide.id == slide_id
+    assert item_slide.title == title
+    assert item_slide.timer == timer
+
+    # Delete slide
+    aux = dbc.get.search_slide(slide_order=1, competition_id=item_comp.id)
+    item_slide = aux[0][0]
+    dbc.delete.slide(item_slide)
diff --git a/server/tests/test_helpers.py b/server/tests/test_helpers.py
index 6ce0f54c..fbb58ac1 100644
--- a/server/tests/test_helpers.py
+++ b/server/tests/test_helpers.py
@@ -1,15 +1,16 @@
 import json
 
 import app.core.controller as dbc
+import app.core.http_codes as codes
 from app.core import db
-from app.core.models import City, MediaType, QuestionType, Role, User
+from app.core.models import City, Role
 
 
 def add_default_values():
     media_types = ["Image", "Video"]
     question_types = ["Boolean", "Multiple", "Text"]
     roles = ["Admin", "Editor"]
-    cities = ["Linköping"]
+    cities = ["Linköping", "Testköping"]
 
     # Add media types
     for item in media_types:
@@ -29,7 +30,26 @@ def add_default_values():
     item_admin = Role.query.filter(Role.name == "Admin").one()
     item_city = City.query.filter(City.name == "Linköping").one()
     # Add user with role and city
-    dbc.add.user("test@test.se", "password", item_admin.id, item_city.id)
+    dbc.add.user("test@test.se", "password", item_admin.id, item_city.id, "Olle Olsson")
+
+    # Add competitions
+    dbc.add.competition("Tom tävling", 2012, item_city.id)
+    for j in range(2):
+        item_comp = dbc.add.competition(f"Tävling {j}", 2012, item_city.id)
+
+        # Add slides
+        for i in range(len(question_types)):
+            # Add slide to competition
+            item_slide = dbc.add.slide(item_comp)
+
+            # Populate slide with data
+            item_slide.title = f"Title {i}"
+            item_slide.body = f"Body {i}"
+            item_slide.timer = 100 + i
+            # item_slide.settings = "{}"
+
+            # Add question to competition
+            dbc.add.question(name=f"Q{i+1}", total_score=i + 1, type_id=i + 1, item_slide=item_slide)
 
 
 def get_body(response):
@@ -40,33 +60,38 @@ def get_body(response):
     return body
 
 
-def post(client, url, data, headers=None):
+def post(client, url, data=None, headers=None):
     if headers is None:
         headers = {}
     headers["Content-Type"] = "application/json"
-    response = client.post(url, data=json.dumps(data), headers=headers)
+    response = client.post(url, json=data, headers=headers)
     body = get_body(response)
     return response, body
 
 
 def get(client, url, query_string=None, headers=None):
+    if headers is None:
+        headers = {}
+    headers["Content-Type"] = ""
     response = client.get(url, query_string=query_string, headers=headers)
     body = get_body(response)
     return response, body
 
 
-def put(client, url, data, headers=None):
+def put(client, url, data=None, headers=None):
     if headers is None:
         headers = {}
     headers["Content-Type"] = "application/json"
-
-    response = client.put(url, data=json.dumps(data), headers=headers)
+    response = client.put(url, json=data, headers=headers)
     body = get_body(response)
     return response, body
 
 
-def delete(client, url, data, headers=None):
-    response = client.delete(url, data=json.dumps(data), headers=headers)
+def delete(client, url, data=None, headers=None):
+    if headers is None:
+        headers = {}
+    headers["Content-Type"] = ""
+    response = client.delete(url, json=data, headers=headers)
     body = get_body(response)
     return response, body
 
@@ -90,3 +115,40 @@ def assert_exists(db_type, length, **kwargs):
 def assert_object_values(obj, values):
     for k, v in values.items():
         assert getattr(obj, k) == v
+
+
+# Changes order of slides
+def change_order_test(client, cid, sid, order, h):
+    sid_at_order = -1
+    actual_order = 0 if order < 0 else order  # used to find the slide_id
+    response, body = get(client, f"/api/competitions/{cid}/slides", headers=h)
+    assert response.status_code == codes.OK
+
+    # Finds the slide_id of the slide that will be swapped with
+    for item_slide in body["items"]:
+        if item_slide["order"] == actual_order:
+            assert item_slide["id"] != sid
+            sid_at_order = item_slide["id"]
+    assert sid_at_order != -1
+
+    # Gets old versions of slides
+    response, item_slide_10 = get(client, f"/api/competitions/{cid}/slides/{sid}", headers=h)
+    assert response.status_code == codes.OK
+    response, item_slide_20 = get(client, f"/api/competitions/{cid}/slides/{sid_at_order}", headers=h)
+    assert response.status_code == codes.OK
+
+    # Changes order
+    response, _ = put(
+        client, f"/api/competitions/{cid}/slides/{sid}/order", {"order": order}, headers=h
+    )  # uses order to be able to test negative order
+    assert response.status_code == codes.OK
+
+    # Gets new versions of slides
+    response, item_slide_11 = get(client, f"/api/competitions/{cid}/slides/{sid}", headers=h)
+    assert response.status_code == codes.OK
+    response, item_slide_21 = get(client, f"/api/competitions/{cid}/slides/{sid_at_order}", headers=h)
+    assert response.status_code == codes.OK
+
+    # Checks that the order was indeed swapped
+    assert item_slide_10["order"] == item_slide_21["order"]
+    assert item_slide_11["order"] == item_slide_20["order"]
-- 
GitLab