import { Chance } from 'chance'; import { whenRequest } from '@wix/fe-essentials/http-client/testkit/client'; import { listTags, listTagsForAccount, queryTags, queryTagsForAccount, createTag, updateTag, deleteTag, createTagForAccount, updateTagForAccount, } from '@wix/bex-utils/@wix/ambassador-os-tags-v1-tag/http'; import { QueryTagsRequest, Tag as TagType, } from '@wix/bex-utils/@wix/ambassador-os-tags-v1-tag/types'; import { InMemoryBackend } from '../testkit/backend'; import { createHttpErrorMock } from './HttpErrorMocks'; import { ExpectedTestError } from '../__tests__/utils/ExpectedTestError'; class ServerError extends ExpectedTestError { readonly code: string; constructor(code: string, message: string) { super(message); this.code = code; } } const chance = new Chance(); const aTag = (overrides: Partial = {}): TagType => { return { id: chance.guid(), name: chance.word({ length: 5 }), createdDate: chance.date({ min: new Date(Date.now() - 365 * 24 * 60 * 60 * 1000), max: new Date(), }) as Date, ...overrides, } as TagType; }; export const aTagsArray = (count: number) => Array.from({ length: count }, () => aTag()); export const generateInvalidLongName = (length: number) => chance.string({ length }); export const TAGS = aTagsArray(2); const buildQueryTagsReply = (tags?: TagType[]) => async ({ query }: Partial) => { const allTags = tags || TAGS; const search = query?.filter?.name?.$startsWith as string | undefined; const filteredTags = search ? allTags.filter((tag) => tag.name?.toLowerCase().startsWith(search.toLowerCase()), ) : allTags; const limit = query?.cursorPaging?.limit ?? filteredTags.length; const cursorToken = query?.cursorPaging?.cursor; const offset = cursorToken ? parseInt(atob(cursorToken), 10) : 0; const pageItems = filteredTags.slice(offset, offset + limit); const nextOffset = offset + limit; const hasNext = nextOffset < filteredTags.length; return { tags: pageItems, pagingMetadata: { count: pageItems.length, cursors: { next: hasNext ? btoa(String(nextOffset)) : null }, hasNext, total: filteredTags.length, }, }; }; export const withQueryTags = (tags?: TagType[]) => whenRequest(queryTags).reply(200, buildQueryTagsReply(tags)).persist(); export const withQueryTagsForAccount = (tags?: TagType[]) => whenRequest(queryTagsForAccount) .reply(200, buildQueryTagsReply(tags)) .persist(); // Bundles the queryTags mock too — tag assignment (fetchTagsByIds) and // TagsFilter still read through queryTags export const withListTags = (tags?: TagType[]) => [ whenRequest(listTags) .reply(200, async () => ({ tags: tags || TAGS })) .persist(), withQueryTags(tags), ]; export const withListTagsForAccount = (tags?: TagType[]) => [ whenRequest(listTagsForAccount) .reply(200, async () => ({ tags: tags || TAGS })) .persist(), withQueryTagsForAccount(tags), ]; const createTagResponseMock = (tag: TagType) => ({ __applicationErrorsType: {}, tag: { createdDate: new Date(), updatedDate: new Date(), ...tag }, }); export const withCreateTag = (mock?: jest.Mock) => whenRequest(createTag) .reply( 200, mock || (async ({ tag }) => { return createTagResponseMock(tag); }), ) .persist(); export const withCreateTagForAccount = (mock?: jest.Mock) => whenRequest(createTagForAccount) .reply( 200, mock || (async ({ tag }) => { return createTagResponseMock(tag); }), ) .persist(); export const withUpdateTag = () => whenRequest(updateTag) .reply(200, async (tag) => { return tag; }) .persist(); export const withUpdateTagForAccount = () => whenRequest(updateTagForAccount) .reply(200, async (tag) => { return tag; }) .persist(); export const withDeleteTag = () => whenRequest(deleteTag) .reply(200, async () => { return {}; }) .persist(); export const getQueryTagsErrorMock = ({ code = 'ERR_NETWORK', status = 429, }: { code?: string; status?: number; } = {}) => whenRequest(queryTags) .reply(200, async () => { return new Promise((_resolve, reject) => { window.setTimeout(() => { reject(createHttpErrorMock({ code, status })); }, 500); }); }) .persist(); export const getListTagsErrorMock = ({ code = 'ERR_NETWORK', status = 429, }: { code?: string; status?: number; } = {}) => [ whenRequest(listTags) .reply(200, async () => { return new Promise((_resolve, reject) => { window.setTimeout(() => { reject(createHttpErrorMock({ code, status })); }, 500); }); }) .persist(), getQueryTagsErrorMock({ code, status }), ]; export const getListTagsForAccountErrorMock = ({ code = 'ERR_NETWORK', status = 429, }: { code?: string; status?: number; } = {}) => [ whenRequest(listTagsForAccount) .reply(200, async () => { return new Promise((_resolve, reject) => { setTimeout(() => { reject(createHttpErrorMock({ code, status })); }, 500); }); }) .persist(), whenRequest(queryTagsForAccount) .reply(200, async () => { return new Promise((_resolve, reject) => { setTimeout(() => { reject(createHttpErrorMock({ code, status })); }, 500); }); }) .persist(), ]; export const getCreateTagErrorMock = ({ code = 'ERR_NETWORK', status = 429, requestId, }: { code?: string; status?: number; requestId?: string; } = {}) => whenRequest(createTag) .reply(200, async () => { return new Promise((_resolve, reject) => { window.setTimeout(() => { reject(createHttpErrorMock({ code, status, requestId })); }, 500); }); }) .persist(); export const getCreateTagForAccountErrorMock = ({ code = 'ERR_NETWORK', status = 429, }: { code?: string; status?: number; } = {}) => whenRequest(createTagForAccount) .reply(200, async () => { return new Promise((_resolve, reject) => { window.setTimeout(() => { reject(createHttpErrorMock({ code, status })); }, 500); }); }) .persist(); export const getUpdateTagErrorMock = ({ code, status, }: { code?: string; status?: number; } = {}) => whenRequest(updateTag) .reply(200, async () => { return new Promise((_resolve, reject) => { window.setTimeout(() => { reject(createHttpErrorMock({ code, status })); }, 500); }); }) .persist(); export const getDeleteTagErrorMock = (code: string = 'ERR_NETWORK') => whenRequest(deleteTag) .reply(200, async () => { return new Promise((_resolve, reject) => { window.setTimeout(() => { reject(new ServerError(code, 'error')); }, 500); }); }) .persist(); export default function createTagImdb({ createdDate = new Date(Date.now() - 365 * 24 * 60 * 60 * 1000), updatedDate = createdDate, fqdn = 'wix.patterns.dummyservice.v1.dummy_entity', ...overrides }: Partial = {}): TagType { return { createdDate, updatedDate, fqdn, ...overrides, }; } export function createBackend(tags: TagType[]) { return new InMemoryBackend({ total: tags.length, paginationMode: 'offset', delay: { min: 100, max: 2000 }, createOne: (index: number) => createTagImdb(tags[index]), predicate: (query) => { const filters = query.filters ?? {}; const { fqdn } = filters; return (item) => item.fqdn === fqdn; }, orderBy: (query) => { return [...(query.sort ?? [])]; }, itemKey: (item) => item.id!, }); } export const tagsAPIHttpClientMocks = (tags: TagType[]) => { const tagsBackend = createBackend(tags); return [ whenRequest(listTags) .reply(200, async ({ fqdn }) => { return tagsBackend .fetchData({ filters: { fqdn, }, limit: tags.length, }) .then(({ items }) => { if (fqdn === 'wix.patterns.dummyservice.v1.empty') { return { tags: [] }; } else if (fqdn === 'wix.patterns.dummyservice.v1.over_limit') { return { tags: aTagsArray(105) }; } else { return { tags: items }; } }); }) .persist(), whenRequest(queryTags) .reply(200, async ({ fqdn, query }) => { if (fqdn === 'wix.patterns.dummyservice.v1.empty') { return { tags: [], pagingMetadata: { count: 0, cursors: { next: null }, hasNext: false, }, }; } const allItems = fqdn === 'wix.patterns.dummyservice.v1.over_limit' ? aTagsArray(105) : (await tagsBackend.fetchData({ filters: { fqdn }, limit: 1000 })) .items; const search = query?.filter?.name?.$startsWith as string | undefined; const filtered = search ? allItems.filter((tag) => tag.name?.toLowerCase().startsWith(search.toLowerCase()), ) : allItems; const limit = query?.cursorPaging?.limit ?? filtered.length; const cursorToken = query?.cursorPaging?.cursor; const offset = cursorToken ? parseInt(atob(cursorToken), 10) : 0; const pageItems = filtered.slice(offset, offset + limit); const nextOffset = offset + limit; const hasNext = nextOffset < filtered.length; return { tags: pageItems, pagingMetadata: { count: pageItems.length, cursors: { next: hasNext ? btoa(String(nextOffset)) : null }, hasNext, total: filtered.length, }, }; }) .persist(), whenRequest(createTag) .reply(200, ({ tag }) => { tagsBackend.push([tag]); return { __applicationErrorsType: {}, tag, }; }) .persist(), whenRequest(updateTag) .reply(200, ({ tag }) => { tagsBackend.update(tag); return { tag }; }) .persist(), whenRequest(deleteTag) .reply(200, ({ tagId }) => { tagsBackend.map.delete(tagId); }) .persist(), ]; };