Merge branch 'master' of github.com:srindom/medusa

This commit is contained in:
Sebastian Rindom
2020-02-05 15:14:31 +01:00
6 changed files with 575 additions and 6509 deletions

View File

@@ -0,0 +1,37 @@
import { IdMap } from "medusa-test-utils"
export const customers = {
testCustomer: {
_id: IdMap.getId("testCustomer"),
email: "oliver@medusa.com",
first_name: "Oliver",
last_name: "Juhl",
billingAddress: {},
password_hash: "123456789",
},
deleteCustomer: {
_id: IdMap.getId("deleteId"),
email: "oliver@medusa.com",
first_name: "Oliver",
last_name: "Juhl",
billingAddress: {},
password_hash: "123456789",
},
}
export const CustomerModelMock = {
create: jest.fn().mockReturnValue(Promise.resolve()),
updateOne: jest.fn().mockImplementation((query, update) => {
return Promise.resolve()
}),
deleteOne: jest.fn().mockReturnValue(Promise.resolve()),
findOne: jest.fn().mockImplementation(query => {
if (query._id === IdMap.getId("testCustomer")) {
return Promise.resolve(customers.testCustomer)
}
if (query._id === IdMap.getId("deleteId")) {
return Promise.resolve(customers.deleteCustomer)
}
return Promise.resolve(undefined)
}),
}

View File

@@ -14,7 +14,6 @@ class CustomerModel extends BaseModel {
first_name: { type: String, required: true }, first_name: { type: String, required: true },
last_name: { type: String, required: true }, last_name: { type: String, required: true },
billing_address: { type: AddressSchema }, billing_address: { type: AddressSchema },
password_hash: { type: String },
metadata: { type: mongoose.Schema.Types.Mixed, default: {} }, metadata: { type: mongoose.Schema.Types.Mixed, default: {} },
} }
} }

View File

