diff --git a/.gitlab/client.gitlab-ci.yml b/.gitlab/client.gitlab-ci.yml
index ad42a68cde4fb6ba4c192fbffe1c81d307f445f6..f35e73aa6da4b396dda7ca7b5847b83158078a41 100644
--- a/.gitlab/client.gitlab-ci.yml
+++ b/.gitlab/client.gitlab-ci.yml
@@ -1,5 +1,5 @@
 client:setup:
-  image: node:10
+  image: node
   stage: setup
   only:
     refs:
@@ -9,14 +9,14 @@ client:setup:
     - cd client
     - npm install
   artifacts:
-    name: "artifacts"
+    name: 'artifacts'
     untracked: true
     expire_in: 15 mins
     paths:
       - client/.npm/
       - client/node_modules/
   cache:
-    key: "$CI_COMMIT_REF_SLUG"
+    key: '$CI_COMMIT_REF_SLUG'
     paths:
       - client/.npm/
       - client/node_modules/
@@ -24,7 +24,7 @@ client:setup:
 client:linting:
   image: node:10
   stage: test
-  needs: ["client:setup"]
+  needs: ['client:setup']
   allow_failure: true
   only:
     refs:
@@ -39,7 +39,7 @@ client:linting:
 client:test:
   image: node:10
   stage: test
-  needs: ["client:setup"]
+  needs: ['client:setup']
   only:
     refs:
       - dev
@@ -56,7 +56,7 @@ client:test:
 client:report:
   image: python
   stage: report
-  needs: ["client:test"]
+  needs: ['client:test']
   only:
     refs:
       - merge_requests
diff --git a/client/src/__mocks__/axios.js b/client/src/__mocks__/axios.js
index c295396124b8f0a928a0f4fd0c999c963f1b0ecc..40f0914f659785d8b4ceac257e64ceaf0ab3e847 100644
--- a/client/src/__mocks__/axios.js
+++ b/client/src/__mocks__/axios.js
@@ -1,4 +1,5 @@
 export default {
   get: jest.fn().mockImplementation(),
   post: jest.fn().mockImplementation(),
+  defaults: { headers: { common: { Authorization: '' } } },
 }
