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({ ok: false, status: 401, json: async () => ({}), }) await expect( getLocationsByCountries({ lang: Lang.en, citiesByCountry: null, serviceToken: "token", }) ).rejects.toThrow("Unauthorized") expect(mockedApiGet).toHaveBeenCalled() }) it("throws forbidden on 403 response", async () => { mockedGetCacheClient.mockResolvedValueOnce(mockedCacheClient) mockedApiGet.mockResolvedValueOnce({ ok: false, status: 403, json: async () => ({}), }) await expect( getLocationsByCountries({ lang: Lang.en, citiesByCountry: null, serviceToken: "token", }) ).rejects.toThrow("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 }