Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 80 additions & 2 deletions packages/favorites/src/lib/Favorites.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { render, waitFor, fireEvent, act } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
// eslint-disable-next-line @nx/enforce-module-boundaries
import { server } from '@availity/mock/src/lib/server';
import { http, HttpResponse } from 'msw';
import avMessages from '@availity/message-core';
import { FavoritesProvider } from './Favorites';
import { FavoriteHeart } from './FavoriteHeart';
Expand All @@ -15,8 +16,9 @@ const domain = () => {
}

if (window.location.hostname) {
return `${window.location.protocol}//${window.location.hostname}${window.location.port ? `:${window.location.port}` : ''
}`;
return `${window.location.protocol}//${window.location.hostname}${
window.location.port ? `:${window.location.port}` : ''
}`;
}

return '*';
Expand Down Expand Up @@ -274,5 +276,81 @@ describe('Favorites', () => {
});
});

// The Settings API PUT responds with the saved settings object ({ favorites: [...] }),
// NOT wrapped in `settings: []` like the GET list response. These tests use a fixed
// response (not an echo of the request) so the mock cannot hide a response-shape mismatch.
describe('Settings PUT response shape', () => {
it('should add favorite when PUT responds with an unwrapped { favorites } body', async () => {
server.use(
http.put('/api/utils/v1/settings', () =>
HttpResponse.json(
{
favorites: [
{ id: '123', pos: 0 },
{ id: '456', pos: 1 },
{ id: '789', pos: 2 },
],
},
{ status: 200 }
)
)
);
const user = userEvent.setup();

const { container } = render(
<QueryClientProvider client={queryClient}>
<FavoritesProvider>
<FavoriteHeart id="789" name="App #3" />
</FavoritesProvider>
</QueryClientProvider>
);

const heart = container.querySelector('#av-favorite-heart-789');

await waitFor(() => expect(heart).not.toBeChecked());

if (heart) await user.click(heart);

await waitFor(() => expect(heart).toBeChecked());

expect(avMessages.send).toHaveBeenCalledWith({
event: 'av:favorites:update',
favorites: [
{ id: '123', pos: 0 },
{ id: '456', pos: 1 },
{ id: '789', pos: 2 },
],
});
});

it('should remove favorite when PUT responds with an unwrapped { favorites } body', async () => {
server.use(
http.put('/api/utils/v1/settings', () =>
HttpResponse.json({ favorites: [{ id: '456', pos: 1 }] }, { status: 200 })
)
);
const user = userEvent.setup();

const { container } = render(
<QueryClientProvider client={queryClient}>
<FavoritesProvider>
<FavoriteHeart id="123" name="App #1" />
</FavoritesProvider>
</QueryClientProvider>
);

const heart = container.querySelector('#av-favorite-heart-123');

await waitFor(() => expect(heart).toBeChecked());

if (heart) await user.click(heart);

await waitFor(() => expect(heart).not.toBeChecked());

expect(avMessages.send).toHaveBeenCalledWith({
event: 'av:favorites:update',
favorites: [{ id: '456', pos: 1 }],
});
});
});
});
28 changes: 18 additions & 10 deletions packages/favorites/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,31 +13,38 @@ export const isFavorite = (arg: Favorite) => Boolean(typeof arg?.id === 'string'
export const validateFavorites = (unvalidatedFavorites: Favorite[]) => {
const validatedFavorites = Array.isArray(unvalidatedFavorites) ? unvalidatedFavorites?.filter(isFavorite) : [];
return validatedFavorites;
}
};

type MutationVariables = {
favorites: Favorite[];
targetFavoriteId: string;
}
};

