Include more details when throwing errors for debugging in Sentry * WIP throw errors with more details for debugging in Sentry * Fix throwing response-data * Clearer message when a response fails * Add message to errors * better typings * . * Try to send profileID and membershipNumber to Sentry when we fail to parse the apiResponse * rename notFound -> notFoundError * Merge branch 'master' of bitbucket.org:scandic-swap/web into chore/add-error-details-for-sentry Approved-by: Linus Flood
369 lines
9.2 KiB
TypeScript
369 lines
9.2 KiB
TypeScript
import { beforeEach, describe, expect, it, type Mock, vi } from "vitest"
|
|
|
|
import { Lang } from "@scandic-hotels/common/constants/language"
|
|
import { getCacheClient } from "@scandic-hotels/common/dataCache"
|
|
|
|
import * as api from "../../../api"
|
|
// import { getCity } from "./getCity"
|
|
import { getLocationsByCountries } from "./getLocationsByCountries"
|
|
|
|
import type { CitiesGroupedByCountry } from "../../../types/locations"
|
|
|
|
// Mocks
|
|
vi.mock("@scandic-hotels/common/dataCache", () => {
|
|
return {
|
|
getCacheClient: vi.fn(),
|
|
}
|
|
})
|
|
vi.mock("../../../api", () => {
|
|
return {
|
|
get: vi.fn(),
|
|
endpoints: {
|
|
v1: {
|
|
Hotel: {
|
|
locations: "/locations",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
})
|
|
vi.mock("./getCity", () => {
|
|
return {
|
|
getCity: vi.fn(),
|
|
}
|
|
})
|
|
vi.mock("@scandic-hotels/common/logger/createLogger", () => {
|
|
return {
|
|
createLogger: () => ({
|
|
info: vi.fn(),
|
|
warn: vi.fn(),
|
|
error: vi.fn(),
|
|
}),
|
|
}
|
|
})
|
|
|
|
const mockedGetCacheClient = getCacheClient as unknown as Mock
|
|
const mockedApiGet = api.get as unknown as Mock
|
|
// const mockedGetCity = getCity as unknown as Mock
|
|
|
|
describe("getLocationsByCountries", () => {
|
|
const mockedCacheClient = {
|
|
cacheOrGet: vi.fn().mockImplementation(async (_key: string, cb: any) => {
|
|
return cb()
|
|
}),
|
|
}
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
it("returns cached value when cache has data", async () => {
|
|
const cacheClient = {
|
|
cacheOrGet: vi.fn().mockResolvedValueOnce("CACHED_VALUE"),
|
|
}
|
|
mockedGetCacheClient.mockResolvedValueOnce(cacheClient)
|
|
|
|
const result = await getLocationsByCountries({
|
|
lang: Lang.en,
|
|
citiesByCountry: null,
|
|
serviceToken: "token",
|
|
} as any)
|
|
|
|
expect(result).toBe("CACHED_VALUE")
|
|
expect(cacheClient.cacheOrGet).toHaveBeenCalled()
|
|
expect(mockedApiGet).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it("throws unauthorized on 401 response", async () => {
|
|
mockedGetCacheClient.mockResolvedValueOnce(mockedCacheClient)
|
|
|
|
mockedApiGet.mockResolvedValueOnce(
|
|
new Response(JSON.stringify({ data: "Unauthorized" }), {
|
|
status: 401,
|
|
statusText: "Unauthorized",
|
|
headers: { "Content-Type": "application/json" },
|
|
})
|
|
)
|
|
|
|
await expect(
|
|
getLocationsByCountries({
|
|
lang: Lang.en,
|
|
citiesByCountry: null,
|
|
serviceToken: "token",
|
|
})
|
|
).rejects.toMatchObject({
|
|
code: "UNAUTHORIZED",
|
|
})
|
|
|
|
expect(mockedApiGet).toHaveBeenCalled()
|
|
})
|
|
|
|
it("throws forbidden on 403 response", async () => {
|
|
mockedGetCacheClient.mockResolvedValueOnce(mockedCacheClient)
|
|
|
|
mockedApiGet.mockResolvedValueOnce(
|
|
new Response(JSON.stringify({ data: "Forbidden" }), {
|
|
status: 403,
|
|
statusText: "Forbidden",
|
|
headers: { "Content-Type": "application/json" },
|
|
})
|
|
)
|
|
|
|
await expect(
|
|
getLocationsByCountries({
|
|
lang: Lang.en,
|
|
citiesByCountry: null,
|
|
serviceToken: "token",
|
|
})
|
|
).rejects.toMatchObject({
|
|
code: "FORBIDDEN",
|
|
})
|
|
})
|
|
|
|
it("parses locations and enriches city country and hotel city via getCity", async () => {
|
|
mockedGetCacheClient.mockResolvedValueOnce(mockedCacheClient)
|
|
|
|
const apiPayload = mockApiData({
|
|
numberOfCities: 1,
|
|
numberOfHotels: 1,
|
|
})
|
|
|
|
mockedApiGet.mockResolvedValueOnce({
|
|
ok: true,
|
|
status: 200,
|
|
json: async () => apiPayload,
|
|
})
|
|
|
|
const citiesByCountry = {
|
|
CountryX: [{ name: "CityAA" }],
|
|
} as unknown as CitiesGroupedByCountry
|
|
|
|
const result = await getLocationsByCountries({
|
|
lang: Lang.en,
|
|
citiesByCountry,
|
|
serviceToken: "token",
|
|
})
|
|
|
|
// Result should be an array with two entries
|
|
expect(Array.isArray(result)).toBe(true)
|
|
expect(result).toHaveLength(2)
|
|
|
|
const cityNode = result
|
|
.filter((n) => n.type === "cities")
|
|
.find((n) => n.name === "City1")
|
|
const hotelNode = result
|
|
.filter((n) => n.type === "hotels")
|
|
.find((n) => n.name === "Hotel1")
|
|
|
|
expect(cityNode).toBeDefined()
|
|
expect(cityNode!.country).toBe("CountryX") // country assigned based on citiesByCountry
|
|
|
|
expect(hotelNode).toBeDefined()
|
|
|
|
// hotel relationships.city should be the object returned by getCity (merged)
|
|
expect(hotelNode?.relationships).toBeDefined()
|
|
expect(hotelNode?.relationships.city).toEqual(
|
|
expect.objectContaining({
|
|
id: "city1",
|
|
name: "City1",
|
|
})
|
|
)
|
|
})
|
|
|
|
it("parses locations and enriches city country and hotel city via getCity", async () => {
|
|
mockedGetCacheClient.mockResolvedValueOnce(mockedCacheClient)
|
|
|
|
const apiPayload = mockApiData({
|
|
numberOfCities: 2,
|
|
numberOfHotels: 2,
|
|
})
|
|
|
|
mockedApiGet.mockResolvedValueOnce({
|
|
ok: true,
|
|
status: 200,
|
|
json: async () => apiPayload,
|
|
})
|
|
|
|
const citiesByCountry = {
|
|
CountryX: [{ name: "CityAA" }],
|
|
} as unknown as CitiesGroupedByCountry
|
|
|
|
const result = await getLocationsByCountries({
|
|
lang: Lang.en,
|
|
citiesByCountry,
|
|
serviceToken: "token",
|
|
})
|
|
|
|
// Result should be an array with two entries
|
|
expect(Array.isArray(result)).toBe(true)
|
|
expect(result).toHaveLength(4)
|
|
|
|
const city1 = result
|
|
.filter((n) => n.type === "cities")
|
|
.find((n) => n.name === "City1")
|
|
|
|
expect(city1).toBeDefined()
|
|
expect(city1?.country).toBe("CountryX")
|
|
|
|
const hotel1 = result
|
|
.filter((n) => n.type === "hotels")
|
|
.find((n) => n.name === "Hotel1")
|
|
expect(hotel1).toBeDefined()
|
|
|
|
// hotel relationships.city should be the object returned by getCity (merged)
|
|
expect(hotel1?.relationships).toBeDefined()
|
|
expect(hotel1?.relationships.city).toEqual(
|
|
expect.objectContaining({
|
|
id: "city1",
|
|
name: "City1",
|
|
})
|
|
)
|
|
|
|
const hotel2 = result
|
|
.filter((n) => n.type === "hotels")
|
|
.find((n) => n.name === "Hotel2")
|
|
expect(hotel2).toBeDefined()
|
|
|
|
// hotel relationships.city should be the object returned by getCity (merged)
|
|
expect(hotel2?.relationships).toBeDefined()
|
|
expect(hotel2?.relationships.city).toEqual(
|
|
expect.objectContaining({
|
|
id: "city2",
|
|
name: "City2",
|
|
})
|
|
)
|
|
})
|
|
|
|
it("filters out unpublished cities", async () => {
|
|
mockedGetCacheClient.mockResolvedValueOnce(mockedCacheClient)
|
|
|
|
const apiPayload = mockApiData({
|
|
numberOfCities: 2,
|
|
numberOfHotels: 2,
|
|
})
|
|
|
|
apiPayload.data[0].attributes.isPublished = false
|
|
|
|
mockedApiGet.mockResolvedValueOnce({
|
|
ok: true,
|
|
status: 200,
|
|
json: async () => apiPayload,
|
|
})
|
|
|
|
const citiesByCountry = {
|
|
CountryX: [{ name: "CityAA" }],
|
|
} as unknown as CitiesGroupedByCountry
|
|
|
|
const result = await getLocationsByCountries({
|
|
lang: Lang.en,
|
|
citiesByCountry,
|
|
serviceToken: "token",
|
|
})
|
|
|
|
// Result should be an array with two entries
|
|
expect(Array.isArray(result)).toBe(true)
|
|
expect(result).toHaveLength(3)
|
|
|
|
const city1 = result
|
|
.filter((n) => n.type === "cities")
|
|
.find((n) => n.name === "City1")
|
|
|
|
expect(city1).toBeUndefined()
|
|
|
|
const hotel1 = result
|
|
.filter((n) => n.type === "hotels")
|
|
.find((n) => n.name === "Hotel1")
|
|
expect(hotel1).toBeDefined()
|
|
|
|
const hotel2 = result
|
|
.filter((n) => n.type === "hotels")
|
|
.find((n) => n.name === "Hotel2")
|
|
expect(hotel2).toBeDefined()
|
|
})
|
|
|
|
it("sorts the result with cities first", async () => {
|
|
mockedGetCacheClient.mockResolvedValueOnce(mockedCacheClient)
|
|
|
|
const apiPayload = mockApiData({
|
|
numberOfCities: 9,
|
|
numberOfHotels: 9,
|
|
})
|
|
apiPayload.data = apiPayload.data.sort(() => Math.random() - 0.5) // shuffle
|
|
mockedApiGet.mockResolvedValueOnce({
|
|
ok: true,
|
|
status: 200,
|
|
json: async () => apiPayload,
|
|
})
|
|
|
|
const citiesByCountry = {
|
|
CountryX: [{ name: "CityAA" }],
|
|
} as unknown as CitiesGroupedByCountry
|
|
|
|
const result = await getLocationsByCountries({
|
|
lang: Lang.en,
|
|
citiesByCountry,
|
|
serviceToken: "token",
|
|
})
|
|
|
|
expect(result.at(0)?.type).toBe("cities")
|
|
expect(result.at(-1)?.type).toBe("hotels")
|
|
|
|
expect(result[0].type === "cities" ? result[0].name : undefined).toBe(
|
|
"City1"
|
|
)
|
|
expect(result[1].type === "cities" ? result[1].name : undefined).toBe(
|
|
"City2"
|
|
)
|
|
|
|
expect(result[9].type === "hotels" ? result[9].name : undefined).toBe(
|
|
"Hotel1"
|
|
)
|
|
expect(result[10].type === "hotels" ? result[10].name : undefined).toBe(
|
|
"Hotel2"
|
|
)
|
|
})
|
|
})
|
|
|
|
function mockApiData({
|
|
numberOfCities,
|
|
numberOfHotels,
|
|
}: {
|
|
numberOfCities: number
|
|
numberOfHotels: number
|
|
}) {
|
|
const cities = Array.from({ length: numberOfCities }, (_, i) => ({
|
|
id: `city${i + 1}`,
|
|
type: "cities" as const,
|
|
attributes: {
|
|
name: `City${i + 1}`,
|
|
countryName: `CountryX`,
|
|
cityIdentifier: `ci-${i + 1}`,
|
|
isPublished: true,
|
|
},
|
|
}))
|
|
|
|
const hotels = Array.from({ length: numberOfHotels }, (_, i) => ({
|
|
id: `hotel${i + 1}`,
|
|
type: "hotels" as const,
|
|
attributes: {
|
|
isActive: true,
|
|
name: `Hotel${i + 1}`,
|
|
operaId: `op-${i + 1}`,
|
|
isPublished: true,
|
|
},
|
|
relationships: {
|
|
city: {
|
|
links: {
|
|
related: `https://api/cities/city${i + 1}`,
|
|
},
|
|
},
|
|
},
|
|
}))
|
|
|
|
const apiPayload = {
|
|
data: [...cities, ...hotels],
|
|
}
|
|
|
|
return apiPayload
|
|
}
|