diff --git a/src/pages/AccountSettings/tabs/Admin/DetailsSection/DetailsSection.test.jsx b/src/pages/AccountSettings/tabs/Admin/DetailsSection/DetailsSection.test.jsx index f9a7fa562e..c5ead2af64 100644 --- a/src/pages/AccountSettings/tabs/Admin/DetailsSection/DetailsSection.test.jsx +++ b/src/pages/AccountSettings/tabs/Admin/DetailsSection/DetailsSection.test.jsx @@ -33,6 +33,10 @@ beforeEach(() => { server.resetHandlers() }) +afterEach(() => { + vi.clearAllMocks() +}) + afterAll(() => { server.close() }) @@ -43,7 +47,7 @@ describe('DetailsSection', () => { const mutate = vi.fn() const addNotification = vi.fn() - useAddNotification.mockReturnValue(addNotification) + vi.mocked(useAddNotification).mockReturnValue(addNotification) server.use( graphql.mutation('UpdateProfile', (info) => { mutate(info.variables) @@ -52,15 +56,23 @@ describe('DetailsSection', () => { data: { updateProfile: { me: { - username: 'donald duck', email: info.variables.input.email ? info.variables.input.email : 'donald@duck.com', - name: info.variables.input.name - ? info.variables.input.name - : 'donald duck', - avatarUrl: 'http://127.0.0.1/avatar-url', - onboardingCompleted: false, + onboardingCompleted: true, + privateAccess: null, + businessEmail: null, + user: { + name: info.variables.input.name + ? info.variables.input.name + : 'donald duck', + username: 'donald duck', + avatarUrl: 'http://127.0.0.1/avatar-url', + avatar: 'http://127.0.0.1/avatar-url', + student: false, + studentCreatedAt: null, + studentUpdatedAt: null, + }, }, }, }, @@ -218,10 +230,15 @@ describe('DetailsSection', () => { ) }) }) - describe('when mutation is not successful', () => { it('adds an error notification', async () => { const { user, addNotification } = setup() + server.use( + graphql.mutation('UpdateProfile', () => { + return HttpResponse.error() + }) + ) + render(, { wrapper, }) diff --git a/src/pages/AccountSettings/tabs/Profile/NameEmailCard/NameEmailCard.test.jsx b/src/pages/AccountSettings/tabs/Profile/NameEmailCard/NameEmailCard.test.jsx index dda1f98247..23a270bb47 100644 --- a/src/pages/AccountSettings/tabs/Profile/NameEmailCard/NameEmailCard.test.jsx +++ b/src/pages/AccountSettings/tabs/Profile/NameEmailCard/NameEmailCard.test.jsx @@ -60,7 +60,18 @@ describe('NameEmailCard', () => { updateProfile: { me: { email: json.variables.input.email || '', - user: { name: json.variables.input.name || '' }, + privateAccess: null, + onboardingCompleted: true, + businessEmail: null, + user: { + name: json.variables.input.name || '', + username: 'test', + avatarUrl: 'http://127.0.0.1/avatar-url', + avatar: 'http://127.0.0.1/avatar-url', + student: false, + studentCreatedAt: null, + studentUpdatedAt: null, + }, }, }, }, diff --git a/src/services/user/useUpdateProfile.test.tsx b/src/services/user/useUpdateProfile.test.tsx index afdb3ad857..47b47ddd9d 100644 --- a/src/services/user/useUpdateProfile.test.tsx +++ b/src/services/user/useUpdateProfile.test.tsx @@ -16,6 +16,11 @@ const user = { name: 'terry', avatarUrl: 'http://127.0.0.1/avatar-url', onboardingCompleted: false, + privateAccess: true, + businessEmail: null, + student: false, + studentCreatedAt: null, + studentUpdatedAt: null, } const queryClient = new QueryClient({ @@ -50,9 +55,19 @@ describe('useUpdateProfile', () => { server.use( graphql.mutation('UpdateProfile', (info) => { const newUser = { - ...user, - name: info.variables.input.name, email: info.variables.input.email, + onboardingCompleted: user.onboardingCompleted, + privateAccess: user.privateAccess, + businessEmail: user.businessEmail, + user: { + name: info.variables.input.name, + username: user.username, + avatarUrl: user.avatarUrl, + avatar: user.avatarUrl, + student: user.student, + studentCreatedAt: user.studentCreatedAt, + studentUpdatedAt: user.studentUpdatedAt, + }, } return HttpResponse.json({ @@ -88,11 +103,19 @@ describe('useUpdateProfile', () => { await waitFor(() => expect(result.current.data).toStrictEqual({ - avatarUrl: 'http://127.0.0.1/avatar-url', email: 'newemail@test.com', - name: 'new name', + privateAccess: true, onboardingCompleted: false, - username: 'TerrySmithDC', + businessEmail: null, + user: { + name: 'new name', + username: 'TerrySmithDC', + avatarUrl: 'http://127.0.0.1/avatar-url', + avatar: 'http://127.0.0.1/avatar-url', + student: false, + studentCreatedAt: null, + studentUpdatedAt: null, + }, }) ) }) @@ -108,11 +131,19 @@ describe('useUpdateProfile', () => { await waitFor(() => expect(result.current.data).toStrictEqual({ - avatarUrl: 'http://127.0.0.1/avatar-url', email: 'newemail@test.com', - name: 'new name', + privateAccess: true, onboardingCompleted: false, - username: 'TerrySmithDC', + businessEmail: null, + user: { + name: 'new name', + username: 'TerrySmithDC', + avatarUrl: 'http://127.0.0.1/avatar-url', + avatar: 'http://127.0.0.1/avatar-url', + student: false, + studentCreatedAt: null, + studentUpdatedAt: null, + }, }) ) }) @@ -153,11 +184,19 @@ describe('useUpdateProfile', () => { await waitFor(() => expect(result.current.data).toStrictEqual({ - avatarUrl: 'http://127.0.0.1/avatar-url', email: 'newemail@test.com', - name: 'new name', + privateAccess: true, onboardingCompleted: false, - username: 'TerrySmithDC', + businessEmail: null, + user: { + name: 'new name', + username: 'TerrySmithDC', + avatarUrl: 'http://127.0.0.1/avatar-url', + avatar: 'http://127.0.0.1/avatar-url', + student: false, + studentCreatedAt: null, + studentUpdatedAt: null, + }, }) ) }) diff --git a/src/services/user/useUpdateProfile.ts b/src/services/user/useUpdateProfile.ts index 17622460dd..beb924bbc3 100644 --- a/src/services/user/useUpdateProfile.ts +++ b/src/services/user/useUpdateProfile.ts @@ -1,8 +1,68 @@ import { useMutation, useQueryClient } from '@tanstack/react-query' +import { z } from 'zod' import config from 'config' import Api from 'shared/api' +import { NetworkErrorObject, rejectNetworkError } from 'shared/api/helpers' + +import { GoalsSchema, TypeProjectsSchema } from './useUser' + +const CurrentUserFragment = z.object({ + email: z.string().nullable(), + privateAccess: z.boolean().nullable(), + onboardingCompleted: z.boolean(), + businessEmail: z.string().nullable(), + user: z.object({ + name: z.string().nullable(), + username: z.string(), + avatarUrl: z.string(), + avatar: z.string(), + student: z.boolean(), + studentCreatedAt: z.string().nullable(), + studentUpdatedAt: z.string().nullable(), + }), + trackingMetadata: z + .object({ + service: z.string(), + ownerid: z.number(), + serviceId: z.string(), + plan: z.string().nullable(), + staff: z.boolean().nullable(), + hasYaml: z.boolean(), + bot: z.string().nullable(), + delinquent: z.boolean().nullable(), + didTrial: z.boolean().nullable(), + planProvider: z.string().nullable(), + planUserCount: z.number().nullable(), + createdAt: z.string().nullable(), + updatedAt: z.string().nullable(), + profile: z + .object({ + createdAt: z.string(), + otherGoal: z.string().nullable(), + typeProjects: TypeProjectsSchema, + goals: GoalsSchema, + }) + .nullable(), + }) + .nullish(), +}) + +const UpdateProfileResponseSchema = z.object({ + updateProfile: z + .object({ + me: CurrentUserFragment.nullish(), + error: z + .discriminatedUnion('__typename', [ + z.object({ + __typename: z.literal('ValidationError'), + }), + ]) + .nullish(), + }) + .nullish(), +}) const currentUserFragment = ` fragment CurrentUserFragment on Me { @@ -26,7 +86,6 @@ fragment CurrentUserFragment on Me { plan staff hasYaml - service bot delinquent didTrial @@ -72,10 +131,20 @@ export function useUpdateProfile({ provider }: { provider: string }) { email, }, }, - }).then((res) => res?.data?.updateProfile?.me) + }).then((res) => { + const parsedData = UpdateProfileResponseSchema.safeParse(res.data) + if (!parsedData.success) { + return rejectNetworkError({ + status: 404, + data: {}, + dev: 'useUpdateProfile - 404 failed to parse', + } satisfies NetworkErrorObject) + } + return parsedData.data.updateProfile?.me + }) }, - onSuccess: (user) => { - queryClient.setQueryData(['currentUser', provider], () => user) + onSuccess: (currentUser) => { + queryClient.setQueryData(['currentUser', provider], () => currentUser) if (config.IS_SELF_HOSTED) { queryClient.invalidateQueries(['SelfHostedCurrentUser']) diff --git a/src/services/user/useUser.ts b/src/services/user/useUser.ts index 6d49884f14..cfaf131aa1 100644 --- a/src/services/user/useUser.ts +++ b/src/services/user/useUser.ts @@ -5,7 +5,7 @@ import { z } from 'zod' import Api from 'shared/api' import { NetworkErrorObject } from 'shared/api/helpers' -const TypeProjectsSchema = z.array( +export const TypeProjectsSchema = z.array( z.union([ z.literal('PERSONAL'), z.literal('YOUR_ORG'), @@ -14,7 +14,7 @@ const TypeProjectsSchema = z.array( ]) ) -const GoalsSchema = z.array( +export const GoalsSchema = z.array( z.union([ z.literal('STARTING_WITH_TESTS'), z.literal('IMPROVE_COVERAGE'),