diff --git a/packages/medusa/src/api/routes/admin/products/__tests__/add-option.js b/packages/medusa/src/api/routes/admin/products/__tests__/add-option.js new file mode 100644 index 0000000000..3c25957fb7 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/products/__tests__/add-option.js @@ -0,0 +1,43 @@ +import { IdMap } from "medusa-test-utils" +import { request } from "../../../../../helpers/test-request" +import { ProductServiceMock } from "../../../../../services/__mocks__/product" + +describe("POST /admin/products/:id/options", () => { + describe("successful add option", () => { + let subject + + beforeAll(async () => { + subject = await request( + "POST", + `/admin/products/${IdMap.getId("productWithOptions")}/options`, + { + payload: { + optionTitle: "Test option", + }, + adminSession: { + jwt: { + userId: IdMap.getId("admin_user"), + }, + }, + } + ) + }) + + it("returns 200", () => { + expect(subject.status).toEqual(200) + }) + + it("calls service addOption", () => { + expect(ProductServiceMock.addOption).toHaveBeenCalledTimes(1) + expect(ProductServiceMock.addOption).toHaveBeenCalledWith( + IdMap.getId("productWithOptions"), + "Test option" + ) + }) + + it("returns the updated product decorated", () => { + expect(subject.body._id).toEqual(IdMap.getId("productWithOptions")) + expect(subject.body.decorated).toEqual(true) + }) + }) +}) diff --git a/packages/medusa/src/api/routes/admin/products/__tests__/add-variant.js b/packages/medusa/src/api/routes/admin/products/__tests__/add-variant.js new file mode 100644 index 0000000000..49137a1d0a --- /dev/null +++ b/packages/medusa/src/api/routes/admin/products/__tests__/add-variant.js @@ -0,0 +1,42 @@ +import { IdMap } from "medusa-test-utils" +import { request } from "../../../../../helpers/test-request" +import { ProductServiceMock } from "../../../../../services/__mocks__/product" + +describe("POST /admin/products/:id/variants/:variantId", () => { + describe("successful add variant", () => { + let subject + + beforeAll(async () => { + subject = await request( + "POST", + `/admin/products/${IdMap.getId( + "productWithOptions" + )}/variants/${IdMap.getId("variant2")}`, + { + adminSession: { + jwt: { + userId: IdMap.getId("admin_user"), + }, + }, + } + ) + }) + + it("returns 200", () => { + expect(subject.status).toEqual(200) + }) + + it("calls service addVariant", () => { + expect(ProductServiceMock.addVariant).toHaveBeenCalledTimes(1) + expect(ProductServiceMock.addVariant).toHaveBeenCalledWith( + IdMap.getId("productWithOptions"), + IdMap.getId("variant2") + ) + }) + + it("returns the updated product decorated", () => { + expect(subject.body._id).toEqual(IdMap.getId("productWithOptions")) + expect(subject.body.decorated).toEqual(true) + }) + }) +}) diff --git a/packages/medusa/src/api/routes/admin/products/__tests__/create-product.js b/packages/medusa/src/api/routes/admin/products/__tests__/create-product.js index a02e1bc200..b4c4695308 100644 --- a/packages/medusa/src/api/routes/admin/products/__tests__/create-product.js +++ b/packages/medusa/src/api/routes/admin/products/__tests__/create-product.js @@ -23,16 +23,12 @@ describe("POST /admin/products", () => { }) it("returns 200", () => { - expect(subject.status).toEqual(201) + expect(subject.status).toEqual(200) }) it("returns created product draft", () => { - expect(subject.body).toEqual({ - title: "Test Product", - description: "Test Description", - tags: "hi,med,dig", - handle: "test-product", - }) + expect(subject.body._id).toEqual(IdMap.getId("product1")) + expect(subject.body.decorated).toEqual(true) }) it("calls service createDraft", () => { diff --git a/packages/medusa/src/api/routes/admin/products/__tests__/delete-option.js b/packages/medusa/src/api/routes/admin/products/__tests__/delete-option.js new file mode 100644 index 0000000000..757eb17c1f --- /dev/null +++ b/packages/medusa/src/api/routes/admin/products/__tests__/delete-option.js @@ -0,0 +1,42 @@ +import { IdMap } from "medusa-test-utils" +import { request } from "../../../../../helpers/test-request" +import { ProductServiceMock } from "../../../../../services/__mocks__/product" + +describe("DELETE /admin/products/:id/options/:optionId", () => { + describe("successfully updates an option", () => { + let subject + + beforeAll(async () => { + subject = await request( + "DELETE", + `/admin/products/${IdMap.getId( + "productWithOptions" + )}/options/${IdMap.getId("option1")}`, + { + adminSession: { + jwt: { + userId: IdMap.getId("admin_user"), + }, + }, + } + ) + }) + + it("returns 200 and correct delete info", () => { + expect(subject.status).toEqual(200) + expect(subject.body).toEqual({ + optionId: IdMap.getId("option1"), + object: "option", + deleted: true, + }) + }) + + it("calls update", () => { + expect(ProductServiceMock.deleteOption).toHaveBeenCalledTimes(1) + expect(ProductServiceMock.deleteOption).toHaveBeenCalledWith( + IdMap.getId("productWithOptions"), + IdMap.getId("option1") + ) + }) + }) +}) diff --git a/packages/medusa/src/api/routes/admin/products/__tests__/delete-product.js b/packages/medusa/src/api/routes/admin/products/__tests__/delete-product.js new file mode 100644 index 0000000000..561551d5a3 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/products/__tests__/delete-product.js @@ -0,0 +1,46 @@ +import { IdMap } from "medusa-test-utils" +import { request } from "../../../../../helpers/test-request" +import { ProductServiceMock } from "../../../../../services/__mocks__/product" + +describe("DELETE /admin/products/:id", () => { + describe("successfully deletes a product", () => { + let subject + + beforeAll(async () => { + subject = await request( + "DELETE", + `/admin/products/${IdMap.getId("product1")}`, + { + adminSession: { + jwt: { + userId: IdMap.getId("admin_user"), + }, + }, + } + ) + }) + + afterAll(() => { + jest.clearAllMocks() + }) + + it("calls ProductService delete", () => { + expect(ProductServiceMock.delete).toHaveBeenCalledTimes(1) + expect(ProductServiceMock.delete).toHaveBeenCalledWith( + IdMap.getId("product1") + ) + }) + + it("returns 200", () => { + expect(subject.status).toEqual(200) + }) + + it("returns correct delete data", () => { + expect(subject.body).toEqual({ + id: IdMap.getId("product1"), + object: "product", + deleted: true, + }) + }) + }) +}) diff --git a/packages/medusa/src/api/routes/admin/products/__tests__/get-product.js b/packages/medusa/src/api/routes/admin/products/__tests__/get-product.js new file mode 100644 index 0000000000..e2f6b7fab5 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/products/__tests__/get-product.js @@ -0,0 +1,39 @@ +import { IdMap } from "medusa-test-utils" +import { request } from "../../../../../helpers/test-request" +import { ProductServiceMock } from "../../../../../services/__mocks__/product" + +describe("GET /admin/products/:id", () => { + describe("successfully gets a product", () => { + let subject + + beforeAll(async () => { + subject = await request( + "GET", + `/admin/products/${IdMap.getId("product1")}`, + { + adminSession: { + jwt: { + userId: IdMap.getId("admin_user"), + }, + }, + } + ) + }) + + afterAll(() => { + jest.clearAllMocks() + }) + + it("calls get product from productSerice", () => { + expect(ProductServiceMock.retrieve).toHaveBeenCalledTimes(1) + expect(ProductServiceMock.retrieve).toHaveBeenCalledWith( + IdMap.getId("product1") + ) + }) + + it("returns product decorated", () => { + expect(subject.body._id).toEqual(IdMap.getId("product1")) + expect(subject.body.decorated).toEqual(true) + }) + }) +}) diff --git a/packages/medusa/src/api/routes/admin/products/__tests__/list-products.js b/packages/medusa/src/api/routes/admin/products/__tests__/list-products.js new file mode 100644 index 0000000000..23ffb60262 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/products/__tests__/list-products.js @@ -0,0 +1,34 @@ +import { IdMap } from "medusa-test-utils" +import { request } from "../../../../../helpers/test-request" +import { + ProductServiceMock, + products, +} from "../../../../../services/__mocks__/product" + +describe("GET /admin/products", () => { + describe("successfully lists products", () => { + let subject + + beforeAll(async () => { + subject = await request("GET", `/admin/products`, { + adminSession: { + jwt: { + userId: IdMap.getId("admin_user"), + }, + }, + }) + }) + + it("returns 200 and decorated products", () => { + expect(subject.status).toEqual(200) + expect(subject.body[0]._id).toEqual(products.product1._id) + expect(subject.body[0].decorated).toEqual(true) + expect(subject.body[1]._id).toEqual(products.product2._id) + expect(subject.body[1].decorated).toEqual(true) + }) + + it("calls update", () => { + expect(ProductServiceMock.list).toHaveBeenCalledTimes(1) + }) + }) +}) diff --git a/packages/medusa/src/api/routes/admin/products/__tests__/publish-product.js b/packages/medusa/src/api/routes/admin/products/__tests__/publish-product.js new file mode 100644 index 0000000000..6e22c7e4b3 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/products/__tests__/publish-product.js @@ -0,0 +1,38 @@ +import { IdMap } from "medusa-test-utils" +import { request } from "../../../../../helpers/test-request" +import { ProductServiceMock } from "../../../../../services/__mocks__/product" + +describe("POST /admin/products/:id/publish", () => { + describe("successful publish", () => { + let subject + + beforeAll(async () => { + subject = await request( + "POST", + `/admin/products/${IdMap.getId("publish")}/publish`, + { + adminSession: { + jwt: { + userId: IdMap.getId("admin_user"), + }, + }, + } + ) + }) + + it("returns 200", () => { + expect(subject.status).toEqual(200) + }) + + it("returns product with published flag true", () => { + expect(subject.body.published).toEqual(true) + }) + + it("calls service publish", () => { + expect(ProductServiceMock.publish).toHaveBeenCalledTimes(1) + expect(ProductServiceMock.publish).toHaveBeenCalledWith( + IdMap.getId("publish") + ) + }) + }) +}) diff --git a/packages/medusa/src/api/routes/admin/products/__tests__/remove-variant.js b/packages/medusa/src/api/routes/admin/products/__tests__/remove-variant.js new file mode 100644 index 0000000000..4147b59d0d --- /dev/null +++ b/packages/medusa/src/api/routes/admin/products/__tests__/remove-variant.js @@ -0,0 +1,42 @@ +import { IdMap } from "medusa-test-utils" +import { request } from "../../../../../helpers/test-request" +import { ProductServiceMock } from "../../../../../services/__mocks__/product" + +describe("POST /admin/products/:id/variants/:variantId", () => { + describe("successful removes variant", () => { + let subject + + beforeAll(async () => { + subject = await request( + "DELETE", + `/admin/products/${IdMap.getId( + "productWithOptions" + )}/variants/${IdMap.getId("variant1")}`, + { + adminSession: { + jwt: { + userId: IdMap.getId("admin_user"), + }, + }, + } + ) + }) + + it("returns 200", () => { + expect(subject.status).toEqual(200) + }) + + it("calls service removeVariant", () => { + expect(ProductServiceMock.removeVariant).toHaveBeenCalledTimes(1) + expect(ProductServiceMock.removeVariant).toHaveBeenCalledWith( + IdMap.getId("productWithOptions"), + IdMap.getId("variant1") + ) + }) + + it("returns decorated product with variant removed", () => { + expect(subject.body._id).toEqual(IdMap.getId("productWithOptions")) + expect(subject.body.decorated).toEqual(true) + }) + }) +}) diff --git a/packages/medusa/src/api/routes/admin/products/__tests__/update-option.js b/packages/medusa/src/api/routes/admin/products/__tests__/update-option.js new file mode 100644 index 0000000000..333774a0f3 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/products/__tests__/update-option.js @@ -0,0 +1,43 @@ +import { IdMap } from "medusa-test-utils" +import { request } from "../../../../../helpers/test-request" +import { ProductServiceMock } from "../../../../../services/__mocks__/product" + +describe("POST /admin/products/:id/options/:optionId", () => { + describe("successfully updates an option", () => { + let subject + + beforeAll(async () => { + subject = await request( + "POST", + `/admin/products/${IdMap.getId( + "productWithOptions" + )}/options/${IdMap.getId("option1")}`, + { + payload: { + title: "Updated option title", + }, + adminSession: { + jwt: { + userId: IdMap.getId("admin_user"), + }, + }, + } + ) + }) + + it("returns 200", () => { + expect(subject.status).toEqual(200) + }) + + it("calls update", () => { + expect(ProductServiceMock.updateOption).toHaveBeenCalledTimes(1) + expect(ProductServiceMock.updateOption).toHaveBeenCalledWith( + IdMap.getId("productWithOptions"), + IdMap.getId("option1"), + { + title: "Updated option title", + } + ) + }) + }) +}) diff --git a/packages/medusa/src/api/routes/admin/products/__tests__/update-product.js b/packages/medusa/src/api/routes/admin/products/__tests__/update-product.js new file mode 100644 index 0000000000..47980eed32 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/products/__tests__/update-product.js @@ -0,0 +1,90 @@ +import { IdMap } from "medusa-test-utils" +import { request } from "../../../../../helpers/test-request" +import { ProductServiceMock } from "../../../../../services/__mocks__/product" + +describe("POST /admin/products/:id", () => { + describe("successfully updates a product", () => { + let subject + + beforeAll(async () => { + subject = await request( + "POST", + `/admin/products/${IdMap.getId("product1")}`, + { + payload: { + title: "Product 1", + description: "Updated test description", + handle: "handle", + }, + adminSession: { + jwt: { + userId: IdMap.getId("admin_user"), + }, + }, + } + ) + }) + + it("returns 200", () => { + expect(subject.status).toEqual(200) + }) + + it("calls update", () => { + expect(ProductServiceMock.update).toHaveBeenCalledTimes(1) + expect(ProductServiceMock.update).toHaveBeenCalledWith( + IdMap.getId("product1"), + { + title: "Product 1", + description: "Updated test description", + handle: "handle", + } + ) + }) + }) + + describe("handles failed update operation", () => { + it("throws if metadata is to be updated", async () => { + try { + await request("POST", `/admin/products/${IdMap.getId("product1")}`, { + payload: { + _id: IdMap.getId("product1"), + title: "Product 1", + metadata: "Test Description", + }, + adminSession: { + jwt: { + userId: IdMap.getId("admin_user"), + }, + }, + }) + } catch (error) { + expect(error.status).toEqual(400) + expect(error.message).toEqual( + "Use setMetadata to update metadata fields" + ) + } + }) + + it("throws if variants is to be updated", async () => { + try { + await request("POST", `/admin/products/${IdMap.getId("product1")}`, { + payload: { + _id: IdMap.getId("product1"), + title: "Product 1", + metadata: "Test Description", + }, + adminSession: { + jwt: { + userId: IdMap.getId("admin_user"), + }, + }, + }) + } catch (error) { + expect(error.status).toEqual(400) + expect(error.message).toEqual( + "Use addVariant, reorderVariants, removeVariant to update Product Variants" + ) + } + }) + }) +}) diff --git a/packages/medusa/src/api/routes/admin/products/add-option.js b/packages/medusa/src/api/routes/admin/products/add-option.js new file mode 100644 index 0000000000..82d25098b9 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/products/add-option.js @@ -0,0 +1,34 @@ +import { MedusaError, Validator } from "medusa-core-utils" + +export default async (req, res) => { + const { id } = req.params + + const schema = Validator.object().keys({ + optionTitle: Validator.string().required(), + }) + const { value, error } = schema.validate(req.body) + if (error) { + throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details) + } + + try { + const productService = req.scope.resolve("productService") + const product = await productService.retrieve(id) + await productService.addOption(product._id, value.optionTitle) + let newProduct = await productService.retrieve(product._id) + newProduct = await productService.decorate(newProduct, [ + "title", + "description", + "tags", + "handle", + "images", + "options", + "variants", + "published", + ]) + res.json(newProduct) + } catch (err) { + console.log(err) + throw err + } +} diff --git a/packages/medusa/src/api/routes/admin/products/add-variant.js b/packages/medusa/src/api/routes/admin/products/add-variant.js new file mode 100644 index 0000000000..c427d0fd00 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/products/add-variant.js @@ -0,0 +1,23 @@ +export default async (req, res) => { + const { id, variantId } = req.params + + try { + const productService = req.scope.resolve("productService") + const product = await productService.retrieve(id) + await productService.addVariant(product._id, variantId) + let newProduct = await productService.retrieve(product._id) + newProduct = await productService.decorate(newProduct, [ + "title", + "description", + "tags", + "handle", + "images", + "options", + "variants", + "published", + ]) + res.json(newProduct) + } catch (err) { + throw err + } +} diff --git a/packages/medusa/src/api/routes/admin/products/create-product.js b/packages/medusa/src/api/routes/admin/products/create-product.js index 901a29b48f..84cc36a596 100644 --- a/packages/medusa/src/api/routes/admin/products/create-product.js +++ b/packages/medusa/src/api/routes/admin/products/create-product.js @@ -18,10 +18,18 @@ export default async (req, res) => { try { const productService = req.scope.resolve("productService") - const data = await productService.createDraft(value) - - // Return the created product draft - res.status(201).json(data) + let newProduct = await productService.createDraft(value) + newProduct = await productService.decorate(newProduct, [ + "title", + "description", + "tags", + "handle", + "images", + "options", + "variants", + "published", + ]) + res.json(newProduct) } catch (err) { throw err } diff --git a/packages/medusa/src/api/routes/admin/products/delete-option.js b/packages/medusa/src/api/routes/admin/products/delete-option.js new file mode 100644 index 0000000000..33911faa91 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/products/delete-option.js @@ -0,0 +1,15 @@ +export default async (req, res) => { + const { id, optionId } = req.params + + try { + const productService = req.scope.resolve("productService") + await productService.deleteOption(id, optionId) + res.json({ + optionId, + object: "option", + deleted: true, + }) + } catch (err) { + throw err + } +} diff --git a/packages/medusa/src/api/routes/admin/products/delete-product.js b/packages/medusa/src/api/routes/admin/products/delete-product.js new file mode 100644 index 0000000000..975f6b26fd --- /dev/null +++ b/packages/medusa/src/api/routes/admin/products/delete-product.js @@ -0,0 +1,15 @@ +export default async (req, res) => { + const { id } = req.params + + try { + const productService = req.scope.resolve("productService") + await productService.delete(id) + res.json({ + id, + object: "product", + deleted: true, + }) + } catch (err) { + throw err + } +} diff --git a/packages/medusa/src/api/routes/admin/products/get-product.js b/packages/medusa/src/api/routes/admin/products/get-product.js new file mode 100644 index 0000000000..2a5a7ad8db --- /dev/null +++ b/packages/medusa/src/api/routes/admin/products/get-product.js @@ -0,0 +1,19 @@ +export default async (req, res) => { + const { id } = req.params + + const productService = req.scope.resolve("productService") + let product = await productService.retrieve(id) + + product = await productService.decorate(product, [ + "title", + "description", + "tags", + "handle", + "images", + "options", + "variants", + "published", + ]) + + res.json(product) +} diff --git a/packages/medusa/src/api/routes/admin/products/index.js b/packages/medusa/src/api/routes/admin/products/index.js index 6b67d36c0b..90782013bc 100644 --- a/packages/medusa/src/api/routes/admin/products/index.js +++ b/packages/medusa/src/api/routes/admin/products/index.js @@ -7,8 +7,33 @@ export default app => { app.use("/products", route) route.post("/", middlewares.wrap(require("./create-product").default)) + route.post("/:id", middlewares.wrap(require("./update-product").default)) + route.post( + "/:id/publish", + middlewares.wrap(require("./publish-product").default) + ) + route.post( + "/:id/variants/:variantId", + middlewares.wrap(require("./add-variant").default) + ) + route.post( + "/:id/options/:optionId", + middlewares.wrap(require("./update-option").default) + ) + route.post("/:id/options", middlewares.wrap(require("./add-option").default)) - // route.get("/:productId", middlewares.wrap(require("./get-product").default)) + route.delete( + "/:id/variants/:variantId", + middlewares.wrap(require("./remove-variant").default) + ) + route.delete("/:id", middlewares.wrap(require("./delete-product").default)) + route.delete( + "/:id/options/:optionId", + middlewares.wrap(require("./delete-option").default) + ) + + route.get("/:id", middlewares.wrap(require("./get-product").default)) + route.get("/", middlewares.wrap(require("./list-products").default)) return app } diff --git a/packages/medusa/src/api/routes/admin/products/list-products.js b/packages/medusa/src/api/routes/admin/products/list-products.js new file mode 100644 index 0000000000..26d6be1d25 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/products/list-products.js @@ -0,0 +1,24 @@ +export default async (req, res) => { + try { + const productService = req.scope.resolve("productService") + let products = await productService.list({}) + products = await Promise.all( + products.map( + async product => + await productService.decorate(product, [ + "title", + "description", + "tags", + "handle", + "images", + "options", + "variants", + "published", + ]) + ) + ) + res.json(products) + } catch (error) { + throw error + } +} diff --git a/packages/medusa/src/api/routes/admin/products/publish-product.js b/packages/medusa/src/api/routes/admin/products/publish-product.js new file mode 100644 index 0000000000..7f622e0c4c --- /dev/null +++ b/packages/medusa/src/api/routes/admin/products/publish-product.js @@ -0,0 +1,23 @@ +export default async (req, res) => { + const { id } = req.params + + try { + const productService = req.scope.resolve("productService") + const product = await productService.retrieve(id) + await productService.publish(product._id) + let publishedProduct = await productService.retrieve(product._id) + publishedProduct = await productService.decorate(publishedProduct, [ + "title", + "description", + "tags", + "handle", + "images", + "options", + "variants", + "published", + ]) + res.json(publishedProduct) + } catch (error) { + throw error + } +} diff --git a/packages/medusa/src/api/routes/admin/products/remove-variant.js b/packages/medusa/src/api/routes/admin/products/remove-variant.js new file mode 100644 index 0000000000..d91212de82 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/products/remove-variant.js @@ -0,0 +1,22 @@ +export default async (req, res) => { + const { id, variantId } = req.params + + try { + const productService = req.scope.resolve("productService") + await productService.removeVariant(id, variantId) + let updatedProduct = await productService.retrieve(id) + updatedProduct = await productService.decorate(updatedProduct, [ + "title", + "description", + "tags", + "handle", + "images", + "options", + "variants", + "published", + ]) + res.json(updatedProduct) + } catch (err) { + throw err + } +} diff --git a/packages/medusa/src/api/routes/admin/products/update-option.js b/packages/medusa/src/api/routes/admin/products/update-option.js new file mode 100644 index 0000000000..2d214ab811 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/products/update-option.js @@ -0,0 +1,38 @@ +import { MedusaError, Validator } from "medusa-core-utils" + +export default async (req, res) => { + const { id, optionId } = req.params + + const schema = Validator.object().keys({ + title: Validator.string(), + values: Validator.array().items(), + }) + + const { value, error } = schema.validate(req.body) + if (error) { + throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details) + } + + try { + const productService = req.scope.resolve("productService") + const product = await productService.retrieve(id) + + await productService.updateOption(product._id, optionId, value) + + let newProduct = await productService.retrieve(product._id) + newProduct = await productService.decorate(newProduct, [ + "title", + "description", + "tags", + "handle", + "images", + "options", + "variants", + "published", + ]) + + res.json(newProduct) + } catch (err) { + throw err + } +} diff --git a/packages/medusa/src/api/routes/admin/products/update-product.js b/packages/medusa/src/api/routes/admin/products/update-product.js new file mode 100644 index 0000000000..91972b240e --- /dev/null +++ b/packages/medusa/src/api/routes/admin/products/update-product.js @@ -0,0 +1,44 @@ +import { MedusaError, Validator } from "medusa-core-utils" + +export default async (req, res) => { + const { id } = req.params + + const schema = Validator.object().keys({ + title: Validator.string().required(), + description: Validator.string().optional(), + tags: Validator.string().optional(), + handle: Validator.string().required(), + images: Validator.array() + .items(Validator.string()) + .optional(), + variants: Validator.array() + .items(Validator.string()) + .optional(), + metadata: Validator.object().optional(), + }) + + const { value, error } = schema.validate(req.body) + if (error) { + throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details) + } + + try { + const productService = req.scope.resolve("productService") + const oldProduct = await productService.retrieve(id) + await productService.update(oldProduct._id, value) + let newProduct = await productService.retrieve(oldProduct._id) + newProduct = await productService.decorate(newProduct, [ + "title", + "description", + "tags", + "handle", + "images", + "options", + "variants", + "published", + ]) + res.json(newProduct) + } catch (err) { + throw err + } +} diff --git a/packages/medusa/src/api/routes/store/products/__tests__/get-product.js b/packages/medusa/src/api/routes/store/products/__tests__/get-product.js index eb749c1ee1..64014cf697 100644 --- a/packages/medusa/src/api/routes/store/products/__tests__/get-product.js +++ b/packages/medusa/src/api/routes/store/products/__tests__/get-product.js @@ -1,40 +1,24 @@ import mongoose from "mongoose" import getProduct from "../get-product" +import { request } from "../../../../../helpers/test-request" +import { IdMap } from "medusa-test-utils" +import { ProductServiceMock } from "../../../../../services/__mocks__/product" describe("Get product by id", () => { - const testId = `${mongoose.Types.ObjectId("56cb91bdc3464f14678934ca")}` - const productServiceMock = { - retrieve: jest.fn().mockImplementation(id => { - if (id === testId) { - return Promise.resolve({ _id: id, title: "test" }) - } - return Promise.resolve(undefined) - }), - } - const reqMock = id => { - return { - params: { - productId: id, - }, - scope: { - resolve: jest.fn().mockImplementation(name => { - if (name === "productService") { - return productServiceMock - } - return undefined - }), - }, - } - } - - const resMock = { - sendStatus: jest.fn().mockReturnValue(), - json: jest.fn().mockReturnValue(), - } - describe("get product by id successfull", () => { + let subject beforeAll(async () => { - await getProduct(reqMock(testId), resMock) + subject = await request( + "GET", + `/admin/products/${IdMap.getId("product1")}`, + { + adminSession: { + jwt: { + userId: IdMap.getId("admin_user"), + }, + }, + } + ) }) afterAll(() => { @@ -42,52 +26,15 @@ describe("Get product by id", () => { }) it("calls get product from productSerice", () => { - expect(productServiceMock.retrieve).toHaveBeenCalledTimes(1) - expect(productServiceMock.retrieve).toHaveBeenCalledWith(testId) + expect(ProductServiceMock.retrieve).toHaveBeenCalledTimes(1) + expect(ProductServiceMock.retrieve).toHaveBeenCalledWith( + IdMap.getId("product1") + ) }) - it("calls res.json", () => { - expect(resMock.json).toHaveBeenCalledTimes(1) - expect(resMock.json).toHaveBeenCalledWith({ - _id: testId, - title: "test", - }) - }) - }) - - describe("returns 404 when product not found", () => { - beforeAll(async () => { - const id = mongoose.Types.ObjectId() - await getProduct(reqMock(`${id}`), resMock) - }) - - afterAll(() => { - jest.clearAllMocks() - }) - - it("return 404", () => { - expect(resMock.sendStatus).toHaveBeenCalledTimes(1) - expect(resMock.json).toHaveBeenCalledTimes(0) - expect(resMock.sendStatus).toHaveBeenCalledWith(404) - }) - }) - - describe("fails when validation fails", () => { - let res - beforeAll(async () => { - try { - await getProduct(reqMock(`not object id`), resMock) - } catch (err) { - res = err - } - }) - - afterAll(() => { - jest.clearAllMocks() - }) - - it("return 404", () => { - expect(res.name).toEqual("ValidationError") + it("returns product decorated", () => { + expect(subject.body._id).toEqual(IdMap.getId("product1")) + expect(subject.body.decorated).toEqual(true) }) }) }) diff --git a/packages/medusa/src/api/routes/store/products/get-product.js b/packages/medusa/src/api/routes/store/products/get-product.js index d9bd258892..94de2897a7 100644 --- a/packages/medusa/src/api/routes/store/products/get-product.js +++ b/packages/medusa/src/api/routes/store/products/get-product.js @@ -11,12 +11,18 @@ export default async (req, res) => { } const productService = req.scope.resolve("productService") - const product = await productService.retrieve(value) + let product = await productService.retrieve(value) - if (!product) { - res.sendStatus(404) - return - } + product = await productService.decorate(product, [ + "title", + "description", + "tags", + "handle", + "images", + "options", + "variants", + "published", + ]) res.json(product) } diff --git a/packages/medusa/src/services/__mocks__/product.js b/packages/medusa/src/services/__mocks__/product.js index 750704a60d..e8bdcfd3a9 100644 --- a/packages/medusa/src/services/__mocks__/product.js +++ b/packages/medusa/src/services/__mocks__/product.js @@ -1,32 +1,84 @@ import { IdMap } from "medusa-test-utils" +import { MedusaError } from "medusa-core-utils" export const products = { product1: { _id: IdMap.getId("product1"), - name: "Product 1", + title: "Product 1", + }, + publishProduct: { + _id: IdMap.getId("publish"), + title: "Product 1", + published: true, }, product2: { _id: IdMap.getId("product2"), - name: "Product 2", + title: "Product 2", + }, + productWithOptions: { + _id: IdMap.getId("productWithOptions"), + title: "Test", + variants: [IdMap.getId("variant1")], + options: [ + { + _id: IdMap.getId("option1"), + title: "Test", + values: [IdMap.getId("optionValue1")], + }, + ], }, } export const ProductServiceMock = { createDraft: jest.fn().mockImplementation(data => { - return Promise.resolve(data) + return Promise.resolve(products.product1) }), - retrieve: jest.fn().mockImplementation(id => { - if (id === IdMap.getId("validId")) { - return Promise.resolve({ _id: IdMap.getId("validId") }) - } - if (id === IdMap.getId("product1")) { + publish: jest.fn().mockImplementation(_ => { + return Promise.resolve({ + _id: IdMap.getId("publish"), + name: "Product 1", + published: true, + }) + }), + delete: jest.fn().mockImplementation(_ => { + return Promise.resolve() + }), + addVariant: jest.fn().mockImplementation((productId, variantId) => { + return Promise.resolve(products.productWithOptions) + }), + removeVariant: jest.fn().mockImplementation((productId, variantId) => { + return Promise.resolve(products.productWithOptions) + }), + decorate: jest.fn().mockImplementation((product, fields) => { + product.decorated = true + return product + }), + addOption: jest.fn().mockImplementation((productId, optionTitle) => { + return Promise.resolve(products.productWithOptions) + }), + updateOption: jest.fn().mockReturnValue(Promise.resolve()), + deleteOption: jest.fn().mockReturnValue(Promise.resolve()), + retrieve: jest.fn().mockImplementation(productId => { + if (productId === IdMap.getId("product1")) { return Promise.resolve(products.product1) } - if (id === IdMap.getId("product2")) { + if (productId === IdMap.getId("product2")) { return Promise.resolve(products.product2) } + if (productId === IdMap.getId("validId")) { + return Promise.resolve({ _id: IdMap.getId("validId") }) + } + if (productId === IdMap.getId("publish")) { + return Promise.resolve(products.publishProduct) + } + if (productId === IdMap.getId("productWithOptions")) { + return Promise.resolve(products.productWithOptions) + } return Promise.resolve(undefined) }), + update: jest.fn().mockImplementation((userId, data) => { + return Promise.resolve() + }), list: jest.fn().mockImplementation(data => { // Used to retrieve a product based on a variant id see // ProductVariantService.addOptionValue @@ -56,7 +108,10 @@ export const ProductServiceMock = { return Promise.resolve([]) } - return Promise.resolve([products.product1, products.product2]) + return Promise.resolve([ + { ...products.product1, decorated: true }, + { ...products.product2, decorated: true }, + ]) }), } diff --git a/packages/medusa/src/services/product.js b/packages/medusa/src/services/product.js index 5e92c9e63d..b5e2e5a255 100644 --- a/packages/medusa/src/services/product.js +++ b/packages/medusa/src/services/product.js @@ -379,7 +379,7 @@ class ProductService extends BaseService { ) } - const { title } = data + const { title, values } = data const titleExists = product.options.some( o => o.title.toUpperCase() === title.toUpperCase() )