diff --git a/client/src/actions/cities.test.ts b/client/src/actions/cities.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d5298d29a010b619ed0c2b25973f8f8a9fb8710b
--- /dev/null
+++ b/client/src/actions/cities.test.ts
@@ -0,0 +1,19 @@
+import mockedAxios from 'axios'
+import expect from 'expect' // You can use any testing library
+import configureMockStore from 'redux-mock-store'
+import thunk from 'redux-thunk'
+import { getCities } from './cities'
+
+const middlewares = [thunk]
+const mockStore = configureMockStore(middlewares)
+
+it('dispatches no actions when failing to get cities', async () => {
+  console.log = jest.fn()
+  ;(mockedAxios.get as jest.Mock).mockImplementation((path: string, params?: any) => {
+    return Promise.reject(new Error('getting cities failed'))
+  })
+  const store = mockStore({ competitions: { filterParams: [] } })
+  await getCities()(store.dispatch)
+  expect(store.getActions()).toEqual([])
+  expect(console.log).toHaveBeenCalled()
+})
diff --git a/client/src/actions/communication.ts b/client/src/actions/communication.ts
deleted file mode 100644
index d0719cd320f73b11d8a8243130a67896a48bf6e0..0000000000000000000000000000000000000000
--- a/client/src/actions/communication.ts
+++ /dev/null
@@ -1,75 +0,0 @@
-import Types from './types.js'
-
-export function axiosPost(
-  path: any,
-  data: any,
-  config = undefined,
-  startCB = undefined,
-  successCB = undefined,
-  errorCB = undefined
-) {
-  return {
-    type: Types.AXIOS_POST,
-    path,
-    data,
-    config,
-    startCB,
-    successCB,
-    errorCB,
-  }
-}
-
-export function axiosPostSuccess(path: any, data: any, previousAction: any) {
-  return {
-    type: Types.AXIOS_POST_SUCCESS,
-    path,
-    data,
-    previousAction,
-  }
-}
-
-export function axiosPostError(path: any, data: any, previousAction: any) {
-  return {
-    type: Types.AXIOS_POST_ERROR,
-    path,
-    data,
-    previousAction,
-  }
-}
-
-export function axiosGet(
-  path: any,
-  data: any,
-  config = undefined,
-  startCB = undefined,
-  successCB = undefined,
-  errorCB = undefined
-) {
-  return {
-    type: Types.AXIOS_GET,
-    path,
-    data,
-    config,
-    startCB,
-    successCB,
-    errorCB,
-  }
-}
-
-export function axiosGetSuccess(path: any, data: any, previousAction: any) {
-  return {
-    type: Types.AXIOS_GET_SUCCESS,
-    path,
-    data,
-    previousAction,
-  }
-}
-
-export function axiosGetError(path: any, data: any, previousAction: any) {
-  return {
-    type: Types.AXIOS_GET_ERROR,
-    path,
-    data,
-    previousAction,
-  }
-}
diff --git a/client/src/actions/competitions.test.ts b/client/src/actions/competitions.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..52be36acb658576e9b5c1ffc1b40394c338d0f5f
--- /dev/null
+++ b/client/src/actions/competitions.test.ts
@@ -0,0 +1,73 @@
+import mockedAxios from 'axios'
+import expect from 'expect' // You can use any testing library
+import configureMockStore from 'redux-mock-store'
+import thunk from 'redux-thunk'
+import { CompetitionFilterParams } from './../interfaces/FilterParams'
+import { getCompetitions, setFilterParams } from './competitions'
+import Types from './types'
+
+const middlewares = [thunk]
+const mockStore = configureMockStore(middlewares)
+
+it('dispatches correct actions when getting competitions', async () => {
+  const compRes: any = {
+    data: {
+      items: [
+        {
+          id: 21,
+          name: 'ggff',
+          year: 2021,
+          style_id: 1,
+          city: { name: 'city_name', id: 5 },
+        },
+        {
+          id: 22,
+          name: 'sssss',
+          year: 2021,
+          style_id: 1,
+          city: { name: 'city_name', id: 5 },
+        },
+      ],
+      count: 2,
+      total_count: 3,
+    },
+  }
+
+  ;(mockedAxios.get as jest.Mock).mockImplementation((path: string, params?: any) => {
+    return Promise.resolve(compRes)
+  })
+  const expectedActions = [
+    { type: Types.SET_COMPETITIONS, payload: compRes.data.items },
+    { type: Types.SET_COMPETITIONS_TOTAL, payload: compRes.data.total_count },
+    { type: Types.SET_COMPETITIONS_COUNT, payload: compRes.data.count },
+  ]
+  const store = mockStore({ competitions: { filterParams: [] } })
+  await getCompetitions()(store.dispatch, store.getState as any)
+  expect(store.getActions()).toEqual(expectedActions)
+})
+
+it('dispatches correct actions when setting filterParams', () => {
+  const testFilterParams: CompetitionFilterParams = {
+    page: 0,
+    pageSize: 3,
+    name: 'name',
+    cityId: 0,
+    styleId: 0,
+    year: 2000,
+  }
+  const expectedActions = [{ type: Types.SET_COMPETITIONS_FILTER_PARAMS, payload: testFilterParams }]
+  const store = mockStore({})
+  setFilterParams(testFilterParams)(store.dispatch)
+  expect(store.getActions()).toEqual(expectedActions)
+})
+
+it('dispatches no actions when failing to get competitions', async () => {
+  console.log = jest.fn()
+  ;(mockedAxios.get as jest.Mock).mockImplementation((path: string, params?: any) => {
+    return Promise.reject(new Error('getting competitions failed'))
+  })
+  const store = mockStore({ competitions: { filterParams: [] } })
+  await getCompetitions()(store.dispatch, store.getState as any)
+  expect(store.getActions()).toEqual([])
+  expect(console.log).toHaveBeenCalled()
+})
diff --git a/client/src/actions/presentation.test.ts b/client/src/actions/presentation.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..53ea4847dcf1aaf3e1ba9a04c643d1d44b5f27f0
--- /dev/null
+++ b/client/src/actions/presentation.test.ts
@@ -0,0 +1,59 @@
+import mockedAxios from 'axios'
+import expect from 'expect' // You can use any testing library
+import configureMockStore from 'redux-mock-store'
+import thunk from 'redux-thunk'
+import { Slide } from '../interfaces/Slide'
+import {
+  getPresentationCompetition,
+  getPresentationTeams,
+  setCurrentSlide,
+  setCurrentSlideNext,
+  setCurrentSlidePrevious,
+} from './presentation'
+import Types from './types'
+
+const middlewares = [thunk]
+const mockStore = configureMockStore(middlewares)
+
+it('dispatches no actions when failing to get competitions', async () => {
+  console.log = jest.fn()
+  ;(mockedAxios.get as jest.Mock).mockImplementation((path: string, params?: any) => {
+    return Promise.reject(new Error('getting competitions failed'))
+  })
+  const store = mockStore({ competitions: { filterParams: [] } })
+  await getPresentationCompetition('0')(store.dispatch)
+  expect(store.getActions()).toEqual([])
+  expect(console.log).toHaveBeenCalled()
+})
+
+it('dispatches no actions when failing to get teams', async () => {
+  console.log = jest.fn()
+  ;(mockedAxios.get as jest.Mock).mockImplementation((path: string, params?: any) => {
+    return Promise.reject(new Error('getting teams failed'))
+  })
+  const store = mockStore({ competitions: { filterParams: [] } })
+  await getPresentationTeams('0')(store.dispatch)
+  expect(store.getActions()).toEqual([])
+  expect(console.log).toHaveBeenCalled()
+})
+it('dispatches correct actions when setting slide', () => {
+  const testSlide: Slide = { competition_id: 0, id: 5, order: 5, timer: 20, title: '' }
+  const expectedActions = [{ type: Types.SET_PRESENTATION_SLIDE, payload: testSlide }]
+  const store = mockStore({})
+  setCurrentSlide(testSlide)(store.dispatch)
+  expect(store.getActions()).toEqual(expectedActions)
+})
+
+it('dispatches correct actions when setting previous slide', () => {
+  const expectedActions = [{ type: Types.SET_PRESENTATION_SLIDE_PREVIOUS }]
+  const store = mockStore({})
+  setCurrentSlidePrevious()(store.dispatch)
+  expect(store.getActions()).toEqual(expectedActions)
+})
+
+it('dispatches correct actions when setting next slide', () => {
+  const expectedActions = [{ type: Types.SET_PRESENTATION_SLIDE_NEXT }]
+  const store = mockStore({})
+  setCurrentSlideNext()(store.dispatch)
+  expect(store.getActions()).toEqual(expectedActions)
+})
diff --git a/client/src/actions/roles.test.ts b/client/src/actions/roles.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..94ca142f23d93601e9fe5afc8df8766d7241e977
--- /dev/null
+++ b/client/src/actions/roles.test.ts
@@ -0,0 +1,19 @@
+import mockedAxios from 'axios'
+import expect from 'expect' // You can use any testing library
+import configureMockStore from 'redux-mock-store'
+import thunk from 'redux-thunk'
+import { getRoles } from './roles'
+
+const middlewares = [thunk]
+const mockStore = configureMockStore(middlewares)
+
+it('dispatches no actions when failing to get roles', async () => {
+  console.log = jest.fn()
+  ;(mockedAxios.get as jest.Mock).mockImplementation((path: string, params?: any) => {
+    return Promise.reject(new Error('getting roles failed'))
+  })
+  const store = mockStore({ competitions: { filterParams: [] } })
+  await getRoles()(store.dispatch)
+  expect(store.getActions()).toEqual([])
+  expect(console.log).toHaveBeenCalled();
+})
diff --git a/client/src/actions/searchUser.test.ts b/client/src/actions/searchUser.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..66c155079a82d0efd21f0df949c669ab70f85c22
--- /dev/null
+++ b/client/src/actions/searchUser.test.ts
@@ -0,0 +1,74 @@
+import mockedAxios from 'axios'
+import expect from 'expect' // You can use any testing library
+import configureMockStore from 'redux-mock-store'
+import thunk from 'redux-thunk'
+import { UserFilterParams } from './../interfaces/FilterParams'
+import { getSearchUsers, setFilterParams } from './searchUser'
+import Types from './types'
+
+const middlewares = [thunk]
+const mockStore = configureMockStore(middlewares)
+it('dispatches correct actions when getting users', async () => {
+  const userRes: any = {
+    data: {
+      items: [
+        {
+          id: 21,
+          name: 'ggff',
+          email: 'email@test.com',
+          year: 2021,
+          role_id: 1,
+          city_id: 0,
+        },
+        {
+          id: 22,
+          name: 'sssss',
+          email: 'email@test.com',
+          year: 2021,
+          role_id: 1,
+          city_id: 0,
+        },
+      ],
+      count: 2,
+      total_count: 3,
+    },
+  }
+
+  ;(mockedAxios.get as jest.Mock).mockImplementation((path: string, params?: any) => {
+    return Promise.resolve(userRes)
+  })
+  const expectedActions = [
+    { type: Types.SET_SEARCH_USERS, payload: userRes.data.items },
+    { type: Types.SET_SEARCH_USERS_TOTAL_COUNT, payload: userRes.data.total_count },
+    { type: Types.SET_SEARCH_USERS_COUNT, payload: userRes.data.count },
+  ]
+  const store = mockStore({ searchUsers: { filterParams: [] } })
+  await getSearchUsers()(store.dispatch, store.getState as any)
+  expect(store.getActions()).toEqual(expectedActions)
+})
+
+it('dispatches correct actions when setting filterParams', () => {
+  const testFilterParams: UserFilterParams = {
+    page: 0,
+    pageSize: 3,
+    name: 'name',
+    cityId: 0,
+    email: 'email@test.com',
+    roleId: 0,
+  }
+  const expectedActions = [{ type: Types.SET_SEARCH_USERS_FILTER_PARAMS, payload: testFilterParams }]
+  const store = mockStore({})
+  setFilterParams(testFilterParams)(store.dispatch)
+  expect(store.getActions()).toEqual(expectedActions)
+})
+
+it('dispatches no actions when failing to get users', async () => {
+  console.log = jest.fn()
+  ;(mockedAxios.get as jest.Mock).mockImplementation((path: string, params?: any) => {
+    return Promise.reject(new Error('getting users failed'))
+  })
+  const store = mockStore({ searchUsers: { filterParams: [] } })
+  await getSearchUsers()(store.dispatch, store.getState as any)
+  expect(store.getActions()).toEqual([])
+  expect(console.log).toHaveBeenCalled()
+})
diff --git a/client/src/actions/types.ts b/client/src/actions/types.ts
index c0b7b629afa54a43de4c8ef8ddfba2cc551aa5d4..a8736c4b26b751a48f6f01d888dc46e630c4f6ea 100644
--- a/client/src/actions/types.ts
+++ b/client/src/actions/types.ts
@@ -8,7 +8,7 @@ export default {
   SET_SEARCH_USERS_COUNT: 'SET_SEARCH_USERS_COUNT',
   SET_SEARCH_USERS_TOTAL_COUNT: 'SET_SEARCH_USERS_TOTAL_COUNT',
   SET_ERRORS: 'SET_ERRORS',
-  CLEAR_ERRORS: 'SET_ERRORS',
+  CLEAR_ERRORS: 'CLEAR_ERRORS',
   SET_UNAUTHENTICATED: 'SET_UNAUTHENTICATED',
   SET_AUTHENTICATED: 'SET_AUTHENTICATED',
   SET_COMPETITIONS: 'SET_COMPETITIONS',
@@ -23,10 +23,4 @@ export default {
   SET_CITIES: 'SET_CITIES',
   SET_CITIES_TOTAL: 'SET_CITIES_TOTAL',
   SET_CITIES_COUNT: 'SET_CITIES_COUNT',
-  AXIOS_GET: 'AXIOS_GET',
-  AXIOS_GET_SUCCESS: 'AXIOS_GET_SUCCESS',
-  AXIOS_GET_ERROR: 'AXIOS_GET_ERROR',
-  AXIOS_POST: 'AXIOS_POST',
-  AXIOS_POST_SUCCESS: 'AXIOS_POST_SUCCESS',
-  AXIOS_POST_ERROR: 'AXIOS_POST_ERROR',
 }
diff --git a/client/src/actions/user.test.ts b/client/src/actions/user.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..156b17183a76117f87ead4f049346d85e499fea0
--- /dev/null
+++ b/client/src/actions/user.test.ts
@@ -0,0 +1,81 @@
+import mockedAxios from 'axios'
+import expect from 'expect' // You can use any testing library
+import { createMemoryHistory } from 'history'
+import configureMockStore from 'redux-mock-store'
+import thunk from 'redux-thunk'
+import Types from './types'
+import { loginUser, logoutUser } from './user'
+
+const middlewares = [thunk]
+const mockStore = configureMockStore(middlewares)
+
+it('dispatches correct actions when logging in user', async () => {
+  const loginRes: any = {
+    data: {
+      access_token: 'TEST_ACCESS_TOKEN',
+    },
+  }
+  const userDataRes: any = {
+    data: {
+      name: 'test_name',
+    },
+  }
+  ;(mockedAxios.post as jest.Mock).mockImplementation((path: string, params?: any) => {
+    return Promise.resolve(loginRes)
+  })
+  ;(mockedAxios.get as jest.Mock).mockImplementation((path: string, params?: any) => {
+    return Promise.resolve(userDataRes)
+  })
+  const expectedActions = [
+    { type: Types.LOADING_UI },
+    { type: Types.LOADING_USER },
+    { type: Types.CLEAR_ERRORS },
+    { type: Types.SET_USER, payload: { name: 'test_name' } },
+  ]
+  const store = mockStore({})
+  const history = createMemoryHistory()
+  await loginUser({ email: 'test@email.com', password: 'testpassword' }, history)(store.dispatch)
+  expect(store.getActions()).toEqual(expectedActions)
+})
+
+it('dispatches correct action when logging out user', async () => {
+  const store = mockStore({})
+  await logoutUser()(store.dispatch)
+  expect(store.getActions()).toEqual([{ type: Types.SET_UNAUTHENTICATED }])
+})
+
+it('dispatches correct action when failing to log in user', async () => {
+  console.log = jest.fn()
+  const errorMessage = 'getting teams failed'
+  ;(mockedAxios.post as jest.Mock).mockImplementation((path: string, params?: any) => {
+    return Promise.reject({ response: { data: errorMessage } })
+  })
+  const store = mockStore({ competitions: { filterParams: [] } })
+  const history = createMemoryHistory()
+  const expectedActions = [{ type: Types.LOADING_UI }, { type: Types.SET_ERRORS, payload: errorMessage }]
+  await loginUser({ email: 'test@email.com', password: 'testpassword' }, history)(store.dispatch)
+  expect(store.getActions()).toEqual(expectedActions)
+  expect(console.log).toHaveBeenCalled()
+})
+
+it('dispatches correct actions when failing to get user data', async () => {
+  console.log = jest.fn()
+  const loginRes: any = {
+    data: {
+      access_token: 'TEST_ACCESS_TOKEN',
+    },
+  }
+  ;(mockedAxios.post as jest.Mock).mockImplementation((path: string, params?: any) => {
+    return Promise.resolve(loginRes)
+  })
+  const errorMessage = 'getting teams failed'
+  ;(mockedAxios.get as jest.Mock).mockImplementation((path: string, params?: any) => {
+    return Promise.reject({ response: { data: errorMessage } })
+  })
+  const store = mockStore({ competitions: { filterParams: [] } })
+  const history = createMemoryHistory()
+  const expectedActions = [{ type: Types.LOADING_UI }, { type: Types.LOADING_USER }, { type: Types.CLEAR_ERRORS }]
+  await loginUser({ email: 'test@email.com', password: 'testpassword' }, history)(store.dispatch)
+  expect(store.getActions()).toEqual(expectedActions)
+  expect(console.log).toHaveBeenCalled()
+})
diff --git a/client/src/actions/user.ts b/client/src/actions/user.ts
index 72d764c861db49024c5fbcf00e3291efc35b2609..30374ccafb38fd3c1f6e9a72378104b2cf2611fd 100644
--- a/client/src/actions/user.ts
+++ b/client/src/actions/user.ts
@@ -17,10 +17,10 @@ export const loginUser = (userData: AccountLoginModel, history: History) => asyn
       history.push('/admin') //redirecting to admin page after login success
     })
     .catch((err) => {
-      console.error(err)
+      console.log(err)
       dispatch({
         type: Types.SET_ERRORS,
-        payload: err.response.data,
+        payload: err && err.response && err.response.data,
       })
     })
 }