@@ -0,0 +1,277 @@
import mongoose from "mongoose"
import { IdMap } from "medusa-test-utils"
import CustomerService from "../customer"
import { CustomerModelMock, customers } from "../../models/__mocks__/customer"
describe("CustomerService", () => {
describe("retrieve", () => {
let result
beforeAll(async () => {
jest.clearAllMocks()
const customerService = new CustomerService({
customerModel: CustomerModelMock,
})
result = await customerService.retrieve(IdMap.getId("testCustomer"))
})
it("calls customer model functions", () => {
expect(CustomerModelMock.findOne).toHaveBeenCalledTimes(1)
expect(CustomerModelMock.findOne).toHaveBeenCalledWith({
_id: IdMap.getId("testCustomer"),
})
})
it("returns the customer", () => {
expect(result).toEqual(customers.testCustomer)
})
})
describe("setMetadata", () => {
const customerService = new CustomerService({
customerModel: CustomerModelMock,
})
beforeEach(() => {
jest.clearAllMocks()
})
it("calls updateOne with correct params", async () => {
const id = mongoose.Types.ObjectId()
await customerService.setMetadata(`${id}`, "metadata", "testMetadata")
expect(CustomerModelMock.updateOne).toBeCalledTimes(1)
expect(CustomerModelMock.updateOne).toBeCalledWith(
{ _id: `${id}` },
{ $set: { "metadata.metadata": "testMetadata" } }
)
})
it("throw error on invalid key type", async () => {
const id = mongoose.Types.ObjectId()
try {
await customerService.setMetadata(`${id}`, 1234, "nono")
} catch (err) {
expect(err.message).toEqual(
"Key type is invalid. Metadata keys must be strings"
)
}
})
})
describe("create", () => {
const customerService = new CustomerService({
customerModel: CustomerModelMock,
})
beforeEach(() => {
jest.clearAllMocks()
})
it("calls model layer create", () => {
customerService.create({
email: "oliver@medusa.com",
first_name: "Oliver",
last_name: "Juhl",
})
expect(CustomerModelMock.create).toBeCalledTimes(1)
expect(CustomerModelMock.create).toBeCalledWith({
email: "oliver@medusa.com",
first_name: "Oliver",
last_name: "Juhl",
})
})
it("fails if email is in incorrect format", () => {
try {
customerService.create({
email: "olivermedusa.com",
first_name: "Oliver",
last_name: "Juhl",
})
} catch (error) {
expect(error.message).toEqual("The email is not valid")
}
})
it("fails if billing address is in incorrect format", () => {
try {
customerService.create({
email: "oliver@medusa.com",
first_name: "Oliver",
last_name: "Juhl",
billing_address: {
first_name: 1234,
},
})
} catch (error) {
expect(error.message).toEqual("The address is not valid")
}
})
})
describe("update", () => {
const customerService = new CustomerService({
customerModel: CustomerModelMock,
})
beforeEach(() => {
jest.clearAllMocks()
})
it("successfully updates a customer", async () => {
await customerService.update(IdMap.getId("testCustomer"), {
first_name: "Olli",
last_name: "Test",
})
expect(CustomerModelMock.updateOne).toBeCalledTimes(1)
expect(CustomerModelMock.updateOne).toBeCalledWith(
{ _id: IdMap.getId("testCustomer") },
{ $set: { first_name: "Olli", last_name: "Test" } },
{ runValidators: true }
)
})
it("fails if metadata updates are attempted", async () => {
try {
await customerService.update(IdMap.getId("testCustomer"), {
metadata: "Nononono",
})
} catch (err) {
expect(err.message).toEqual("Use setMetadata to update metadata fields")
}
})
it("fails if billing address updates are attempted", async () => {
try {
await customerService.update(IdMap.getId("testCustomer"), {
billing_address: {
last_name: "nnonono",
},
})
} catch (err) {
expect(err.message).toEqual(
"Use updateBillingAddress to update billing address"
)
}
})
})
describe("updateEmail", () => {
const customerService = new CustomerService({
customerModel: CustomerModelMock,
})
beforeEach(() => {
jest.clearAllMocks()
})
it("successfully updates an email", async () => {
await customerService.updateEmail(
IdMap.getId("testCustomer"),
"oliver@medusa2.com"
)
expect(CustomerModelMock.updateOne).toHaveBeenCalledTimes(1)
expect(CustomerModelMock.updateOne).toHaveBeenCalledWith(
{
_id: IdMap.getId("testCustomer"),
},
{
$set: { email: "oliver@medusa2.com" },
}
)
})
it("throws on invalid email", async () => {
try {
await customerService.updateEmail(
IdMap.getId("testCustomer"),
"olivermedusa"
)
} catch (err) {
expect(err.message).toEqual("The email is not valid")
}
expect(CustomerModelMock.updateOne).toHaveBeenCalledTimes(0)
})
describe("updateBillingAddress", () => {
const customerService = new CustomerService({
customerModel: CustomerModelMock,
})
beforeEach(() => {
jest.clearAllMocks()
})
it("successfully updates billing address", async () => {
await customerService.updateBillingAddress(
IdMap.getId("testCustomer"),
{
first_name: "Olli",
last_name: "Juhl",
address_1: "Laksegade",
city: "Copenhagen",
country_code: "DK",
postal_code: "2100",
}
)
expect(CustomerModelMock.updateOne).toHaveBeenCalledTimes(1)
expect(CustomerModelMock.updateOne).toHaveBeenCalledWith(
{
_id: IdMap.getId("testCustomer"),
},
{
$set: {
billing_address: {
first_name: "Olli",
last_name: "Juhl",
address_1: "Laksegade",
city: "Copenhagen",
country_code: "DK",
postal_code: "2100",
},
},
}
)
})
it("throws on invalid address", async () => {
try {
await customerService.updateBillingAddress(
IdMap.getId("testCustomer"),
{
first_name: 1234,
}
)
} catch (err) {
expect(err.message).toEqual("The address is not valid")
}
expect(CustomerModelMock.updateOne).toHaveBeenCalledTimes(0)
})
})
})
describe("delete", () => {
const customerService = new CustomerService({
customerModel: CustomerModelMock,
})
beforeEach(() => {
jest.clearAllMocks()
})
it("deletes customer successfully", async () => {
await customerService.delete(IdMap.getId("deleteId"))
expect(CustomerModelMock.deleteOne).toBeCalledTimes(1)
expect(CustomerModelMock.deleteOne).toBeCalledWith({
_id: IdMap.getId("deleteId"),
})
})
})
})

View File

