* 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>
529 lines
15 KiB
TypeScript
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
|
|
//
|
|
},
|
|
})
|