diff --git a/src/stores/projects.test.ts b/src/stores/projects.test.ts new file mode 100644 index 0000000000..1c87a81e24 --- /dev/null +++ b/src/stores/projects.test.ts @@ -0,0 +1,158 @@ +import Vue from 'vue'; +import VueCompositionAPI from '@vue/composition-api'; +import api from '@/api'; +import { useProjectsStore } from './projects'; +import { setActiveReq } from 'pinia'; + +describe('Stores / Projects', () => { + beforeAll(() => { + Vue.use(VueCompositionAPI); + }); + + beforeEach(() => { + setActiveReq({}); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('getProjects', () => { + it('Fetches the project info for each individual project', async () => { + const spy = jest.spyOn(api, 'get'); + + spy.mockImplementation((path: string) => { + switch (path) { + case '/server/projects': + return Promise.resolve({ + data: { data: ['my-project', 'another-project'] } + }); + case '/my-project/': + case '/another-project/': + return Promise.resolve({ + data: { + data: {} + } + }); + } + return Promise.resolve(); + }); + + const projectsStore = useProjectsStore(); + await projectsStore.getProjects(); + + expect(spy).toHaveBeenCalledWith('/server/projects'); + expect(spy).toHaveBeenCalledWith('/my-project/'); + expect(spy).toHaveBeenCalledWith('/another-project/'); + + expect(projectsStore.state.error).toBe(null); + expect(projectsStore.state.projects).toEqual([ + { key: 'my-project' }, + { key: 'another-project' } + ]); + }); + + it('Sets the error state if the API errors out while fetching project keys', async () => { + const spy = jest.spyOn(api, 'get'); + + spy.mockImplementation((path: string) => { + switch (path) { + case '/server/projects': + return Promise.reject({ response: { status: 500 }, message: 'Error' }); + } + return Promise.resolve(); + }); + + const projectsStore = useProjectsStore(); + await projectsStore.getProjects(); + + expect(projectsStore.state.error).toEqual({ status: 500, error: 'Error' }); + }); + + it('Sets the needsInstall boolean to true if the API returns a 503 error on projects retrieving', async () => { + const spy = jest.spyOn(api, 'get'); + + spy.mockImplementation((path: string) => { + switch (path) { + case '/server/projects': + return Promise.reject({ response: { status: 503 } }); + } + return Promise.resolve(); + }); + + const projectsStore = useProjectsStore(); + await projectsStore.getProjects(); + + expect(projectsStore.state.error).toBe(null); + expect(projectsStore.state.needsInstall).toBe(true); + }); + + it('Adds an error key to the individual project if one of them fails', async () => { + const spy = jest.spyOn(api, 'get'); + + spy.mockImplementation((path: string) => { + switch (path) { + case '/server/projects': + return Promise.resolve({ + data: { data: ['my-project', 'another-project'] } + }); + case '/my-project/': + return Promise.resolve({ data: {} }); + case '/another-project/': + return Promise.reject({ + response: { + status: 500, + data: { + error: { + code: 10, + message: 'error message' + } + } + } + }); + } + return Promise.resolve(); + }); + + const projectsStore = useProjectsStore(); + await projectsStore.getProjects(); + + expect(projectsStore.state.projects).toEqual([ + { key: 'my-project' }, + { key: 'another-project', error: 'error message', status: 500 } + ]); + }); + + it('Uses the error message of the request if API did not return any data', async () => { + const spy = jest.spyOn(api, 'get'); + + spy.mockImplementation((path: string) => { + switch (path) { + case '/server/projects': + return Promise.resolve({ + data: { data: ['my-project', 'another-project'] } + }); + case '/my-project/': + return Promise.resolve({ data: {} }); + case '/another-project/': + return Promise.reject({ + message: 'Error fallback', + response: { + status: 500, + data: {} + } + }); + } + return Promise.resolve(); + }); + + const projectsStore = useProjectsStore(); + await projectsStore.getProjects(); + + expect(projectsStore.state.projects).toEqual([ + { key: 'my-project' }, + { key: 'another-project', error: 'Error fallback', status: 500 } + ]); + }); + }); +}); diff --git a/src/stores/projects.ts b/src/stores/projects.ts new file mode 100644 index 0000000000..55750e9b67 --- /dev/null +++ b/src/stores/projects.ts @@ -0,0 +1,63 @@ +import { createStore } from 'pinia'; +import { Project } from '@/types/project'; +import api from '@/api'; + +interface ProjectWithKey extends Project { + key: string; +} + +interface ProjectError { + key: string; + status: number; + error: { + code: number; + message: string; + } | null; +} + +type Projects = (ProjectWithKey | ProjectError)[]; + +export const useProjectsStore = createStore({ + id: 'projects', + state: () => ({ + needsInstall: false, + error: null as any, + projects: [] as Projects + }), + actions: { + async getProjects() { + try { + const projectsResponse = await api.get('/server/projects'); + const projectKeys: string[] = projectsResponse.data.data; + let projects: Projects = []; + + for (let index = 0; index < projectKeys.length; index++) { + try { + const projectInfoResponse = await api.get(`/${projectKeys[index]}/`); + projects.push({ + key: projectKeys[index], + ...projectInfoResponse.data.data + }); + } catch (error) { + projects.push({ + key: projectKeys[index], + status: error.response.status, + error: error.response.data?.error?.message || error.message + }); + } + } + + this.state.projects = projects; + } catch (error) { + if (error.response.status === 503) { + this.state.needsInstall = true; + } else { + this.state.error = { + status: error.response.status, + error: error.message + }; + } + } + } + } +});