@@ -30,7 +30,6 @@ export const getUserData = () => async (dispatch: AppDispatch) => {
   await axios
     .get('/users')
     .then((res) => {
-      console.log(res.data)
       dispatch({
         type: Types.SET_USER,
         payload: res.data,
diff --git a/client/src/pages/admin/regions/AddRegion.test.tsx b/client/src/pages/admin/regions/AddRegion.test.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..06b2052a55f14482dbe1474f03aad76894f2677f
--- /dev/null
+++ b/client/src/pages/admin/regions/AddRegion.test.tsx
@@ -0,0 +1,16 @@
+import { render } from '@testing-library/react'
+import React from 'react'
+import { Provider } from 'react-redux'
+import { BrowserRouter } from 'react-router-dom'
+import store from '../../../store'
+import AddRegion from './AddRegion'
+
+it('renders add region', () => {
+  render(
+    <BrowserRouter>
+      <Provider store={store}>
+        <AddRegion />
+      </Provider>
+    </BrowserRouter>
+  )
+})
diff --git a/client/src/pages/admin/regions/Regions.test.tsx b/client/src/pages/admin/regions/Regions.test.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..7046bff04fc8fc9fc8cb3f08d0d140d0d475b5a3
--- /dev/null
+++ b/client/src/pages/admin/regions/Regions.test.tsx
@@ -0,0 +1,37 @@
+import { render } from '@testing-library/react'
+import mockedAxios from 'axios'
+import React from 'react'
+import { Provider } from 'react-redux'
+import { BrowserRouter } from 'react-router-dom'
+import store from '../../../store'
+import RegionManager from './Regions'
+
+it('renders region manager', () => {
+  const cityRes: any = {
+    data: {
+      items: [
+        {
+          id: 1,
+          name: 'Link\u00f6ping',
+        },
+        {
+          id: 2,
+          name: 'Stockholm',
+        },
+      ],
+      count: 2,
+      total_count: 3,
+    },
+  }
+
+  ;(mockedAxios.get as jest.Mock).mockImplementation((path: string, params?: any) => {
+    return Promise.resolve(cityRes)
+  })
+  render(
+    <BrowserRouter>
+      <Provider store={store}>
+        <RegionManager />
+      </Provider>
+    </BrowserRouter>
+  )
+})
diff --git a/client/src/pages/admin/users/AddUser.test.tsx b/client/src/pages/admin/users/AddUser.test.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..5c825570418e638403040ae3ba73423a01acda24
--- /dev/null
+++ b/client/src/pages/admin/users/AddUser.test.tsx
@@ -0,0 +1,16 @@
+import { render } from '@testing-library/react'
+import React from 'react'
+import { Provider } from 'react-redux'
+import { BrowserRouter } from 'react-router-dom'
+import store from '../../../store'
+import EditUser from './EditUser'
+
+it('renders edit user', () => {
+  render(
+    <BrowserRouter>
+      <Provider store={store}>
+        <EditUser user={{ id: 0, name: '', email: '', role_id: 0, city_id: 0 }} />
+      </Provider>
+    </BrowserRouter>
+  )
+})
diff --git a/client/src/pages/admin/users/EditUser.test.tsx b/client/src/pages/admin/users/EditUser.test.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..bf2b00900792996caa42100b61dac03138401c70
--- /dev/null
+++ b/client/src/pages/admin/users/EditUser.test.tsx
@@ -0,0 +1,16 @@
+import { render } from '@testing-library/react'
+import React from 'react'
+import { Provider } from 'react-redux'
+import { BrowserRouter } from 'react-router-dom'
+import store from '../../../store'
+import AddUser from './AddUser'
+
+it('renders edit user', () => {
+  render(
+    <BrowserRouter>
+      <Provider store={store}>
+        <AddUser />
+      </Provider>
+    </BrowserRouter>
+  )
+})
diff --git a/client/src/pages/admin/users/UserManager.test.tsx b/client/src/pages/admin/users/UserManager.test.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..f50cbed8ded2b12d3d487e96f1d185bca2a2cbf8
--- /dev/null
+++ b/client/src/pages/admin/users/UserManager.test.tsx
@@ -0,0 +1,43 @@
+import { render } from '@testing-library/react'
+import mockedAxios from 'axios'
+import React from 'react'
+import { Provider } from 'react-redux'
+import { BrowserRouter } from 'react-router-dom'
+import store from '../../../store'
+import UserManager from './UserManager'
+
+it('renders user manager', () => {
+  const userRes: any = {
+    data: {
+      items: [
+        {
+          id: 1,
+          name: 'user1',
+          email: 'user1@email.com',
+          role_id: 0,
+          city_id: 0,
+        },
+        {
+          id: 2,
+          name: 'Stockholm',
+          email: 'user2@email.com',
+          role_id: 0,
+          city_id: 0,
+        },
+      ],
+      count: 2,
+      total_count: 3,
+    },
+  }
+
+  ;(mockedAxios.get as jest.Mock).mockImplementation((path: string, params?: any) => {
+    return Promise.resolve(userRes)
+  })
+  render(
+    <BrowserRouter>
+      <Provider store={store}>
+        <UserManager />
+      </Provider>
+    </BrowserRouter>
+  )
+})
diff --git a/client/src/pages/admin/users/UserManager.tsx b/client/src/pages/admin/users/UserManager.tsx
index 11a34128b9093784cb79cb7e19bada1a457e6e71..df9cdec0b1618291b3efbe903aea69f94351aaad 100644
--- a/client/src/pages/admin/users/UserManager.tsx
+++ b/client/src/pages/admin/users/UserManager.tsx
@@ -70,7 +70,6 @@ const UserManager: React.FC = (props: any) => {
   }, [])
 
   useEffect(() => {
-    console.log('asd')
     setEditAnchorEl(null)
     setAnchorEl(null)
   }, [users])
diff --git a/client/src/pages/presentationEditor/components/CompetitionSettings.test.tsx b/client/src/pages/presentationEditor/components/CompetitionSettings.test.tsx
index e6f87e7e2beba3141c73d9bca5064d5f8e19ad4d..592581d4f8b3eaa884b7e0b409f39ee5ae8e5344 100644
--- a/client/src/pages/presentationEditor/components/CompetitionSettings.test.tsx
+++ b/client/src/pages/presentationEditor/components/CompetitionSettings.test.tsx
@@ -1,7 +1,7 @@
 import { render } from '@testing-library/react'
 import React from 'react'
-import CompetitionSettings from './CompetitionSettings'
+import ImageComponentDisplay from './ImageComponentDisplay'
 
-it('renders competition settings', () => {
-  render(<CompetitionSettings />)
+it('renders image component display', () => {
+  render(<ImageComponentDisplay component={{ id: 0, x: 0, y: 0, w: 0, h: 0, type: 0, media_id: 0 }} />)
 })
diff --git a/client/src/pages/presentationEditor/components/ImageComponentDisplay.test.tsx b/client/src/pages/presentationEditor/components/ImageComponentDisplay.test.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..b726023b8e6079e7a631c2be396857e9ca0ef4c1
--- /dev/null
+++ b/client/src/pages/presentationEditor/components/ImageComponentDisplay.test.tsx
@@ -0,0 +1,7 @@
+import { render } from '@testing-library/react'
+import React from 'react'
+import ImageComponentDisplay from './ImageComponentDisplay'
+
+it('renders competition settings', () => {
+  render(<ImageComponentDisplay component={{ id: 0, x: 0, y: 0, w: 0, h: 0, media_id: 0, type: 2 }} />)
+})
diff --git a/client/src/pages/presentationEditor/components/TextComponentDisplay.test.tsx b/client/src/pages/presentationEditor/components/TextComponentDisplay.test.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..8f61ee36f8bb86e9e422b135c76448c31d60a6d4
--- /dev/null
+++ b/client/src/pages/presentationEditor/components/TextComponentDisplay.test.tsx
@@ -0,0 +1,12 @@
+import { Editor } from '@tinymce/tinymce-react'
+import { mount } from 'enzyme'
+import React from 'react'
+import TextComponentDisplay from './TextComponentDisplay'
+
+it('renders text component display', () => {
+  const testText = 'TEST'
+  const container = mount(
+    <TextComponentDisplay component={{ id: 0, x: 0, y: 0, w: 0, h: 0, text: testText, type: 2, font: '123123' }} />
+  )
+  expect(container.find(Editor).prop('initialValue')).toBe(testText)
+})
diff --git a/client/src/reducers/citiesReducer.test.ts b/client/src/reducers/citiesReducer.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..320268952b997a799b1a6bf35191cfe88cdfd2b5
--- /dev/null
+++ b/client/src/reducers/citiesReducer.test.ts
@@ -0,0 +1,53 @@
+import Types from '../actions/types'
+import citiesReducer from './citiesReducer'
+
+const initialState = {
+  cities: [],
+  total: 0,
+  count: 0,
+}
+
+it('should return the initial state', () => {
+  expect(citiesReducer(undefined, {} as any)).toEqual(initialState)
+})
+
+it('should handle SET_CITIES', () => {
+  const testCities = [{ name: 'testName', id: 0 }]
+  expect(
+    citiesReducer(initialState, {
+      type: Types.SET_CITIES,
+      payload: testCities,
+    })
+  ).toEqual({
+    cities: testCities,
+    total: 0,
+    count: 0,
+  })
+})
+
+it('should handle SET_CITIES_TOTAL', () => {
+  const testTotal = 123123
+  expect(
+    citiesReducer(initialState, {
+      type: Types.SET_CITIES_TOTAL,
+      payload: testTotal,
+    })
+  ).toEqual({
+    cities: [],
+    total: testTotal,
+    count: 0,
+  })
+})
+it('should handle SET_CITIES_COUNT', () => {
+  const testCount = 456456
+  expect(
+    citiesReducer(initialState, {
+      type: Types.SET_CITIES_COUNT,
+      payload: testCount,
+    })
+  ).toEqual({
+    cities: [],
+    total: 0,
+    count: testCount,
+  })
+})
diff --git a/client/src/reducers/presentationReducer.test.ts b/client/src/reducers/presentationReducer.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..cf4ded4132b1fa1cb1490a89584791032746385b
--- /dev/null
+++ b/client/src/reducers/presentationReducer.test.ts
@@ -0,0 +1,193 @@
+import Types from '../actions/types'
+import { RichSlide } from '../interfaces/ApiRichModels'
+import { Slide } from '../interfaces/Slide'
+import presentationReducer from './presentationReducer'
+
+const initialState = {
+  competition: {
+    name: '',
+    id: 0,
+    city: {
+      id: 0,
+      name: '',
+    },
+    slides: [],
+    year: 0,
+    teams: [],
+  },
+  slide: {
+    competition_id: 0,
+    id: 0,
+    order: 0,
+    timer: 0,
+    title: '',
+  },
+  teams: [],
+}
+
+it('should return the initial state', () => {
+  expect(presentationReducer(undefined, {} as any)).toEqual(initialState)
+})
+
+it('should handle SET_PRESENTATION_COMPETITION', () => {
+  const testCompetition = {
+    name: 'testCompName',
+    id: 4,
+    city: {
+      id: 3,
+      name: 'testCityName',
+    },
+    slides: [{ id: 20 }],
+    year: 1999,
+    teams: [],
+  }
+  expect(
+    presentationReducer(initialState, {
+      type: Types.SET_PRESENTATION_COMPETITION,
+      payload: testCompetition,
+    })
+  ).toEqual({
+    competition: testCompetition,
+    slide: testCompetition.slides[0],
+    teams: [],
+  })
+})
+
+it('should handle SET_PRESENTATION_TEAMS', () => {
+  const testTeams = [
+    {
+      name: 'testTeamName1',
+      id: 3,
+    },
+    {
+      name: 'testTeamName2',
+      id: 5,
+    },
+  ]
+  expect(
+    presentationReducer(initialState, {
+      type: Types.SET_PRESENTATION_TEAMS,
+      payload: testTeams,
+    })
+  ).toEqual({
+    competition: initialState.competition,
+    slide: initialState.slide,
+    teams: testTeams,
+  })
+})
+
+it('should handle SET_PRESENTATION_SLIDE', () => {
+  const testSlide = [
+    {
+      competition_id: 20,
+      id: 4,
+      order: 3,
+      timer: 123,
+      title: 'testSlideTitle',
+    },
+  ]
+  expect(
+    presentationReducer(initialState, {
+      type: Types.SET_PRESENTATION_SLIDE,
+      payload: testSlide,
+    })
+  ).toEqual({
+    competition: initialState.competition,
+    slide: testSlide,
+    teams: initialState.teams,
+  })
+})
+
+describe('should handle SET_PRESENTATION_SLIDE_PREVIOUS', () => {
+  it('by changing slide to the previous if there is one', () => {
+    const testPresentationState = {
+      competition: {
+        ...initialState.competition,
+        slides: [
+          { competition_id: 0, order: 0 },
+          { competition_id: 0, order: 1 },
+        ] as RichSlide[],
+      },
+      teams: initialState.teams,
+      slide: { competition_id: 0, order: 1 } as Slide,
+    }
+    expect(
+      presentationReducer(testPresentationState, {
+        type: Types.SET_PRESENTATION_SLIDE_PREVIOUS,
+      })
+    ).toEqual({
+      competition: testPresentationState.competition,
+      slide: testPresentationState.competition.slides[0],
+      teams: testPresentationState.teams,
+    })
+  })
+  it('by not changing slide if there is no previous one', () => {
+    const testPresentationState = {
+      competition: {
+        ...initialState.competition,
+        slides: [
+          { competition_id: 0, order: 0 },
+          { competition_id: 0, order: 1 },
+        ] as RichSlide[],
+      },
+      teams: initialState.teams,
+      slide: { competition_id: 0, order: 0 } as Slide,
+    }
+    expect(
+      presentationReducer(testPresentationState, {
+        type: Types.SET_PRESENTATION_SLIDE_PREVIOUS,
+      })
+    ).toEqual({
+      competition: testPresentationState.competition,
+      slide: testPresentationState.competition.slides[0],
+      teams: testPresentationState.teams,
+    })
+  })
+})
+
+describe('should handle SET_PRESENTATION_SLIDE_NEXT', () => {
+  it('by changing slide to the next if there is one', () => {
+    const testPresentationState = {
+      competition: {
+        ...initialState.competition,
+        slides: [
+          { competition_id: 0, order: 0 },
+          { competition_id: 0, order: 1 },
+        ] as RichSlide[],
+      },
+      teams: initialState.teams,
+      slide: { competition_id: 0, order: 0 } as Slide,
+    }
+    expect(
+      presentationReducer(testPresentationState, {
+        type: Types.SET_PRESENTATION_SLIDE_NEXT,
+      })
+    ).toEqual({
+      competition: testPresentationState.competition,
+      slide: testPresentationState.competition.slides[1],
+      teams: testPresentationState.teams,
+    })
+  })
+  it('by not changing slide if there is no next one', () => {
+    const testPresentationState = {
+      competition: {
+        ...initialState.competition,
+        slides: [
+          { competition_id: 0, order: 0 },
+          { competition_id: 0, order: 1 },
+        ] as RichSlide[],
+      },
+      teams: initialState.teams,
+      slide: { competition_id: 0, order: 1 } as Slide,
+    }
+    expect(
+      presentationReducer(testPresentationState, {
+        type: Types.SET_PRESENTATION_SLIDE_NEXT,
+      })
+    ).toEqual({
+      competition: testPresentationState.competition,
+      slide: testPresentationState.competition.slides[1],
+      teams: testPresentationState.teams,
+    })
+  })
+})
diff --git a/client/src/reducers/uiReducer.test.ts b/client/src/reducers/uiReducer.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..cea5c3ad137dc8f4fbcc2bb88ccd136fd487f5f9
--- /dev/null
+++ b/client/src/reducers/uiReducer.test.ts
@@ -0,0 +1,64 @@
+import Types from '../actions/types'
+import userReducer from './userReducer'
+
+const initialState = {
+  authenticated: false,
+  loading: false,
+  userInfo: null,
+}
+
+it('should return the initial state', () => {
+  expect(userReducer(undefined, {} as any)).toEqual(initialState)
+})
+
+it('should handle SET_AUTHENTICATED', () => {
+  expect(
+    userReducer(initialState, {
+      type: Types.SET_AUTHENTICATED,
+    })
+  ).toEqual({
+    authenticated: true,
+    loading: initialState.loading,
+    userInfo: initialState.userInfo,
+  })
+})
+
+it('should handle SET_UNAUTHENTICATED', () => {
+  expect(
+    userReducer(initialState, {
+      type: Types.SET_UNAUTHENTICATED,
+    })
+  ).toEqual(initialState)
+})
+
+it('should handle SET_USER', () => {
+  const testUserInfo = {
+    name: 'testName',
+    email: 'test@email.com',
+    role: { id: 0, name: 'roleName' },
+    city: { id: 0, name: 'cityName' },
+    id: 0,
+  }
+  expect(
+    userReducer(initialState, {
+      type: Types.SET_USER,
+      payload: testUserInfo,
+    })
+  ).toEqual({
+    authenticated: true,
+    loading: false,
+    userInfo: testUserInfo,
+  })
+})
+
+it('should handle LOADING_USER', () => {
+  expect(
+    userReducer(initialState, {
+      type: Types.LOADING_USER,
+    })
+  ).toEqual({
+    loading: true,
+    authenticated: initialState.authenticated,
+    userInfo: initialState.userInfo,
+  })
+})
diff --git a/client/src/reducers/userReducer.test.ts b/client/src/reducers/userReducer.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..6ccf026801679367515f6fcc7756554e54845889
--- /dev/null
+++ b/client/src/reducers/userReducer.test.ts
@@ -0,0 +1,45 @@
+import Types from '../actions/types'
+import uiReducer from './uiReducer'
+
+const initialState = {
+  loading: false,
+  errors: null,
+}
+
+it('should return the initial state', () => {
+  expect(uiReducer(undefined, {} as any)).toEqual(initialState)
+})
+
+it('should handle SET_ERRORS', () => {
+  const testError = { message: 'errorMessage' }
+  expect(
+    uiReducer(initialState, {
+      type: Types.SET_ERRORS,
+      payload: testError,
+    })
+  ).toEqual({
+    loading: false,
+    errors: testError,
+  })
+})
+
+it('should handle CLEAR_ERRORS', () => {
+  expect(
+    uiReducer(initialState, {
+      type: Types.CLEAR_ERRORS,
+    })
+  ).toEqual({
+    loading: false,
+    errors: null,
+  })
+})
+it('should handle LOADING_UI', () => {
+  expect(
+    uiReducer(initialState, {
+      type: Types.LOADING_UI,
+    })
+  ).toEqual({
+    loading: true,
+    errors: initialState.errors,
+  })
+})
diff --git a/client/src/utils/checkAuthentication.test.ts b/client/src/utils/checkAuthentication.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..901f331d6cc32c68e2a87c32338e879719c736af
--- /dev/null
+++ b/client/src/utils/checkAuthentication.test.ts
@@ -0,0 +1,59 @@
+import mockedAxios from 'axios'
+import Types from '../actions/types'
+import store from '../store'
+import { CheckAuthentication } from './checkAuthentication'
+
+it('dispatches correct actions when auth token is ok', async () => {
+  const userRes: any = {
+    data: {
+      name: 'username',
+    },
+  }
+
+  ;(mockedAxios.get as jest.Mock).mockImplementation((path: string, params?: any) => {
+    return Promise.resolve(userRes)
+  })
+
+  const spy = jest.spyOn(store, 'dispatch')
+  const testToken =
+    'Bearer eyJ0eXAiOiJeyJ0eXAiOiJKV1QeyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2MTgzMDQ5MzksImV4cCI6MzI1MTI1MjU3NTcsImF1ZCI6Ind3dy5leGFtcGxlLmNvbSIsInN1YiI6Impyb2NrZXRAZXhhbXBsZS5jb20iLCJHaXZlbk5hbWUiOiJKb2hubnkiLCJTdXJuYW1lIjoiUm9ja2V0IiwiRW1haWwiOiJqcm9ja2V0QGV4YW1wbGUuY29tIiwiUm9sZSI6WyJNYW5hZ2VyIiwiUHJvamVjdCBBZG1pbmlzdHJhdG9yIl19.DrOOcCo5jMR-SNERE0uRp_kolQ2HjX8-hHXYEMnIxSceyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2MTgzMDQ5MzksImV4cCI6MzI1MTI1MjU3NTcsImF1ZCI6Ind3dy5leGFtcGxlLmNvbSIsInN1YiI6Impyb2NrZXRAZXhhbXBsZS5jb20iLCJHaXZlbk5hbWUiOiJKb2hubnkiLCJTdXJuYW1lIjoiUm9ja2V0IiwiRW1haWwiOiJqcm9ja2V0QGV4YW1wbGUuY29tIiwiUm9sZSI6WyJNYW5hZ2VyIiwiUHJvamVjdCBBZG1pbmlzdHJhdG9yIl19.DrOOcCo5jMR-SNERE0uRp_kolQ2HjX8-hHXYEMnIxSceyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2MTgzMDQ5MzksImV4cCI6MzI1MTI1MjU3NTcsImF1ZCI6Ind3dy5leGFtcGxlLmNvbSIsInN1YiI6Impyb2NrZXRAZXhhbXBsZS5jb20iLCJHaXZlbk5hbWUiOiJKb2hubnkiLCJTdXJuYW1lIjoiUm9ja2V0IiwiRW1haWwiOiJqcm9ja2V0QGV4YW1wbGUuY29tIiwiUm9sZSI6WyJNYW5hZ2VyIiwiUHJvamVjdCBBZG1pbmlzdHJhdG9yIl19.DrOOcCo5jMR-SNERE0uRp_kolQ2HjX8-hHXYEMnIxSceyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2MTgzMDQ5MzksImV4cCI6MzI1MTI1MjU3NTcsImF1ZCI6Ind3dy5leGFtcGxlLmNvbSIsInN1YiI6Impyb2NrZXRAZXhhbXBsZS5jb20iLCJHaXZlbk5hbWUiOiJKb2hubnkiLCJTdXJuYW1lIjoiUm9ja2V0IiwiRW1haWwiOiJqcm9ja2V0QGV4YW1wbGUuY29tIiwiUm9sZSI6WyJNYW5hZ2VyIiwiUHJvamVjdCBBZG1pbmlzdHJhdG9yIl19.DrOOcCo5jMR-SNERE0uRp_kolQ2HjX8-hHXYEMnIxSciLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2MTgzMDQ5MzksImV4cCI6MzI1MTI1MjU3NTcsImF1ZCI6Ind3dy5leGFtcGxlLmNvbSIsInN1YiI6Impyb2NrZXRAZXhhbXBsZS5jb20iLCJHaXZlbk5hbWUiOiJKb2hubnkiLCJTdXJuYW1lIjoiUm9ja2V0IiwiRW1haWwiOiJqcm9ja2V0QGV4YW1wbGUuY29tIiwiUm9sZSI6WyJNYW5hZ2VyIiwiUHJvamVjdCBBZG1pbmlzdHJhdG9yIl19.DrOOcCo5jMR-SNERE0uRp_kolQ2HjX8-hHXYEMnIxScKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2MTgzMDQ5MzksImV4cCI6MzI1MTI1MjU3NTcsImF1ZCI6Ind3dy5leGFtcGxlLmNvbSIsInN1YiI6Impyb2NrZXRAZXhhbXBsZS5jb20iLCJHaXZlbk5hbWUiOiJKb2hubnkiLCJTdXJuYW1lIjoiUm9ja2V0IiwiRW1haWwiOiJqcm9ja2V0QGV4YW1wbGUuY29tIiwiUm9sZSI6WyJNYW5hZ2VyIiwiUHJvamVjdCBBZG1pbmlzdHJhdG9yIl19.DrOOcCo5jMR-SNERE0uRp_kolQ2HjX8-hHXYEMnIxSc'
+  localStorage.setItem('token', testToken)
+  await CheckAuthentication()
+  expect(spy).toBeCalledWith({ type: Types.LOADING_USER })
+  expect(spy).toBeCalledWith({ type: Types.SET_AUTHENTICATED })
+  expect(spy).toBeCalledWith({ type: Types.SET_USER, payload: userRes.data })
+  expect(spy).toBeCalledTimes(3)
+})
+
+it('dispatches correct actions when getting user data fails', async () => {
+  console.log = jest.fn()
+  ;(mockedAxios.get as jest.Mock).mockImplementation((path: string, params?: any) => {
+    return Promise.reject(new Error('failed getting user data'))
+  })
+
+  const spy = jest.spyOn(store, 'dispatch')
+  const testToken =
+    'Bearer eyJ0eXAiOiJeyJ0eXAiOiJKV1QeyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2MTgzMDQ5MzksImV4cCI6MzI1MTI1MjU3NTcsImF1ZCI6Ind3dy5leGFtcGxlLmNvbSIsInN1YiI6Impyb2NrZXRAZXhhbXBsZS5jb20iLCJHaXZlbk5hbWUiOiJKb2hubnkiLCJTdXJuYW1lIjoiUm9ja2V0IiwiRW1haWwiOiJqcm9ja2V0QGV4YW1wbGUuY29tIiwiUm9sZSI6WyJNYW5hZ2VyIiwiUHJvamVjdCBBZG1pbmlzdHJhdG9yIl19.DrOOcCo5jMR-SNERE0uRp_kolQ2HjX8-hHXYEMnIxSceyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2MTgzMDQ5MzksImV4cCI6MzI1MTI1MjU3NTcsImF1ZCI6Ind3dy5leGFtcGxlLmNvbSIsInN1YiI6Impyb2NrZXRAZXhhbXBsZS5jb20iLCJHaXZlbk5hbWUiOiJKb2hubnkiLCJTdXJuYW1lIjoiUm9ja2V0IiwiRW1haWwiOiJqcm9ja2V0QGV4YW1wbGUuY29tIiwiUm9sZSI6WyJNYW5hZ2VyIiwiUHJvamVjdCBBZG1pbmlzdHJhdG9yIl19.DrOOcCo5jMR-SNERE0uRp_kolQ2HjX8-hHXYEMnIxSceyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2MTgzMDQ5MzksImV4cCI6MzI1MTI1MjU3NTcsImF1ZCI6Ind3dy5leGFtcGxlLmNvbSIsInN1YiI6Impyb2NrZXRAZXhhbXBsZS5jb20iLCJHaXZlbk5hbWUiOiJKb2hubnkiLCJTdXJuYW1lIjoiUm9ja2V0IiwiRW1haWwiOiJqcm9ja2V0QGV4YW1wbGUuY29tIiwiUm9sZSI6WyJNYW5hZ2VyIiwiUHJvamVjdCBBZG1pbmlzdHJhdG9yIl19.DrOOcCo5jMR-SNERE0uRp_kolQ2HjX8-hHXYEMnIxSceyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2MTgzMDQ5MzksImV4cCI6MzI1MTI1MjU3NTcsImF1ZCI6Ind3dy5leGFtcGxlLmNvbSIsInN1YiI6Impyb2NrZXRAZXhhbXBsZS5jb20iLCJHaXZlbk5hbWUiOiJKb2hubnkiLCJTdXJuYW1lIjoiUm9ja2V0IiwiRW1haWwiOiJqcm9ja2V0QGV4YW1wbGUuY29tIiwiUm9sZSI6WyJNYW5hZ2VyIiwiUHJvamVjdCBBZG1pbmlzdHJhdG9yIl19.DrOOcCo5jMR-SNERE0uRp_kolQ2HjX8-hHXYEMnIxSciLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2MTgzMDQ5MzksImV4cCI6MzI1MTI1MjU3NTcsImF1ZCI6Ind3dy5leGFtcGxlLmNvbSIsInN1YiI6Impyb2NrZXRAZXhhbXBsZS5jb20iLCJHaXZlbk5hbWUiOiJKb2hubnkiLCJTdXJuYW1lIjoiUm9ja2V0IiwiRW1haWwiOiJqcm9ja2V0QGV4YW1wbGUuY29tIiwiUm9sZSI6WyJNYW5hZ2VyIiwiUHJvamVjdCBBZG1pbmlzdHJhdG9yIl19.DrOOcCo5jMR-SNERE0uRp_kolQ2HjX8-hHXYEMnIxScKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2MTgzMDQ5MzksImV4cCI6MzI1MTI1MjU3NTcsImF1ZCI6Ind3dy5leGFtcGxlLmNvbSIsInN1YiI6Impyb2NrZXRAZXhhbXBsZS5jb20iLCJHaXZlbk5hbWUiOiJKb2hubnkiLCJTdXJuYW1lIjoiUm9ja2V0IiwiRW1haWwiOiJqcm9ja2V0QGV4YW1wbGUuY29tIiwiUm9sZSI6WyJNYW5hZ2VyIiwiUHJvamVjdCBBZG1pbmlzdHJhdG9yIl19.DrOOcCo5jMR-SNERE0uRp_kolQ2HjX8-hHXYEMnIxSc'
+  localStorage.setItem('token', testToken)
+  await CheckAuthentication()
+  expect(spy).toBeCalledWith({ type: Types.LOADING_USER })
+  expect(spy).toBeCalledWith({ type: Types.SET_UNAUTHENTICATED })
+  expect(spy).toBeCalledTimes(2)
+  expect(console.log).toHaveBeenCalled()
+})
+
+it('dispatches no actions when no token exists', async () => {
+  const spy = jest.spyOn(store, 'dispatch')
+  await CheckAuthentication()
+  expect(spy).not.toBeCalled()
+})
+
+it('dispatches correct actions when token is expired', async () => {
+  const testToken =
+    'Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2MTgzMDY1MTUsImV4cCI6MTU4Njc3MDUxNSwiYXVkIjoid3d3LmV4YW1wbGUuY29tIiwic3ViIjoianJvY2tldEBleGFtcGxlLmNvbSIsIkdpdmVuTmFtZSI6IkpvaG5ueSIsIlN1cm5hbWUiOiJSb2NrZXQiLCJFbWFpbCI6Impyb2NrZXRAZXhhbXBsZS5jb20iLCJSb2xlIjpbIk1hbmFnZXIiLCJQcm9qZWN0IEFkbWluaXN0cmF0b3IiXX0.R5-oWGGumd-YWPoKyziJmVB8SdX6B9SsV6m7novIfgg'
+  localStorage.setItem('token', testToken)
+  const spy = jest.spyOn(store, 'dispatch')
+  await CheckAuthentication()
+  expect(spy).toBeCalledWith({ type: Types.SET_UNAUTHENTICATED })
+  expect(spy).toBeCalledTimes(1)
+})
diff --git a/client/src/utils/checkAuthentication.ts b/client/src/utils/checkAuthentication.ts
index 83b735543422b79e6c2323243d8fa1468566cbf4..231781543c4b8c3088585568fbeedbcab4e16f41 100644
--- a/client/src/utils/checkAuthentication.ts
+++ b/client/src/utils/checkAuthentication.ts
@@ -15,7 +15,6 @@ export const CheckAuthentication = async () => {
     if (decodedToken.exp * 1000 >= Date.now()) {
       axios.defaults.headers.common['Authorization'] = authToken
       store.dispatch({ type: Types.LOADING_USER })
-      console.log('loading user')
       await axios
         .get('/users')
         .then((res) => {
@@ -26,7 +25,7 @@ export const CheckAuthentication = async () => {
           })
         })
         .catch((error) => {
-          console.error(error)
+          console.log(error)
           UnAuthorized()
         })
     } else {
diff --git a/server/app/__init__.py b/server/app/__init__.py
index d1bbd3af80ccef9d5692f55d3a48ce2e34d920f6..48a104f83ffd051af99dc24b5c528b3057aa480f 100644
--- a/server/app/__init__.py
+++ b/server/app/__init__.py
@@ -1,6 +1,6 @@
 from flask import Flask, redirect, request
 
-import app.core.models as models
+import app.database.models as models
 from app.core import bcrypt, db, jwt, ma
 
 
diff --git a/server/app/apis/__init__.py b/server/app/apis/__init__.py
index 061a45194c07c4a7dc655cac28e256bfea1b2c06..996c14bea098059678ac53028c28cced40853416 100644
--- a/server/app/apis/__init__.py
+++ b/server/app/apis/__init__.py
@@ -58,3 +58,4 @@ 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")
+#flask_api.add_namespace(question_ns, path="/api/competitions/<CID>/slides/<SID>/question")
diff --git a/server/app/apis/auth.py b/server/app/apis/auth.py
index df3a8e5c388510fe349b07fa9aff0b8f06edbbf6..1510df642682f2d00d94bb695af45e7fb1cf24f4 100644
--- a/server/app/apis/auth.py
+++ b/server/app/apis/auth.py
@@ -1,9 +1,9 @@
-import app.core.controller as dbc
 import app.core.http_codes as codes
+import app.database.controller as dbc
 from app.apis import admin_required, item_response, text_response
 from app.core.dto import AuthDTO
-from app.core.models import User
 from app.core.parsers import create_user_parser, login_parser
+from app.database.models import User
 from flask_jwt_extended import (
     create_access_token,
     create_refresh_token,
@@ -30,14 +30,10 @@ class AuthSignup(Resource):
         args = create_user_parser.parse_args(strict=True)
         email = args.get("email")
 
-        if User.query.filter(User.email == email).count() > 0:
+        if dbc.get.user_exists(email):
             api.abort(codes.BAD_REQUEST, "User already exists")
 
         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")
-
         return item_response(schema.dump(item_user))
 
 
@@ -46,10 +42,7 @@ class AuthSignup(Resource):
 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}.")
+        item_user = dbc.get.user(ID)
 
         dbc.delete.default(item_user)
         if int(ID) == get_jwt_identity():
@@ -64,7 +57,7 @@ class AuthLogin(Resource):
         args = login_parser.parse_args(strict=True)
         email = args.get("email")
         password = args.get("password")
-        item_user = User.query.filter_by(email=email).first()
+        item_user = dbc.get.user_by_email(email, required=False)
 
         if not item_user or not item_user.is_correct_password(password):
             api.abort(codes.UNAUTHORIZED, "Invalid email or password")
@@ -92,7 +85,7 @@ class AuthRefresh(Resource):
     def post(self):
         old_jti = get_raw_jwt()["jti"]
 
-        item_user = User.query.filter_by(id=get_jwt_identity()).first()
+        item_user = dbc.get.user(get_jwt_identity())
         access_token = create_access_token(item_user.id, user_claims=get_user_claims(item_user))
         dbc.add.blacklist(old_jti)
         response = {"access_token": access_token}
diff --git a/server/app/apis/competitions.py b/server/app/apis/competitions.py
index 93f032279eb6580c90251f94309e2eaf1132ffec..be3762062647a576be9f76861961d2a30cb5d5fe 100644
--- a/server/app/apis/competitions.py
+++ b/server/app/apis/competitions.py
@@ -1,8 +1,8 @@
-import app.core.controller as dbc
+import app.database.controller as dbc
 from app.apis import admin_required, item_response, list_response
 from app.core.dto import CompetitionDTO
-from app.core.models import Competition
 from app.core.parsers import competition_parser, competition_search_parser
+from app.database.models import Competition
 from flask_jwt_extended import jwt_required
 from flask_restx import Resource
 
@@ -11,10 +11,6 @@ schema = CompetitionDTO.schema
 list_schema = CompetitionDTO.list_schema
 
 
-def get_comp(CID):
-    return Competition.query.filter(Competition.id == CID).first()
-
-
 @api.route("/")
 class CompetitionsList(Resource):
     @jwt_required
@@ -34,20 +30,22 @@ class CompetitionsList(Resource):
 class Competitions(Resource):
     @jwt_required
     def get(self, CID):
-        item = get_comp(CID)
+        item = dbc.get.competition(CID)
         return item_response(schema.dump(item))
 
     @jwt_required
     def put(self, CID):
         args = competition_parser.parse_args(strict=True)
-        item = get_comp(CID)
+        item = dbc.get.competition(CID)
         item = dbc.edit.competition(item, **args)
+
         return item_response(schema.dump(item))
 
     @jwt_required
     def delete(self, CID):
-        item = get_comp(CID)
+        item = dbc.get.competition(CID)
         dbc.delete.competition(item)
+
         return "deleted"
 
 
@@ -56,5 +54,5 @@ class CompetitionSearch(Resource):
     @jwt_required
     def get(self):
         args = competition_search_parser.parse_args(strict=True)
-        items, total = dbc.get.search_competitions(**args)
+        items, total = dbc.search.competition(**args)
         return list_response(list_schema.dump(items), total)
diff --git a/server/app/apis/misc.py b/server/app/apis/misc.py
index f5b42fda6ab46fc76dd70a774fd3baf9ef467938..7fbb222d23408b59de47485e21e8c16653ce1ffa 100644
--- a/server/app/apis/misc.py
+++ b/server/app/apis/misc.py
@@ -1,7 +1,7 @@
-import app.core.controller as dbc
+import app.database.controller as dbc
 from app.apis import admin_required, item_response, list_response
 from app.core.dto import MiscDTO
-from app.core.models import City, MediaType, QuestionType, Role
+from app.database.models import City, MediaType, QuestionType, Role
 from flask_jwt_extended import jwt_required
 from flask_restx import Resource, reqparse
 
diff --git a/server/app/apis/questions.py b/server/app/apis/questions.py
index 76ca53f900135a7af50e553953467f8cc34f9e8c..86929bb4eb6f9dc90164845e45c281e4b3620afc 100644
--- a/server/app/apis/questions.py
+++ b/server/app/apis/questions.py
@@ -1,12 +1,11 @@
-import app.core.controller as dbc
 import app.core.http_codes as codes
+import app.database.controller as dbc
 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
+from app.database.models import Question
+from flask_jwt_extended import jwt_required
+from flask_restx import Resource
 
 api = QuestionDTO.api
 schema = QuestionDTO.schema
@@ -18,8 +17,8 @@ list_schema = QuestionDTO.list_schema
 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)
+        items = dbc.get.question_list(CID)
+        return list_response(list_schema.dump(items))
 
     @jwt_required
     def post(self, CID):
@@ -41,25 +40,14 @@ class QuestionsList(Resource):
 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}.")
-
+        item_question = dbc.get.question(CID, QID)
         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.get.question(CID, QID)
         item_question = dbc.edit.question(item_question, **args)
 
         return item_response(schema.dump(item_question))
@@ -67,8 +55,5 @@ class Questions(Resource):
     @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 ab7caa59d9eae2c9eab8d48206524422090b0b2e..972e1bd622e31d4fa18d717c9ba5718d7a75b837 100644
--- a/server/app/apis/slides.py
+++ b/server/app/apis/slides.py
@@ -1,9 +1,9 @@
-import app.core.controller as dbc
 import app.core.http_codes as codes
+import app.database.controller as dbc
 from app.apis import admin_required, item_response, list_response
 from app.core.dto import SlideDTO
-from app.core.models import Competition, Slide
 from app.core.parsers import slide_parser
+from app.database.models import Competition, Slide
 from flask_jwt_extended import jwt_required
 from flask_restx import Resource
 
@@ -12,22 +12,19 @@ schema = SlideDTO.schema
 list_schema = SlideDTO.list_schema
 
 
-def get_comp(CID):
-    return Competition.query.filter(Competition.id == CID).first()
-
-
 @api.route("/")
 @api.param("CID")
 class SlidesList(Resource):
     @jwt_required
     def get(self, CID):
-        item_comp = get_comp(CID)
-        return list_response(list_schema.dump(item_comp.slides))
+        items = dbc.get.slide_list(CID)
+        return list_response(list_schema.dump(items))
 
     @jwt_required
     def post(self, CID):
-        item_comp = get_comp(CID)
-        dbc.add.slide(item_comp)
+        item_comp = dbc.get.competition(CID)
+        item_slide = dbc.add.slide(item_comp)
+        dbc.add.question(f"Fråga {item_slide.order + 1}", 10, 0, item_slide)
         dbc.refresh(item_comp)
         return list_response(list_schema.dump(item_comp.slides))
 
@@ -54,8 +51,6 @@ 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 {}, codes.NO_CONTENT
@@ -72,10 +67,10 @@ class SlidesOrder(Resource):
         item_slide = dbc.get.slide(CID, SID)
 
         if order == item_slide.order:
-            api.abort(codes.BAD_REQUEST)
+            return item_response(schema.dump(item_slide))
 
         # clamp order between 0 and max
-        order_count = Slide.query.filter(Slide.competition_id == item_slide.competition_id).count()
+        order_count = dbc.get.slide_count(CID)
         if order < 0:
             order = 0
         elif order >= order_count - 1:
diff --git a/server/app/apis/teams.py b/server/app/apis/teams.py
index 7f7fdf0e8a66208acbd71b910e5ef232f094bf2e..9600e2a4c4ce7fe170162e5e7e2eedeed617b5b8 100644
--- a/server/app/apis/teams.py
+++ b/server/app/apis/teams.py
@@ -1,9 +1,9 @@
-import app.core.controller as dbc
 import app.core.http_codes as codes
+import app.database.controller as dbc
 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 app.database.models import Competition, Team
 from flask_jwt_extended import get_jwt_identity, jwt_required
 from flask_restx import Namespace, Resource, reqparse
 
@@ -12,22 +12,18 @@ schema = TeamDTO.schema
 list_schema = TeamDTO.list_schema
 
 
-def get_comp(CID):
-    return Competition.query.filter(Competition.id == CID).first()
-
-
 @api.route("/")
 @api.param("CID")
 class TeamsList(Resource):
     @jwt_required
     def get(self, CID):
-        item_comp = get_comp(CID)
-        return list_response(list_schema.dump(item_comp.teams))
+        items = dbc.get.team_list(CID)
+        return list_response(list_schema.dump(items))
 
     @jwt_required
     def post(self, CID):
         args = team_parser.parse_args(strict=True)
-        item_comp = get_comp(CID)
+        item_comp = dbc.get.competition(CID)
         item_team = dbc.add.team(args["name"], item_comp)
         return item_response(schema.dump(item_team))
 
@@ -43,8 +39,6 @@ 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 {}, codes.NO_CONTENT
@@ -55,8 +49,6 @@ class Teams(Resource):
         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 07e2484d6cbf67efd23ba2132af6a17f963babce..28642b0dfa47ef9bcd3392c0016d49017f41f8a3 100644
--- a/server/app/apis/users.py
+++ b/server/app/apis/users.py
@@ -1,9 +1,9 @@
-import app.core.controller as dbc
 import app.core.http_codes as codes
+import app.database.controller as dbc
 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 app.database.models import User
 from flask import request
 from flask_jwt_extended import get_jwt_identity, jwt_required
 from flask_restx import Namespace, Resource
@@ -26,13 +26,13 @@ def edit_user(item_user, args):
 class UsersList(Resource):
     @jwt_required
     def get(self):
-        item = User.query.filter(User.id == get_jwt_identity()).first()
+        item = dbc.get.user(get_jwt_identity())
         return item_response(schema.dump(item))
 
     @jwt_required
     def put(self):
         args = user_parser.parse_args(strict=True)
-        item = User.query.filter(User.id == get_jwt_identity()).first()
+        item = dbc.get.user(get_jwt_identity())
         item = edit_user(item, args)
         return item_response(schema.dump(item))
 
@@ -42,13 +42,13 @@ class UsersList(Resource):
 class Users(Resource):
     @jwt_required
     def get(self, ID):
-        item = User.query.filter(User.id == ID).first()
+        item = dbc.get.user(ID)
         return item_response(schema.dump(item))
 
     @jwt_required
     def put(self, ID):
         args = user_parser.parse_args(strict=True)
-        item = User.query.filter(User.id == ID).first()
+        item = dbc.get.user(ID)
         item = edit_user(item, args)
         return item_response(schema.dump(item))
 
@@ -58,5 +58,5 @@ class UserSearch(Resource):
     @jwt_required
     def get(self):
         args = user_search_parser.parse_args(strict=True)
-        items, total = dbc.get.search_user(**args)
+        items, total = dbc.search.user(**args)
         return list_response(list_schema.dump(items), total)
diff --git a/server/app/core/__init__.py b/server/app/core/__init__.py
index e9d132cb332637eb5f064c37427d228c6e57d87f..09c321efa46844b46acc67a0e5513df9d553746b 100644
--- a/server/app/core/__init__.py
+++ b/server/app/core/__init__.py
@@ -1,19 +1,10 @@
-import sqlalchemy as sa
+from app.database.base import Base, ExtendedQuery
 from flask_bcrypt import Bcrypt
 from flask_jwt_extended.jwt_manager import JWTManager
 from flask_marshmallow import Marshmallow
 from flask_sqlalchemy import SQLAlchemy
-from flask_sqlalchemy.model import Model
-from sqlalchemy.sql import func
 
-
-class Base(Model):
-    __abstract__ = True
-    _created = sa.Column(sa.DateTime(timezone=True), server_default=func.now())
-    _updated = sa.Column(sa.DateTime(timezone=True), onupdate=func.now())
-
-
-db = SQLAlchemy(model_class=Base)
+db = SQLAlchemy(model_class=Base, query_class=ExtendedQuery)
 bcrypt = Bcrypt()
 jwt = JWTManager()
 ma = Marshmallow()
diff --git a/server/app/core/rich_schemas.py b/server/app/core/rich_schemas.py
index d714d76db33882b794e494618ca142aabc4fe83d..ab1b3abb9ff50f8888fe13bf5c89b4580faee01b 100644
--- a/server/app/core/rich_schemas.py
+++ b/server/app/core/rich_schemas.py
@@ -1,5 +1,5 @@
-import app.core.models as models
 import app.core.schemas as schemas
+import app.database.models as models
 from app.core import ma
 from marshmallow_sqlalchemy import fields
 
@@ -29,8 +29,30 @@ class QuestionSchemaRich(RichSchema):
     id = ma.auto_field()
     name = ma.auto_field()
     total_score = ma.auto_field()
+    slide_id = ma.auto_field()
     type = fields.Nested(schemas.QuestionTypeSchema, many=False)
-    slide = fields.Nested(schemas.SlideSchema, many=False)
+
+
+class TeamSchemaRich(RichSchema):
+    class Meta(RichSchema.Meta):
+        model = models.Team
+
+    id = ma.auto_field()
+    name = ma.auto_field()
+    competition_id = ma.auto_field()
+    question_answers = fields.Nested(schemas.QuestionAnswerSchema, many=True)
+
+
+class SlideSchemaRich(RichSchema):
+    class Meta(RichSchema.Meta):
+        model = models.Slide
+
+    id = ma.auto_field()
+    order = ma.auto_field()
+    title = ma.auto_field()
+    timer = ma.auto_field()
+    competition_id = ma.auto_field()
+    questions = fields.Nested(QuestionSchemaRich, many=True)
 
 
 class CompetitionSchemaRich(RichSchema):
@@ -40,5 +62,9 @@ class CompetitionSchemaRich(RichSchema):
     id = ma.auto_field()
     name = ma.auto_field()
     year = ma.auto_field()
-    slides = fields.Nested(schemas.SlideSchema, many=True)
     city = fields.Nested(schemas.CitySchema, many=False)
+    slides = fields.Nested(
+        SlideSchemaRich,
+        many=True,
+    )
+    teams = fields.Nested(TeamSchemaRich, many=True)
diff --git a/server/app/core/schemas.py b/server/app/core/schemas.py
index c1d91c0d8d59ba5e14153134e937e9ba408093b9..274406420a2f7f55832f8676b2995276704a8f31 100644
--- a/server/app/core/schemas.py
+++ b/server/app/core/schemas.py
@@ -1,4 +1,4 @@
-import app.core.models as models
+import app.database.models as models
 from app.core import ma
 from marshmallow_sqlalchemy import fields
 
@@ -29,6 +29,17 @@ class QuestionSchema(BaseSchema):
     slide_id = ma.auto_field()
 
 
+class QuestionAnswerSchema(BaseSchema):
+    class Meta(BaseSchema.Meta):
+        model = models.QuestionAnswer
+
+    id = ma.auto_field()
+    data = ma.auto_field()
+    score = ma.auto_field()
+    question_id = ma.auto_field()
+    team_id = ma.auto_field()
+
+
 class MediaTypeSchema(BaseSchema):
     class Meta(BaseSchema.Meta):
         model = models.MediaType
diff --git a/server/app/database/__init__.py b/server/app/database/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/server/app/database/base.py b/server/app/database/base.py
new file mode 100644
index 0000000000000000000000000000000000000000..7a16a7525d73a87b1bc4b19680ac950c15860a6a
--- /dev/null
+++ b/server/app/database/base.py
@@ -0,0 +1,37 @@
+import app.core.http_codes as codes
+import sqlalchemy as sa
+from flask_restx import abort
+from flask_sqlalchemy import BaseQuery, SQLAlchemy
+from flask_sqlalchemy.model import Model
+from sqlalchemy.sql import func
+
+
+class Base(Model):
+    __abstract__ = True
+    _created = sa.Column(sa.DateTime(timezone=True), server_default=func.now())
+    _updated = sa.Column(sa.DateTime(timezone=True), onupdate=func.now())
+
+
+class ExtendedQuery(BaseQuery):
+    def first_extended(self, required=True, error_message=None, error_code=codes.NOT_FOUND):
+        item = self.first()
+
+        if required and not item:
+            if not error_message:
+                error_message = "Object not found"
+            abort(error_code, error_message)
+
+        return item
+
+    def pagination(self, page=0, page_size=15, order_column=None, order=1):
+        query = self
+        if order_column:
+            if order == 1:
+                query = query.order_by(order_column)
+            else:
+                query = query.order_by(order_column.desc())
+
+        total = query.count()
+        query = query.limit(page_size).offset(page * page_size)
+        items = query.all()
+        return items, total
diff --git a/server/app/core/controller/__init__.py b/server/app/database/controller/__init__.py
similarity index 76%
rename from server/app/core/controller/__init__.py
rename to server/app/database/controller/__init__.py
index 57f9b429697d46cab59aa8e8028f27990edf5b5e..d31ded5698957d50c02634f8ca3eff9f90f756c6 100644
--- a/server/app/core/controller/__init__.py
+++ b/server/app/database/controller/__init__.py
@@ -1,6 +1,6 @@
 # import add, get
 from app.core import db
-from app.core.controller import add, delete, edit, get
+from app.database.controller import add, delete, edit, get, search
 
 
 def commit_and_refresh(item):
diff --git a/server/app/core/controller/add.py b/server/app/database/controller/add.py
similarity index 77%
rename from server/app/core/controller/add.py
rename to server/app/database/controller/add.py
index 38c2a28358841da520b2dc3cf3b89627c92366c6..37f34dd731c7af5ba6016eedf0b095d970d113fb 100644
--- a/server/app/core/controller/add.py
+++ b/server/app/database/controller/add.py
@@ -1,5 +1,18 @@
+import app.core.http_codes as codes
 from app.core import db
-from app.core.models import Blacklist, City, Competition, MediaType, Question, QuestionType, Role, Slide, Team, User
+from app.database.models import (
+    Blacklist,
+    City,
+    Competition,
+    MediaType,
+    Question,
+    QuestionType,
+    Role,
+    Slide,
+    Team,
+    User,
+)
+from flask_restx import abort
 
 
 def db_add(func):
@@ -8,6 +21,10 @@ def db_add(func):
         db.session.add(item)
         db.session.commit()
         db.session.refresh(item)
+
+        if not item:
+            abort(codes.BAD_REQUEST, f"Object could not be created")
+
         return item
 
     return wrapper
diff --git a/server/app/core/controller/delete.py b/server/app/database/controller/delete.py
similarity index 86%
rename from server/app/core/controller/delete.py
rename to server/app/database/controller/delete.py
index ac6ddf70efb7584b0161cd8d8a3240b49ecd6ad1..65527ee9dafe1f80169b408e83546e5f4a404dc3 100644
--- a/server/app/core/controller/delete.py
+++ b/server/app/database/controller/delete.py
@@ -1,6 +1,6 @@
-import app.core.controller as dbc
+import app.database.controller as dbc
 from app.core import db
-from app.core.models import Blacklist, City, Competition, Role, Slide, User
+from app.database.models import Blacklist, City, Competition, Role, Slide, User
 
 
 def default(item):
@@ -17,7 +17,7 @@ def slide(item_slide):
     default(item_slide)
 
     # Update slide order for all slides after the deleted slide
-    slides_in_same_competition, _ = dbc.get.search_slide(competition_id=deleted_slide_competition_id)
+    slides_in_same_competition = dbc.get.slide_list(deleted_slide_competition_id)
     for other_slide in slides_in_same_competition:
         if other_slide.order > deleted_slide_order:
             other_slide.order -= 1
diff --git a/server/app/core/controller/edit.py b/server/app/database/controller/edit.py
similarity index 100%
rename from server/app/core/controller/edit.py
rename to server/app/database/controller/edit.py
diff --git a/server/app/database/controller/get.py b/server/app/database/controller/get.py
new file mode 100644
index 0000000000000000000000000000000000000000..a2b45fedf42bbec2c1874cdf35060d9c177582ef
--- /dev/null
+++ b/server/app/database/controller/get.py
@@ -0,0 +1,56 @@
+from app.database.models import Competition, Question, Slide, Team, User
+from sqlalchemy.sql.expression import outerjoin
+
+
+def user_exists(email):
+    return User.query.filter(User.email == email).count() > 0
+
+
+def competition(CID, required=True, error_msg=None):
+    return Competition.query.filter(Competition.id == CID).first_extended(required, error_msg)
+
+
+def user(UID, required=True, error_msg=None):
+    return User.query.filter(User.id == UID).first_extended(required, error_msg)
+
+
+def user_by_email(email, required=True, error_msg=None):
+    return User.query.filter(User.email == email).first_extended(required, error_msg)
+
+
+def slide_by_order(CID, order, required=True, error_msg=None):
+    return Slide.query.filter((Slide.competition_id == CID) & (Slide.order == order)).first_extended(
+        required, error_msg
+    )
+
+
+def slide(CID, SID, required=True, error_msg=None):
+    return Slide.query.filter((Slide.competition_id == CID) & (Slide.id == SID)).first_extended(required, error_msg)
+
+
+def team(CID, TID, required=True, error_msg=None):
+    return Team.query.filter((Team.competition_id == CID) & (Team.id == TID)).first_extended(required, error_msg)
+
+
+def question(CID, QID, required=True, error_msg=None):
+    return (
+        Question.query.join(Slide, (Slide.competition_id == CID) & (Slide.id == Question.slide_id))
+        .filter(Question.id == QID)
+        .first_extended(required, error_msg)
+    )
+
+
+def question_list(CID):
+    return Question.query.join(Slide, (Slide.competition_id == CID) & (Slide.id == Question.slide_id)).all()
+
+
+def team_list(CID):
+    return Team.query.filter(Team.competition_id == CID).all()
+
+
+def slide_list(CID):
+    return Slide.query.filter(Slide.competition_id == CID).all()
+
+
+def slide_count(CID):
+    return Slide.query.filter(Slide.competition_id == CID).count()
diff --git a/server/app/core/controller/get.py b/server/app/database/controller/search.py
similarity index 58%
rename from server/app/core/controller/get.py
rename to server/app/database/controller/search.py
index f695deeea3b4c51b41aa2d639431a27cd59d3ab0..a2ca6b842377cba5d523ea1374de39757e3ddb4b 100644
--- a/server/app/core/controller/get.py
+++ b/server/app/database/controller/search.py
@@ -1,38 +1,7 @@
-from app.core.models import Competition, Question, Slide, Team, User
+from app.database.models import Competition, Question, Slide, Team, User
 
 
-def slide_by_order(CID, order):
-    return Slide.query.filter((Slide.competition_id == CID) & (Slide.order == order)).first()
-
-
-def slide(CID, SID):
-    return Slide.query.filter((Slide.competition_id == CID) & (Slide.id == SID)).first()
-
-
-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)
-    else:
-        query = query.order_by(order_column.desc())
-
-    total = query.count()
-    query = query.limit(page_size).offset(page * page_size)
-    items = query.all()
-    return items, total
-
-
-def search_user(email=None, name=None, city_id=None, role_id=None, page=0, page_size=15, order=1, order_by=None):
+def user(email=None, name=None, city_id=None, role_id=None, page=0, page_size=15, order=1, order_by=None):
     query = User.query
     if name:
         query = query.filter(User.name.like(f"%{name}%"))
@@ -47,12 +16,26 @@ def search_user(email=None, name=None, city_id=None, role_id=None, page=0, page_
     if order_by:
         order_column = getattr(User.__table__.c, order_by)
 
-    return _search(query, order_column, page, page_size, order)
+    return query.pagination(page, page_size, order_column, order)
 
 
-def search_slide(
-    slide_order=None, title=None, body=None, competition_id=None, page=0, page_size=15, order=1, order_by=None
-):
+def competition(name=None, year=None, city_id=None, page=0, page_size=15, order=1, order_by=None):
+    query = Competition.query
+    if name:
+        query = query.filter(Competition.name.like(f"%{name}%"))
+    if year:
+        query = query.filter(Competition.year == year)
+    if city_id:
+        query = query.filter(Competition.city_id == city_id)
+
+    order_column = Competition.year  # Default order_by
+    if order_by:
+        order_column = getattr(Competition.columns, order_by)
+
+    return query.pagination(page, page_size, order_column, order)
+
+
+def 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)
@@ -67,10 +50,10 @@ def search_slide(
     if order_by:
         order_column = getattr(Slide.__table__.c, order_by)
 
-    return _search(query, order_column, page, page_size, order)
+    return query.pagination(page, page_size, order_column, order)
 
 
-def search_questions(
+def questions(
     name=None,
     total_score=None,
     type_id=None,
@@ -100,20 +83,4 @@ def search_questions(
     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:
-        query = query.filter(Competition.name.like(f"%{name}%"))
-    if year:
-        query = query.filter(Competition.year == year)
-    if city_id:
-        query = query.filter(Competition.city_id == city_id)
-
-    order_column = Competition.year  # Default order_by
-    if order_by:
-        order_column = getattr(Competition.columns, order_by)
-
-    return _search(query, order_column, page, page_size, order)
+    return query.pagination(page, page_size, order_column, order)
diff --git a/server/app/core/models.py b/server/app/database/models.py
similarity index 99%
rename from server/app/core/models.py
rename to server/app/database/models.py
index bd23f0d715d0245f361c75b11beddaa42e9d084a..c3bc3d56cef44c65df080d7a9ae97afbb9e03b93 100644
--- a/server/app/core/models.py
+++ b/server/app/database/models.py
@@ -123,8 +123,6 @@ class Slide(db.Model):
     settings = db.Column(db.Text, nullable=False, default="{}")
     competition_id = db.Column(db.Integer, db.ForeignKey("competition.id"), nullable=False)
 
-    questions = db.relationship("Question", backref="slide")
-
     def __init__(self, order, competition_id):
         self.order = order
         self.competition_id = competition_id
@@ -138,6 +136,7 @@ class Question(db.Model):
     type_id = db.Column(db.Integer, db.ForeignKey("question_type.id"), nullable=False)
     slide_id = db.Column(db.Integer, db.ForeignKey("slide.id"), nullable=False)
 
+    slide = db.relationship("Slide", backref="questions")
     question_answers = db.relationship("QuestionAnswer", backref="question")
     alternatives = db.relationship("QuestionAlternative", backref="question")
 
diff --git a/server/populate.py b/server/populate.py
index ba1648fa28e89399c4b0a57f2788a0b73244a91e..e2ca47767f3414e1a26915a98d4b95e17edb059f 100644
--- a/server/populate.py
+++ b/server/populate.py
@@ -1,8 +1,8 @@
 from sqlalchemy.sql.expression import true
 
-import app.core.controller as dbc
+import app.database.controller as dbc
 from app import create_app, db
-from app.core.models import City, Competition, MediaType, QuestionType, Role
+from app.database.models import City, Competition, MediaType, QuestionType, Role
 
 
 def _add_items():
diff --git a/server/tests/test_app.py b/server/tests/test_app.py
index 38a103b9e0bfcfcaf13ce62740965f05ff88af70..1cbacc33844b2cc4b180d87a51b440b0588d8c88 100644
--- a/server/tests/test_app.py
+++ b/server/tests/test_app.py
@@ -1,8 +1,9 @@
 import app.core.http_codes as codes
-from app.core.models import Slide
+from app.database.models import Slide
 
 from tests import app, client, db
-from tests.test_helpers import add_default_values, change_order_test, delete, get, post, put
+from tests.test_helpers import (add_default_values, change_order_test, delete,
+                                get, post, put)
 
 
 def test_misc_api(client):
@@ -301,7 +302,7 @@ def test_slide_api(client):
     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
+    assert response.status_code == codes.OK
 
     # Changes the order
     change_order_test(client, CID, SID, order + 1, headers)
@@ -330,6 +331,7 @@ def test_question_api(client):
     num_questions = 3
     response, body = get(client, f"/api/competitions/{CID}/questions", headers=headers)
     assert response.status_code == codes.OK
+    print(body)
     assert body["count"] == num_questions
 
     # # Get specific question
@@ -368,7 +370,7 @@ def test_question_api(client):
     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
+    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
@@ -413,7 +415,7 @@ def test_question_api(client):
     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
+    assert item_question["slide_id"] != slide_id
     response, item_question = put(
         client,
         f"/api/competitions/{CID}/questions/{QID}",
@@ -425,7 +427,7 @@ def test_question_api(client):
     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
+    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
diff --git a/server/tests/test_db.py b/server/tests/test_db.py
index 68f49d5a09ad3e91d628582a8704215e8035defa..3bb998aec9674b9ab0cc865ec2e9c22a9640e73a 100644
--- a/server/tests/test_db.py
+++ b/server/tests/test_db.py
@@ -1,5 +1,5 @@
-import app.core.controller as dbc
-from app.core.models import City, Competition, Media, MediaType, Question, QuestionType, Role, Slide, Team, User
+import app.database.controller as dbc
+from app.database.models import City, Competition, Media, MediaType, Question, QuestionType, Role, Slide, Team, User
 
 from tests import app, client, db
 from tests.test_helpers import add_default_values, assert_exists, assert_insert_fail
@@ -40,6 +40,7 @@ def test_media(client):
     assert item_media.upload_by.email == "test@test.se"
 
 
+"""
 def test_question(client):
     add_default_values()
     item_user = User.query.filter_by(email="test@test.se").first()
@@ -168,3 +169,4 @@ def test_slide(client):
     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 fbb58ac1725bdc36923c0ec0c9d2fe09a202a28b..7cbdec8757d66b5a934c7e086f836a4057563f92 100644
--- a/server/tests/test_helpers.py
+++ b/server/tests/test_helpers.py
@@ -1,9 +1,9 @@
 import json
 
-import app.core.controller as dbc
 import app.core.http_codes as codes
+import app.database.controller as dbc
 from app.core import db
-from app.core.models import City, Role
+from app.database.models import City, Role
 
 
 def add_default_values():