interface FavoritesSettings {
/** Settings API GET response: the saved settings are wrapped in a `settings` list. */
interface FavoritesGetResponse {
settings: { favorites: Favorite[] }[];
}

const submit = async ({ favorites, targetFavoriteId }: MutationVariables, applicationId: string) => {
const response = await avSettingsApi.setApplication<FavoritesSettings>(applicationId, { favorites });
return { favorites: response.data.settings[0].favorites, targetFavoriteId };
/** Settings API PUT response: the saved settings object is returned directly, NOT wrapped in `settings`. */
interface FavoritesPutResponse {
favorites: Favorite[];
}

const submit = async ({ favorites, targetFavoriteId }: MutationVariables, applicationId: string) => {
const response = await avSettingsApi.setApplication<FavoritesPutResponse>(applicationId, { favorites });
return { favorites: response.data.favorites, targetFavoriteId };
};

const getFavorites = async (applicationId: string) => {
const result = await avSettingsApi.getApplication<FavoritesSettings>(applicationId);
const result = await avSettingsApi.getApplication<FavoritesGetResponse>(applicationId);
const unvalidatedFavorites = result?.data?.settings?.[0]?.favorites;
const validatedFavorites = validateFavorites(unvalidatedFavorites);

return validatedFavorites;
};

export const useFavoritesQuery = (enabled: boolean, applicationId: string): UseQueryResult<Favorite[], Error> => useQuery({ queryKey: ['favorites'], queryFn: () => getFavorites(applicationId), enabled });
export const useFavoritesQuery = (enabled: boolean, applicationId: string): UseQueryResult<Favorite[], Error> =>
useQuery({ queryKey: ['favorites'], queryFn: () => getFavorites(applicationId), enabled });

type MutationOptions = {
onMutationStart?: (targetFavoriteId: string) => void;
Expand All @@ -46,13 +53,14 @@ type MutationOptions = {
export const useSubmitFavorites = ({ onMutationStart }: MutationOptions, applicationId: string) => {
const queryClient = useQueryClient();
const { mutateAsync: submitFavorites, ...rest } = useMutation({
mutationFn: ({ favorites, targetFavoriteId }: MutationVariables) => submit({ favorites, targetFavoriteId }, applicationId),
mutationFn: ({ favorites, targetFavoriteId }: MutationVariables) =>
submit({ favorites, targetFavoriteId }, applicationId),
onMutate(variables) {
onMutationStart?.(variables.targetFavoriteId);
},
onSuccess(data) {
queryClient.setQueryData(['favorites'], data.favorites);
}
},
});
return { submitFavorites, ...rest };
};
Expand Down
77 changes: 39 additions & 38 deletions packages/mock/src/lib/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,8 @@ export const handlers = [
if (params.applicationId === 'Gateway-AvNavigation') {
await delay(defaultDelay);
return HttpResponse.json(settings, { status: 200 });
} if (params.applicationId === 'AVATAR') {
}
if (params.applicationId === 'AVATAR') {
await delay(defaultDelay);
return HttpResponse.json(avatarSettings, { status: 200 });
}
Expand All @@ -105,7 +106,8 @@ export const handlers = [
const body = await context.request.json();

await delay(defaultDelay);
return HttpResponse.json({ settings: [body] }, { status: 201 });
// The real Settings API PUT returns the saved settings object directly (not wrapped in `settings`)
return HttpResponse.json(body, { status: 201 });
}),

// Codes
Expand Down Expand Up @@ -365,42 +367,41 @@ export const handlers = [
};

if (password) {
return password === "1234" ?
new HttpResponse(null, {
status: 200,
headers: {
'av-scan-bytes': fileSize,
'av-scan-result': 'accepted',
'cache-control': 'no-store',
'decryption-result': 'accepted',
references: `["approved/${params.bucket}/${params.location}"]`,
's3-references': `["s3://path-to-vault/approved/${params.bucket}/${params.location}"]`,
'transfer-encoding': 'chunked',
'tus-resumable': '1.0.0',
'upload-length': fileSize,
'upload-metadata': metadata,
'upload-offset': fileSize,
'upload-result': 'accepted',
},
})
:
new HttpResponse(null, {
status: 200,
headers: {
'av-scan-bytes': fileSize,
'av-scan-result': 'accepted',
'cache-control': 'no-store',
'decryption-result': 'rejected',
references: `["approved/${params.bucket}/${params.location}"]`,
's3-references': `["s3://path-to-vault/approved/${params.bucket}/${params.location}"]`,
'transfer-encoding': 'chunked',
'tus-resumable': '1.0.0',
'upload-length': fileSize,
'upload-metadata': metadata,
'upload-offset': fileSize,
'upload-result': 'encrypted',
},
});
return password === '1234'
? new HttpResponse(null, {
status: 200,
headers: {
'av-scan-bytes': fileSize,
'av-scan-result': 'accepted',
'cache-control': 'no-store',
'decryption-result': 'accepted',
references: `["approved/${params.bucket}/${params.location}"]`,
's3-references': `["s3://path-to-vault/approved/${params.bucket}/${params.location}"]`,
'transfer-encoding': 'chunked',
'tus-resumable': '1.0.0',
'upload-length': fileSize,
'upload-metadata': metadata,
'upload-offset': fileSize,
'upload-result': 'accepted',
},
})
: new HttpResponse(null, {
status: 200,
headers: {
'av-scan-bytes': fileSize,
'av-scan-result': 'accepted',
'cache-control': 'no-store',
'decryption-result': 'rejected',
references: `["approved/${params.bucket}/${params.location}"]`,
's3-references': `["s3://path-to-vault/approved/${params.bucket}/${params.location}"]`,
'transfer-encoding': 'chunked',
'tus-resumable': '1.0.0',
'upload-length': fileSize,
'upload-metadata': metadata,
'upload-offset': fileSize,
'upload-result': 'encrypted',
},
});
}

await delay(1000);
Expand Down
Loading