Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(medusa,types): added store apis for products #7144

Merged
merged 15 commits into from Apr 29, 2024
6 changes: 6 additions & 0 deletions .changeset/khaki-mice-peel.md
@@ -0,0 +1,6 @@
---
"@medusajs/medusa": patch
"@medusajs/types": patch
---

feat(medusa,types): added store apis for products
332 changes: 332 additions & 0 deletions integration-tests/modules/__tests__/product/store/index.spec.ts
@@ -0,0 +1,332 @@
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@riqwan I know you wrote a lot of tests here, but does it make sense to actually make the integration-tests/api tests work instead? If you feel like it will take a lot of time I'm fine with doing it later on, but we really need to try and clean up the tests

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 points:

  • I'd like these tests to be run on the CI, which it won't with the api tests.
  • I don't get why we do that. Wouldn't it just be better to move over the test cases to modules and make it work for v2? We're not really writing more tests for v1, so we wouldn't be missing out much. We do the same everywhere else - v2 routes, v2 dashboard, why not the same here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@riqwan The idea is that, unless we intended a breaking change, the test scenarios in api are still valid and should work regardless if it is v1 or v2. Eg. for the product endpoints, we have very few breaking changes, and that gives us some confidence that we are compliant with v1, except for the intentional breaking changes.

But if you feel like that is not a valid point, then we should discuss and settle on a single approach, as currently we have a split between the two

import {
CreateProductDTO,
IPricingModuleService,
IProductModuleService,
ISalesChannelModuleService,
ProductDTO,
ProductVariantDTO,
} from "@medusajs/types"
import {
ContainerRegistrationKeys,
Modules,
ProductStatus,
} from "@medusajs/utils"
import { medusaIntegrationTestRunner } from "medusa-test-utils"
import { createAdminUser } from "../../../../helpers/create-admin-user"
import { createDefaultRuleTypes } from "../../../helpers/create-default-rule-types"
import { createVariantPriceSet } from "../../../helpers/create-variant-price-set"

jest.setTimeout(50000)

const env = { MEDUSA_FF_MEDUSA_V2: true }
const adminHeaders = { headers: { "x-medusa-access-token": "test_token" } }

async function createProductsWithVariants(
productModule: IProductModuleService,
productsData: CreateProductDTO
): Promise<[ProductDTO, ProductVariantDTO[]]> {
const { variants: variantsData, ...productData } = productsData

const [product] = await productModule.create([productData])
riqwan marked this conversation as resolved.
Show resolved Hide resolved
const variantsDataWithProductId = variantsData?.map((variantData) => {
return { ...variantData, product_id: product.id }
})

const variants = variantsDataWithProductId
? await productModule.createVariants(variantsDataWithProductId)
: []

return [product, variants]
}

