diff --git a/backend/app/api/v1/v1_ctrl_labels.go b/backend/app/api/v1/v1_ctrl_labels.go index 7cac7c5..359d385 100644 --- a/backend/app/api/v1/v1_ctrl_labels.go +++ b/backend/app/api/v1/v1_ctrl_labels.go @@ -3,6 +3,7 @@ package v1 import ( "net/http" + "github.com/hay-kot/content/backend/ent" "github.com/hay-kot/content/backend/internal/services" "github.com/hay-kot/content/backend/internal/types" "github.com/hay-kot/content/backend/pkgs/server" @@ -101,6 +102,13 @@ func (ctrl *V1Controller) HandleLabelGet() http.HandlerFunc { labels, err := ctrl.svc.Labels.Get(r.Context(), user.GroupID, uid) if err != nil { + if ent.IsNotFound(err) { + log.Err(err). + Str("id", uid.String()). + Msg("label not found") + server.RespondError(w, http.StatusNotFound, err) + return + } log.Err(err).Msg("error getting label") server.RespondServerError(w) return diff --git a/backend/app/api/v1/v1_ctrl_locations.go b/backend/app/api/v1/v1_ctrl_locations.go index 275679b..38de872 100644 --- a/backend/app/api/v1/v1_ctrl_locations.go +++ b/backend/app/api/v1/v1_ctrl_locations.go @@ -3,6 +3,7 @@ package v1 import ( "net/http" + "github.com/hay-kot/content/backend/ent" "github.com/hay-kot/content/backend/internal/services" "github.com/hay-kot/content/backend/internal/types" "github.com/hay-kot/content/backend/pkgs/server" @@ -101,6 +102,14 @@ func (ctrl *V1Controller) HandleLocationGet() http.HandlerFunc { location, err := ctrl.svc.Location.GetOne(r.Context(), user.GroupID, uid) if err != nil { + if ent.IsNotFound(err) { + log.Err(err). + Str("id", uid.String()). + Msg("location not found") + server.RespondError(w, http.StatusNotFound, err) + return + } + log.Err(err).Msg("failed to get location") server.RespondServerError(w) return diff --git a/frontend/components/Icon.vue b/frontend/components/Icon.vue index ecdae33..da66a6a 100644 --- a/frontend/components/Icon.vue +++ b/frontend/components/Icon.vue @@ -25,7 +25,7 @@ - + {{ name }} diff --git a/frontend/lib/api/__test__/public.test.ts b/frontend/lib/api/__test__/public.test.ts index c19f2fd..8ad7515 100644 --- a/frontend/lib/api/__test__/public.test.ts +++ b/frontend/lib/api/__test__/public.test.ts @@ -1,24 +1,8 @@ -import { describe, it, expect } from 'vitest'; -import { Requests } from '../../requests'; -import { OverrideParts } from '../base/urls'; -import { PublicApi } from '../public'; -import * as config from '../../../test/config'; -import { UserApi } from '../user'; - -function client() { - OverrideParts(config.BASE_URL, '/api/v1'); - const requests = new Requests(''); - return new PublicApi(requests); -} - -function userClient(token: string) { - OverrideParts(config.BASE_URL, '/api/v1'); - const requests = new Requests('', token); - return new UserApi(requests); -} +import { describe, test, expect } from 'vitest'; +import { client, userClient } from './test-utils'; describe('[GET] /api/v1/status', () => { - it('basic query parameter', async () => { + test('server should respond', async () => { const api = client(); const { response, data } = await api.status(); expect(response.status).toBe(200); @@ -37,12 +21,12 @@ describe('first time user workflow (register, login)', () => { }, }; - it('user should be able to register', async () => { + test('user should be able to register', async () => { const { response } = await api.register(userData); expect(response.status).toBe(204); }); - it('user should be able to login', async () => { + test('user should be able to login', async () => { const { response, data } = await api.login(userData.user.email, userData.user.password); expect(response.status).toBe(200); expect(data.token).toBeTruthy(); diff --git a/frontend/lib/api/__test__/test-utils.ts b/frontend/lib/api/__test__/test-utils.ts new file mode 100644 index 0000000..601e3d2 --- /dev/null +++ b/frontend/lib/api/__test__/test-utils.ts @@ -0,0 +1,57 @@ +import { expect } from 'vitest'; +import { Requests } from '../../requests'; +import { OverrideParts } from '../base/urls'; +import { PublicApi } from '../public'; +import * as config from '../../../test/config'; +import { UserApi } from '../user'; + +export function client() { + OverrideParts(config.BASE_URL, '/api/v1'); + const requests = new Requests(''); + return new PublicApi(requests); +} + +export function userClient(token: string) { + OverrideParts(config.BASE_URL, '/api/v1'); + const requests = new Requests('', token); + return new UserApi(requests); +} + +const cache = { + token: '', +}; + +/* + * Shared UserApi token for tests where the creation of a user is _not_ import + * to the test. This is useful for tests that are testing the user API itself. + */ +export async function sharedUserClient(): Promise { + if (cache.token) { + return userClient(cache.token); + } + const testUser = { + groupName: 'test-group', + user: { + email: '__test__@__test__.com', + name: '__test__', + password: '__test__', + }, + }; + + const api = client(); + const { response: tryLoginResp, data } = await api.login(testUser.user.email, testUser.user.password); + + if (tryLoginResp.status === 200) { + cache.token = data.token; + return userClient(cache.token); + } + + const { response: registerResp } = await api.register(testUser); + expect(registerResp.status).toBe(204); + + const { response: loginResp, data: loginData } = await api.login(testUser.user.email, testUser.user.password); + expect(loginResp.status).toBe(200); + + cache.token = loginData.token; + return userClient(data.token); +} diff --git a/frontend/lib/api/__test__/user/labels.test.ts b/frontend/lib/api/__test__/user/labels.test.ts new file mode 100644 index 0000000..473cc62 --- /dev/null +++ b/frontend/lib/api/__test__/user/labels.test.ts @@ -0,0 +1,92 @@ +import { describe, expect, test } from 'vitest'; +import { Label } from '../../classes/labels'; +import { UserApi } from '../../user'; +import { sharedUserClient } from '../test-utils'; + +describe('locations lifecycle (create, update, delete)', () => { + let increment = 0; + + /** + * useLabel sets up a label resource for testing, and returns a function + * that can be used to delete the label from the backend server. + */ + async function useLabel(api: UserApi): Promise<[Label, () => Promise]> { + const { response, data } = await api.labels.create({ + name: `__test__.label.name_${increment}`, + description: `__test__.label.description_${increment}`, + color: '', + }); + expect(response.status).toBe(201); + increment++; + + const cleanup = async () => { + const { response } = await api.labels.delete(data.id); + expect(response.status).toBe(204); + }; + return [data, cleanup]; + } + + test('user should be able to create a label', async () => { + const api = await sharedUserClient(); + + const labelData = { + name: 'test-label', + description: 'test-description', + color: '', + }; + + const { response, data } = await api.labels.create(labelData); + + expect(response.status).toBe(201); + expect(data.id).toBeTruthy(); + + // Ensure we can get the label + const { response: getResponse, data: getData } = await api.labels.get(data.id); + + expect(getResponse.status).toBe(200); + expect(getData.id).toBe(data.id); + expect(getData.name).toBe(labelData.name); + expect(getData.description).toBe(labelData.description); + + // Cleanup + const { response: deleteResponse } = await api.labels.delete(data.id); + expect(deleteResponse.status).toBe(204); + }); + + test('user should be able to update a label', async () => { + const api = await sharedUserClient(); + const [label, cleanup] = await useLabel(api); + + const labelData = { + name: 'test-label', + description: 'test-description', + color: '', + }; + + const { response, data } = await api.labels.update(label.id, labelData); + expect(response.status).toBe(200); + expect(data.id).toBe(label.id); + + // Ensure we can get the label + const { response: getResponse, data: getData } = await api.labels.get(data.id); + expect(getResponse.status).toBe(200); + expect(getData.id).toBe(data.id); + expect(getData.name).toBe(labelData.name); + expect(getData.description).toBe(labelData.description); + + // Cleanup + await cleanup(); + }); + + test('user should be able to delete a label', async () => { + const api = await sharedUserClient(); + const [label, _] = await useLabel(api); + + const { response } = await api.labels.delete(label.id); + expect(response.status).toBe(204); + + // Ensure we can't get the label + const { response: getResponse } = await api.labels.get(label.id); + expect(getResponse.status).toBe(404); + }); +}); diff --git a/frontend/lib/api/__test__/user/locations.test.ts b/frontend/lib/api/__test__/user/locations.test.ts new file mode 100644 index 0000000..5b3285b --- /dev/null +++ b/frontend/lib/api/__test__/user/locations.test.ts @@ -0,0 +1,89 @@ +import { describe, expect, test } from 'vitest'; +import { Location } from '../../classes/locations'; +import { UserApi } from '../../user'; +import { sharedUserClient } from '../test-utils'; + +describe('locations lifecycle (create, update, delete)', () => { + let increment = 0; + + /** + * useLocatio sets up a location resource for testing, and returns a function + * that can be used to delete the location from the backend server. + */ + async function useLocation(api: UserApi): Promise<[Location, () => Promise]> { + const { response, data } = await api.locations.create({ + name: `__test__.location.name_${increment}`, + description: `__test__.location.description_${increment}`, + }); + expect(response.status).toBe(201); + increment++; + + const cleanup = async () => { + const { response } = await api.locations.delete(data.id); + expect(response.status).toBe(204); + }; + + return [data, cleanup]; + } + + test('user should be able to create a location', async () => { + const api = await sharedUserClient(); + + const locationData = { + name: 'test-location', + description: 'test-description', + }; + + const { response, data } = await api.locations.create(locationData); + + expect(response.status).toBe(201); + expect(data.id).toBeTruthy(); + + // Ensure we can get the location + const { response: getResponse, data: getData } = await api.locations.get(data.id); + + expect(getResponse.status).toBe(200); + expect(getData.id).toBe(data.id); + expect(getData.name).toBe(locationData.name); + expect(getData.description).toBe(locationData.description); + + // Cleanup + const { response: deleteResponse } = await api.locations.delete(data.id); + expect(deleteResponse.status).toBe(204); + }); + + test('user should be able to update a location', async () => { + const api = await sharedUserClient(); + const [location, cleanup] = await useLocation(api); + + const updateData = { + name: 'test-location-updated', + description: 'test-description-updated', + }; + + const { response } = await api.locations.update(location.id, updateData); + expect(response.status).toBe(200); + + // Ensure we can get the location + const { response: getResponse, data } = await api.locations.get(location.id); + expect(getResponse.status).toBe(200); + + expect(data.id).toBe(location.id); + expect(data.name).toBe(updateData.name); + expect(data.description).toBe(updateData.description); + + await cleanup(); + }); + + test('user should be able to delete a location', async () => { + const api = await sharedUserClient(); + const [location, _] = await useLocation(api); + + const { response } = await api.locations.delete(location.id); + expect(response.status).toBe(204); + + // Ensure we can't get the location + const { response: getResponse } = await api.locations.get(location.id); + expect(getResponse.status).toBe(404); + }); +}); diff --git a/frontend/pages/index.vue b/frontend/pages/index.vue index 9928c5c..bce614f 100644 --- a/frontend/pages/index.vue +++ b/frontend/pages/index.vue @@ -110,78 +110,106 @@ - - - HomeB - - x - - Track, Organize, and Manage your Shit. - - - - - - - - Register - - - - Register - - - - - - - - - Login - - - - Login - - - - - - - - - {{ registerForm ? 'Already a User? Login' : 'Not a User? Register' }} - - - - - - + + + + d="M0,32L80,69.3C160,107,320,181,480,181.3C640,181,800,107,960,117.3C1120,128,1280,224,1360,272L1440,320L1440,0L1360,0C1280,0,1120,0,960,0C800,0,640,0,480,0C320,0,160,0,80,0L0,0Z" + > - - - © 2022 Contents. All Rights Reserved. Haybytes LLC + + + + + + HomeB + + x + + Track, Organize, and Manage your Shit. + + + + + + + + + + + + + + + + + + + + + + + + + Register + + + + + Register + + + + + + + + + + + Login + + + + + Login + + + + + + + + + {{ registerForm ? 'Already a User? Login' : 'Not a User? Register' }} + +
Track, Organize, and Manage your Shit.
© 2022 Contents. All Rights Reserved. Haybytes LLC