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:
Stevche Radevski
2024-06-04 21:00:07 +02:00
committed by GitHub
parent d104d1a256
commit e44fe78b96
24 changed files with 2301 additions and 2763 deletions

View File

@@ -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",
}),
])
)
})
})
})
},
})

View File

@@ -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.`
// )
// }
// })
// })
// })

View File

@@ -3,48 +3,10 @@ import {
adminHeaders,
createAdminUser,
} from "../../../../helpers/create-admin-user"
import { getProductFixture } from "../../../../helpers/fixtures"
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({
testSuite: ({ dbConnection, getContainer, api }) => {
let baseProduct
@@ -56,7 +18,6 @@ medusaIntegrationTestRunner({
let publishedCollection
let baseType
let baseRegion
beforeEach(async () => {
await createAdminUser(dbConnection, adminHeaders, getContainer())
@@ -85,18 +46,6 @@ medusaIntegrationTestRunner({
)
).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 = (
await api.post(
"/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", () => {
it("successfully deletes a product", async () => {
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
)
})
})
})
},
})

View File

@@ -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&currency_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&region_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&region_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
)
})
})
},
})

View File

@@ -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)
})
})
},
})

View File

@@ -1,39 +1,47 @@
import { medusaIntegrationTestRunner } from "medusa-test-utils"
import {
adminHeaders,
createAdminUser,
adminHeaders,
createAdminUser,
} from "../../../../helpers/create-admin-user"
jest.setTimeout(60000)
medusaIntegrationTestRunner({
testSuite: ({ dbConnection, getContainer, api }) => {
beforeAll(() => {})
let salesChannel1
let salesChannel2
beforeEach(async () => {
const container = getContainer()
await createAdminUser(dbConnection, adminHeaders, container)
salesChannel1 = (
await api.post(
"/admin/sales-channels",
{
name: "test name",
description: "test description",
},
adminHeaders
)
).data.sales_channel
salesChannel2 = (
await api.post(
"/admin/sales-channels",
{
name: "test name 2",
description: "test description 2",
},
adminHeaders
)
).data.sales_channel
})
describe("GET /admin/sales-channels/:id", () => {
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 () => {
const response = await api.get(
`/admin/sales-channels/${salesChannel.id}`,
`/admin/sales-channels/${salesChannel1.id}`,
adminHeaders
)
@@ -42,8 +50,8 @@ medusaIntegrationTestRunner({
expect(response.data.sales_channel).toEqual(
expect.objectContaining({
id: expect.any(String),
name: salesChannel.name,
description: salesChannel.description,
name: salesChannel1.name,
description: salesChannel1.description,
created_at: expect.any(String),
updated_at: expect.any(String),
})
@@ -52,33 +60,6 @@ medusaIntegrationTestRunner({
})
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 () => {
const response = await api.get(`/admin/sales-channels`, adminHeaders)
@@ -183,21 +164,6 @@ medusaIntegrationTestRunner({
})
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 () => {
const payload = {
name: "updated name",
@@ -206,7 +172,7 @@ medusaIntegrationTestRunner({
}
const response = await api.post(
`/admin/sales-channels/${sc.id}`,
`/admin/sales-channels/${salesChannel1.id}`,
payload,
adminHeaders
)
@@ -279,46 +245,19 @@ medusaIntegrationTestRunner({
})
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 () => {
const toDelete = (
await api.get(
`/admin/sales-channels/${salesChannel.id}`,
`/admin/sales-channels/${salesChannel1.id}`,
adminHeaders
)
).data.sales_channel
expect(toDelete.id).toEqual(salesChannel.id)
expect(toDelete.id).toEqual(salesChannel1.id)
expect(toDelete.deleted_at).toEqual(null)
const response = await api.delete(
`/admin/sales-channels/${salesChannel.id}`,
`/admin/sales-channels/${salesChannel1.id}`,
adminHeaders
)
@@ -331,13 +270,13 @@ medusaIntegrationTestRunner({
await api
.get(
`/admin/sales-channels/${salesChannel.id}?fields=id,deleted_at`,
`/admin/sales-channels/${salesChannel1.id}?fields=id,deleted_at`,
adminHeaders
)
.catch((err) => {
expect(err.response.data.type).toEqual("not_found")
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(
`/admin/stock-locations/${location.id}/sales-channels`,
{
add: [salesChannel.id, salesChannel2.id],
add: [salesChannel1.id, salesChannel2.id],
},
adminHeaders
)
await api.delete(
`/admin/sales-channels/${salesChannel.id}`,
`/admin/sales-channels/${salesChannel1.id}`,
adminHeaders
)
@@ -381,21 +320,8 @@ medusaIntegrationTestRunner({
// BREAKING CHANGE: Endpoint has changed
// from: /admin/sales-channels/:id/products/batch
// to: /admin/sales-channels/:id/products
let salesChannel
let product
beforeEach(async () => {
salesChannel = (
await api.post(
"/admin/sales-channels",
{
name: "test name",
description: "test description",
},
adminHeaders
)
).data.sales_channel
product = (
await api.post(
"/admin/products",
@@ -409,7 +335,7 @@ medusaIntegrationTestRunner({
it("should add products to a sales channel", async () => {
const response = await api.post(
`/admin/sales-channels/${salesChannel.id}/products`,
`/admin/sales-channels/${salesChannel1.id}/products`,
{ add: [product.id] },
adminHeaders
)
@@ -449,7 +375,7 @@ medusaIntegrationTestRunner({
it("should remove products from a sales channel", async () => {
await api.post(
`/admin/sales-channels/${salesChannel.id}/products`,
`/admin/sales-channels/${salesChannel1.id}/products`,
{ add: [product.id] },
adminHeaders
)
@@ -474,7 +400,7 @@ medusaIntegrationTestRunner({
)
const response = await api.post(
`/admin/sales-channels/${salesChannel.id}/products`,
`/admin/sales-channels/${salesChannel1.id}/products`,
{ remove: [product.id] },
adminHeaders
)
@@ -499,7 +425,50 @@ medusaIntegrationTestRunner({
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
// - Mutation sales channels on products
// - POST /admin/products

View File

@@ -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",
}),
])
})
})
})
},
})