medusaIntegrationTestRunner({
env,
testSuite: ({ dbConnection, getContainer, api }) => {
describe("Store: Products API", () => {
let appContainer
let product
let product2
let product3
let product4
let variant
let variant2
let variant3
let variant4
let pricingModule: IPricingModuleService
riqwan marked this conversation as resolved.
Show resolved Hide resolved
let productModule: IProductModuleService
let salesChannelModule: ISalesChannelModuleService

beforeAll(async () => {
appContainer = getContainer()
pricingModule = appContainer.resolve(ModuleRegistrationName.PRICING)
productModule = appContainer.resolve(ModuleRegistrationName.PRODUCT)
salesChannelModule = appContainer.resolve(
ModuleRegistrationName.SALES_CHANNEL
)
})

beforeEach(async () => {
await createAdminUser(dbConnection, adminHeaders, appContainer)
await createDefaultRuleTypes(appContainer)
})

describe("GET /store/products", () => {
beforeEach(async () => {
;[product, [variant]] = await createProductsWithVariants(
productModule,
{
title: "test product 1",
status: ProductStatus.PUBLISHED,
variants: [{ title: "test variant 1" }],
}
)
;[product2, [variant2]] = await createProductsWithVariants(
productModule,
{
title: "test product 2 uniquely",
status: ProductStatus.PUBLISHED,
variants: [{ title: "test variant 2" }],
}
)
;[product3, [variant3]] = await createProductsWithVariants(
productModule,
{
title: "product not in price list",
status: ProductStatus.PUBLISHED,
variants: [{ title: "test variant 3" }],
}
)
;[product4, [variant4]] = await createProductsWithVariants(
productModule,
{
title: "draft product",
status: ProductStatus.DRAFT,
variants: [{ title: "test variant 4" }],
}
)
})

it("should list all published products", async () => {
let response = await api.get(`/store/products`)

expect(response.status).toEqual(200)
expect(response.data.count).toEqual(3)
expect(response.data.products).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: product.id,
}),
expect.objectContaining({
id: product2.id,
}),
expect.objectContaining({
id: product3.id,
}),
])
)

response = await api.get(`/store/products?q=uniquely`)

expect(response.status).toEqual(200)
expect(response.data.count).toEqual(1)
expect(response.data.products).toEqual([
expect.objectContaining({
id: product2.id,
}),
])
})

it("should list all products for a sales channel", async () => {
const salesChannel = await salesChannelModule.create({
riqwan marked this conversation as resolved.
Show resolved Hide resolved
name: "sales channel test",
})

const remoteLink = appContainer.resolve(
ContainerRegistrationKeys.REMOTE_LINK
)

await remoteLink.create([
{
[Modules.PRODUCT]: { product_id: product.id },
[Modules.SALES_CHANNEL]: { sales_channel_id: salesChannel.id },
},
])

let response = await api.get(
`/store/products?sales_channel_id[]=${salesChannel.id}`
)

expect(response.status).toEqual(200)
expect(response.data.count).toEqual(1)
expect(response.data.products).toEqual([
expect.objectContaining({
id: product.id,
}),
])
})

it("should throw error when calculating prices without context", async () => {
let error = await api
.get(`/store/products?fields=*variants.prices`)
.catch((e) => e)

expect(error.response.status).toEqual(400)
expect(error.response.data).toEqual({
message:
"Pricing parameters (currency_code or region_id) are required to calculate prices",
type: "invalid_data",
})
})

it("should list products with prices when context is present", async () => {
await createVariantPriceSet({
container: appContainer,
variantId: variant.id,
prices: [{ amount: 3000, currency_code: "usd" }],
rules: [],
})

let response = await api.get(
`/store/products?fields=*variants.prices&currency_code=usd`
)

expect(response.status).toEqual(200)
expect(response.data.count).toEqual(3)
expect(response.data.products).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: product.id,
variants: [
expect.objectContaining({
price: {
id: expect.any(String),
is_calculated_price_price_list: false,
calculated_amount: 3000,
is_original_price_price_list: false,
original_amount: 3000,
currency_code: "usd",
calculated_price: {
id: expect.any(String),
price_list_id: null,
price_list_type: null,
min_quantity: null,
max_quantity: null,
},
original_price: {
id: expect.any(String),
price_list_id: null,
price_list_type: null,
min_quantity: null,
max_quantity: null,
},
},
}),
],
}),
expect.objectContaining({
id: product2.id,
variants: [
expect.objectContaining({
price: null,
}),
],
}),
expect.objectContaining({
id: product3.id,
}),
])
)
})
})

describe("GET /store/products/:id", () => {
beforeEach(async () => {
;[product, [variant]] = await createProductsWithVariants(
productModule,
{
title: "test product 1",
status: ProductStatus.PUBLISHED,
variants: [{ title: "test variant 1" }],
}
)
})

it("should retrieve product successfully", async () => {
let response = await api.get(`/store/products/${product.id}`)

expect(response.status).toEqual(200)
expect(response.data.product).toEqual(
expect.objectContaining({
id: product.id,
variants: [
expect.objectContaining({
id: expect.any(String),
}),
],
})
)
})

it("should throw error when calculating prices without context", async () => {
let error = await api
.get(`/store/products/${product.id}?fields=*variants.prices`)
.catch((e) => e)

expect(error.response.status).toEqual(400)
expect(error.response.data).toEqual({
message:
"Pricing parameters (currency_code or region_id) are required to calculate prices",
type: "invalid_data",
})
})

it("should get product with prices when context is present", async () => {
await createVariantPriceSet({
riqwan marked this conversation as resolved.
Show resolved Hide resolved
container: appContainer,
variantId: variant.id,
prices: [{ amount: 3000, currency_code: "usd" }],
rules: [],
})

let response = await api.get(
`/store/products/${product.id}?fields=*variants.prices&currency_code=usd`
)

expect(response.status).toEqual(200)
expect(response.data.product).toEqual(
expect.objectContaining({
id: product.id,
variants: [
expect.objectContaining({
price: {
id: expect.any(String),
is_calculated_price_price_list: false,
calculated_amount: 3000,
is_original_price_price_list: false,
original_amount: 3000,
currency_code: "usd",
calculated_price: {
id: expect.any(String),
price_list_id: null,
price_list_type: null,
min_quantity: null,
max_quantity: null,
},
original_price: {
id: expect.any(String),
price_list_id: null,
price_list_type: null,
min_quantity: null,
max_quantity: null,
},
},
}),
],
})
)
})
})
})
},
})