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():