Adds ProductVariant endpoints (#59)

Adds endpoints for product variants with tests.

Closes #58
This commit is contained in:
Oliver Windall Juhl authored and GitHub committed 2020-05-14 11:05:26 +02:00
1 parent 809133fa2a
commit 420c1d8cdb
24 files changed
+732 -3

No files matched your search

@@ -36,7 +36,7 @@ export default (app, container) => {
shippingProfileRoutes(route)
discountRoutes(route)
orderRoutes(route)
// productVariantRoutes(route)
productVariantRoutes(route)
return app
}
@@ -0,0 +1,40 @@
import { IdMap } from "medusa-test-utils"
import { request } from "../../../../../helpers/test-request"
import { ProductVariantServiceMock } from "../../../../../services/__mocks__/product-variant"
describe("POST /admin/product-variants/:id/options", () => {
describe("successful add option value", () => {
let subject
beforeAll(async () => {
subject = await request(
"POST",
`/admin/product-variants/${IdMap.getId("testVariant")}/options`,
{
payload: {
optionId: IdMap.getId("testOption"),
optionValue: "test",
},
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
}
)
})
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("calls service addOption", () => {
expect(ProductVariantServiceMock.addOptionValue).toHaveBeenCalledTimes(1)
expect(ProductVariantServiceMock.addOptionValue).toHaveBeenCalledWith(
IdMap.getId("testVariant"),
IdMap.getId("testOption"),
"test"
)
})
})
})
@@ -0,0 +1,65 @@
import { IdMap } from "medusa-test-utils"
import { request } from "../../../../../helpers/test-request"
import { ProductVariantServiceMock } from "../../../../../services/__mocks__/product-variant"
describe("POST /admin/product-variants", () => {
describe("successful creation", () => {
let subject
beforeAll(async () => {
subject = await request("POST", "/admin/product-variants", {
payload: {
title: "Test Product Variant",
prices: [{}],
},
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
})
})
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("returns created product draft", () => {
expect(subject.body._id).toEqual(IdMap.getId("testVariant"))
})
it("calls service createDraft", () => {
expect(ProductVariantServiceMock.createDraft).toHaveBeenCalledTimes(1)
expect(ProductVariantServiceMock.createDraft).toHaveBeenCalledWith({
title: "Test Product Variant",
prices: [{}],
})
})
})
describe("invalid data returns error details", () => {
let subject
beforeAll(async () => {
subject = await request("POST", "/admin/products", {
payload: {
image: "image",
},
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
})
})
it("returns 400", () => {
expect(subject.status).toEqual(400)
})
it("returns error details", () => {
expect(subject.body.name).toEqual("invalid_data")
expect(subject.body.message[0].message).toEqual(`"title" is required`)
})
})
})
@@ -0,0 +1,39 @@
import { IdMap } from "medusa-test-utils"
import { request } from "../../../../../helpers/test-request"
import { ProductVariantServiceMock } from "../../../../../services/__mocks__/product-variant"
describe("DELETE /admin/product-variants/:id/options", () => {
describe("successfully deletes option value", () => {
let subject
beforeAll(async () => {
subject = await request(
"DELETE",
`/admin/product-variants/${IdMap.getId("testVariant")}/options`,
{
payload: {
optionId: IdMap.getId("testOption"),
},
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
}
)
})
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("calls service deleteOptionValue", () => {
expect(ProductVariantServiceMock.deleteOptionValue).toHaveBeenCalledTimes(
1
)
expect(ProductVariantServiceMock.deleteOptionValue).toHaveBeenCalledWith(
IdMap.getId("testVariant"),
IdMap.getId("testOption")
)
})
})
})
@@ -0,0 +1,46 @@
import { IdMap } from "medusa-test-utils"
import { request } from "../../../../../helpers/test-request"
import { ProductVariantServiceMock } from "../../../../../services/__mocks__/product-variant"
describe("DELETE /admin/product-variants/:id", () => {
describe("successfully deletes a product variant", () => {
let subject
beforeAll(async () => {
subject = await request(
"DELETE",
`/admin/product-variants/${IdMap.getId("testVariant")}`,
{
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
}
)
})
afterAll(() => {
jest.clearAllMocks()
})
it("calls ProductVariantService delete", () => {
expect(ProductVariantServiceMock.delete).toHaveBeenCalledTimes(1)
expect(ProductVariantServiceMock.delete).toHaveBeenCalledWith(
IdMap.getId("testVariant")
)
})
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("returns correct delete data", () => {
expect(subject.body).toEqual({
id: IdMap.getId("testVariant"),
object: "productVariant",
deleted: true,
})
})
})
})
@@ -0,0 +1,38 @@
import { IdMap } from "medusa-test-utils"
import { request } from "../../../../../helpers/test-request"
import { ProductVariantServiceMock } from "../../../../../services/__mocks__/product-variant"
describe("GET /admin/product-variants/:id", () => {
describe("successfully gets a product variant", () => {
let subject
beforeAll(async () => {
subject = await request(
"GET",
`/admin/product-variants/${IdMap.getId("testVariant")}`,
{
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
}
)
})
afterAll(() => {
jest.clearAllMocks()
})
it("calls productVariantService retrieve", () => {
expect(ProductVariantServiceMock.retrieve).toHaveBeenCalledTimes(1)
expect(ProductVariantServiceMock.retrieve).toHaveBeenCalledWith(
IdMap.getId("testVariant")
)
})
it("returns product", () => {
expect(subject.body._id).toEqual(IdMap.getId("testVariant"))
})
})
})
@@ -0,0 +1,28 @@
import { IdMap } from "medusa-test-utils"
import { request } from "../../../../../helpers/test-request"
import { ProductVariantServiceMock } from "../../../../../services/__mocks__/product-variant"
describe("GET /admin/product-variants", () => {
describe("successfully lists product variants", () => {
let subject
beforeAll(async () => {
subject = await request("GET", `/admin/product-variants`, {
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
})
})
it("calls ProductVariantService list", () => {
expect(ProductVariantServiceMock.list).toHaveBeenCalledTimes(1)
})
it("returns 200 and product variants", () => {
expect(subject.status).toEqual(200)
expect(subject.body[0]._id).toEqual(IdMap.getId("testVariant"))
})
})
})
@@ -0,0 +1,38 @@
import { IdMap } from "medusa-test-utils"
import { request } from "../../../../../helpers/test-request"
import { ProductVariantServiceMock } from "../../../../../services/__mocks__/product-variant"
describe("POST /admin/product-variants/:id/publish", () => {
describe("successful publish", () => {
let subject
beforeAll(async () => {
subject = await request(
"POST",
`/admin/product-variants/${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(ProductVariantServiceMock.publish).toHaveBeenCalledTimes(1)
expect(ProductVariantServiceMock.publish).toHaveBeenCalledWith(
IdMap.getId("publish")
)
})
})
})
@@ -0,0 +1,42 @@
import { IdMap } from "medusa-test-utils"
import { request } from "../../../../../helpers/test-request"
import { ProductVariantServiceMock } from "../../../../../services/__mocks__/product-variant"
describe("POST /admin/product-variants/:id/currency-price", () => {
describe("successful sets currency price", () => {
let subject
beforeAll(async () => {
subject = await request(
"POST",
`/admin/product-variants/${IdMap.getId("testVariant")}/currency-price`,
{
payload: {
currencyCode: "DKK",
amount: 100,
},
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
}
)
})
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("calls service setCurrencyPrice", () => {
expect(ProductVariantServiceMock.setCurrencyPrice).toHaveBeenCalledTimes(
1
)
expect(ProductVariantServiceMock.setCurrencyPrice).toHaveBeenCalledWith(
IdMap.getId("testVariant"),
"DKK",
100
)
})
})
})
@@ -0,0 +1,40 @@
import { IdMap } from "medusa-test-utils"
import { request } from "../../../../../helpers/test-request"
import { ProductVariantServiceMock } from "../../../../../services/__mocks__/product-variant"
describe("POST /admin/product-variants/:id/region-price", () => {
describe("successfully sets region price", () => {
let subject
beforeAll(async () => {
subject = await request(
"POST",
`/admin/product-variants/${IdMap.getId("testVariant")}/region-price`,
{
payload: {
regionId: IdMap.getId("region-fr"),
amount: 100,
},
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
}
)
})
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("calls service setCurrencyPrice", () => {
expect(ProductVariantServiceMock.setRegionPrice).toHaveBeenCalledTimes(1)
expect(ProductVariantServiceMock.setRegionPrice).toHaveBeenCalledWith(
IdMap.getId("testVariant"),
IdMap.getId("region-fr"),
100
)
})
})
})
@@ -0,0 +1,70 @@
import { IdMap } from "medusa-test-utils"
import { request } from "../../../../../helpers/test-request"
import { ProductVariantServiceMock } from "../../../../../services/__mocks__/product-variant"
describe("POST /admin/product-variants/:id", () => {
describe("successful update", () => {
let subject
beforeAll(async () => {
subject = await request(
"POST",
`/admin/product-variants/${IdMap.getId("testVariant")}`,
{
payload: {
title: "Test Product Variant Updated",
prices: [{}],
},
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
}
)
})
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("calls service update", () => {
expect(ProductVariantServiceMock.update).toHaveBeenCalledTimes(1)
expect(ProductVariantServiceMock.update).toHaveBeenCalledWith(
IdMap.getId("testVariant"),
{
title: "Test Product Variant Updated",
prices: [{}],
}
)
})
})
describe("handles failed update operation", () => {
it("throws if metadata is to be updated", async () => {
try {
await request(
"POST",
`/admin/product-variants/${IdMap.getId("testVariant")}`,
{
payload: {
_id: IdMap.getId("testVariant"),
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"
)
}
})
})
})
@@ -0,0 +1,28 @@
import { MedusaError, Validator } from "medusa-core-utils"
export default async (req, res) => {
const { id } = req.params
const schema = Validator.object().keys({
optionId: Validator.objectId().required(),
optionValue: Validator.string().required(),
})
const { value, error } = schema.validate(req.body)
if (error) {
throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details)
}
try {
const productVariantService = req.scope.resolve("productVariantService")
const productVariant = await productVariantService.addOptionValue(
id,
value.optionId,
value.optionValue
)
res.status(200).json(productVariant)
} catch (err) {
throw err
}
}
@@ -0,0 +1,30 @@
import { MedusaError, Validator } from "medusa-core-utils"
export default async (req, res) => {
const schema = Validator.object().keys({
title: Validator.string().required(),
prices: Validator.array()
.items({})
.required(),
options: Validator.array().items({}),
image: Validator.string().optional(),
inventory_quantity: Validator.number().optional(),
allow_backorder: Validator.boolean().optional(),
manage_inventory: Validator.boolean().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 productVariantService = req.scope.resolve("productVariantService")
const productVariant = await productVariantService.createDraft(value)
res.status(200).json(productVariant)
} catch (err) {
throw err
}
}
@@ -0,0 +1,26 @@
import { MedusaError, Validator } from "medusa-core-utils"
export default async (req, res) => {
const { id } = req.params
const schema = Validator.object().keys({
optionId: Validator.objectId().required(),
})
const { value, error } = schema.validate(req.body)
if (error) {
throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details)
}
try {
const productVariantService = req.scope.resolve("productVariantService")
const productVariant = await productVariantService.deleteOptionValue(
id,
value.optionId
)
res.status(200).json(productVariant)
} catch (err) {
throw err
}
}
@@ -0,0 +1,16 @@
export default async (req, res) => {
const { id } = req.params
try {
const productVariantService = req.scope.resolve("productVariantService")
await productVariantService.delete(id)
res.json({
id: id,
object: "productVariant",
deleted: true,
})
} catch (err) {
throw err
}
}
@@ -0,0 +1,12 @@
export default async (req, res) => {
const { id } = req.params
try {
const productVariantService = req.scope.resolve("productVariantService")
const productVariant = await productVariantService.retrieve(id)
res.json(productVariant)
} catch (error) {
throw error
}
}
@@ -4,5 +4,46 @@ import middlewares from "../../../middlewares"
const route = Router()
export default app => {
app.use("/product-variants", route)
route.post("/", middlewares.wrap(require("./create-product-variant").default))
route.post(
"/:id",
middlewares.wrap(require("./update-product-variant").default)
)
route.post(
"/:id/publish",
middlewares.wrap(require("./publish-product-variant").default)
)
route.post(
"/:id/currency-price",
middlewares.wrap(require("./set-currency-price").default)
)
route.post(
"/:id/region-price",
middlewares.wrap(require("./set-region-price").default)
)
route.post(
"/:id/options",
middlewares.wrap(require("./add-option-value").default)
)
route.delete(
"/:id/options",
middlewares.wrap(require("./delete-option-value").default)
)
route.delete(
"/:id",
middlewares.wrap(require("./delete-product-variant").default)
)
route.get("/:id", middlewares.wrap(require("./get-product-variant").default))
route.get("/", middlewares.wrap(require("./list-product-variants").default))
return app
}
@@ -0,0 +1,10 @@
export default async (req, res) => {
try {
const productVariantService = req.scope.resolve("productVariantService")
const productVariants = await productVariantService.list({})
res.json(productVariants)
} catch (error) {
throw error
}
}
@@ -0,0 +1,12 @@
export default async (req, res) => {
const { id } = req.params
try {
const productVariantService = req.scope.resolve("productVariantService")
const productVariant = await productVariantService.publish(id)
res.json(productVariant)
} catch (error) {
throw error
}
}
@@ -0,0 +1,28 @@
import { MedusaError, Validator } from "medusa-core-utils"
export default async (req, res) => {
const { id } = req.params
const schema = Validator.object().keys({
currencyCode: Validator.string().required(),
amount: Validator.number().required(),
})
const { value, error } = schema.validate(req.body)
if (error) {
throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details)
}
try {
const productVariantService = req.scope.resolve("productVariantService")
const productVariant = await productVariantService.setCurrencyPrice(
id,
value.currencyCode,
value.amount
)
res.status(200).json(productVariant)
} catch (err) {
throw err
}
}
@@ -0,0 +1,28 @@
import { MedusaError, Validator } from "medusa-core-utils"
export default async (req, res) => {
const { id } = req.params
const schema = Validator.object().keys({
regionId: Validator.objectId().required(),
amount: Validator.number().required(),
})
const { value, error } = schema.validate(req.body)
if (error) {
throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details)
}
try {
const productVariantService = req.scope.resolve("productVariantService")
const productVariant = await productVariantService.setRegionPrice(
id,
value.regionId,
value.amount
)
res.status(200).json(productVariant)
} catch (err) {
throw err
}
}
@@ -0,0 +1,33 @@
import { MedusaError, Validator } from "medusa-core-utils"
export default async (req, res) => {
const { id } = req.params
const schema = Validator.object().keys({
title: Validator.string().optional(),
prices: Validator.array()
.items({})
.optional(),
options: Validator.array()
.items({})
.optional(),
image: Validator.string().optional(),
inventory_quantity: Validator.number().optional(),
allow_backorder: Validator.boolean().optional(),
manage_inventory: Validator.boolean().optional(),
})
const { value, error } = schema.validate(req.body)
if (error) {
throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details)
}
try {
const productVariantService = req.scope.resolve("productVariantService")
const productVariant = await productVariantService.update(id, value)
res.status(200).json(productVariant)
} catch (err) {
throw err
}
}
@@ -117,6 +117,16 @@ export const variants = {
}
export const ProductVariantServiceMock = {
createDraft: jest.fn().mockImplementation(data => {
return Promise.resolve(testVariant)
}),
publish: jest.fn().mockImplementation(_ => {
return Promise.resolve({
_id: IdMap.getId("publish"),
name: "Product Variant",
published: true,
})
}),
retrieve: jest.fn().mockImplementation(variantId => {
if (variantId === "1") {
return Promise.resolve(variant1)
@@ -133,6 +143,9 @@ export const ProductVariantServiceMock = {
if (variantId === IdMap.getId("validId")) {
return Promise.resolve(variant5)
}
if (variantId === IdMap.getId("testVariant")) {
return Promise.resolve(testVariant)
}
if (variantId === "invalid_option") {
return Promise.resolve(invalidVariant)
}
@@ -177,9 +190,15 @@ export const ProductVariantServiceMock = {
return Promise.reject(new Error("Not found"))
}),
delete: jest.fn().mockReturnValue(Promise.resolve()),
update: jest.fn().mockReturnValue(Promise.resolve()),
setCurrencyPrice: jest.fn().mockReturnValue(Promise.resolve()),
setRegionPrice: jest.fn().mockReturnValue(Promise.resolve()),
addOptionValue: jest.fn().mockImplementation((variantId, optionId, value) => {
return Promise.resolve({})
}),
list: jest.fn().mockImplementation(data => {
return Promise.resolve([testVariant])
}),
deleteOptionValue: jest.fn().mockImplementation((variantId, optionId) => {
return Promise.resolve({})
}),
@@ -66,7 +66,7 @@ class ProductVariantService extends BaseService {
* @param {object} variant - the variant to create
* @return {Promise} resolves to the creation result.
*/
createDraft(productVariant) {
async createDraft(productVariant) {
return this.productVariantModel_
.create({
...productVariant,
@@ -99,7 +99,7 @@ class ProductVariantService extends BaseService {
* @param {object} update - an object with the update values.
* @return {Promise} resolves to the update result.
*/
update(variantId, update) {
async update(variantId, update) {
const validatedId = this.validateId_(variantId)
if (update.prices) {