fix: Several fixes to store product endpoints, moved several test suites to HTTP (#7601)
* chore: Move publishable api key tests to HTTP * chore: Move store tests to HTTP folder * fix: Add tests for store products, fix several bugs around publishable keys
This commit is contained in:
1 parent
d104d1a256
commit
e44fe78b96
24 files changed
+2301
-2763
No files matched your search
File diff suppressed because it is too large.
Load diff
@@ -1,283 +0,0 @@
|
|||||||
const {
|
|
||||||
createAdminUser,
|
|
||||||
adminHeaders,
|
|
||||||
} = require("../../../helpers/create-admin-user")
|
|
||||||
const { breaking } = require("../../../helpers/breaking")
|
|
||||||
const { ModuleRegistrationName } = require("@medusajs/modules-sdk")
|
|
||||||
const { medusaIntegrationTestRunner } = require("medusa-test-utils")
|
|
||||||
|
|
||||||
jest.setTimeout(90000)
|
|
||||||
|
|
||||||
medusaIntegrationTestRunner({
|
|
||||||
testSuite: ({ dbConnection, getContainer, api }) => {
|
|
||||||
describe("/admin/store", () => {
|
|
||||||
let dbStore
|
|
||||||
let container
|
|
||||||
|
|
||||||
// Note: The tests rely on the loader running and populating clean data before and after the test, so we have to do this in a beforeEach
|
|
||||||
beforeEach(async () => {
|
|
||||||
container = getContainer()
|
|
||||||
|
|
||||||
await createAdminUser(dbConnection, adminHeaders, container)
|
|
||||||
await breaking(
|
|
||||||
async () => {
|
|
||||||
const { Store } = require("@medusajs/medusa/dist/models/store")
|
|
||||||
const manager = dbConnection.manager
|
|
||||||
dbStore = await manager.findOne(Store, {
|
|
||||||
where: { name: "Medusa Store" },
|
|
||||||
})
|
|
||||||
await manager.query(
|
|
||||||
`INSERT INTO store_currencies (store_id, currency_code)
|
|
||||||
VALUES ('${dbStore.id}', 'dkk')`
|
|
||||||
)
|
|
||||||
},
|
|
||||||
async () => {
|
|
||||||
const service = container.resolve(ModuleRegistrationName.STORE)
|
|
||||||
dbStore = await service.create({
|
|
||||||
supported_currency_codes: ["usd", "dkk"],
|
|
||||||
default_currency_code: "usd",
|
|
||||||
default_sales_channel_id: "sc_12345",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("Store creation", () => {
|
|
||||||
it("has created store with default currency", async () => {
|
|
||||||
const store = await breaking(
|
|
||||||
() =>
|
|
||||||
api.get("/admin/store", adminHeaders).then((r) => r.data.store),
|
|
||||||
() =>
|
|
||||||
api
|
|
||||||
.get("/admin/stores", adminHeaders)
|
|
||||||
.then((r) =>
|
|
||||||
r.data.stores.find(
|
|
||||||
(s) => s.default_sales_channel_id === "sc_12345"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(store).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
id: expect.any(String),
|
|
||||||
name: "Medusa Store",
|
|
||||||
default_currency_code: "usd",
|
|
||||||
default_sales_channel_id: expect.any(String),
|
|
||||||
...breaking(
|
|
||||||
() => ({
|
|
||||||
default_sales_channel: expect.objectContaining({
|
|
||||||
id: expect.any(String),
|
|
||||||
}),
|
|
||||||
currencies: expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
code: "usd",
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
modules: expect.any(Array),
|
|
||||||
feature_flags: expect.any(Array),
|
|
||||||
}),
|
|
||||||
() => ({
|
|
||||||
supported_currency_codes: ["usd", "dkk"],
|
|
||||||
})
|
|
||||||
),
|
|
||||||
created_at: expect.any(String),
|
|
||||||
updated_at: expect.any(String),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("POST /admin/store", () => {
|
|
||||||
it("fails to update default currency if not in store currencies", async () => {
|
|
||||||
try {
|
|
||||||
await api.post(
|
|
||||||
breaking(
|
|
||||||
() => "/admin/store",
|
|
||||||
() => `/admin/stores/${dbStore.id}`
|
|
||||||
),
|
|
||||||
{
|
|
||||||
default_currency_code: "eur",
|
|
||||||
},
|
|
||||||
adminHeaders
|
|
||||||
)
|
|
||||||
} catch (e) {
|
|
||||||
expect(e.response.data).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
type: "invalid_data",
|
|
||||||
message: "Store does not have currency: eur",
|
|
||||||
})
|
|
||||||
)
|
|
||||||
expect(e.response.status).toBe(400)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
it("fails to remove default currency from currencies without replacing it", async () => {
|
|
||||||
try {
|
|
||||||
await api.post(
|
|
||||||
breaking(
|
|
||||||
() => "/admin/store",
|
|
||||||
() => `/admin/stores/${dbStore.id}`
|
|
||||||
),
|
|
||||||
breaking(
|
|
||||||
() => ({
|
|
||||||
currencies: ["usd"],
|
|
||||||
}),
|
|
||||||
() => ({ supported_currency_codes: ["dkk"] })
|
|
||||||
),
|
|
||||||
adminHeaders
|
|
||||||
)
|
|
||||||
} catch (e) {
|
|
||||||
expect(e.response.data).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
type: "invalid_data",
|
|
||||||
message:
|
|
||||||
"You are not allowed to remove default currency from store currencies without replacing it as well",
|
|
||||||
})
|
|
||||||
)
|
|
||||||
expect(e.response.status).toBe(400)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
it("successfully updates default currency code", async () => {
|
|
||||||
const response = await api
|
|
||||||
.post(
|
|
||||||
breaking(
|
|
||||||
() => "/admin/store",
|
|
||||||
() => `/admin/stores/${dbStore.id}`
|
|
||||||
),
|
|
||||||
{
|
|
||||||
default_currency_code: "dkk",
|
|
||||||
},
|
|
||||||
adminHeaders
|
|
||||||
)
|
|
||||||
.catch((err) => console.log(err))
|
|
||||||
|
|
||||||
expect(response.status).toEqual(200)
|
|
||||||
expect(response.data.store).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
id: expect.any(String),
|
|
||||||
name: "Medusa Store",
|
|
||||||
default_currency_code: "dkk",
|
|
||||||
created_at: expect.any(String),
|
|
||||||
updated_at: expect.any(String),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("successfully updates default currency and store currencies", async () => {
|
|
||||||
const response = await api.post(
|
|
||||||
breaking(
|
|
||||||
() => "/admin/store",
|
|
||||||
() => `/admin/stores/${dbStore.id}`
|
|
||||||
),
|
|
||||||
{
|
|
||||||
default_currency_code: "jpy",
|
|
||||||
...breaking(
|
|
||||||
() => ({ currencies: ["jpy", "usd"] }),
|
|
||||||
() => ({ supported_currency_codes: ["jpy", "usd"] })
|
|
||||||
),
|
|
||||||
},
|
|
||||||
adminHeaders
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(response.status).toEqual(200)
|
|
||||||
expect(response.data.store).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
id: expect.any(String),
|
|
||||||
name: "Medusa Store",
|
|
||||||
default_sales_channel_id: expect.any(String),
|
|
||||||
...breaking(
|
|
||||||
() => ({
|
|
||||||
currencies: expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
code: "jpy",
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
|
||||||
code: "usd",
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
}),
|
|
||||||
() => ({ supported_currency_codes: ["jpy", "usd"] })
|
|
||||||
),
|
|
||||||
default_currency_code: "jpy",
|
|
||||||
created_at: expect.any(String),
|
|
||||||
updated_at: expect.any(String),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("successfully updates and store currencies", async () => {
|
|
||||||
const response = await api.post(
|
|
||||||
breaking(
|
|
||||||
() => "/admin/store",
|
|
||||||
() => `/admin/stores/${dbStore.id}`
|
|
||||||
),
|
|
||||||
breaking(
|
|
||||||
() => ({
|
|
||||||
currencies: ["jpy", "usd"],
|
|
||||||
}),
|
|
||||||
() => ({
|
|
||||||
supported_currency_codes: ["jpy", "usd"],
|
|
||||||
})
|
|
||||||
),
|
|
||||||
adminHeaders
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(response.status).toEqual(200)
|
|
||||||
expect(response.data.store).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
id: expect.any(String),
|
|
||||||
default_sales_channel_id: expect.any(String),
|
|
||||||
name: "Medusa Store",
|
|
||||||
...breaking(
|
|
||||||
() => ({
|
|
||||||
currencies: expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
code: "jpy",
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
|
||||||
code: "usd",
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
}),
|
|
||||||
() => ({ supported_currency_codes: ["jpy", "usd"] })
|
|
||||||
),
|
|
||||||
default_currency_code: "usd",
|
|
||||||
created_at: expect.any(String),
|
|
||||||
updated_at: expect.any(String),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("GET /admin/store", () => {
|
|
||||||
it("supports searching of stores", async () => {
|
|
||||||
await breaking(
|
|
||||||
() => {},
|
|
||||||
async () => {
|
|
||||||
const service = container.resolve(ModuleRegistrationName.STORE)
|
|
||||||
const secondStore = await service.create({
|
|
||||||
name: "Second Store",
|
|
||||||
supported_currency_codes: ["eur"],
|
|
||||||
default_currency_code: "eur",
|
|
||||||
})
|
|
||||||
|
|
||||||
const response = await api.get(
|
|
||||||
"/admin/stores?q=second",
|
|
||||||
adminHeaders
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(response.status).toEqual(200)
|
|
||||||
expect(response.data.stores).toEqual([
|
|
||||||
expect.objectContaining({
|
|
||||||
id: secondStore.id,
|
|
||||||
name: "Second Store",
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@@ -1,293 +0,0 @@
|
|||||||
const path = require("path")
|
|
||||||
|
|
||||||
const setupServer = require("../../../environment-helpers/setup-server")
|
|
||||||
const { useApi } = require("../../../environment-helpers/use-api")
|
|
||||||
const { initDb, useDb } = require("../../../environment-helpers/use-db")
|
|
||||||
const { simpleProductFactory } = require("../../../factories")
|
|
||||||
|
|
||||||
const adminSeeder = require("../../../helpers/admin-seeder")
|
|
||||||
const adminVariantsSeeder = require("../../../helpers/admin-variants-seeder")
|
|
||||||
const productSeeder = require("../../../helpers/product-seeder")
|
|
||||||
|
|
||||||
const adminHeaders = {
|
|
||||||
headers: {
|
|
||||||
"x-medusa-access-token": "test_token",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
jest.setTimeout(30000)
|
|
||||||
|
|
||||||
describe("/admin/products", () => {
|
|
||||||
let medusaProcess
|
|
||||||
let dbConnection
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
const cwd = path.resolve(path.join(__dirname, "..", ".."))
|
|
||||||
dbConnection = await initDb({ cwd })
|
|
||||||
medusaProcess = await setupServer({ cwd })
|
|
||||||
})
|
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
const db = useDb()
|
|
||||||
await db.shutdown()
|
|
||||||
|
|
||||||
medusaProcess.kill()
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("GET /admin/product-variants", () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
await productSeeder(dbConnection)
|
|
||||||
await adminSeeder(dbConnection)
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
const db = useDb()
|
|
||||||
await db.teardown()
|
|
||||||
})
|
|
||||||
|
|
||||||
it("lists all product variants", async () => {
|
|
||||||
const api = useApi()
|
|
||||||
|
|
||||||
const response = await api
|
|
||||||
.get("/admin/variants/", adminHeaders)
|
|
||||||
.catch((err) => {
|
|
||||||
console.log(err)
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(response.status).toEqual(200)
|
|
||||||
expect(response.data.variants).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
expect.objectContaining(
|
|
||||||
{
|
|
||||||
id: "test-variant",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "test-variant_2",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "test-variant_1",
|
|
||||||
}
|
|
||||||
),
|
|
||||||
])
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("lists all product variants matching a specific sku", async () => {
|
|
||||||
const api = useApi()
|
|
||||||
const response = await api
|
|
||||||
.get("/admin/variants?q=sku2", adminHeaders)
|
|
||||||
.catch((err) => {
|
|
||||||
console.log(err)
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(response.status).toEqual(200)
|
|
||||||
expect(response.data.variants.length).toEqual(1)
|
|
||||||
expect(response.data.variants).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
sku: "test-sku2",
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("lists all product variants matching a specific variant title", async () => {
|
|
||||||
const api = useApi()
|
|
||||||
const response = await api
|
|
||||||
.get("/admin/variants?q=rank (1)", adminHeaders)
|
|
||||||
.catch((err) => {
|
|
||||||
console.log(err)
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(response.status).toEqual(200)
|
|
||||||
expect(response.data.variants.length).toEqual(1)
|
|
||||||
expect(response.data.variants).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
id: "test-variant_1",
|
|
||||||
sku: "test-sku1",
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("lists all product variants matching a specific product title", async () => {
|
|
||||||
const api = useApi()
|
|
||||||
|
|
||||||
const response = await api
|
|
||||||
.get("/admin/variants?q=Test product1", adminHeaders)
|
|
||||||
.catch((err) => {
|
|
||||||
console.log(err)
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(response.status).toEqual(200)
|
|
||||||
expect(response.data.variants.length).toEqual(2)
|
|
||||||
expect(response.data.variants).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
product_id: "test-product1",
|
|
||||||
id: "test-variant_3",
|
|
||||||
sku: "test-sku3",
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
|
||||||
product_id: "test-product1",
|
|
||||||
id: "test-variant_4",
|
|
||||||
sku: "test-sku4",
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("GET /admin/variants price selection strategy", () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
try {
|
|
||||||
await adminVariantsSeeder(dbConnection)
|
|
||||||
} catch (err) {
|
|
||||||
console.log(err)
|
|
||||||
}
|
|
||||||
await adminSeeder(dbConnection)
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
const db = useDb()
|
|
||||||
await db.teardown()
|
|
||||||
})
|
|
||||||
|
|
||||||
it("selects prices based on the passed currency code", async () => {
|
|
||||||
const api = useApi()
|
|
||||||
|
|
||||||
const response = await api.get(
|
|
||||||
"/admin/variants?id=test-variant¤cy_code=usd",
|
|
||||||
adminHeaders
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(response.data).toMatchSnapshot({
|
|
||||||
variants: [
|
|
||||||
{
|
|
||||||
id: "test-variant",
|
|
||||||
original_price: 100,
|
|
||||||
calculated_price: 80,
|
|
||||||
calculated_price_type: "sale",
|
|
||||||
original_price_incl_tax: null,
|
|
||||||
calculated_price_incl_tax: null,
|
|
||||||
original_tax: null,
|
|
||||||
calculated_tax: null,
|
|
||||||
options: expect.any(Array),
|
|
||||||
prices: expect.any(Array),
|
|
||||||
product: expect.any(Object),
|
|
||||||
created_at: expect.any(String),
|
|
||||||
updated_at: expect.any(String),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it("selects prices based on the passed region id", async () => {
|
|
||||||
const api = useApi()
|
|
||||||
|
|
||||||
const response = await api.get(
|
|
||||||
"/admin/variants?id=test-variant®ion_id=reg-europe",
|
|
||||||
adminHeaders
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(response.data).toMatchSnapshot({
|
|
||||||
variants: [
|
|
||||||
{
|
|
||||||
id: "test-variant",
|
|
||||||
original_price: 100,
|
|
||||||
calculated_price: 80,
|
|
||||||
calculated_price_type: "sale",
|
|
||||||
original_price_incl_tax: 100,
|
|
||||||
calculated_price_incl_tax: 80,
|
|
||||||
original_tax: 0,
|
|
||||||
calculated_tax: 0,
|
|
||||||
options: expect.any(Array),
|
|
||||||
prices: expect.any(Array),
|
|
||||||
product: expect.any(Object),
|
|
||||||
created_at: expect.any(String),
|
|
||||||
updated_at: expect.any(String),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it("selects prices based on the passed region id and customer id", async () => {
|
|
||||||
const api = useApi()
|
|
||||||
|
|
||||||
const response = await api.get(
|
|
||||||
"/admin/variants?id=test-variant®ion_id=reg-europe&customer_id=test-customer",
|
|
||||||
adminHeaders
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(response.data).toMatchSnapshot({
|
|
||||||
variants: [
|
|
||||||
{
|
|
||||||
id: "test-variant",
|
|
||||||
original_price: 100,
|
|
||||||
calculated_price: 40,
|
|
||||||
calculated_price_type: "sale",
|
|
||||||
original_price_incl_tax: 100,
|
|
||||||
calculated_price_incl_tax: 40,
|
|
||||||
original_tax: 0,
|
|
||||||
calculated_tax: 0,
|
|
||||||
prices: expect.any(Array),
|
|
||||||
options: expect.any(Array),
|
|
||||||
product: expect.any(Object),
|
|
||||||
created_at: expect.any(String),
|
|
||||||
updated_at: expect.any(String),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it("returns a list of variants matching the given ids", async () => {
|
|
||||||
const api = useApi()
|
|
||||||
|
|
||||||
const productData = {
|
|
||||||
id: "test-product_filtering_by_variant_id",
|
|
||||||
title: "Test product filtering by variant id",
|
|
||||||
handle: "test-product_filtering_by_variant_id",
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
id: "test-product_filtering_by_variant_id-option",
|
|
||||||
title: "Size",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
variants: [],
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < 25; i++) {
|
|
||||||
productData.variants.push({
|
|
||||||
product_id: productData.id,
|
|
||||||
sku: `test-product_filtering_by_variant_id-${i}`,
|
|
||||||
title: `test-product_filtering_by_variant_id-${i}`,
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
option_id: "test-product_filtering_by_variant_id-option",
|
|
||||||
value: `Large-${i}`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const product = await simpleProductFactory(dbConnection, productData)
|
|
||||||
|
|
||||||
const variantIds = product.variants.map((v) => v.id)
|
|
||||||
const qs = "id[]=" + variantIds.join("&id[]=")
|
|
||||||
|
|
||||||
const response = await api
|
|
||||||
.get("/admin/variants?limit=30&" + qs, adminHeaders)
|
|
||||||
.catch((err) => {
|
|
||||||
console.log(err)
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(response.status).toEqual(200)
|
|
||||||
expect(response.data.variants.length).toEqual(variantIds.length)
|
|
||||||
|
|
||||||
for (const variant of response.data.variants) {
|
|
||||||
expect(variantIds).toContain(variant.id)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { HttpTypes } from "@medusajs/types"
|
||||||
|
|
||||||
|
export const getProductFixture = (
|
||||||
|
overrides: Partial<HttpTypes.AdminProduct>
|
||||||
|
) => ({
|
||||||
|
title: "Test fixture",
|
||||||
|
description: "test-product-description",
|
||||||
|
status: "draft",
|
||||||
|
// BREAKING: Images input changed from string[] to {url: string}[]
|
||||||
|
images: [{ url: "test-image.png" }, { url: "test-image-2.png" }],
|
||||||
|
tags: [{ value: "123" }, { value: "456" }],
|
||||||
|
// BREAKING: Options input changed from {title: string}[] to {title: string, values: string[]}[]
|
||||||
|
options: [
|
||||||
|
{ title: "size", values: ["large", "small"] },
|
||||||
|
{ title: "color", values: ["green"] },
|
||||||
|
],
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
title: "Test variant",
|
||||||
|
prices: [
|
||||||
|
{
|
||||||
|
currency_code: "usd",
|
||||||
|
amount: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
currency_code: "eur",
|
||||||
|
amount: 45,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
currency_code: "dkk",
|
||||||
|
amount: 30,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
// BREAKING: Options input changed from {value: string}[] to {[optionTitle]: optionValue} map
|
||||||
|
options: {
|
||||||
|
size: "large",
|
||||||
|
color: "green",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
...(overrides ?? {}),
|
||||||
|
})
|
||||||
@@ -0,0 +1,291 @@
|
|||||||
|
import { medusaIntegrationTestRunner } from "medusa-test-utils"
|
||||||
|
import {
|
||||||
|
adminHeaders,
|
||||||
|
createAdminUser,
|
||||||
|
} from "../../../../helpers/create-admin-user"
|
||||||
|
|
||||||
|
jest.setTimeout(50000)
|
||||||
|
|
||||||
|
medusaIntegrationTestRunner({
|
||||||
|
env: {},
|
||||||
|
testSuite: ({ dbConnection, getContainer, api }) => {
|
||||||
|
describe("Publishable Keys - Admin", () => {
|
||||||
|
let pubKey1
|
||||||
|
let pubKey2
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await createAdminUser(dbConnection, adminHeaders, getContainer())
|
||||||
|
|
||||||
|
// BREAKING: Before the ID of the token was used in request headers, now there is a separate `token` field that should be used
|
||||||
|
pubKey1 = (
|
||||||
|
await api.post(
|
||||||
|
"/admin/api-keys",
|
||||||
|
{ title: "sample key", type: "publishable" },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.api_key
|
||||||
|
pubKey2 = (
|
||||||
|
await api.post(
|
||||||
|
"/admin/api-keys",
|
||||||
|
// BREAKING: The type field is now required
|
||||||
|
{ title: "just a title", type: "publishable" },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.api_key
|
||||||
|
})
|
||||||
|
|
||||||
|
// BREAKING: The URL changed from /admin/publishable-api-keys to /admin/api-keys, as well as the response field
|
||||||
|
describe("GET /admin/api-keys/:id", () => {
|
||||||
|
it("retrieve a publishable key by id ", async () => {
|
||||||
|
const response = await api.get(
|
||||||
|
`/admin/api-keys/${pubKey1.id}`,
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.status).toBe(200)
|
||||||
|
|
||||||
|
expect(response.data.api_key).toMatchObject({
|
||||||
|
id: pubKey1.id,
|
||||||
|
created_at: expect.any(String),
|
||||||
|
created_by: expect.stringContaining("user_"),
|
||||||
|
revoked_by: null,
|
||||||
|
revoked_at: null,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("GET /admin/api-keys", () => {
|
||||||
|
it("list publishable keys", async () => {
|
||||||
|
const response = await api.get(
|
||||||
|
`/admin/api-keys?limit=2`,
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.data.count).toBe(2)
|
||||||
|
expect(response.data.limit).toBe(2)
|
||||||
|
expect(response.data.offset).toBe(0)
|
||||||
|
expect(response.data.api_keys).toHaveLength(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("list publishable keys with query search", async () => {
|
||||||
|
const response = await api.get(
|
||||||
|
`/admin/api-keys?q=sample`,
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.data.count).toBe(1)
|
||||||
|
expect(response.data.limit).toBe(20)
|
||||||
|
expect(response.data.offset).toBe(0)
|
||||||
|
expect(response.data.api_keys).toHaveLength(1)
|
||||||
|
expect(response.data.api_keys).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
title: "sample key",
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("POST /admin/api-keys", () => {
|
||||||
|
it("crete a publishable keys", async () => {
|
||||||
|
const response = await api.post(
|
||||||
|
`/admin/api-keys`,
|
||||||
|
{ title: "Store api key", type: "publishable" },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.status).toBe(200)
|
||||||
|
expect(response.data.api_key).toMatchObject({
|
||||||
|
created_by: expect.any(String),
|
||||||
|
id: expect.any(String),
|
||||||
|
title: "Store api key",
|
||||||
|
revoked_by: null,
|
||||||
|
revoked_at: null,
|
||||||
|
created_at: expect.any(String),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("POST /admin/api-keys/:id", () => {
|
||||||
|
it("update a publishable key", async () => {
|
||||||
|
const response = await api.post(
|
||||||
|
`/admin/api-keys/${pubKey1.id}`,
|
||||||
|
{ title: "Changed title" },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.status).toBe(200)
|
||||||
|
expect(response.data.api_key).toMatchObject({
|
||||||
|
id: pubKey1.id,
|
||||||
|
title: "Changed title",
|
||||||
|
revoked_by: null,
|
||||||
|
revoked_at: null,
|
||||||
|
created_at: expect.any(String),
|
||||||
|
updated_at: expect.any(String),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("DELETE /admin/api-keys/:id", () => {
|
||||||
|
it("delete a publishable key", async () => {
|
||||||
|
const response1 = await api.delete(
|
||||||
|
`/admin/api-keys/${pubKey1.id}`,
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response1.status).toBe(200)
|
||||||
|
expect(response1.data).toEqual({
|
||||||
|
id: pubKey1.id,
|
||||||
|
object: "api_key",
|
||||||
|
deleted: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const err = await api
|
||||||
|
.get(`/admin/api-keys/${pubKey1.id}`, adminHeaders)
|
||||||
|
.catch((e) => e)
|
||||||
|
|
||||||
|
expect(err.response.status).toBe(404)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("POST /admin/api-keys/:id/revoke", () => {
|
||||||
|
it("revoke a publishable key", async () => {
|
||||||
|
const response = await api.post(
|
||||||
|
`/admin/api-keys/${pubKey1.id}/revoke`,
|
||||||
|
{},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.status).toBe(200)
|
||||||
|
|
||||||
|
expect(response.data.api_key).toMatchObject({
|
||||||
|
id: pubKey1.id,
|
||||||
|
created_at: expect.any(String),
|
||||||
|
updated_at: expect.any(String),
|
||||||
|
revoked_by: expect.stringContaining("user_"),
|
||||||
|
revoked_at: expect.any(String),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// BREAKING: The GET /admin/api-keys/:id/sales-channels endpoint was removed.
|
||||||
|
// It was replaced by the GET /admin/sales-channels endpoint where you can filter by publishable key
|
||||||
|
// BREAKING: Batch route and input changed (no more batch suffix, and the input takes ids to add and remove)
|
||||||
|
describe("Add /admin/api-keys/:id/sales-channels", () => {
|
||||||
|
let salesChannel1
|
||||||
|
let salesChannel2
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
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
|
||||||
|
})
|
||||||
|
|
||||||
|
it("add sales channels to the publishable api key scope", async () => {
|
||||||
|
const response = await api.post(
|
||||||
|
`/admin/api-keys/${pubKey1.id}/sales-channels`,
|
||||||
|
{
|
||||||
|
add: [salesChannel1.id, salesChannel2.id],
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.status).toBe(200)
|
||||||
|
const keyWithChannels = (
|
||||||
|
await api.get(`/admin/api-keys/${pubKey1.id}`, adminHeaders)
|
||||||
|
).data.api_key
|
||||||
|
|
||||||
|
expect(keyWithChannels.sales_channels).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: salesChannel1.id,
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
id: salesChannel2.id,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("remove sales channels from the publishable api key scope", async () => {
|
||||||
|
await api.post(
|
||||||
|
`/admin/api-keys/${pubKey1.id}/sales-channels`,
|
||||||
|
{
|
||||||
|
add: [salesChannel1.id, salesChannel2.id],
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
const response = await api.post(
|
||||||
|
`/admin/api-keys/${pubKey1.id}/sales-channels`,
|
||||||
|
{
|
||||||
|
remove: [salesChannel1.id],
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.status).toBe(200)
|
||||||
|
const keyWithChannels = (
|
||||||
|
await api.get(`/admin/api-keys/${pubKey1.id}`, adminHeaders)
|
||||||
|
).data.api_key
|
||||||
|
|
||||||
|
expect(keyWithChannels.sales_channels).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: salesChannel2.id,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("list sales channels from the publishable api key", async () => {
|
||||||
|
await api.post(
|
||||||
|
`/admin/api-keys/${pubKey1.id}/sales-channels`,
|
||||||
|
{
|
||||||
|
add: [salesChannel1.id, salesChannel2.id],
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
const response = await api.get(
|
||||||
|
`/admin/api-keys/${pubKey1.id}`,
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
const salesChannels = response.data.api_key.sales_channels
|
||||||
|
expect(response.status).toBe(200)
|
||||||
|
expect(salesChannels.length).toEqual(2)
|
||||||
|
expect(salesChannels).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: salesChannel1.id,
|
||||||
|
name: "test name",
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
id: salesChannel2.id,
|
||||||
|
name: "test name 2",
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
import { medusaIntegrationTestRunner } from "medusa-test-utils"
|
||||||
|
import {
|
||||||
|
adminHeaders,
|
||||||
|
createAdminUser,
|
||||||
|
} from "../../../../helpers/create-admin-user"
|
||||||
|
|
||||||
|
jest.setTimeout(50000)
|
||||||
|
|
||||||
|
medusaIntegrationTestRunner({
|
||||||
|
testSuite: ({ dbConnection, getContainer, api }) => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await createAdminUser(dbConnection, adminHeaders, getContainer())
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("noop", () => {
|
||||||
|
it("noop", () => {})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO: Implement the tests below, which were migrated from v1.
|
||||||
|
// describe("POST /store/carts/:id", () => {
|
||||||
|
// let product
|
||||||
|
// const pubKeyId = IdMap.getId("pubkey-get-id")
|
||||||
|
|
||||||
|
// beforeEach(async () => {
|
||||||
|
// await adminSeeder(dbConnection)
|
||||||
|
|
||||||
|
// await simpleRegionFactory(dbConnection, {
|
||||||
|
// id: "test-region",
|
||||||
|
// })
|
||||||
|
|
||||||
|
// await simplePublishableApiKeyFactory(dbConnection, {
|
||||||
|
// id: pubKeyId,
|
||||||
|
// created_by: adminUserId,
|
||||||
|
// })
|
||||||
|
|
||||||
|
// product = await simpleProductFactory(dbConnection, {
|
||||||
|
// sales_channels: [
|
||||||
|
// {
|
||||||
|
// id: "sales-channel",
|
||||||
|
// name: "Sales channel",
|
||||||
|
// description: "Sales channel",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: "sales-channel2",
|
||||||
|
// name: "Sales channel2",
|
||||||
|
// description: "Sales channel2",
|
||||||
|
// },
|
||||||
|
// ],
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
|
||||||
|
// afterEach(async () => {
|
||||||
|
// const db = useDb()
|
||||||
|
// return await db.teardown()
|
||||||
|
// })
|
||||||
|
|
||||||
|
// it("should assign sales channel to order on cart completion if PK is present in the header", async () => {
|
||||||
|
// const api = useApi()
|
||||||
|
|
||||||
|
// await api.post(
|
||||||
|
// `/admin/api-keys/${pubKeyId}/sales-channels/batch`,
|
||||||
|
// {
|
||||||
|
// sales_channel_ids: [{ id: "sales-channel" }],
|
||||||
|
// },
|
||||||
|
// adminHeaders
|
||||||
|
// )
|
||||||
|
|
||||||
|
// const customerRes = await api.post("/store/customers", customerData, {
|
||||||
|
// withCredentials: true,
|
||||||
|
// })
|
||||||
|
|
||||||
|
// const createCartRes = await api.post(
|
||||||
|
// "/store/carts",
|
||||||
|
// {
|
||||||
|
// region_id: "test-region",
|
||||||
|
// items: [
|
||||||
|
// {
|
||||||
|
// variant_id: product.variants[0].id,
|
||||||
|
// quantity: 1,
|
||||||
|
// },
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// headers: {
|
||||||
|
// "x-medusa-access-token": "test_token",
|
||||||
|
// "x-publishable-api-key": pubKeyId,
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
// )
|
||||||
|
|
||||||
|
// const cart = createCartRes.data.cart
|
||||||
|
|
||||||
|
// await api.post(`/store/carts/${cart.id}`, {
|
||||||
|
// customer_id: customerRes.data.customer.id,
|
||||||
|
// })
|
||||||
|
|
||||||
|
// await api.post(`/store/carts/${cart.id}/payment-sessions`)
|
||||||
|
|
||||||
|
// const createdOrder = await api.post(
|
||||||
|
// `/store/carts/${cart.id}/complete-cart`
|
||||||
|
// )
|
||||||
|
|
||||||
|
// expect(createdOrder.data.type).toEqual("order")
|
||||||
|
// expect(createdOrder.status).toEqual(200)
|
||||||
|
// expect(createdOrder.data.data).toEqual(
|
||||||
|
// expect.objectContaining({
|
||||||
|
// sales_channel_id: "sales-channel",
|
||||||
|
// })
|
||||||
|
// )
|
||||||
|
// })
|
||||||
|
|
||||||
|
// it("SC from params defines where product is assigned (passed SC still has to be in the scope of PK from the header)", async () => {
|
||||||
|
// const api = useApi()
|
||||||
|
|
||||||
|
// await api.post(
|
||||||
|
// `/admin/api-keys/${pubKeyId}/sales-channels/batch`,
|
||||||
|
// {
|
||||||
|
// sales_channel_ids: [
|
||||||
|
// { id: "sales-channel" },
|
||||||
|
// { id: "sales-channel2" },
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
|
// adminHeaders
|
||||||
|
// )
|
||||||
|
|
||||||
|
// const customerRes = await api.post("/store/customers", customerData, {
|
||||||
|
// withCredentials: true,
|
||||||
|
// })
|
||||||
|
|
||||||
|
// const createCartRes = await api.post(
|
||||||
|
// "/store/carts",
|
||||||
|
// {
|
||||||
|
// sales_channel_id: "sales-channel2",
|
||||||
|
// region_id: "test-region",
|
||||||
|
// items: [
|
||||||
|
// {
|
||||||
|
// variant_id: product.variants[0].id,
|
||||||
|
// quantity: 1,
|
||||||
|
// },
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// headers: {
|
||||||
|
// "x-medusa-access-token": "test_token",
|
||||||
|
// "x-publishable-api-key": pubKeyId,
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
// )
|
||||||
|
|
||||||
|
// const cart = createCartRes.data.cart
|
||||||
|
|
||||||
|
// await api.post(`/store/carts/${cart.id}`, {
|
||||||
|
// customer_id: customerRes.data.customer.id,
|
||||||
|
// })
|
||||||
|
|
||||||
|
// await api.post(`/store/carts/${cart.id}/payment-sessions`)
|
||||||
|
|
||||||
|
// const createdOrder = await api.post(
|
||||||
|
// `/store/carts/${cart.id}/complete-cart`
|
||||||
|
// )
|
||||||
|
|
||||||
|
// expect(createdOrder.data.type).toEqual("order")
|
||||||
|
// expect(createdOrder.status).toEqual(200)
|
||||||
|
// expect(createdOrder.data.data).toEqual(
|
||||||
|
// expect.objectContaining({
|
||||||
|
// sales_channel_id: "sales-channel2",
|
||||||
|
// })
|
||||||
|
// )
|
||||||
|
// })
|
||||||
|
|
||||||
|
// it("should throw because SC id in the body is not in the scope of PK from the header", async () => {
|
||||||
|
// const api = useApi()
|
||||||
|
|
||||||
|
// await api.post(
|
||||||
|
// `/admin/api-keys/${pubKeyId}/sales-channels/batch`,
|
||||||
|
// {
|
||||||
|
// sales_channel_ids: [{ id: "sales-channel" }],
|
||||||
|
// },
|
||||||
|
// adminHeaders
|
||||||
|
// )
|
||||||
|
|
||||||
|
// try {
|
||||||
|
// await api.post(
|
||||||
|
// "/store/carts",
|
||||||
|
// {
|
||||||
|
// sales_channel_id: "sales-channel2", // SC not in the PK scope
|
||||||
|
// region_id: "test-region",
|
||||||
|
// items: [
|
||||||
|
// {
|
||||||
|
// variant_id: product.variants[0].id,
|
||||||
|
// quantity: 1,
|
||||||
|
// },
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// headers: {
|
||||||
|
// "x-medusa-access-token": "test_token",
|
||||||
|
// "x-publishable-api-key": pubKeyId,
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
// )
|
||||||
|
// } catch (error) {
|
||||||
|
// expect(error.response.status).toEqual(400)
|
||||||
|
// expect(error.response.data.errors[0]).toEqual(
|
||||||
|
// `Provided sales channel id param: sales-channel2 is not associated with the Publishable API Key passed in the header of the request.`
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
// })
|
||||||
@@ -3,48 +3,10 @@ import {
|
|||||||
adminHeaders,
|
adminHeaders,
|
||||||
createAdminUser,
|
createAdminUser,
|
||||||
} from "../../../../helpers/create-admin-user"
|
} from "../../../../helpers/create-admin-user"
|
||||||
|
import { getProductFixture } from "../../../../helpers/fixtures"
|
||||||
|
|
||||||
jest.setTimeout(50000)
|
jest.setTimeout(50000)
|
||||||
|
|
||||||
const getProductFixture = (overrides) => ({
|
|
||||||
title: "Test fixture",
|
|
||||||
description: "test-product-description",
|
|
||||||
status: "draft",
|
|
||||||
// BREAKING: Images input changed from string[] to {url: string}[]
|
|
||||||
images: [{ url: "test-image.png" }, { url: "test-image-2.png" }],
|
|
||||||
tags: [{ value: "123" }, { value: "456" }],
|
|
||||||
// BREAKING: Options input changed from {title: string}[] to {title: string, values: string[]}[]
|
|
||||||
options: [
|
|
||||||
{ title: "size", values: ["large", "small"] },
|
|
||||||
{ title: "color", values: ["green"] },
|
|
||||||
],
|
|
||||||
variants: [
|
|
||||||
{
|
|
||||||
title: "Test variant",
|
|
||||||
prices: [
|
|
||||||
{
|
|
||||||
currency_code: "usd",
|
|
||||||
amount: 100,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
currency_code: "eur",
|
|
||||||
amount: 45,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
currency_code: "dkk",
|
|
||||||
amount: 30,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
// BREAKING: Options input changed from {value: string}[] to {[optionTitle]: optionValue} map
|
|
||||||
options: {
|
|
||||||
size: "large",
|
|
||||||
color: "green",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
...(overrides ?? {}),
|
|
||||||
})
|
|
||||||
|
|
||||||
medusaIntegrationTestRunner({
|
medusaIntegrationTestRunner({
|
||||||
testSuite: ({ dbConnection, getContainer, api }) => {
|
testSuite: ({ dbConnection, getContainer, api }) => {
|
||||||
let baseProduct
|
let baseProduct
|
||||||
@@ -56,7 +18,6 @@ medusaIntegrationTestRunner({
|
|||||||
let publishedCollection
|
let publishedCollection
|
||||||
|
|
||||||
let baseType
|
let baseType
|
||||||
let baseRegion
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await createAdminUser(dbConnection, adminHeaders, getContainer())
|
await createAdminUser(dbConnection, adminHeaders, getContainer())
|
||||||
@@ -85,18 +46,6 @@ medusaIntegrationTestRunner({
|
|||||||
)
|
)
|
||||||
).data.product_type
|
).data.product_type
|
||||||
|
|
||||||
// BREAKING: Creating a region no longer takes tax_rate, payment_providers, fulfillment_providers, countriesr
|
|
||||||
baseRegion = (
|
|
||||||
await api.post(
|
|
||||||
"/admin/regions",
|
|
||||||
{
|
|
||||||
name: "Test region",
|
|
||||||
currency_code: "USD",
|
|
||||||
},
|
|
||||||
adminHeaders
|
|
||||||
)
|
|
||||||
).data.region
|
|
||||||
|
|
||||||
baseProduct = (
|
baseProduct = (
|
||||||
await api.post(
|
await api.post(
|
||||||
"/admin/products",
|
"/admin/products",
|
||||||
@@ -1947,527 +1896,6 @@ medusaIntegrationTestRunner({
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("GET /admin/products/:id/variants", () => {
|
|
||||||
it("should return the variants related to the requested product", async () => {
|
|
||||||
const res = await api
|
|
||||||
.get(`/admin/products/${baseProduct.id}/variants`, adminHeaders)
|
|
||||||
.catch((err) => {
|
|
||||||
console.log(err)
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(res.status).toEqual(200)
|
|
||||||
expect(res.data.variants.length).toBe(1)
|
|
||||||
expect(res.data.variants).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
id: baseProduct.variants[0].id,
|
|
||||||
product_id: baseProduct.id,
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should allow searching of variants", async () => {
|
|
||||||
const newProduct = (
|
|
||||||
await api.post(
|
|
||||||
"/admin/products",
|
|
||||||
getProductFixture({
|
|
||||||
variants: [
|
|
||||||
{ title: "First variant", prices: [] },
|
|
||||||
{ title: "Second variant", prices: [] },
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
adminHeaders
|
|
||||||
)
|
|
||||||
).data.product
|
|
||||||
|
|
||||||
const res = await api
|
|
||||||
.get(
|
|
||||||
`/admin/products/${newProduct.id}/variants?q=first`,
|
|
||||||
adminHeaders
|
|
||||||
)
|
|
||||||
.catch((err) => {
|
|
||||||
console.log(err)
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(res.status).toEqual(200)
|
|
||||||
expect(res.data.variants).toHaveLength(1)
|
|
||||||
expect(res.data.variants).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
title: "First variant",
|
|
||||||
product_id: newProduct.id,
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("updates a variant's default prices (ignores prices associated with a Price List)", () => {
|
|
||||||
it("successfully updates a variant's default prices by changing an existing price (currency_code)", async () => {
|
|
||||||
const data = {
|
|
||||||
prices: [
|
|
||||||
{
|
|
||||||
currency_code: "usd",
|
|
||||||
amount: 1500,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await api.post(
|
|
||||||
`/admin/products/${baseProduct.id}/variants/${baseProduct.variants[0].id}`,
|
|
||||||
data,
|
|
||||||
adminHeaders
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(
|
|
||||||
baseProduct.variants[0].prices.find(
|
|
||||||
(p) => p.currency_code === "usd"
|
|
||||||
).amount
|
|
||||||
).toEqual(100)
|
|
||||||
expect(response.status).toEqual(200)
|
|
||||||
expect(response.data).toEqual({
|
|
||||||
product: expect.objectContaining({
|
|
||||||
id: baseProduct.id,
|
|
||||||
variants: expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
id: baseProduct.variants[0].id,
|
|
||||||
prices: expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
amount: 1500,
|
|
||||||
currency_code: "usd",
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// TODO: Do we want to add support for region prices through the product APIs?
|
|
||||||
it.skip("successfully updates a variant's price by changing an existing price (given a region_id)", async () => {
|
|
||||||
const data = {
|
|
||||||
prices: [
|
|
||||||
{
|
|
||||||
region_id: "test-region",
|
|
||||||
amount: 1500,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await api
|
|
||||||
.post(
|
|
||||||
"/admin/products/test-product1/variants/test-variant_3",
|
|
||||||
data,
|
|
||||||
adminHeaders
|
|
||||||
)
|
|
||||||
.catch((err) => {
|
|
||||||
console.log(err)
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(response.status).toEqual(200)
|
|
||||||
|
|
||||||
expect(response.data.product).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
variants: expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
id: "test-variant_3",
|
|
||||||
prices: expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
amount: 1500,
|
|
||||||
currency_code: "usd",
|
|
||||||
region_id: "test-region",
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("successfully updates a variant's prices by adding a new price", async () => {
|
|
||||||
const usdPrice = baseProduct.variants[0].prices.find(
|
|
||||||
(p) => p.currency_code === "usd"
|
|
||||||
)
|
|
||||||
const data = {
|
|
||||||
title: "Test variant prices",
|
|
||||||
prices: [
|
|
||||||
{
|
|
||||||
id: usdPrice.id,
|
|
||||||
currency_code: "usd",
|
|
||||||
amount: 100,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
currency_code: "eur",
|
|
||||||
amount: 4500,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await api.post(
|
|
||||||
`/admin/products/${baseProduct.id}/variants/${baseProduct.variants[0].id}`,
|
|
||||||
data,
|
|
||||||
adminHeaders
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(response.status).toEqual(200)
|
|
||||||
|
|
||||||
expect(response.data).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
product: expect.objectContaining({
|
|
||||||
id: baseProduct.id,
|
|
||||||
variants: expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
id: baseProduct.variants[0].id,
|
|
||||||
prices: expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
amount: 100,
|
|
||||||
currency_code: "usd",
|
|
||||||
id: usdPrice.id,
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
|
||||||
amount: 4500,
|
|
||||||
currency_code: "eur",
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("successfully updates a variant's prices by deleting a price and adding another price", async () => {
|
|
||||||
const data = {
|
|
||||||
prices: [
|
|
||||||
{
|
|
||||||
currency_code: "dkk",
|
|
||||||
amount: 8000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
currency_code: "eur",
|
|
||||||
amount: 900,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await api
|
|
||||||
.post(
|
|
||||||
`/admin/products/${baseProduct.id}/variants/${baseProduct.variants[0].id}`,
|
|
||||||
data,
|
|
||||||
adminHeaders
|
|
||||||
)
|
|
||||||
.catch((err) => {
|
|
||||||
console.log(err)
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(response.status).toEqual(200)
|
|
||||||
|
|
||||||
const variant = response.data.product.variants[0]
|
|
||||||
expect(variant.prices.length).toEqual(2)
|
|
||||||
|
|
||||||
expect(variant.prices).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
amount: 8000,
|
|
||||||
currency_code: "dkk",
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
|
||||||
amount: 900,
|
|
||||||
currency_code: "eur",
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
// TODO: Similarly we need to decide how to handle regions
|
|
||||||
it.skip("successfully updates a variant's prices by updating an existing price (using region_id) and adding another price", async () => {
|
|
||||||
const data = {
|
|
||||||
prices: [
|
|
||||||
{
|
|
||||||
region_id: "test-region",
|
|
||||||
amount: 8000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
currency_code: "eur",
|
|
||||||
amount: 900,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
const variantId = "test-variant_3"
|
|
||||||
const response = await api
|
|
||||||
.post(
|
|
||||||
`/admin/products/test-product1/variants/${variantId}`,
|
|
||||||
data,
|
|
||||||
adminHeaders
|
|
||||||
)
|
|
||||||
.catch((err) => {
|
|
||||||
console.log(err)
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(response.status).toEqual(200)
|
|
||||||
|
|
||||||
const variant = response.data.product.variants.find(
|
|
||||||
(v) => v.id === variantId
|
|
||||||
)
|
|
||||||
expect(variant.prices.length).toEqual(data.prices.length)
|
|
||||||
|
|
||||||
expect(variant.prices).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
amount: 8000,
|
|
||||||
currency_code: "usd",
|
|
||||||
region_id: "test-region",
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
|
||||||
amount: 900,
|
|
||||||
currency_code: "eur",
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
// TODO: Similarly we need to decide how to handle regions
|
|
||||||
it.skip("successfully deletes a region price", async () => {
|
|
||||||
const createRegionPricePayload = {
|
|
||||||
prices: [
|
|
||||||
{
|
|
||||||
currency_code: "usd",
|
|
||||||
amount: 1000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
region_id: "test-region",
|
|
||||||
amount: 8000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
currency_code: "eur",
|
|
||||||
amount: 900,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
const variantId = "test-variant_3"
|
|
||||||
|
|
||||||
const createRegionPriceResponse = await api.post(
|
|
||||||
"/admin/products/test-product1/variants/test-variant_3",
|
|
||||||
createRegionPricePayload,
|
|
||||||
adminHeaders
|
|
||||||
)
|
|
||||||
|
|
||||||
const initialPriceArray =
|
|
||||||
createRegionPriceResponse.data.product.variants.find(
|
|
||||||
(v) => v.id === variantId
|
|
||||||
).prices
|
|
||||||
|
|
||||||
expect(createRegionPriceResponse.status).toEqual(200)
|
|
||||||
expect(initialPriceArray).toHaveLength(3)
|
|
||||||
expect(initialPriceArray).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
amount: 1000,
|
|
||||||
currency_code: "usd",
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
|
||||||
amount: 8000,
|
|
||||||
currency_code: "usd",
|
|
||||||
region_id: "test-region",
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
|
||||||
amount: 900,
|
|
||||||
currency_code: "eur",
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
)
|
|
||||||
|
|
||||||
const deleteRegionPricePayload = {
|
|
||||||
prices: [
|
|
||||||
{
|
|
||||||
currency_code: "usd",
|
|
||||||
amount: 1000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
currency_code: "eur",
|
|
||||||
amount: 900,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteRegionPriceResponse = await api.post(
|
|
||||||
"/admin/products/test-product1/variants/test-variant_3",
|
|
||||||
deleteRegionPricePayload,
|
|
||||||
adminHeaders
|
|
||||||
)
|
|
||||||
|
|
||||||
const finalPriceArray =
|
|
||||||
deleteRegionPriceResponse.data.product.variants.find(
|
|
||||||
(v) => v.id === variantId
|
|
||||||
).prices
|
|
||||||
|
|
||||||
expect(deleteRegionPriceResponse.status).toEqual(200)
|
|
||||||
expect(finalPriceArray).toHaveLength(2)
|
|
||||||
expect(finalPriceArray).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
amount: 1000,
|
|
||||||
currency_code: "usd",
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
|
||||||
amount: 900,
|
|
||||||
currency_code: "eur",
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
// TODO: Similarly we need to decide how to handle regions
|
|
||||||
it.skip("successfully updates a variants prices by deleting both a currency and region price", async () => {
|
|
||||||
// await Promise.all(
|
|
||||||
// ["reg_1", "reg_2", "reg_3"].map(async (regionId) => {
|
|
||||||
// return await simpleRegionFactory(dbConnection, {
|
|
||||||
// id: regionId,
|
|
||||||
// currency_code: regionId === "reg_1" ? "eur" : "usd",
|
|
||||||
// })
|
|
||||||
// })
|
|
||||||
// )
|
|
||||||
|
|
||||||
const createPrices = {
|
|
||||||
prices: [
|
|
||||||
{
|
|
||||||
region_id: "reg_1",
|
|
||||||
amount: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
region_id: "reg_2",
|
|
||||||
amount: 2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
currency_code: "usd",
|
|
||||||
amount: 3,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
region_id: "reg_3",
|
|
||||||
amount: 4,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
currency_code: "eur",
|
|
||||||
amount: 5,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
const variantId = "test-variant_3"
|
|
||||||
|
|
||||||
await api
|
|
||||||
.post(
|
|
||||||
`/admin/products/test-product1/variants/${variantId}`,
|
|
||||||
createPrices,
|
|
||||||
adminHeaders
|
|
||||||
)
|
|
||||||
.catch((err) => {
|
|
||||||
console.log(err)
|
|
||||||
})
|
|
||||||
|
|
||||||
const updatePrices = {
|
|
||||||
prices: [
|
|
||||||
{
|
|
||||||
region_id: "reg_1",
|
|
||||||
amount: 100,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
region_id: "reg_2",
|
|
||||||
amount: 200,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
currency_code: "usd",
|
|
||||||
amount: 300,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await api.post(
|
|
||||||
`/admin/products/test-product1/variants/${variantId}`,
|
|
||||||
updatePrices,
|
|
||||||
adminHeaders
|
|
||||||
)
|
|
||||||
|
|
||||||
const finalPriceArray = response.data.product.variants.find(
|
|
||||||
(v) => v.id === variantId
|
|
||||||
).prices
|
|
||||||
|
|
||||||
expect(response.status).toEqual(200)
|
|
||||||
expect(finalPriceArray).toHaveLength(3)
|
|
||||||
expect(finalPriceArray).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
amount: 100,
|
|
||||||
region_id: "reg_1",
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
|
||||||
amount: 200,
|
|
||||||
region_id: "reg_2",
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
|
||||||
amount: 300,
|
|
||||||
currency_code: "usd",
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("variant creation", () => {
|
|
||||||
it("create a product variant with prices (regional and currency)", async () => {
|
|
||||||
const payload = {
|
|
||||||
title: "Created variant",
|
|
||||||
sku: "new-sku",
|
|
||||||
ean: "new-ean",
|
|
||||||
upc: "new-upc",
|
|
||||||
barcode: "new-barcode",
|
|
||||||
prices: [
|
|
||||||
{
|
|
||||||
currency_code: "usd",
|
|
||||||
amount: 100,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
currency_code: "eur",
|
|
||||||
amount: 200,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await api
|
|
||||||
.post(
|
|
||||||
`/admin/products/${baseProduct.id}/variants`,
|
|
||||||
payload,
|
|
||||||
adminHeaders
|
|
||||||
)
|
|
||||||
.catch((err) => console.log(err))
|
|
||||||
|
|
||||||
const insertedVariant = res.data.product.variants.find(
|
|
||||||
(v) => v.sku === "new-sku"
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(res.status).toEqual(200)
|
|
||||||
|
|
||||||
expect(insertedVariant.prices).toHaveLength(2)
|
|
||||||
expect(insertedVariant.prices).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
currency_code: "usd",
|
|
||||||
amount: 100,
|
|
||||||
variant_id: insertedVariant.id,
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
|
||||||
currency_code: "eur",
|
|
||||||
amount: 200,
|
|
||||||
variant_id: insertedVariant.id,
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("testing for soft-deletion + uniqueness on handles, collection and variant properties", () => {
|
describe("testing for soft-deletion + uniqueness on handles, collection and variant properties", () => {
|
||||||
it("successfully deletes a product", async () => {
|
it("successfully deletes a product", async () => {
|
||||||
const response = await api
|
const response = await api
|
||||||
@@ -3029,314 +2457,6 @@ medusaIntegrationTestRunner({
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("POST /admin/products/:id/variants/:variant_id/inventory-items", () => {
|
|
||||||
it("should throw an error when required attributes are not passed", async () => {
|
|
||||||
const { response } = await api
|
|
||||||
.post(
|
|
||||||
`/admin/products/${baseProduct.id}/variants/${baseProduct.variants[0].id}/inventory-items`,
|
|
||||||
{},
|
|
||||||
adminHeaders
|
|
||||||
)
|
|
||||||
.catch((e) => e)
|
|
||||||
|
|
||||||
expect(response.status).toEqual(400)
|
|
||||||
expect(response.data).toEqual({
|
|
||||||
type: "invalid_data",
|
|
||||||
message:
|
|
||||||
"Invalid request: Field 'required_quantity' is required; Field 'inventory_item_id' is required",
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it("successfully adds inventory item to a variant", async () => {
|
|
||||||
const inventoryItem = (
|
|
||||||
await api.post(
|
|
||||||
`/admin/inventory-items`,
|
|
||||||
{ sku: "12345" },
|
|
||||||
adminHeaders
|
|
||||||
)
|
|
||||||
).data.inventory_item
|
|
||||||
|
|
||||||
const res = await api.post(
|
|
||||||
`/admin/products/${baseProduct.id}/variants/${baseProduct.variants[0].id}/inventory-items?fields=inventory_items.inventory.*,inventory_items.*`,
|
|
||||||
{
|
|
||||||
inventory_item_id: inventoryItem.id,
|
|
||||||
required_quantity: 5,
|
|
||||||
},
|
|
||||||
adminHeaders
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(res.status).toEqual(200)
|
|
||||||
expect(res.data.variant.inventory_items).toHaveLength(2)
|
|
||||||
expect(res.data.variant.inventory_items).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
required_quantity: 5,
|
|
||||||
inventory_item_id: inventoryItem.id,
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("POST /admin/products/:id/variants/:variant_id/inventory-items/:inventory_id", () => {
|
|
||||||
let inventoryItem
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
inventoryItem = (
|
|
||||||
await api.post(
|
|
||||||
`/admin/inventory-items`,
|
|
||||||
{ sku: "12345" },
|
|
||||||
adminHeaders
|
|
||||||
)
|
|
||||||
).data.inventory_item
|
|
||||||
|
|
||||||
await api.post(
|
|
||||||
`/admin/products/${baseProduct.id}/variants/${baseProduct.variants[0].id}/inventory-items`,
|
|
||||||
{
|
|
||||||
inventory_item_id: inventoryItem.id,
|
|
||||||
required_quantity: 5,
|
|
||||||
},
|
|
||||||
adminHeaders
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should throw an error when required attributes are not passed", async () => {
|
|
||||||
const { response } = await api
|
|
||||||
.post(
|
|
||||||
`/admin/products/${baseProduct.id}/variants/${baseProduct.variants[0].id}/inventory-items/${inventoryItem.id}`,
|
|
||||||
{},
|
|
||||||
adminHeaders
|
|
||||||
)
|
|
||||||
.catch((e) => e)
|
|
||||||
|
|
||||||
expect(response.status).toEqual(400)
|
|
||||||
expect(response.data).toEqual({
|
|
||||||
type: "invalid_data",
|
|
||||||
message: "Invalid request: Field 'required_quantity' is required",
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it("successfully updates an inventory item link to a variant", async () => {
|
|
||||||
const res = await api.post(
|
|
||||||
`/admin/products/${baseProduct.id}/variants/${baseProduct.variants[0].id}/inventory-items/${inventoryItem.id}?fields=inventory_items.inventory.*,inventory_items.*`,
|
|
||||||
{ required_quantity: 10 },
|
|
||||||
adminHeaders
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(res.status).toEqual(200)
|
|
||||||
expect(res.data.variant.inventory_items).toHaveLength(2)
|
|
||||||
expect(res.data.variant.inventory_items).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
required_quantity: 10,
|
|
||||||
inventory_item_id: inventoryItem.id,
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("DELETE /admin/products/:id/variants/:variant_id/inventory-items/:inventory_id", () => {
|
|
||||||
let inventoryItem
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
inventoryItem = (
|
|
||||||
await api.post(
|
|
||||||
`/admin/inventory-items`,
|
|
||||||
{ sku: "12345" },
|
|
||||||
adminHeaders
|
|
||||||
)
|
|
||||||
).data.inventory_item
|
|
||||||
|
|
||||||
await api.post(
|
|
||||||
`/admin/products/${baseProduct.id}/variants/${baseProduct.variants[0].id}/inventory-items`,
|
|
||||||
{
|
|
||||||
inventory_item_id: inventoryItem.id,
|
|
||||||
required_quantity: 5,
|
|
||||||
},
|
|
||||||
adminHeaders
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("successfully deletes an inventory item link from a variant", async () => {
|
|
||||||
await api.post(
|
|
||||||
`/admin/products/${baseProduct.id}/variants/${baseProduct.variants[0].id}/inventory-items`,
|
|
||||||
{ inventory_item_id: inventoryItem.id, required_quantity: 5 },
|
|
||||||
adminHeaders
|
|
||||||
)
|
|
||||||
|
|
||||||
const res = await api.delete(
|
|
||||||
`/admin/products/${baseProduct.id}/variants/${baseProduct.variants[0].id}/inventory-items/${inventoryItem.id}?fields=inventory_items.inventory.*,inventory_items.*`,
|
|
||||||
adminHeaders
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(res.status).toEqual(200)
|
|
||||||
expect(res.data.parent.inventory_items).toHaveLength(1)
|
|
||||||
expect(res.data.parent.inventory_items[0].id).not.toBe(
|
|
||||||
inventoryItem.id
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("POST /admin/products/:id/variants/:variant_id/inventory-items/batch", () => {
|
|
||||||
let inventoryItemToUpdate
|
|
||||||
let inventoryItemToDelete
|
|
||||||
let inventoryItemToCreate
|
|
||||||
let inventoryProduct
|
|
||||||
let inventoryVariant1
|
|
||||||
let inventoryVariant2
|
|
||||||
let inventoryVariant3
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
inventoryProduct = (
|
|
||||||
await api.post(
|
|
||||||
"/admin/products",
|
|
||||||
{
|
|
||||||
title: "product 1",
|
|
||||||
variants: [
|
|
||||||
{
|
|
||||||
title: "variant 1",
|
|
||||||
prices: [{ currency_code: "usd", amount: 100 }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "variant 2",
|
|
||||||
prices: [{ currency_code: "usd", amount: 100 }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "variant 3",
|
|
||||||
prices: [{ currency_code: "usd", amount: 100 }],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
adminHeaders
|
|
||||||
)
|
|
||||||
).data.product
|
|
||||||
|
|
||||||
inventoryVariant1 = inventoryProduct.variants[0]
|
|
||||||
inventoryVariant2 = inventoryProduct.variants[1]
|
|
||||||
inventoryVariant3 = inventoryProduct.variants[2]
|
|
||||||
|
|
||||||
inventoryItemToCreate = (
|
|
||||||
await api.post(
|
|
||||||
`/admin/inventory-items`,
|
|
||||||
{ sku: "to-create" },
|
|
||||||
adminHeaders
|
|
||||||
)
|
|
||||||
).data.inventory_item
|
|
||||||
|
|
||||||
inventoryItemToUpdate = (
|
|
||||||
await api.post(
|
|
||||||
`/admin/inventory-items`,
|
|
||||||
{ sku: "to-update" },
|
|
||||||
adminHeaders
|
|
||||||
)
|
|
||||||
).data.inventory_item
|
|
||||||
|
|
||||||
inventoryItemToDelete = (
|
|
||||||
await api.post(
|
|
||||||
`/admin/inventory-items`,
|
|
||||||
{ sku: "to-delete" },
|
|
||||||
adminHeaders
|
|
||||||
)
|
|
||||||
).data.inventory_item
|
|
||||||
|
|
||||||
await api.post(
|
|
||||||
`/admin/products/${baseProduct.id}/variants/${inventoryVariant1.id}/inventory-items`,
|
|
||||||
{
|
|
||||||
inventory_item_id: inventoryItemToUpdate.id,
|
|
||||||
required_quantity: 5,
|
|
||||||
},
|
|
||||||
adminHeaders
|
|
||||||
)
|
|
||||||
|
|
||||||
await api.post(
|
|
||||||
`/admin/products/${baseProduct.id}/variants/${inventoryVariant2.id}/inventory-items`,
|
|
||||||
{
|
|
||||||
inventory_item_id: inventoryItemToDelete.id,
|
|
||||||
required_quantity: 10,
|
|
||||||
},
|
|
||||||
adminHeaders
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("successfully creates, updates and deletes an inventory item link from a variant", async () => {
|
|
||||||
const res = await api.post(
|
|
||||||
`/admin/products/${baseProduct.id}/variants/inventory-items/batch`,
|
|
||||||
{
|
|
||||||
create: [
|
|
||||||
{
|
|
||||||
required_quantity: 15,
|
|
||||||
inventory_item_id: inventoryItemToCreate.id,
|
|
||||||
variant_id: inventoryVariant3.id,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
update: [
|
|
||||||
{
|
|
||||||
required_quantity: 25,
|
|
||||||
inventory_item_id: inventoryItemToUpdate.id,
|
|
||||||
variant_id: inventoryVariant1.id,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
delete: [
|
|
||||||
{
|
|
||||||
inventory_item_id: inventoryItemToDelete.id,
|
|
||||||
variant_id: inventoryVariant2.id,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
adminHeaders
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(res.status).toEqual(200)
|
|
||||||
|
|
||||||
const createdLinkVariant = (
|
|
||||||
await api.get(
|
|
||||||
`/admin/products/${baseProduct.id}/variants/${inventoryVariant3.id}?fields=inventory_items.inventory.*,inventory_items.*`,
|
|
||||||
adminHeaders
|
|
||||||
)
|
|
||||||
).data.variant
|
|
||||||
|
|
||||||
expect(createdLinkVariant.inventory_items).toHaveLength(2)
|
|
||||||
expect(createdLinkVariant.inventory_items).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
required_quantity: 15,
|
|
||||||
inventory_item_id: inventoryItemToCreate.id,
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
)
|
|
||||||
|
|
||||||
const updatedLinkVariant = (
|
|
||||||
await api.get(
|
|
||||||
`/admin/products/${baseProduct.id}/variants/${inventoryVariant1.id}?fields=inventory_items.inventory.*,inventory_items.*`,
|
|
||||||
adminHeaders
|
|
||||||
)
|
|
||||||
).data.variant
|
|
||||||
|
|
||||||
expect(updatedLinkVariant.inventory_items).toHaveLength(2)
|
|
||||||
expect(updatedLinkVariant.inventory_items).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
required_quantity: 25,
|
|
||||||
inventory_item_id: inventoryItemToUpdate.id,
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
)
|
|
||||||
|
|
||||||
const deletedLinkVariant = (
|
|
||||||
await api.get(
|
|
||||||
`/admin/products/${baseProduct.id}/variants/${inventoryVariant2.id}?fields=inventory_items.inventory.*,inventory_items.*`,
|
|
||||||
adminHeaders
|
|
||||||
)
|
|
||||||
).data.variant
|
|
||||||
|
|
||||||
expect(deletedLinkVariant.inventory_items).toHaveLength(1)
|
|
||||||
expect(deletedLinkVariant.inventory_items[0].id).not.toEqual(
|
|
||||||
inventoryItemToDelete.id
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -0,0 +1,943 @@
|
|||||||
|
import { medusaIntegrationTestRunner } from "medusa-test-utils"
|
||||||
|
import {
|
||||||
|
adminHeaders,
|
||||||
|
createAdminUser,
|
||||||
|
} from "../../../../helpers/create-admin-user"
|
||||||
|
import { getProductFixture } from "../../../../helpers/fixtures"
|
||||||
|
|
||||||
|
jest.setTimeout(50000)
|
||||||
|
|
||||||
|
medusaIntegrationTestRunner({
|
||||||
|
testSuite: ({ dbConnection, getContainer, api }) => {
|
||||||
|
let baseProduct
|
||||||
|
let baseRegion
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await createAdminUser(dbConnection, adminHeaders, getContainer())
|
||||||
|
// BREAKING: Creating a region no longer takes tax_rate, payment_providers, fulfillment_providers, countriesr
|
||||||
|
baseRegion = (
|
||||||
|
await api.post(
|
||||||
|
"/admin/regions",
|
||||||
|
{
|
||||||
|
name: "Test region",
|
||||||
|
currency_code: "USD",
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.region
|
||||||
|
|
||||||
|
baseProduct = (
|
||||||
|
await api.post(
|
||||||
|
"/admin/products",
|
||||||
|
getProductFixture({
|
||||||
|
title: "Base product",
|
||||||
|
}),
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.product
|
||||||
|
})
|
||||||
|
|
||||||
|
// BREAKING: We no longer have `/admin/variants` endpoint. Instead, variant is scoped by product ID, `/admin/products/:id/variants`
|
||||||
|
describe("GET /admin/products/:id/variants", () => {
|
||||||
|
it("should return the variants related to the requested product", async () => {
|
||||||
|
const res = await api
|
||||||
|
.get(`/admin/products/${baseProduct.id}/variants`, adminHeaders)
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(res.status).toEqual(200)
|
||||||
|
expect(res.data.variants.length).toBe(1)
|
||||||
|
expect(res.data.variants).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: baseProduct.variants[0].id,
|
||||||
|
product_id: baseProduct.id,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should allow searching of variants", async () => {
|
||||||
|
const newProduct = (
|
||||||
|
await api.post(
|
||||||
|
"/admin/products",
|
||||||
|
getProductFixture({
|
||||||
|
variants: [
|
||||||
|
{ title: "First variant", prices: [] },
|
||||||
|
{ title: "Second variant", prices: [] },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.product
|
||||||
|
|
||||||
|
const res = await api
|
||||||
|
.get(
|
||||||
|
`/admin/products/${newProduct.id}/variants?q=first`,
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(res.status).toEqual(200)
|
||||||
|
expect(res.data.variants).toHaveLength(1)
|
||||||
|
expect(res.data.variants).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
title: "First variant",
|
||||||
|
product_id: newProduct.id,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("updates a variant's default prices (ignores prices associated with a Price List)", () => {
|
||||||
|
it("successfully updates a variant's default prices by changing an existing price (currency_code)", async () => {
|
||||||
|
const data = {
|
||||||
|
prices: [
|
||||||
|
{
|
||||||
|
currency_code: "usd",
|
||||||
|
amount: 1500,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await api.post(
|
||||||
|
`/admin/products/${baseProduct.id}/variants/${baseProduct.variants[0].id}`,
|
||||||
|
data,
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(
|
||||||
|
baseProduct.variants[0].prices.find((p) => p.currency_code === "usd")
|
||||||
|
.amount
|
||||||
|
).toEqual(100)
|
||||||
|
expect(response.status).toEqual(200)
|
||||||
|
expect(response.data).toEqual({
|
||||||
|
product: expect.objectContaining({
|
||||||
|
id: baseProduct.id,
|
||||||
|
variants: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: baseProduct.variants[0].id,
|
||||||
|
prices: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
amount: 1500,
|
||||||
|
currency_code: "usd",
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO: Do we want to add support for region prices through the product APIs?
|
||||||
|
it.skip("successfully updates a variant's price by changing an existing price (given a region_id)", async () => {
|
||||||
|
const data = {
|
||||||
|
prices: [
|
||||||
|
{
|
||||||
|
region_id: "test-region",
|
||||||
|
amount: 1500,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await api
|
||||||
|
.post(
|
||||||
|
"/admin/products/test-product1/variants/test-variant_3",
|
||||||
|
data,
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(response.status).toEqual(200)
|
||||||
|
|
||||||
|
expect(response.data.product).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
variants: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: "test-variant_3",
|
||||||
|
prices: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
amount: 1500,
|
||||||
|
currency_code: "usd",
|
||||||
|
region_id: "test-region",
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("successfully updates a variant's prices by adding a new price", async () => {
|
||||||
|
const usdPrice = baseProduct.variants[0].prices.find(
|
||||||
|
(p) => p.currency_code === "usd"
|
||||||
|
)
|
||||||
|
const data = {
|
||||||
|
title: "Test variant prices",
|
||||||
|
prices: [
|
||||||
|
{
|
||||||
|
id: usdPrice.id,
|
||||||
|
currency_code: "usd",
|
||||||
|
amount: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
currency_code: "eur",
|
||||||
|
amount: 4500,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await api.post(
|
||||||
|
`/admin/products/${baseProduct.id}/variants/${baseProduct.variants[0].id}`,
|
||||||
|
data,
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.status).toEqual(200)
|
||||||
|
|
||||||
|
expect(response.data).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
product: expect.objectContaining({
|
||||||
|
id: baseProduct.id,
|
||||||
|
variants: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: baseProduct.variants[0].id,
|
||||||
|
prices: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
amount: 100,
|
||||||
|
currency_code: "usd",
|
||||||
|
id: usdPrice.id,
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
amount: 4500,
|
||||||
|
currency_code: "eur",
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("successfully updates a variant's prices by deleting a price and adding another price", async () => {
|
||||||
|
const data = {
|
||||||
|
prices: [
|
||||||
|
{
|
||||||
|
currency_code: "dkk",
|
||||||
|
amount: 8000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
currency_code: "eur",
|
||||||
|
amount: 900,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await api
|
||||||
|
.post(
|
||||||
|
`/admin/products/${baseProduct.id}/variants/${baseProduct.variants[0].id}`,
|
||||||
|
data,
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(response.status).toEqual(200)
|
||||||
|
|
||||||
|
const variant = response.data.product.variants[0]
|
||||||
|
expect(variant.prices.length).toEqual(2)
|
||||||
|
|
||||||
|
expect(variant.prices).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
amount: 8000,
|
||||||
|
currency_code: "dkk",
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
amount: 900,
|
||||||
|
currency_code: "eur",
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO: Similarly we need to decide how to handle regions
|
||||||
|
it.skip("successfully updates a variant's prices by updating an existing price (using region_id) and adding another price", async () => {
|
||||||
|
const data = {
|
||||||
|
prices: [
|
||||||
|
{
|
||||||
|
region_id: "test-region",
|
||||||
|
amount: 8000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
currency_code: "eur",
|
||||||
|
amount: 900,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const variantId = "test-variant_3"
|
||||||
|
const response = await api
|
||||||
|
.post(
|
||||||
|
`/admin/products/test-product1/variants/${variantId}`,
|
||||||
|
data,
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(response.status).toEqual(200)
|
||||||
|
|
||||||
|
const variant = response.data.product.variants.find(
|
||||||
|
(v) => v.id === variantId
|
||||||
|
)
|
||||||
|
expect(variant.prices.length).toEqual(data.prices.length)
|
||||||
|
|
||||||
|
expect(variant.prices).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
amount: 8000,
|
||||||
|
currency_code: "usd",
|
||||||
|
region_id: "test-region",
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
amount: 900,
|
||||||
|
currency_code: "eur",
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO: Similarly we need to decide how to handle regions
|
||||||
|
it.skip("successfully deletes a region price", async () => {
|
||||||
|
const createRegionPricePayload = {
|
||||||
|
prices: [
|
||||||
|
{
|
||||||
|
currency_code: "usd",
|
||||||
|
amount: 1000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
region_id: "test-region",
|
||||||
|
amount: 8000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
currency_code: "eur",
|
||||||
|
amount: 900,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const variantId = "test-variant_3"
|
||||||
|
|
||||||
|
const createRegionPriceResponse = await api.post(
|
||||||
|
"/admin/products/test-product1/variants/test-variant_3",
|
||||||
|
createRegionPricePayload,
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
const initialPriceArray =
|
||||||
|
createRegionPriceResponse.data.product.variants.find(
|
||||||
|
(v) => v.id === variantId
|
||||||
|
).prices
|
||||||
|
|
||||||
|
expect(createRegionPriceResponse.status).toEqual(200)
|
||||||
|
expect(initialPriceArray).toHaveLength(3)
|
||||||
|
expect(initialPriceArray).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
amount: 1000,
|
||||||
|
currency_code: "usd",
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
amount: 8000,
|
||||||
|
currency_code: "usd",
|
||||||
|
region_id: "test-region",
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
amount: 900,
|
||||||
|
currency_code: "eur",
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
|
||||||
|
const deleteRegionPricePayload = {
|
||||||
|
prices: [
|
||||||
|
{
|
||||||
|
currency_code: "usd",
|
||||||
|
amount: 1000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
currency_code: "eur",
|
||||||
|
amount: 900,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteRegionPriceResponse = await api.post(
|
||||||
|
"/admin/products/test-product1/variants/test-variant_3",
|
||||||
|
deleteRegionPricePayload,
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
const finalPriceArray =
|
||||||
|
deleteRegionPriceResponse.data.product.variants.find(
|
||||||
|
(v) => v.id === variantId
|
||||||
|
).prices
|
||||||
|
|
||||||
|
expect(deleteRegionPriceResponse.status).toEqual(200)
|
||||||
|
expect(finalPriceArray).toHaveLength(2)
|
||||||
|
expect(finalPriceArray).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
amount: 1000,
|
||||||
|
currency_code: "usd",
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
amount: 900,
|
||||||
|
currency_code: "eur",
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO: Similarly we need to decide how to handle regions
|
||||||
|
it.skip("successfully updates a variants prices by deleting both a currency and region price", async () => {
|
||||||
|
// await Promise.all(
|
||||||
|
// ["reg_1", "reg_2", "reg_3"].map(async (regionId) => {
|
||||||
|
// return await simpleRegionFactory(dbConnection, {
|
||||||
|
// id: regionId,
|
||||||
|
// currency_code: regionId === "reg_1" ? "eur" : "usd",
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
// )
|
||||||
|
|
||||||
|
const createPrices = {
|
||||||
|
prices: [
|
||||||
|
{
|
||||||
|
region_id: "reg_1",
|
||||||
|
amount: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
region_id: "reg_2",
|
||||||
|
amount: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
currency_code: "usd",
|
||||||
|
amount: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
region_id: "reg_3",
|
||||||
|
amount: 4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
currency_code: "eur",
|
||||||
|
amount: 5,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const variantId = "test-variant_3"
|
||||||
|
|
||||||
|
await api
|
||||||
|
.post(
|
||||||
|
`/admin/products/test-product1/variants/${variantId}`,
|
||||||
|
createPrices,
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err)
|
||||||
|
})
|
||||||
|
|
||||||
|
const updatePrices = {
|
||||||
|
prices: [
|
||||||
|
{
|
||||||
|
region_id: "reg_1",
|
||||||
|
amount: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
region_id: "reg_2",
|
||||||
|
amount: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
currency_code: "usd",
|
||||||
|
amount: 300,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await api.post(
|
||||||
|
`/admin/products/test-product1/variants/${variantId}`,
|
||||||
|
updatePrices,
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
const finalPriceArray = response.data.product.variants.find(
|
||||||
|
(v) => v.id === variantId
|
||||||
|
).prices
|
||||||
|
|
||||||
|
expect(response.status).toEqual(200)
|
||||||
|
expect(finalPriceArray).toHaveLength(3)
|
||||||
|
expect(finalPriceArray).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
amount: 100,
|
||||||
|
region_id: "reg_1",
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
amount: 200,
|
||||||
|
region_id: "reg_2",
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
amount: 300,
|
||||||
|
currency_code: "usd",
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO: Do we want to support price calculation on the admin endpoints? Enable this suite if we do, otherwise remove it
|
||||||
|
describe.skip("variant pricing calculations", () => {
|
||||||
|
it("selects prices based on the passed currency code", async () => {
|
||||||
|
const response = await api.get(
|
||||||
|
`/admin/products/${baseProduct.id}/variants/${baseProduct.variants[0].id}?fields=calculated_price¤cy_code=usd`,
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.data.variant).toEqual({
|
||||||
|
id: baseProduct.variants[0].id,
|
||||||
|
original_price: 100,
|
||||||
|
calculated_price: 80,
|
||||||
|
calculated_price_type: "sale",
|
||||||
|
original_price_incl_tax: null,
|
||||||
|
calculated_price_incl_tax: null,
|
||||||
|
original_tax: null,
|
||||||
|
calculated_tax: null,
|
||||||
|
options: expect.any(Array),
|
||||||
|
prices: expect.any(Array),
|
||||||
|
product: expect.any(Object),
|
||||||
|
created_at: expect.any(String),
|
||||||
|
updated_at: expect.any(String),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("selects prices based on the passed region id", async () => {
|
||||||
|
const response = await api.get(
|
||||||
|
`/admin/products/${baseProduct.id}/variants/${baseProduct.variants[0].id}?fields=calculated_price®ion_id=${baseRegion.id}`,
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.data.variant).toEqual({
|
||||||
|
id: "test-variant",
|
||||||
|
original_price: 100,
|
||||||
|
calculated_price: 80,
|
||||||
|
calculated_price_type: "sale",
|
||||||
|
original_price_incl_tax: 100,
|
||||||
|
calculated_price_incl_tax: 80,
|
||||||
|
original_tax: 0,
|
||||||
|
calculated_tax: 0,
|
||||||
|
options: expect.any(Array),
|
||||||
|
prices: expect.any(Array),
|
||||||
|
product: expect.any(Object),
|
||||||
|
created_at: expect.any(String),
|
||||||
|
updated_at: expect.any(String),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("selects prices based on the passed region id and customer id", async () => {
|
||||||
|
const response = await api.get(
|
||||||
|
`/admin/products/${baseProduct.id}/variants/${
|
||||||
|
baseProduct.variants[0].id
|
||||||
|
}?fields=calculated_price®ion_id=${
|
||||||
|
baseRegion.id
|
||||||
|
}&customer_id=${""}`,
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.data.variant).toEqual({
|
||||||
|
id: "test-variant",
|
||||||
|
original_price: 100,
|
||||||
|
calculated_price: 40,
|
||||||
|
calculated_price_type: "sale",
|
||||||
|
original_price_incl_tax: 100,
|
||||||
|
calculated_price_incl_tax: 40,
|
||||||
|
original_tax: 0,
|
||||||
|
calculated_tax: 0,
|
||||||
|
prices: expect.any(Array),
|
||||||
|
options: expect.any(Array),
|
||||||
|
product: expect.any(Object),
|
||||||
|
created_at: expect.any(String),
|
||||||
|
updated_at: expect.any(String),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("variant creation", () => {
|
||||||
|
it("create a product variant with prices (regional and currency)", async () => {
|
||||||
|
const payload = {
|
||||||
|
title: "Created variant",
|
||||||
|
sku: "new-sku",
|
||||||
|
ean: "new-ean",
|
||||||
|
upc: "new-upc",
|
||||||
|
barcode: "new-barcode",
|
||||||
|
prices: [
|
||||||
|
{
|
||||||
|
currency_code: "usd",
|
||||||
|
amount: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
currency_code: "eur",
|
||||||
|
amount: 200,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await api
|
||||||
|
.post(
|
||||||
|
`/admin/products/${baseProduct.id}/variants`,
|
||||||
|
payload,
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
.catch((err) => console.log(err))
|
||||||
|
|
||||||
|
const insertedVariant = res.data.product.variants.find(
|
||||||
|
(v) => v.sku === "new-sku"
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(res.status).toEqual(200)
|
||||||
|
|
||||||
|
expect(insertedVariant.prices).toHaveLength(2)
|
||||||
|
expect(insertedVariant.prices).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
currency_code: "usd",
|
||||||
|
amount: 100,
|
||||||
|
variant_id: insertedVariant.id,
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
currency_code: "eur",
|
||||||
|
amount: 200,
|
||||||
|
variant_id: insertedVariant.id,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("POST /admin/products/:id/variants/:variant_id/inventory-items", () => {
|
||||||
|
it("should throw an error when required attributes are not passed", async () => {
|
||||||
|
const { response } = await api
|
||||||
|
.post(
|
||||||
|
`/admin/products/${baseProduct.id}/variants/${baseProduct.variants[0].id}/inventory-items`,
|
||||||
|
{},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
.catch((e) => e)
|
||||||
|
|
||||||
|
expect(response.status).toEqual(400)
|
||||||
|
expect(response.data).toEqual({
|
||||||
|
type: "invalid_data",
|
||||||
|
message:
|
||||||
|
"Invalid request: Field 'required_quantity' is required; Field 'inventory_item_id' is required",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("successfully adds inventory item to a variant", async () => {
|
||||||
|
const inventoryItem = (
|
||||||
|
await api.post(
|
||||||
|
`/admin/inventory-items`,
|
||||||
|
{ sku: "12345" },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.inventory_item
|
||||||
|
|
||||||
|
const res = await api.post(
|
||||||
|
`/admin/products/${baseProduct.id}/variants/${baseProduct.variants[0].id}/inventory-items?fields=inventory_items.inventory.*,inventory_items.*`,
|
||||||
|
{
|
||||||
|
inventory_item_id: inventoryItem.id,
|
||||||
|
required_quantity: 5,
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(res.status).toEqual(200)
|
||||||
|
expect(res.data.variant.inventory_items).toHaveLength(2)
|
||||||
|
expect(res.data.variant.inventory_items).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
required_quantity: 5,
|
||||||
|
inventory_item_id: inventoryItem.id,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("POST /admin/products/:id/variants/:variant_id/inventory-items/:inventory_id", () => {
|
||||||
|
let inventoryItem
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
inventoryItem = (
|
||||||
|
await api.post(
|
||||||
|
`/admin/inventory-items`,
|
||||||
|
{ sku: "12345" },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.inventory_item
|
||||||
|
|
||||||
|
await api.post(
|
||||||
|
`/admin/products/${baseProduct.id}/variants/${baseProduct.variants[0].id}/inventory-items`,
|
||||||
|
{
|
||||||
|
inventory_item_id: inventoryItem.id,
|
||||||
|
required_quantity: 5,
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should throw an error when required attributes are not passed", async () => {
|
||||||
|
const { response } = await api
|
||||||
|
.post(
|
||||||
|
`/admin/products/${baseProduct.id}/variants/${baseProduct.variants[0].id}/inventory-items/${inventoryItem.id}`,
|
||||||
|
{},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
.catch((e) => e)
|
||||||
|
|
||||||
|
expect(response.status).toEqual(400)
|
||||||
|
expect(response.data).toEqual({
|
||||||
|
type: "invalid_data",
|
||||||
|
message: "Invalid request: Field 'required_quantity' is required",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("successfully updates an inventory item link to a variant", async () => {
|
||||||
|
const res = await api.post(
|
||||||
|
`/admin/products/${baseProduct.id}/variants/${baseProduct.variants[0].id}/inventory-items/${inventoryItem.id}?fields=inventory_items.inventory.*,inventory_items.*`,
|
||||||
|
{ required_quantity: 10 },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(res.status).toEqual(200)
|
||||||
|
expect(res.data.variant.inventory_items).toHaveLength(2)
|
||||||
|
expect(res.data.variant.inventory_items).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
required_quantity: 10,
|
||||||
|
inventory_item_id: inventoryItem.id,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("DELETE /admin/products/:id/variants/:variant_id/inventory-items/:inventory_id", () => {
|
||||||
|
let inventoryItem
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
inventoryItem = (
|
||||||
|
await api.post(
|
||||||
|
`/admin/inventory-items`,
|
||||||
|
{ sku: "12345" },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.inventory_item
|
||||||
|
|
||||||
|
await api.post(
|
||||||
|
`/admin/products/${baseProduct.id}/variants/${baseProduct.variants[0].id}/inventory-items`,
|
||||||
|
{
|
||||||
|
inventory_item_id: inventoryItem.id,
|
||||||
|
required_quantity: 5,
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("successfully deletes an inventory item link from a variant", async () => {
|
||||||
|
await api.post(
|
||||||
|
`/admin/products/${baseProduct.id}/variants/${baseProduct.variants[0].id}/inventory-items`,
|
||||||
|
{ inventory_item_id: inventoryItem.id, required_quantity: 5 },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
const res = await api.delete(
|
||||||
|
`/admin/products/${baseProduct.id}/variants/${baseProduct.variants[0].id}/inventory-items/${inventoryItem.id}?fields=inventory_items.inventory.*,inventory_items.*`,
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(res.status).toEqual(200)
|
||||||
|
expect(res.data.parent.inventory_items).toHaveLength(1)
|
||||||
|
expect(res.data.parent.inventory_items[0].id).not.toBe(inventoryItem.id)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("POST /admin/products/:id/variants/:variant_id/inventory-items/batch", () => {
|
||||||
|
let inventoryItemToUpdate
|
||||||
|
let inventoryItemToDelete
|
||||||
|
let inventoryItemToCreate
|
||||||
|
let inventoryProduct
|
||||||
|
let inventoryVariant1
|
||||||
|
let inventoryVariant2
|
||||||
|
let inventoryVariant3
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
inventoryProduct = (
|
||||||
|
await api.post(
|
||||||
|
"/admin/products",
|
||||||
|
{
|
||||||
|
title: "product 1",
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
title: "variant 1",
|
||||||
|
prices: [{ currency_code: "usd", amount: 100 }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "variant 2",
|
||||||
|
prices: [{ currency_code: "usd", amount: 100 }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "variant 3",
|
||||||
|
prices: [{ currency_code: "usd", amount: 100 }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.product
|
||||||
|
|
||||||
|
inventoryVariant1 = inventoryProduct.variants[0]
|
||||||
|
inventoryVariant2 = inventoryProduct.variants[1]
|
||||||
|
inventoryVariant3 = inventoryProduct.variants[2]
|
||||||
|
|
||||||
|
inventoryItemToCreate = (
|
||||||
|
await api.post(
|
||||||
|
`/admin/inventory-items`,
|
||||||
|
{ sku: "to-create" },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.inventory_item
|
||||||
|
|
||||||
|
inventoryItemToUpdate = (
|
||||||
|
await api.post(
|
||||||
|
`/admin/inventory-items`,
|
||||||
|
{ sku: "to-update" },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.inventory_item
|
||||||
|
|
||||||
|
inventoryItemToDelete = (
|
||||||
|
await api.post(
|
||||||
|
`/admin/inventory-items`,
|
||||||
|
{ sku: "to-delete" },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.inventory_item
|
||||||
|
|
||||||
|
await api.post(
|
||||||
|
`/admin/products/${baseProduct.id}/variants/${inventoryVariant1.id}/inventory-items`,
|
||||||
|
{
|
||||||
|
inventory_item_id: inventoryItemToUpdate.id,
|
||||||
|
required_quantity: 5,
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
await api.post(
|
||||||
|
`/admin/products/${baseProduct.id}/variants/${inventoryVariant2.id}/inventory-items`,
|
||||||
|
{
|
||||||
|
inventory_item_id: inventoryItemToDelete.id,
|
||||||
|
required_quantity: 10,
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("successfully creates, updates and deletes an inventory item link from a variant", async () => {
|
||||||
|
const res = await api.post(
|
||||||
|
`/admin/products/${baseProduct.id}/variants/inventory-items/batch`,
|
||||||
|
{
|
||||||
|
create: [
|
||||||
|
{
|
||||||
|
required_quantity: 15,
|
||||||
|
inventory_item_id: inventoryItemToCreate.id,
|
||||||
|
variant_id: inventoryVariant3.id,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
update: [
|
||||||
|
{
|
||||||
|
required_quantity: 25,
|
||||||
|
inventory_item_id: inventoryItemToUpdate.id,
|
||||||
|
variant_id: inventoryVariant1.id,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
delete: [
|
||||||
|
{
|
||||||
|
inventory_item_id: inventoryItemToDelete.id,
|
||||||
|
variant_id: inventoryVariant2.id,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(res.status).toEqual(200)
|
||||||
|
|
||||||
|
const createdLinkVariant = (
|
||||||
|
await api.get(
|
||||||
|
`/admin/products/${baseProduct.id}/variants/${inventoryVariant3.id}?fields=inventory_items.inventory.*,inventory_items.*`,
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.variant
|
||||||
|
|
||||||
|
expect(createdLinkVariant.inventory_items).toHaveLength(2)
|
||||||
|
expect(createdLinkVariant.inventory_items).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
required_quantity: 15,
|
||||||
|
inventory_item_id: inventoryItemToCreate.id,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
|
||||||
|
const updatedLinkVariant = (
|
||||||
|
await api.get(
|
||||||
|
`/admin/products/${baseProduct.id}/variants/${inventoryVariant1.id}?fields=inventory_items.inventory.*,inventory_items.*`,
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.variant
|
||||||
|
|
||||||
|
expect(updatedLinkVariant.inventory_items).toHaveLength(2)
|
||||||
|
expect(updatedLinkVariant.inventory_items).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
required_quantity: 25,
|
||||||
|
inventory_item_id: inventoryItemToUpdate.id,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
|
||||||
|
const deletedLinkVariant = (
|
||||||
|
await api.get(
|
||||||
|
`/admin/products/${baseProduct.id}/variants/${inventoryVariant2.id}?fields=inventory_items.inventory.*,inventory_items.*`,
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.variant
|
||||||
|
|
||||||
|
expect(deletedLinkVariant.inventory_items).toHaveLength(1)
|
||||||
|
expect(deletedLinkVariant.inventory_items[0].id).not.toEqual(
|
||||||
|
inventoryItemToDelete.id
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -0,0 +1,405 @@
|
|||||||
|
import { medusaIntegrationTestRunner } from "medusa-test-utils"
|
||||||
|
import {
|
||||||
|
adminHeaders,
|
||||||
|
createAdminUser,
|
||||||
|
} from "../../../../helpers/create-admin-user"
|
||||||
|
import { getProductFixture } from "../../../../helpers/fixtures"
|
||||||
|
import { ModuleRegistrationName } from "@medusajs/utils"
|
||||||
|
import { IStoreModuleService } from "@medusajs/types"
|
||||||
|
|
||||||
|
jest.setTimeout(30000)
|
||||||
|
|
||||||
|
medusaIntegrationTestRunner({
|
||||||
|
testSuite: ({ dbConnection, api, getContainer }) => {
|
||||||
|
let store
|
||||||
|
let product1
|
||||||
|
let product2
|
||||||
|
let product3
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const appContainer = getContainer()
|
||||||
|
await createAdminUser(dbConnection, adminHeaders, appContainer)
|
||||||
|
|
||||||
|
const storeModule: IStoreModuleService = appContainer.resolve(
|
||||||
|
ModuleRegistrationName.STORE
|
||||||
|
)
|
||||||
|
// A default store is created when the app is started, so we want to delete that one and create one specifically for our tests.
|
||||||
|
const defaultId = (await api.get("/admin/stores", adminHeaders)).data
|
||||||
|
.stores?.[0]?.id
|
||||||
|
if (defaultId) {
|
||||||
|
storeModule.delete(defaultId)
|
||||||
|
}
|
||||||
|
|
||||||
|
store = await storeModule.create({
|
||||||
|
name: "New store",
|
||||||
|
supported_currency_codes: ["usd", "dkk"],
|
||||||
|
default_currency_code: "usd",
|
||||||
|
})
|
||||||
|
|
||||||
|
product1 = (
|
||||||
|
await api.post(
|
||||||
|
"/admin/products",
|
||||||
|
getProductFixture({ title: "test1", status: "published" }),
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.product
|
||||||
|
product2 = (
|
||||||
|
await api.post(
|
||||||
|
"/admin/products",
|
||||||
|
getProductFixture({ title: "test2", status: "published" }),
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.product
|
||||||
|
product3 = (
|
||||||
|
await api.post(
|
||||||
|
"/admin/products",
|
||||||
|
getProductFixture({ title: "test3", status: "published" }),
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.product
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("Get products based on publishable key", () => {
|
||||||
|
let pubKey1
|
||||||
|
let salesChannel1
|
||||||
|
let salesChannel2
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
pubKey1 = (
|
||||||
|
await api.post(
|
||||||
|
"/admin/api-keys",
|
||||||
|
{ title: "sample key", type: "publishable" },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.api_key
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
await api.post(
|
||||||
|
`/admin/sales-channels/${salesChannel1.id}/products`,
|
||||||
|
{ add: [product1.id] },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
await api.post(
|
||||||
|
`/admin/sales-channels/${salesChannel2.id}/products`,
|
||||||
|
{ add: [product2.id] },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
await api.post(
|
||||||
|
`/admin/stores/${store.id}`,
|
||||||
|
{ default_sales_channel_id: salesChannel1.id },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns products from a specific channel associated with a publishable key", async () => {
|
||||||
|
await api.post(
|
||||||
|
`/admin/api-keys/${pubKey1.id}/sales-channels`,
|
||||||
|
{
|
||||||
|
add: [salesChannel1.id],
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
const response = await api.get(`/store/products`, {
|
||||||
|
headers: {
|
||||||
|
...adminHeaders.headers,
|
||||||
|
"x-publishable-api-key": pubKey1.token,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(response.data.products.length).toBe(1)
|
||||||
|
expect(response.data.products).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: product1.id,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns products from multiples sales channels associated with a publishable key", async () => {
|
||||||
|
await api.post(
|
||||||
|
`/admin/api-keys/${pubKey1.id}/sales-channels`,
|
||||||
|
{
|
||||||
|
add: [salesChannel1.id, salesChannel2.id],
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
const response = await api.get(`/store/products`, {
|
||||||
|
headers: {
|
||||||
|
...adminHeaders.headers,
|
||||||
|
"x-publishable-api-key": pubKey1.token,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(response.data.products.length).toBe(2)
|
||||||
|
expect(response.data.products).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: product2.id,
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
id: product1.id,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("SC param overrides PK channels (but SK still needs to be in the PK's scope", async () => {
|
||||||
|
await api.post(
|
||||||
|
`/admin/api-keys/${pubKey1.id}/sales-channels`,
|
||||||
|
{
|
||||||
|
add: [salesChannel1.id, salesChannel2.id],
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
const response = await api.get(
|
||||||
|
`/store/products?sales_channel_id[0]=${salesChannel2.id}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
...adminHeaders.headers,
|
||||||
|
"x-publishable-api-key": pubKey1.token,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.data.products.length).toBe(1)
|
||||||
|
expect(response.data.products).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: product2.id,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns default product from default sales channel if PK is not passed", async () => {
|
||||||
|
await api.post(
|
||||||
|
`/admin/stores/${store.id}`,
|
||||||
|
{ default_sales_channel_id: salesChannel2.id },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
await api.post(
|
||||||
|
`/admin/api-keys/${pubKey1.id}/sales-channels`,
|
||||||
|
{
|
||||||
|
add: [salesChannel1.id, salesChannel2.id],
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
const response = await api.get(`/store/products`, {
|
||||||
|
adminHeaders,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(response.data.products.length).toBe(1)
|
||||||
|
expect(response.data.products).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: product2.id,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO: Decide if this is the behavior we want to keep in v2, as it seems a bit strange
|
||||||
|
it.skip("returns all products if passed PK doesn't have associated channels", async () => {
|
||||||
|
const response = await api.get(`/store/products`, {
|
||||||
|
headers: {
|
||||||
|
...adminHeaders.headers,
|
||||||
|
"x-publishable-api-key": pubKey1.token,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(response.data.products.length).toBe(3)
|
||||||
|
expect(response.data.products).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: product1.id,
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
id: product2.id,
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
id: product3.id,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("throws because sales channel param is not in the scope of passed PK", async () => {
|
||||||
|
await api.post(
|
||||||
|
`/admin/api-keys/${pubKey1.id}/sales-channels`,
|
||||||
|
{
|
||||||
|
add: [salesChannel1.id],
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
const err = await api
|
||||||
|
.get(`/store/products?sales_channel_id[]=${salesChannel2.id}`, {
|
||||||
|
headers: {
|
||||||
|
...adminHeaders.headers,
|
||||||
|
"x-publishable-api-key": pubKey1.token,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.catch((e) => e)
|
||||||
|
|
||||||
|
expect(err.response.status).toEqual(400)
|
||||||
|
expect(err.response.data.message).toEqual(
|
||||||
|
`Requested sales channel is not part of the publishable key mappings`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("retrieve a product from a specific channel associated with a publishable key", async () => {
|
||||||
|
await api.post(
|
||||||
|
`/admin/api-keys/${pubKey1.id}/sales-channels`,
|
||||||
|
{
|
||||||
|
add: [salesChannel1.id],
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
const response = await api.get(`/store/products/${product1.id}`, {
|
||||||
|
headers: {
|
||||||
|
...adminHeaders.headers,
|
||||||
|
"x-publishable-api-key": pubKey1.token,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(response.data.product).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: product1.id,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
// BREAKING: If product not in sales channel we used to return 400, we return 404 instead.
|
||||||
|
it("return 404 because requested product is not in the SC associated with a publishable key", async () => {
|
||||||
|
await api.post(
|
||||||
|
`/admin/api-keys/${pubKey1.id}/sales-channels`,
|
||||||
|
{
|
||||||
|
add: [salesChannel1.id],
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
const err = await api
|
||||||
|
.get(`/store/products/${product2.id}`, {
|
||||||
|
headers: {
|
||||||
|
...adminHeaders.headers,
|
||||||
|
"x-publishable-api-key": pubKey1.token,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.catch((e) => e)
|
||||||
|
|
||||||
|
expect(err.response.status).toEqual(404)
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO: Add variant endpoints to the store API (if that is what we want)
|
||||||
|
it.skip("should return 404 when the requested variant doesn't exist", async () => {
|
||||||
|
await api.post(
|
||||||
|
`/admin/api-keys/${pubKey1.id}/sales-channels`,
|
||||||
|
{
|
||||||
|
add: [salesChannel1.id],
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
const response = await api
|
||||||
|
.get(`/store/variants/does-not-exist`, {
|
||||||
|
headers: {
|
||||||
|
...adminHeaders.headers,
|
||||||
|
"x-publishable-api-key": pubKey1.token,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
return err.response
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(response.status).toEqual(404)
|
||||||
|
expect(response.data.message).toEqual(
|
||||||
|
"Variant with id: does-not-exist was not found"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return 404 when the requested product doesn't exist", async () => {
|
||||||
|
await api.post(
|
||||||
|
`/admin/api-keys/${pubKey1.id}/sales-channels`,
|
||||||
|
{
|
||||||
|
add: [salesChannel1.id],
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
const response = await api
|
||||||
|
.get(`/store/products/does-not-exist`, {
|
||||||
|
headers: {
|
||||||
|
...adminHeaders.headers,
|
||||||
|
"x-publishable-api-key": pubKey1.token,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
return err.response
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(response.status).toEqual(404)
|
||||||
|
expect(response.data.message).toEqual(
|
||||||
|
"Product with id: does-not-exist was not found"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO: Similar to above, decide what the behavior should be in v2
|
||||||
|
it.skip("correctly returns a product if passed PK has no associated SCs", async () => {
|
||||||
|
let response = await api
|
||||||
|
.get(`/store/products/${product1.id}`, {
|
||||||
|
headers: {
|
||||||
|
...adminHeaders.headers,
|
||||||
|
"x-publishable-api-key": pubKey1.token,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
return err.response
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(response.status).toEqual(200)
|
||||||
|
|
||||||
|
response = await api
|
||||||
|
.get(`/store/products/${product2.id}`, {
|
||||||
|
headers: {
|
||||||
|
...adminHeaders.headers,
|
||||||
|
"x-publishable-api-key": pubKey1.token,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
return err.response
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(response.status).toEqual(200)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -1,39 +1,47 @@
|
|||||||
import { medusaIntegrationTestRunner } from "medusa-test-utils"
|
import { medusaIntegrationTestRunner } from "medusa-test-utils"
|
||||||
import {
|
import {
|
||||||
adminHeaders,
|
adminHeaders,
|
||||||
createAdminUser,
|
createAdminUser,
|
||||||
} from "../../../../helpers/create-admin-user"
|
} from "../../../../helpers/create-admin-user"
|
||||||
|
|
||||||
jest.setTimeout(60000)
|
jest.setTimeout(60000)
|
||||||
|
|
||||||
medusaIntegrationTestRunner({
|
medusaIntegrationTestRunner({
|
||||||
testSuite: ({ dbConnection, getContainer, api }) => {
|
testSuite: ({ dbConnection, getContainer, api }) => {
|
||||||
beforeAll(() => {})
|
let salesChannel1
|
||||||
|
let salesChannel2
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const container = getContainer()
|
const container = getContainer()
|
||||||
await createAdminUser(dbConnection, adminHeaders, container)
|
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", () => {
|
describe("GET /admin/sales-channels/:id", () => {
|
||||||
let salesChannel
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
salesChannel = (
|
|
||||||
await api.post(
|
|
||||||
"/admin/sales-channels",
|
|
||||||
{
|
|
||||||
name: "test name",
|
|
||||||
description: "test description",
|
|
||||||
},
|
|
||||||
adminHeaders
|
|
||||||
)
|
|
||||||
).data.sales_channel
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should retrieve the requested sales channel", async () => {
|
it("should retrieve the requested sales channel", async () => {
|
||||||
const response = await api.get(
|
const response = await api.get(
|
||||||
`/admin/sales-channels/${salesChannel.id}`,
|
`/admin/sales-channels/${salesChannel1.id}`,
|
||||||
adminHeaders
|
adminHeaders
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -42,8 +50,8 @@ medusaIntegrationTestRunner({
|
|||||||
expect(response.data.sales_channel).toEqual(
|
expect(response.data.sales_channel).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
name: salesChannel.name,
|
name: salesChannel1.name,
|
||||||
description: salesChannel.description,
|
description: salesChannel1.description,
|
||||||
created_at: expect.any(String),
|
created_at: expect.any(String),
|
||||||
updated_at: expect.any(String),
|
updated_at: expect.any(String),
|
||||||
})
|
})
|
||||||
@@ -52,33 +60,6 @@ medusaIntegrationTestRunner({
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe("GET /admin/sales-channels", () => {
|
describe("GET /admin/sales-channels", () => {
|
||||||
let salesChannel1
|
|
||||||
let salesChannel2
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
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
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should list the sales channel", async () => {
|
it("should list the sales channel", async () => {
|
||||||
const response = await api.get(`/admin/sales-channels`, adminHeaders)
|
const response = await api.get(`/admin/sales-channels`, adminHeaders)
|
||||||
|
|
||||||
@@ -183,21 +164,6 @@ medusaIntegrationTestRunner({
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe("POST /admin/sales-channels/:id", () => {
|
describe("POST /admin/sales-channels/:id", () => {
|
||||||
let sc
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
sc = (
|
|
||||||
await api.post(
|
|
||||||
"/admin/sales-channels",
|
|
||||||
{
|
|
||||||
name: "test name",
|
|
||||||
description: "test description",
|
|
||||||
},
|
|
||||||
adminHeaders
|
|
||||||
)
|
|
||||||
).data.sales_channel
|
|
||||||
})
|
|
||||||
|
|
||||||
it("updates sales channel properties", async () => {
|
it("updates sales channel properties", async () => {
|
||||||
const payload = {
|
const payload = {
|
||||||
name: "updated name",
|
name: "updated name",
|
||||||
@@ -206,7 +172,7 @@ medusaIntegrationTestRunner({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const response = await api.post(
|
const response = await api.post(
|
||||||
`/admin/sales-channels/${sc.id}`,
|
`/admin/sales-channels/${salesChannel1.id}`,
|
||||||
payload,
|
payload,
|
||||||
adminHeaders
|
adminHeaders
|
||||||
)
|
)
|
||||||
@@ -279,46 +245,19 @@ medusaIntegrationTestRunner({
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe("DELETE /admin/sales-channels/:id", () => {
|
describe("DELETE /admin/sales-channels/:id", () => {
|
||||||
let salesChannel
|
|
||||||
let salesChannel2
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
salesChannel = (
|
|
||||||
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
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should delete the requested sales channel", async () => {
|
it("should delete the requested sales channel", async () => {
|
||||||
const toDelete = (
|
const toDelete = (
|
||||||
await api.get(
|
await api.get(
|
||||||
`/admin/sales-channels/${salesChannel.id}`,
|
`/admin/sales-channels/${salesChannel1.id}`,
|
||||||
adminHeaders
|
adminHeaders
|
||||||
)
|
)
|
||||||
).data.sales_channel
|
).data.sales_channel
|
||||||
|
|
||||||
expect(toDelete.id).toEqual(salesChannel.id)
|
expect(toDelete.id).toEqual(salesChannel1.id)
|
||||||
expect(toDelete.deleted_at).toEqual(null)
|
expect(toDelete.deleted_at).toEqual(null)
|
||||||
|
|
||||||
const response = await api.delete(
|
const response = await api.delete(
|
||||||
`/admin/sales-channels/${salesChannel.id}`,
|
`/admin/sales-channels/${salesChannel1.id}`,
|
||||||
adminHeaders
|
adminHeaders
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -331,13 +270,13 @@ medusaIntegrationTestRunner({
|
|||||||
|
|
||||||
await api
|
await api
|
||||||
.get(
|
.get(
|
||||||
`/admin/sales-channels/${salesChannel.id}?fields=id,deleted_at`,
|
`/admin/sales-channels/${salesChannel1.id}?fields=id,deleted_at`,
|
||||||
adminHeaders
|
adminHeaders
|
||||||
)
|
)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
expect(err.response.data.type).toEqual("not_found")
|
expect(err.response.data.type).toEqual("not_found")
|
||||||
expect(err.response.data.message).toEqual(
|
expect(err.response.data.message).toEqual(
|
||||||
`Sales channel with id: ${salesChannel.id} not found`
|
`Sales channel with id: ${salesChannel1.id} not found`
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -356,13 +295,13 @@ medusaIntegrationTestRunner({
|
|||||||
await api.post(
|
await api.post(
|
||||||
`/admin/stock-locations/${location.id}/sales-channels`,
|
`/admin/stock-locations/${location.id}/sales-channels`,
|
||||||
{
|
{
|
||||||
add: [salesChannel.id, salesChannel2.id],
|
add: [salesChannel1.id, salesChannel2.id],
|
||||||
},
|
},
|
||||||
adminHeaders
|
adminHeaders
|
||||||
)
|
)
|
||||||
|
|
||||||
await api.delete(
|
await api.delete(
|
||||||
`/admin/sales-channels/${salesChannel.id}`,
|
`/admin/sales-channels/${salesChannel1.id}`,
|
||||||
adminHeaders
|
adminHeaders
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -381,21 +320,8 @@ medusaIntegrationTestRunner({
|
|||||||
// BREAKING CHANGE: Endpoint has changed
|
// BREAKING CHANGE: Endpoint has changed
|
||||||
// from: /admin/sales-channels/:id/products/batch
|
// from: /admin/sales-channels/:id/products/batch
|
||||||
// to: /admin/sales-channels/:id/products
|
// to: /admin/sales-channels/:id/products
|
||||||
|
|
||||||
let salesChannel
|
|
||||||
let product
|
let product
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
salesChannel = (
|
|
||||||
await api.post(
|
|
||||||
"/admin/sales-channels",
|
|
||||||
{
|
|
||||||
name: "test name",
|
|
||||||
description: "test description",
|
|
||||||
},
|
|
||||||
adminHeaders
|
|
||||||
)
|
|
||||||
).data.sales_channel
|
|
||||||
|
|
||||||
product = (
|
product = (
|
||||||
await api.post(
|
await api.post(
|
||||||
"/admin/products",
|
"/admin/products",
|
||||||
@@ -409,7 +335,7 @@ medusaIntegrationTestRunner({
|
|||||||
|
|
||||||
it("should add products to a sales channel", async () => {
|
it("should add products to a sales channel", async () => {
|
||||||
const response = await api.post(
|
const response = await api.post(
|
||||||
`/admin/sales-channels/${salesChannel.id}/products`,
|
`/admin/sales-channels/${salesChannel1.id}/products`,
|
||||||
{ add: [product.id] },
|
{ add: [product.id] },
|
||||||
adminHeaders
|
adminHeaders
|
||||||
)
|
)
|
||||||
@@ -449,7 +375,7 @@ medusaIntegrationTestRunner({
|
|||||||
|
|
||||||
it("should remove products from a sales channel", async () => {
|
it("should remove products from a sales channel", async () => {
|
||||||
await api.post(
|
await api.post(
|
||||||
`/admin/sales-channels/${salesChannel.id}/products`,
|
`/admin/sales-channels/${salesChannel1.id}/products`,
|
||||||
{ add: [product.id] },
|
{ add: [product.id] },
|
||||||
adminHeaders
|
adminHeaders
|
||||||
)
|
)
|
||||||
@@ -474,7 +400,7 @@ medusaIntegrationTestRunner({
|
|||||||
)
|
)
|
||||||
|
|
||||||
const response = await api.post(
|
const response = await api.post(
|
||||||
`/admin/sales-channels/${salesChannel.id}/products`,
|
`/admin/sales-channels/${salesChannel1.id}/products`,
|
||||||
{ remove: [product.id] },
|
{ remove: [product.id] },
|
||||||
adminHeaders
|
adminHeaders
|
||||||
)
|
)
|
||||||
@@ -499,7 +425,50 @@ medusaIntegrationTestRunner({
|
|||||||
expect(product.sales_channels.length).toBe(0)
|
expect(product.sales_channels.length).toBe(0)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
// DELETED TESTS:
|
|
||||||
|
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_api_key=${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
|
// - POST /admin/products/:id
|
||||||
// - Mutation sales channels on products
|
// - Mutation sales channels on products
|
||||||
// - POST /admin/products
|
// - POST /admin/products
|
||||||
|
|||||||
@@ -0,0 +1,194 @@
|
|||||||
|
import { medusaIntegrationTestRunner } from "medusa-test-utils"
|
||||||
|
import {
|
||||||
|
adminHeaders,
|
||||||
|
createAdminUser,
|
||||||
|
} from "../../../../helpers/create-admin-user"
|
||||||
|
import { ModuleRegistrationName } from "@medusajs/utils"
|
||||||
|
import { IStoreModuleService } from "@medusajs/types"
|
||||||
|
|
||||||
|
jest.setTimeout(90000)
|
||||||
|
|
||||||
|
medusaIntegrationTestRunner({
|
||||||
|
testSuite: ({ dbConnection, getContainer, api }) => {
|
||||||
|
describe("/admin/stores", () => {
|
||||||
|
let store
|
||||||
|
let container
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
container = getContainer()
|
||||||
|
const storeModule: IStoreModuleService = container.resolve(
|
||||||
|
ModuleRegistrationName.STORE
|
||||||
|
)
|
||||||
|
await createAdminUser(dbConnection, adminHeaders, container)
|
||||||
|
|
||||||
|
// A default store is created when the app is started, so we want to delete that one and create one specifically for our tests.
|
||||||
|
const defaultId = (await api.get("/admin/stores", adminHeaders)).data
|
||||||
|
.stores?.[0]?.id
|
||||||
|
if (defaultId) {
|
||||||
|
storeModule.delete(defaultId)
|
||||||
|
}
|
||||||
|
|
||||||
|
store = await storeModule.create({
|
||||||
|
name: "New store",
|
||||||
|
supported_currency_codes: ["usd", "dkk"],
|
||||||
|
default_currency_code: "usd",
|
||||||
|
default_sales_channel_id: "sc_12345",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// BREAKING: The URL changed from `GET /admin/store` to `GET /admin/stores`
|
||||||
|
describe("Store creation", () => {
|
||||||
|
it("has created store with default currency", async () => {
|
||||||
|
const resStore = (
|
||||||
|
await api.get("/admin/stores", adminHeaders)
|
||||||
|
).data.stores.find((s) => s.id === store.id)
|
||||||
|
|
||||||
|
// BREAKING: The store response contained currencies, modules, and feature flags, which are not present anymore
|
||||||
|
expect(resStore).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(String),
|
||||||
|
name: "New store",
|
||||||
|
default_currency_code: "usd",
|
||||||
|
default_sales_channel_id: expect.any(String),
|
||||||
|
supported_currency_codes: ["usd", "dkk"],
|
||||||
|
created_at: expect.any(String),
|
||||||
|
updated_at: expect.any(String),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("POST /admin/stores", () => {
|
||||||
|
it("fails to update default currency if not in store currencies", async () => {
|
||||||
|
const err = await api
|
||||||
|
.post(
|
||||||
|
`/admin/stores/${store.id}`,
|
||||||
|
{
|
||||||
|
default_currency_code: "eur",
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
.catch((e) => e)
|
||||||
|
|
||||||
|
expect(err.response.status).toBe(400)
|
||||||
|
expect(err.response.data).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
type: "invalid_data",
|
||||||
|
message: "Store does not have currency: eur",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
// BREAKING: `currencies` was renamed to `supported_currency_codes`
|
||||||
|
it("fails to remove default currency from currencies without replacing it", async () => {
|
||||||
|
const err = await api
|
||||||
|
.post(
|
||||||
|
`/admin/stores/${store.id}`,
|
||||||
|
{ supported_currency_codes: ["dkk"] },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
.catch((e) => e)
|
||||||
|
|
||||||
|
expect(err.response.status).toBe(400)
|
||||||
|
expect(err.response.data).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
type: "invalid_data",
|
||||||
|
message:
|
||||||
|
"You are not allowed to remove default currency from store currencies without replacing it as well",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("successfully updates default currency code", async () => {
|
||||||
|
const response = await api
|
||||||
|
.post(
|
||||||
|
`/admin/stores/${store.id}`,
|
||||||
|
{
|
||||||
|
default_currency_code: "dkk",
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
.catch((err) => console.log(err))
|
||||||
|
|
||||||
|
expect(response.status).toEqual(200)
|
||||||
|
expect(response.data.store).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(String),
|
||||||
|
name: "New store",
|
||||||
|
default_currency_code: "dkk",
|
||||||
|
created_at: expect.any(String),
|
||||||
|
updated_at: expect.any(String),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("successfully updates default currency and store currencies", async () => {
|
||||||
|
const response = await api.post(
|
||||||
|
`/admin/stores/${store.id}`,
|
||||||
|
{
|
||||||
|
default_currency_code: "jpy",
|
||||||
|
supported_currency_codes: ["jpy", "usd"],
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.status).toEqual(200)
|
||||||
|
expect(response.data.store).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(String),
|
||||||
|
name: "New store",
|
||||||
|
default_sales_channel_id: expect.any(String),
|
||||||
|
supported_currency_codes: ["jpy", "usd"],
|
||||||
|
default_currency_code: "jpy",
|
||||||
|
created_at: expect.any(String),
|
||||||
|
updated_at: expect.any(String),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("successfully updates and store currencies", async () => {
|
||||||
|
const response = await api.post(
|
||||||
|
`/admin/stores/${store.id}`,
|
||||||
|
{
|
||||||
|
supported_currency_codes: ["jpy", "usd"],
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.status).toEqual(200)
|
||||||
|
expect(response.data.store).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(String),
|
||||||
|
default_sales_channel_id: expect.any(String),
|
||||||
|
name: "New store",
|
||||||
|
supported_currency_codes: ["jpy", "usd"],
|
||||||
|
default_currency_code: "usd",
|
||||||
|
created_at: expect.any(String),
|
||||||
|
updated_at: expect.any(String),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("GET /admin/stores", () => {
|
||||||
|
it("supports searching of stores", async () => {
|
||||||
|
const response = await api.get(
|
||||||
|
"/admin/stores?q=nonexistent",
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
expect(response.status).toEqual(200)
|
||||||
|
expect(response.data.stores).toHaveLength(0)
|
||||||
|
|
||||||
|
const response2 = await api.get("/admin/stores?q=store", adminHeaders)
|
||||||
|
expect(response.status).toEqual(200)
|
||||||
|
expect(response2.data.stores).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: store.id,
|
||||||
|
name: "New store",
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -317,7 +317,7 @@ medusaIntegrationTestRunner({
|
|||||||
|
|
||||||
expect(error.response.status).toEqual(400)
|
expect(error.response.status).toEqual(400)
|
||||||
expect(error.response.data).toEqual({
|
expect(error.response.data).toEqual({
|
||||||
message: `Invalid sales channel filters provided - does-not-exist`,
|
message: `Requested sales channel is not part of the publishable key mappings`,
|
||||||
type: "invalid_data",
|
type: "invalid_data",
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -331,7 +331,7 @@ medusaIntegrationTestRunner({
|
|||||||
|
|
||||||
expect(error.response.status).toEqual(400)
|
expect(error.response.status).toEqual(400)
|
||||||
expect(error.response.data).toEqual({
|
expect(error.response.data).toEqual({
|
||||||
message: `Invalid sales channel filters provided - ${salesChannel2.id}`,
|
message: `Requested sales channel is not part of the publishable key mappings`,
|
||||||
type: "invalid_data",
|
type: "invalid_data",
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -153,6 +153,7 @@ export function medusaIntegrationTestRunner({
|
|||||||
let isFirstTime = true
|
let isFirstTime = true
|
||||||
|
|
||||||
const beforeAll_ = async () => {
|
const beforeAll_ = async () => {
|
||||||
|
console.log(`Creating database ${dbName}`)
|
||||||
await dbUtils.create(dbName)
|
await dbUtils.create(dbName)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
|
|
||||||
import { refetchApiKey } from "../helpers"
|
import { refetchApiKey } from "../helpers"
|
||||||
import { AdminUpdateApiKeyType } from "../validators"
|
import { AdminUpdateApiKeyType } from "../validators"
|
||||||
|
import { MedusaError } from "@medusajs/utils"
|
||||||
|
|
||||||
export const GET = async (
|
export const GET = async (
|
||||||
req: AuthenticatedMedusaRequest,
|
req: AuthenticatedMedusaRequest,
|
||||||
@@ -20,6 +21,13 @@ export const GET = async (
|
|||||||
req.remoteQueryConfig.fields
|
req.remoteQueryConfig.fields
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (!apiKey) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.NOT_FOUND,
|
||||||
|
`API Key with id: ${req.params.id} was not found`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
res.status(200).json({ api_key: apiKey })
|
res.status(200).json({ api_key: apiKey })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export const defaultAdminApiKeyFields = [
|
|||||||
"redacted",
|
"redacted",
|
||||||
"type",
|
"type",
|
||||||
"last_used_at",
|
"last_used_at",
|
||||||
|
"updated_at",
|
||||||
"created_at",
|
"created_at",
|
||||||
"created_by",
|
"created_by",
|
||||||
"revoked_at",
|
"revoked_at",
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export const AdminGetApiKeyParams = createSelectParams()
|
|||||||
export type AdminGetApiKeysParamsType = z.infer<typeof AdminGetApiKeysParams>
|
export type AdminGetApiKeysParamsType = z.infer<typeof AdminGetApiKeysParams>
|
||||||
export const AdminGetApiKeysParams = createFindParams({
|
export const AdminGetApiKeysParams = createFindParams({
|
||||||
offset: 0,
|
offset: 0,
|
||||||
limit: 50,
|
limit: 20,
|
||||||
}).merge(
|
}).merge(
|
||||||
z.object({
|
z.object({
|
||||||
q: z.string().optional(),
|
q: z.string().optional(),
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
remapProductResponse,
|
remapProductResponse,
|
||||||
} from "../helpers"
|
} from "../helpers"
|
||||||
import { AdminUpdateProductType } from "../validators"
|
import { AdminUpdateProductType } from "../validators"
|
||||||
|
import { MedusaError } from "@medusajs/utils"
|
||||||
|
|
||||||
export const GET = async (
|
export const GET = async (
|
||||||
req: AuthenticatedMedusaRequest,
|
req: AuthenticatedMedusaRequest,
|
||||||
@@ -35,6 +36,9 @@ export const GET = async (
|
|||||||
})
|
})
|
||||||
|
|
||||||
const [product] = await remoteQuery(queryObject)
|
const [product] = await remoteQuery(queryObject)
|
||||||
|
if (!product) {
|
||||||
|
throw new MedusaError(MedusaError.Types.NOT_FOUND, "Product not found")
|
||||||
|
}
|
||||||
|
|
||||||
res.status(200).json({ product: remapProductResponse(product) })
|
res.status(200).json({ product: remapProductResponse(product) })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { isPresent } from "@medusajs/utils"
|
|||||||
import { MedusaRequest, MedusaResponse } from "../../../../types/routing"
|
import { MedusaRequest, MedusaResponse } from "../../../../types/routing"
|
||||||
import { refetchProduct, wrapVariantsWithInventoryQuantity } from "../helpers"
|
import { refetchProduct, wrapVariantsWithInventoryQuantity } from "../helpers"
|
||||||
import { StoreGetProductsParamsType } from "../validators"
|
import { StoreGetProductsParamsType } from "../validators"
|
||||||
|
import { MedusaError } from "@medusajs/utils"
|
||||||
|
|
||||||
export const GET = async (
|
export const GET = async (
|
||||||
req: MedusaRequest<StoreGetProductsParamsType>,
|
req: MedusaRequest<StoreGetProductsParamsType>,
|
||||||
@@ -34,6 +35,13 @@ export const GET = async (
|
|||||||
req.remoteQueryConfig.fields
|
req.remoteQueryConfig.fields
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (!product) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.NOT_FOUND,
|
||||||
|
`Product with id: ${req.params.id} was not found`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (withInventoryQuantity) {
|
if (withInventoryQuantity) {
|
||||||
await wrapVariantsWithInventoryQuantity(req, product.variants || [])
|
await wrapVariantsWithInventoryQuantity(req, product.variants || [])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
StoreGetProductsParams,
|
StoreGetProductsParams,
|
||||||
StoreGetProductsParamsType,
|
StoreGetProductsParamsType,
|
||||||
} from "./validators"
|
} from "./validators"
|
||||||
|
import { applyParamsAsFilters } from "../../utils/middlewares/common/apply-params-as-filters"
|
||||||
|
|
||||||
export const storeProductRoutesMiddlewares: MiddlewareRoute[] = [
|
export const storeProductRoutesMiddlewares: MiddlewareRoute[] = [
|
||||||
{
|
{
|
||||||
@@ -57,6 +58,7 @@ export const storeProductRoutesMiddlewares: MiddlewareRoute[] = [
|
|||||||
StoreGetProductsParams,
|
StoreGetProductsParams,
|
||||||
QueryConfig.retrieveProductQueryConfig
|
QueryConfig.retrieveProductQueryConfig
|
||||||
),
|
),
|
||||||
|
applyParamsAsFilters({ id: "id" }),
|
||||||
filterByValidSalesChannels(),
|
filterByValidSalesChannels(),
|
||||||
setContext({
|
setContext({
|
||||||
stock_location_id: maybeApplyStockLocationId,
|
stock_location_id: maybeApplyStockLocationId,
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { NextFunction } from "express"
|
||||||
|
import { MedusaRequest } from "../../../../types/routing"
|
||||||
|
|
||||||
|
export function applyParamsAsFilters<TFilter extends object>(mappings: {
|
||||||
|
[param: string]: string
|
||||||
|
}) {
|
||||||
|
return async (req: MedusaRequest, _, next: NextFunction) => {
|
||||||
|
for (const [param, paramValue] of Object.entries(req.params)) {
|
||||||
|
if (mappings[param]) {
|
||||||
|
req.filterableFields[mappings[param]] = paramValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
}
|
||||||
+54
-56
@@ -1,8 +1,13 @@
|
|||||||
import { isPresent, MedusaError } from "@medusajs/utils"
|
import { MedusaError } from "@medusajs/utils"
|
||||||
import { NextFunction } from "express"
|
import { NextFunction } from "express"
|
||||||
import { AuthenticatedMedusaRequest } from "../../../../types/routing"
|
import { AuthenticatedMedusaRequest } from "../../../../types/routing"
|
||||||
import { refetchEntity } from "../../refetch-entity"
|
import { refetchEntity } from "../../refetch-entity"
|
||||||
|
import { arrayDifference } from "@medusajs/utils"
|
||||||
|
|
||||||
|
// Selection of sales channels happens in the following priority:
|
||||||
|
// - If a publishable API key is passed, we take the sales channels attached to it and filter them down based on the query params
|
||||||
|
// - If a sales channel id is passed through query params, we use that
|
||||||
|
// - If not, we use the default sales channel for the store
|
||||||
export function filterByValidSalesChannels() {
|
export function filterByValidSalesChannels() {
|
||||||
return async (req: AuthenticatedMedusaRequest, _, next: NextFunction) => {
|
return async (req: AuthenticatedMedusaRequest, _, next: NextFunction) => {
|
||||||
const publishableApiKey = req.get("x-publishable-api-key")
|
const publishableApiKey = req.get("x-publishable-api-key")
|
||||||
@@ -10,68 +15,61 @@ export function filterByValidSalesChannels() {
|
|||||||
| string[]
|
| string[]
|
||||||
| undefined
|
| undefined
|
||||||
|
|
||||||
|
if (publishableApiKey) {
|
||||||
|
const apiKey = await refetchEntity(
|
||||||
|
"api_key",
|
||||||
|
{ token: publishableApiKey },
|
||||||
|
req.scope,
|
||||||
|
["id", "sales_channels_link.sales_channel_id"]
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!apiKey) {
|
||||||
|
return next(
|
||||||
|
new MedusaError(
|
||||||
|
MedusaError.Types.INVALID_DATA,
|
||||||
|
`Publishable API key not found`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
let result = apiKey.sales_channels_link.map(
|
||||||
|
(link) => link.sales_channel_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if (salesChannelIds?.length) {
|
||||||
|
// If all sales channel ids are not in the publishable key, we throw an error
|
||||||
|
const uniqueInParams = arrayDifference(salesChannelIds, result)
|
||||||
|
if (uniqueInParams.length) {
|
||||||
|
return next(
|
||||||
|
new MedusaError(
|
||||||
|
MedusaError.Types.INVALID_DATA,
|
||||||
|
`Requested sales channel is not part of the publishable key mappings`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
result = salesChannelIds
|
||||||
|
}
|
||||||
|
|
||||||
|
req.filterableFields.sales_channel_id = result
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (salesChannelIds?.length) {
|
||||||
|
req.filterableFields.sales_channel_id = salesChannelIds
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
|
||||||
const store = await refetchEntity("stores", {}, req.scope, [
|
const store = await refetchEntity("stores", {}, req.scope, [
|
||||||
"default_sales_channel_id",
|
"default_sales_channel_id",
|
||||||
])
|
])
|
||||||
|
|
||||||
if (!store) {
|
if (!store) {
|
||||||
try {
|
return next(
|
||||||
throw new MedusaError(MedusaError.Types.INVALID_DATA, `Store not found`)
|
new MedusaError(MedusaError.Types.INVALID_DATA, `Store not found`)
|
||||||
} catch (e) {
|
)
|
||||||
return next(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Always set sales channels in the following priority
|
|
||||||
// - any existing sales chennel ids passed through query params
|
|
||||||
// - if none, we set the default sales channel
|
|
||||||
req.filterableFields.sales_channel_id = salesChannelIds ?? [
|
|
||||||
store.default_sales_channel_id,
|
|
||||||
]
|
|
||||||
|
|
||||||
// Return early if no publishable keys are found
|
|
||||||
if (!isPresent(publishableApiKey)) {
|
|
||||||
return next()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// When publishable keys are present, we fetch for all sales chennels attached
|
if (store.default_sales_channel_id) {
|
||||||
// to the publishable key and validate the sales channel filter against it
|
req.filterableFields.sales_channel_id = [store.default_sales_channel_id]
|
||||||
const apiKey = await refetchEntity(
|
|
||||||
"api_key",
|
|
||||||
{ token: publishableApiKey },
|
|
||||||
req.scope,
|
|
||||||
["id", "sales_channels_link.sales_channel_id"]
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!apiKey) {
|
|
||||||
try {
|
|
||||||
throw new MedusaError(
|
|
||||||
MedusaError.Types.INVALID_DATA,
|
|
||||||
`Publishable API key not found`
|
|
||||||
)
|
|
||||||
} catch (e) {
|
|
||||||
return next(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const validSalesChannelIds = apiKey.sales_channels_link.map(
|
|
||||||
(link) => link.sales_channel_id
|
|
||||||
)
|
|
||||||
|
|
||||||
const invalidSalesChannelIds = (salesChannelIds || []).filter(
|
|
||||||
(id) => !validSalesChannelIds.includes(id)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (invalidSalesChannelIds.length) {
|
|
||||||
try {
|
|
||||||
throw new MedusaError(
|
|
||||||
MedusaError.Types.INVALID_DATA,
|
|
||||||
`Invalid sales channel filters provided - ${invalidSalesChannelIds.join(
|
|
||||||
", "
|
|
||||||
)}`
|
|
||||||
)
|
|
||||||
} catch (e) {
|
|
||||||
return next(e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return next()
|
return next()
|
||||||
|
|||||||
@@ -90,6 +90,17 @@
|
|||||||
"default": "now()",
|
"default": "now()",
|
||||||
"mappedType": "datetime"
|
"mappedType": "datetime"
|
||||||
},
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamptz",
|
||||||
|
"unsigned": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"primary": false,
|
||||||
|
"nullable": false,
|
||||||
|
"length": 6,
|
||||||
|
"default": "now()",
|
||||||
|
"mappedType": "datetime"
|
||||||
|
},
|
||||||
"revoked_by": {
|
"revoked_by": {
|
||||||
"name": "revoked_by",
|
"name": "revoked_by",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { Migration } from '@mikro-orm/migrations';
|
||||||
|
|
||||||
|
export class Migration20240604080145 extends Migration {
|
||||||
|
|
||||||
|
async up(): Promise<void> {
|
||||||
|
this.addSql('alter table if exists "api_key" add column if not exists "updated_at" timestamptz not null default now();');
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(): Promise<void> {
|
||||||
|
this.addSql('alter table if exists "api_key" drop column if exists "updated_at";');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -65,6 +65,14 @@ export default class ApiKey {
|
|||||||
})
|
})
|
||||||
created_at: Date
|
created_at: Date
|
||||||
|
|
||||||
|
@Property({
|
||||||
|
onCreate: () => new Date(),
|
||||||
|
onUpdate: () => new Date(),
|
||||||
|
columnType: "timestamptz",
|
||||||
|
defaultRaw: "now()",
|
||||||
|
})
|
||||||
|
updated_at?: Date
|
||||||
|
|
||||||
@Property({ columnType: "text", nullable: true })
|
@Property({ columnType: "text", nullable: true })
|
||||||
revoked_by: string | null = null
|
revoked_by: string | null = null
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user