@@ -167,7 +167,7 @@ class CartService extends BaseService {
*/ */
async decorate(cart, fields, expandFields = []) { async decorate(cart, fields, expandFields = []) {
const requiredFields = ["_id", "metadata"] const requiredFields = ["_id", "metadata"]
const decorated = _.pick(product, fields.concat(requiredFields)) const decorated = _.pick(cart, fields.concat(requiredFields))
return decorated return decorated
} }

View File

@@ -0,0 +1,260 @@
import mongoose from "mongoose"
import _ from "lodash"
import { Validator, MedusaError } from "medusa-core-utils"
import { BaseService } from "medusa-interfaces"
/**
* Provides layer to manipulate customers.
* @implements BaseService
*/
class CustomerService extends BaseService {
constructor({ customerModel, eventBusService }) {
super()
/** @private @const {CustomerModel} */
this.customerModel_ = customerModel
/** @private @const {EventBus} */
this.eventBus_ = eventBusService
}
/**
* Used to validate customer ids. Throws an error if the cast fails
* @param {string} rawId - the raw customer id to validate.
* @return {string} the validated id
*/
validateId_(rawId) {
const schema = Validator.objectId()
const { value, error } = schema.validate(rawId)
if (error) {
throw new MedusaError(
MedusaError.Types.INVALID_ARGUMENT,
"The customerId could not be casted to an ObjectId"
)
}
return value
}
/**
* Used to validate customer email.
* @param {string} email - email to validate
* @return {string} the validated email
*/
validateEmail_(email) {
const schema = Validator.string()
.email()
.required()
const { value, error } = schema.validate(email)
if (error) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"The email is not valid"
)
}
return value
}
validateBillingAddress_(address) {
const { value, error } = Validator.address().validate(address)
if (error) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"The address is not valid"
)
}
return value
}
/**
* @param {Object} selector - the query object for find
* @return {Promise} the result of the find operation
*/
list(selector) {
return this.customerModel_.find(selector)
}
/**
* Gets a customer by id.
* @param {string} customerId - the id of the customer to get.
* @return {Promise<Customer>} the customer document.
*/
retrieve(customerId) {
const validatedId = this.validateId_(customerId)
return this.customerModel_.findOne({ _id: validatedId }).catch(err => {
throw new MedusaError(MedusaError.Types.DB_ERROR, err.message)
})
}
/**
* Creates a customer with email and billing address
* (if provided) being validated.
* @param {object} customer - the customer to create
* @return {Promise} the result of create
*/
create(customer) {
const { email, billing_address } = customer
this.validateEmail_(email)
if (billing_address) {
this.validateBillingAddress_(billing_address)
}
this.customerModel_.create(customer).catch(err => {
throw new MedusaError(MedusaError.Types.DB_ERROR, err.message)
})
}
/**
* Sets the email of a customer
* @param {string} customerId - the id of the customer to update
* @param {string} email - the email to add to customer
* @return {Promise} the result of the update operation
*/
async updateEmail(customerId, email) {
const customer = await this.retrieve(customerId)
if (!customer) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Customer with ${customerId} was not found`
)
}
this.validateEmail_(email)
return this.customerModel_.updateOne(
{
_id: customerId,
},
{
$set: { email },
}
)
}
/**
* Sets the billing address of a customer
* @param {*} customerId - the customer to update address on
* @param {*} address - the new address to replace the current one
* @return {Promise} the result of the update operation
*/
async updateBillingAddress(customerId, address) {
const customer = await this.retrieve(customerId)
if (!customer) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Customer with ${customerId} was not found`
)
}
this.validateBillingAddress_(address)
return this.customerModel_.updateOne(
{
_id: customerId,
},
{
$set: { billing_address: address },
}
)
}
/**
* Updates a customer. Metadata updates and address updates should
* use dedicated methods, e.g. `setMetadata`, etc. The function
* will throw errors if metadata updates and address updates are attempted.
* @param {string} variantId - the id of the variant. Must be a string that
* can be casted to an ObjectId
* @param {object} update - an object with the update values.
* @return {Promise} resolves to the update result.
*/
async update(customerId, update) {
const customer = await this.retrieve(customerId)
if (!customer) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Customer with ${customerId} was not found`
)
}
if (update.metadata) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"Use setMetadata to update metadata fields"
)
}
if (update.billing_address) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"Use updateBillingAddress to update billing address"
)
}
return this.customerModel_
.updateOne({ _id: customerId }, { $set: update }, { runValidators: true })
.catch(err => {
throw new MedusaError(MedusaError.Types.DB_ERROR, err.message)
})
}
/**
* Deletes a customer from a given customer id.
* @param {string} customerId - the id of the customer to delete. Must be
* castable as an ObjectId
* @return {Promise} the result of the delete operation.
*/
async delete(customerId) {
const customer = await this.retrieve(customerId)
// Delete is idempotent, but we return a promise to allow then-chaining
if (!customer) {
return Promise.resolve()
}
return this.customerModel_.deleteOne({ _id: customer._id }).catch(err => {
throw new MedusaError(MedusaError.Types.DB_ERROR, err.message)
})
}
/**
* Decorates a customer.
* @param {Customer} customer - the cart to decorate.
* @param {string[]} fields - the fields to include.
* @param {string[]} expandFields - fields to expand.
* @return {Customer} return the decorated customer.
*/
async decorate(customer, fields, expandFields = []) {
const requiredFields = ["_id", "metadata"]
const decorated = _.pick(customer, fields.concat(requiredFields))
return decorated
}
/**
* Dedicated method to set metadata for a customer.
* To ensure that plugins does not overwrite each
* others metadata fields, setMetadata is provided.
* @param {string} customerId - the customer to apply metadata to.
* @param {string} key - key for metadata field
* @param {string} value - value for metadata field.
* @return {Promise} resolves to the updated result.
*/
setMetadata(customerId, key, value) {
const validatedId = this.validateId_(customerId)
if (typeof key !== "string") {
throw new MedusaError(
MedusaError.Types.INVALID_ARGUMENT,
"Key type is invalid. Metadata keys must be strings"
)
}
const keyPath = `metadata.${key}`
return this.customerModel_
.updateOne({ _id: validatedId }, { $set: { [keyPath]: value } })
.catch(err => {
throw new MedusaError(MedusaError.Types.DB_ERROR, err.message)
})
}
}
export default CustomerService

File diff suppressed because it is too large Load Diff