Files
web/packages/trpc/lib/routers/hotels/services/getLocationsByCountries.test.ts
Joakim Jäderberg 99537b13e8 Merged in chore/add-error-details-for-sentry (pull request #3378)
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
2026-01-12 09:01:44 +00:00

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
}