Files
medusa-store/integration-tests/http/__tests__/sales-channel/admin/sales-channel.spec.ts
Adrien de Peretti 6dc0b8bed8 feat(): Introduce translation module and preliminary application of them (#14189)
* feat(): Translation first steps

* feat(): locale middleware

* feat(): readonly links

* feat(): feature flag

* feat(): modules sdk

* feat(): translation module re export

* start adding workflows

* update typings

* update typings

* test(): Add integration tests

* test(): centralize filters preparation

* test(): centralize filters preparation

* remove unnecessary importy

* fix workflows

* Define StoreLocale inside Store Module

* Link definition to extend Store with supported_locales

* store_locale migration

* Add supported_locales handling in Store Module

* Tests

* Accept supported_locales in Store endpoints

* Add locales to js-sdk

* Include locale list and default locale in Store Detail section

* Initialize local namespace in js-sdk

* Add locales route

* Make code primary key of locale table to facilitate upserts

* Add locales routes

* Show locale code as is

* Add list translations api route

* Batch endpoint

* Types

* New batchTranslationsWorkflow and various updates to existent ones

* Edit default locale UI

* WIP

* Apply translation agnostically

* middleware

* Apply translation agnostically

* fix Apply translation agnostically

* apply translations to product list

* Add feature flag

* fetch translations by batches of 250 max

* fix apply

* improve and test util

* apply to product list

* dont manage translations if no locale

* normalize locale

* potential todo

* Protect translations routes with feature flag

* Extract normalize locale util to core/utils

* Normalize locale on write

* Normalize locale for read

* Use feature flag to guard translations UI across the board

* Avoid throwing incorrectly when locale_code not present in partial updates

* move applyTranslations util

* remove old tests

* fix util tests

* fix(): product end points

* cleanup

* update lock

* remove unused var

* cleanup

* fix apply locale

* missing new dep for test utils

* Change entity_type, entity_id to reference, reference_id

* Remove comment

* Avoid registering translations route if ff not enabled

* Prevent registering express handler for disabled route via defineFileConfig

* Add tests

* Add changeset

* Update test

* fix integration tests, module and internals

* Add locale id plus fixed

* Allow to pass array of reference_id

* fix unit tests

* fix link loading

* fix store route

* fix sales channel test

* fix tests

---------

Co-authored-by: Nicolas Gorga <nicogorga11@gmail.com>
Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>
2025-12-08 19:33:08 +01:00

529 lines
15 KiB
TypeScript

import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
import {
adminHeaders,
createAdminUser,
} from "../../../../helpers/create-admin-user"
import { Modules } from "@medusajs/framework/utils"
jest.setTimeout(60000)
medusaIntegrationTestRunner({
testSuite: ({ dbConnection, getContainer, api }) => {
let salesChannel1
let salesChannel2
let container
beforeEach(async () => {
container = getContainer()
await createAdminUser(dbConnection, adminHeaders, container)
salesChannel1 = (
await api.post(
"/admin/sales-channels",
{
name: "test name",
description: "test description",
},
adminHeaders
)
).data.sales_channel
salesChannel2 = (
await api.post(
"/admin/sales-channels",
{
name: "test name 2",
description: "test description 2",
},
adminHeaders
)
).data.sales_channel
})
describe("GET /admin/sales-channels/:id", () => {
it("should retrieve the requested sales channel", async () => {
const response = await api.get(
`/admin/sales-channels/${salesChannel1.id}`,
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data.sales_channel).toBeTruthy()
expect(response.data.sales_channel).toEqual(
expect.objectContaining({
id: expect.any(String),
name: salesChannel1.name,
description: salesChannel1.description,
created_at: expect.any(String),
updated_at: expect.any(String),
})
)
})
})
describe("GET /admin/sales-channels", () => {
it("should list the sales channel", async () => {
const response = await api.get(`/admin/sales-channels`, adminHeaders)
expect(response.status).toEqual(200)
expect(response.data.sales_channels).toBeTruthy()
expect(response.data.sales_channels.length).toBe(3) // includes the default sales channel
expect(response.data).toEqual(
expect.objectContaining({
sales_channels: expect.arrayContaining([
expect.objectContaining({
name: salesChannel1.name,
description: salesChannel1.description,
}),
expect.objectContaining({
name: salesChannel2.name,
description: salesChannel2.description,
}),
]),
})
)
})
it("should list the sales channel using filters", async () => {
const response = await api.get(
`/admin/sales-channels?q=2`,
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data.sales_channels).toBeTruthy()
expect(response.data.sales_channels.length).toBe(1)
expect(response.data).toEqual({
count: 1,
limit: 20,
offset: 0,
sales_channels: expect.arrayContaining([
expect.objectContaining({
id: expect.any(String),
name: salesChannel2.name,
description: salesChannel2.description,
is_disabled: false,
deleted_at: null,
created_at: expect.any(String),
updated_at: expect.any(String),
}),
]),
})
})
it("should list the sales channel using properties filters", async () => {
const response = await api.get(
`/admin/sales-channels?name=test+name`,
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data.sales_channels).toBeTruthy()
expect(response.data.sales_channels.length).toBe(1)
expect(response.data).toEqual({
count: 1,
limit: 20,
offset: 0,
sales_channels: expect.arrayContaining([
expect.objectContaining({
id: expect.any(String),
name: salesChannel1.name,
description: salesChannel1.description,
is_disabled: false,
deleted_at: null,
created_at: expect.any(String),
updated_at: expect.any(String),
}),
]),
})
})
it("should support searching of sales channels", async () => {
await api.post(
"/admin/sales-channels",
{ name: "first channel", description: "to fetch" },
adminHeaders
)
await api.post(
"/admin/sales-channels",
{ name: "second channel", description: "not in response" },
adminHeaders
)
const response = await api.get(
`/admin/sales-channels?q=fetch`,
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data.sales_channels).toEqual([
expect.objectContaining({
name: "first channel",
}),
])
})
})
describe("POST /admin/sales-channels/:id", () => {
it("updates sales channel properties", async () => {
const payload = {
name: "updated name",
description: "updated description",
is_disabled: true,
}
const response = await api.post(
`/admin/sales-channels/${salesChannel1.id}`,
payload,
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data.sales_channel).toEqual(
expect.objectContaining({
id: expect.any(String),
name: payload.name,
description: payload.description,
is_disabled: payload.is_disabled,
created_at: expect.any(String),
updated_at: expect.any(String),
})
)
})
})
describe("POST /admin/sales-channels", () => {
it("successfully creates a disabled sales channel", async () => {
const newSalesChannel = {
name: "sales channel name",
is_disabled: true,
}
const response = await api.post(
"/admin/sales-channels",
newSalesChannel,
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data.sales_channel).toBeTruthy()
expect(response.data).toEqual(
expect.objectContaining({
sales_channel: expect.objectContaining({
name: newSalesChannel.name,
is_disabled: true,
}),
})
)
})
it("successfully creates a sales channel", async () => {
const newSalesChannel = {
name: "sales channel name",
description: "sales channel description",
}
const response = await api.post(
"/admin/sales-channels",
newSalesChannel,
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data.sales_channel).toBeTruthy()
expect(response.data).toEqual(
expect.objectContaining({
sales_channel: expect.objectContaining({
name: newSalesChannel.name,
description: newSalesChannel.description,
is_disabled: false,
}),
})
)
})
})
describe("DELETE /admin/sales-channels/:id", () => {
it("should fail to delete the requested sales channel if it is used as a default sales channel", async () => {
const salesChannel = (
await api.post(
"/admin/sales-channels",
{ name: "Test channel", description: "Test" },
adminHeaders
)
).data.sales_channel
const storeModule = container.resolve(Modules.STORE)
await storeModule.createStores({
name: "New store",
supported_currencies: [
{ currency_code: "usd", is_default: true },
{ currency_code: "dkk" },
],
default_sales_channel_id: salesChannel.id,
})
const errorResponse = await api
.delete(`/admin/sales-channels/${salesChannel.id}`, adminHeaders)
.catch((err) => err)
expect(errorResponse.response.data.message).toEqual(
`Cannot delete default sales channels: ${salesChannel.id}`
)
})
it("should delete the requested sales channel", async () => {
const toDelete = (
await api.get(
`/admin/sales-channels/${salesChannel1.id}`,
adminHeaders
)
).data.sales_channel
expect(toDelete.id).toEqual(salesChannel1.id)
expect(toDelete.deleted_at).toEqual(null)
const response = await api.delete(
`/admin/sales-channels/${salesChannel1.id}`,
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data).toEqual({
deleted: true,
id: expect.any(String),
object: "sales-channel",
})
const err = await api
.get(
`/admin/sales-channels/${salesChannel1.id}?fields=id,deleted_at`,
adminHeaders
)
.catch((err) => {
return err
})
expect(err.response.data.type).toEqual("not_found")
expect(err.response.data.message).toEqual(
`Sales channel with id: ${salesChannel1.id} not found`
)
})
it("should successfully delete channel associations", async () => {
let location = (
await api.post(
`/admin/stock-locations`,
{
name: "test location",
},
adminHeaders
)
).data.stock_location
await api.post(
`/admin/stock-locations/${location.id}/sales-channels`,
{
add: [salesChannel1.id, salesChannel2.id],
},
adminHeaders
)
await api.delete(
`/admin/sales-channels/${salesChannel1.id}`,
adminHeaders
)
location = (
await api.get(
`/admin/stock-locations/${location.id}?fields=*sales_channels`,
adminHeaders
)
).data.stock_location
expect(location.sales_channels).toHaveLength(1)
})
})
describe("POST /admin/sales-channels/:id/products", () => {
// BREAKING CHANGE: Endpoint has changed
// from: /admin/sales-channels/:id/products/batch
// to: /admin/sales-channels/:id/products
let product
beforeEach(async () => {
const shippingProfile = (
await api.post(
`/admin/shipping-profiles`,
{ name: "Test", type: "default" },
adminHeaders
)
).data.shipping_profile
product = (
await api.post(
"/admin/products",
{
title: "test name",
shipping_profile_id: shippingProfile.id,
options: [{ title: "size", values: ["large"] }],
},
adminHeaders
)
).data.product
})
it("should add products to a sales channel", async () => {
const response = await api.post(
`/admin/sales-channels/${salesChannel1.id}/products`,
{ add: [product.id] },
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data.sales_channel).toEqual(
expect.objectContaining({
id: expect.any(String),
name: "test name",
description: "test description",
is_disabled: false,
created_at: expect.any(String),
updated_at: expect.any(String),
deleted_at: null,
})
)
product = (
await api.get(
`/admin/products/${product.id}?fields=*sales_channels`,
adminHeaders
)
).data.product
expect(product.sales_channels.length).toBe(1)
expect(product.sales_channels).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: expect.any(String),
name: "test name",
description: "test description",
is_disabled: false,
}),
])
)
})
it("should remove products from a sales channel", async () => {
await api.post(
`/admin/sales-channels/${salesChannel1.id}/products`,
{ add: [product.id] },
adminHeaders
)
product = (
await api.get(
`/admin/products/${product.id}?fields=*sales_channels`,
adminHeaders
)
).data.product
expect(product.sales_channels.length).toBe(1)
expect(product.sales_channels).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: expect.any(String),
name: "test name",
description: "test description",
is_disabled: false,
}),
])
)
const response = await api.post(
`/admin/sales-channels/${salesChannel1.id}/products`,
{ remove: [product.id] },
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data.sales_channel).toEqual(
expect.objectContaining({
id: expect.any(String),
name: "test name",
description: "test description",
is_disabled: false,
})
)
product = (
await api.get(
`/admin/products/${product.id}?fields=*sales_channels`,
adminHeaders
)
).data.product
expect(product.sales_channels.length).toBe(0)
})
})
describe("Sales channels with publishable key", () => {
let pubKey1
beforeEach(async () => {
pubKey1 = (
await api.post(
"/admin/api-keys",
{ title: "sample key", type: "publishable" },
adminHeaders
)
).data.api_key
await api.post(
`/admin/api-keys/${pubKey1.id}/sales-channels`,
{
add: [salesChannel1.id, salesChannel2.id],
},
adminHeaders
)
})
it("list sales channels from the publishable api key with free text search filter", async () => {
const response = await api.get(
`/admin/sales-channels?q=2&publishable_key_id=${pubKey1.id}`,
adminHeaders
)
expect(response.status).toBe(200)
expect(response.data.sales_channels.length).toEqual(1)
expect(response.data.sales_channels).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: salesChannel2.id,
deleted_at: null,
name: "test name 2",
description: "test description 2",
is_disabled: false,
}),
])
)
})
})
// BREAKING: DELETED TESTS:
// - POST /admin/products/:id
// - Mutation sales channels on products
// - POST /admin/products
// - Creating a product with a sales channel
// - GET /admin/products
// - Filtering products by sales channel
// - Expanding with a sales channel
// - GET /admin/orders
// - Filtering orders by sales channel
// - Expanding with a sales channel
// - POST /admin/orders/:id/swaps
// - Creating a swap with a sales channel
//
},
})