feat(utils,types,framework,medusa): store endpoints should require publishable key (#9068)
* feat(utils,types,framework,medusa): store endpoints should require publishable key * chore: fix specs * chore: fix more specs * chore: update js-sdk * chore: fix specs wrt to default SC * chore: revert custom headers + change error message * chore: fix specs * chore: fix new store specs
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
import { medusaIntegrationTestRunner } from "medusa-test-utils"
|
||||
import {
|
||||
createAdminUser,
|
||||
adminHeaders,
|
||||
createAdminUser,
|
||||
generatePublishableKey,
|
||||
generateStoreHeaders,
|
||||
} from "../../../../helpers/create-admin-user"
|
||||
|
||||
jest.setTimeout(30000)
|
||||
@@ -12,9 +14,12 @@ medusaIntegrationTestRunner({
|
||||
let baseCollection
|
||||
let baseCollection1
|
||||
let baseCollection2
|
||||
let storeHeaders
|
||||
|
||||
beforeEach(async () => {
|
||||
const container = getContainer()
|
||||
const publishableKey = await generatePublishableKey(container)
|
||||
storeHeaders = generateStoreHeaders({ publishableKey })
|
||||
await createAdminUser(dbConnection, adminHeaders, container)
|
||||
|
||||
baseCollection = (
|
||||
@@ -46,7 +51,8 @@ medusaIntegrationTestRunner({
|
||||
describe("/store/collections/:id", () => {
|
||||
it("gets collection", async () => {
|
||||
const response = await api.get(
|
||||
`/store/collections/${baseCollection.id}`
|
||||
`/store/collections/${baseCollection.id}`,
|
||||
storeHeaders
|
||||
)
|
||||
|
||||
expect(response.data.collection).toEqual(
|
||||
@@ -61,7 +67,7 @@ medusaIntegrationTestRunner({
|
||||
|
||||
describe("/store/collections", () => {
|
||||
it("lists collections", async () => {
|
||||
const response = await api.get("/store/collections")
|
||||
const response = await api.get("/store/collections", storeHeaders)
|
||||
|
||||
expect(response.data).toEqual({
|
||||
collections: [
|
||||
|
||||
@@ -5,6 +5,8 @@ import { medusaIntegrationTestRunner } from "medusa-test-utils"
|
||||
import {
|
||||
adminHeaders,
|
||||
createAdminUser,
|
||||
generatePublishableKey,
|
||||
generateStoreHeaders,
|
||||
} from "../../../../helpers/create-admin-user"
|
||||
|
||||
jest.setTimeout(30000)
|
||||
@@ -17,9 +19,13 @@ medusaIntegrationTestRunner({
|
||||
let customer4
|
||||
let customer5
|
||||
let container
|
||||
let storeHeaders
|
||||
|
||||
beforeEach(async () => {
|
||||
container = getContainer()
|
||||
await createAdminUser(dbConnection, adminHeaders, container)
|
||||
const publishableKey = await generatePublishableKey(container)
|
||||
storeHeaders = generateStoreHeaders({ publishableKey })
|
||||
|
||||
customer1 = (
|
||||
await api.post(
|
||||
@@ -415,6 +421,7 @@ medusaIntegrationTestRunner({
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${registeredCustomerToken}`,
|
||||
...storeHeaders.headers,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -3,6 +3,8 @@ import { medusaIntegrationTestRunner } from "medusa-test-utils"
|
||||
import {
|
||||
adminHeaders,
|
||||
createAdminUser,
|
||||
generatePublishableKey,
|
||||
generateStoreHeaders,
|
||||
} from "../../../../helpers/create-admin-user"
|
||||
|
||||
jest.setTimeout(30000)
|
||||
@@ -10,20 +12,27 @@ jest.setTimeout(30000)
|
||||
medusaIntegrationTestRunner({
|
||||
testSuite: ({ dbConnection, api, getContainer }) => {
|
||||
let appContainer: MedusaContainer
|
||||
let storeHeaders
|
||||
|
||||
beforeEach(async () => {
|
||||
appContainer = getContainer()
|
||||
const publishableKey = await generatePublishableKey(appContainer)
|
||||
storeHeaders = generateStoreHeaders({ publishableKey })
|
||||
await createAdminUser(dbConnection, adminHeaders, appContainer)
|
||||
})
|
||||
|
||||
describe("POST /admin/customers", () => {
|
||||
describe("POST /store/customers", () => {
|
||||
it("should fails to create a customer without an identity", async () => {
|
||||
const customer = await api
|
||||
.post("/store/customers", {
|
||||
email: "newcustomer@medusa.js",
|
||||
first_name: "John",
|
||||
last_name: "Doe",
|
||||
})
|
||||
.post(
|
||||
"/store/customers",
|
||||
{
|
||||
email: "newcustomer@medusa.js",
|
||||
first_name: "John",
|
||||
last_name: "Doe",
|
||||
},
|
||||
storeHeaders
|
||||
)
|
||||
.catch((e) => e)
|
||||
|
||||
expect(customer.response.status).toEqual(401)
|
||||
@@ -48,6 +57,7 @@ medusaIntegrationTestRunner({
|
||||
{
|
||||
headers: {
|
||||
authorization: `Bearer ${signup.data.token}`,
|
||||
...storeHeaders.headers,
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -102,6 +112,7 @@ medusaIntegrationTestRunner({
|
||||
{
|
||||
headers: {
|
||||
authorization: `Bearer ${signup.data.token}`,
|
||||
...storeHeaders.headers,
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -161,6 +172,7 @@ medusaIntegrationTestRunner({
|
||||
{
|
||||
headers: {
|
||||
authorization: `Bearer ${firstSignup.data.token}`,
|
||||
...storeHeaders.headers,
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -181,6 +193,7 @@ medusaIntegrationTestRunner({
|
||||
{
|
||||
headers: {
|
||||
authorization: `Bearer ${firstSignin.data.token}`,
|
||||
...storeHeaders.headers,
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -191,6 +204,54 @@ medusaIntegrationTestRunner({
|
||||
"Request already authenticated as a customer."
|
||||
)
|
||||
})
|
||||
|
||||
describe("With ensurePublishableApiKey middleware", () => {
|
||||
it("should fail when no publishable key is passed in the header", async () => {
|
||||
const { response } = await api
|
||||
.post(
|
||||
"/store/customers",
|
||||
{
|
||||
email: "newcustomer@medusa.js",
|
||||
first_name: "John",
|
||||
last_name: "Doe",
|
||||
},
|
||||
{
|
||||
headers: {},
|
||||
}
|
||||
)
|
||||
.catch((e) => e)
|
||||
|
||||
expect(response.data).toEqual({
|
||||
message:
|
||||
"Publishable API key required in the request header: x-publishable-api-key. You can manage your keys in settings in the dashboard.",
|
||||
type: "not_allowed",
|
||||
})
|
||||
})
|
||||
|
||||
it("should fail when publishable keys are invalid", async () => {
|
||||
const { response } = await api
|
||||
.post(
|
||||
"/store/customers",
|
||||
{
|
||||
email: "newcustomer@medusa.js",
|
||||
first_name: "John",
|
||||
last_name: "Doe",
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"x-publishable-api-key": ["test1", "test2"],
|
||||
},
|
||||
}
|
||||
)
|
||||
.catch((e) => e)
|
||||
|
||||
expect(response.data).toEqual({
|
||||
message:
|
||||
"A valid publishable key is required to proceed with the request",
|
||||
type: "not_allowed",
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import { adminHeaders } from "../../../helpers/create-admin-user"
|
||||
import {
|
||||
adminHeaders,
|
||||
generatePublishableKey,
|
||||
generateStoreHeaders,
|
||||
} from "../../../helpers/create-admin-user"
|
||||
|
||||
export async function createOrderSeeder({ api, container }) {
|
||||
const publishableKey = await generatePublishableKey(container)
|
||||
const storeHeaders = generateStoreHeaders({ publishableKey })
|
||||
|
||||
export async function createOrderSeeder({ api }) {
|
||||
const region = (
|
||||
await api.post(
|
||||
"/admin/regions",
|
||||
@@ -145,36 +152,46 @@ export async function createOrderSeeder({ api }) {
|
||||
).data.shipping_option
|
||||
|
||||
const cart = (
|
||||
await api.post(`/store/carts`, {
|
||||
currency_code: "usd",
|
||||
email: "tony@stark-industries.com",
|
||||
region_id: region.id,
|
||||
shipping_address: {
|
||||
address_1: "test address 1",
|
||||
address_2: "test address 2",
|
||||
city: "ny",
|
||||
country_code: "us",
|
||||
province: "ny",
|
||||
postal_code: "94016",
|
||||
await api.post(
|
||||
`/store/carts`,
|
||||
{
|
||||
currency_code: "usd",
|
||||
email: "tony@stark-industries.com",
|
||||
region_id: region.id,
|
||||
shipping_address: {
|
||||
address_1: "test address 1",
|
||||
address_2: "test address 2",
|
||||
city: "ny",
|
||||
country_code: "us",
|
||||
province: "ny",
|
||||
postal_code: "94016",
|
||||
},
|
||||
sales_channel_id: salesChannel.id,
|
||||
items: [{ quantity: 1, variant_id: product.variants[0].id }],
|
||||
},
|
||||
sales_channel_id: salesChannel.id,
|
||||
items: [{ quantity: 1, variant_id: product.variants[0].id }],
|
||||
})
|
||||
storeHeaders
|
||||
)
|
||||
).data.cart
|
||||
|
||||
const paymentCollection = (
|
||||
await api.post(`/store/payment-collections`, {
|
||||
cart_id: cart.id,
|
||||
})
|
||||
await api.post(
|
||||
`/store/payment-collections`,
|
||||
{
|
||||
cart_id: cart.id,
|
||||
},
|
||||
storeHeaders
|
||||
)
|
||||
).data.payment_collection
|
||||
|
||||
await api.post(
|
||||
`/store/payment-collections/${paymentCollection.id}/payment-sessions`,
|
||||
{ provider_id: "pp_system_default" }
|
||||
{ provider_id: "pp_system_default" },
|
||||
storeHeaders
|
||||
)
|
||||
|
||||
let order = (await api.post(`/store/carts/${cart.id}/complete`, {})).data
|
||||
.order
|
||||
let order = (
|
||||
await api.post(`/store/carts/${cart.id}/complete`, {}, storeHeaders)
|
||||
).data.order
|
||||
|
||||
order = (await api.get(`/admin/orders/${order.id}`, adminHeaders)).data.order
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ medusaIntegrationTestRunner({
|
||||
|
||||
await setupTaxStructure(container.resolve(ModuleRegistrationName.TAX))
|
||||
await createAdminUser(dbConnection, adminHeaders, container)
|
||||
order = await createOrderSeeder({ api })
|
||||
order = await createOrderSeeder({ api, container })
|
||||
|
||||
shippingProfile = (
|
||||
await api.post(
|
||||
|
||||
@@ -2,6 +2,8 @@ import { medusaIntegrationTestRunner } from "medusa-test-utils"
|
||||
import {
|
||||
adminHeaders,
|
||||
createAdminUser,
|
||||
generatePublishableKey,
|
||||
generateStoreHeaders,
|
||||
} from "../../../../helpers/create-admin-user"
|
||||
import { getProductFixture } from "../../../../helpers/fixtures"
|
||||
|
||||
@@ -9,10 +11,12 @@ jest.setTimeout(30000)
|
||||
|
||||
medusaIntegrationTestRunner({
|
||||
testSuite: ({ dbConnection, getContainer, api }) => {
|
||||
beforeAll(() => {})
|
||||
let storeHeaders
|
||||
|
||||
beforeEach(async () => {
|
||||
const container = getContainer()
|
||||
const publishableKey = await generatePublishableKey(container)
|
||||
storeHeaders = generateStoreHeaders({ publishableKey })
|
||||
await createAdminUser(dbConnection, adminHeaders, container)
|
||||
})
|
||||
|
||||
@@ -55,23 +59,32 @@ medusaIntegrationTestRunner({
|
||||
).data.product
|
||||
|
||||
cart = (
|
||||
await api.post("/store/carts", {
|
||||
region_id: region.id,
|
||||
items: [{ variant_id: product.variants[0].id, quantity: 1 }],
|
||||
})
|
||||
await api.post(
|
||||
"/store/carts",
|
||||
{
|
||||
region_id: region.id,
|
||||
items: [{ variant_id: product.variants[0].id, quantity: 1 }],
|
||||
},
|
||||
storeHeaders
|
||||
)
|
||||
).data.cart
|
||||
})
|
||||
|
||||
it("should create a payment session", async () => {
|
||||
const paymentCollection = (
|
||||
await api.post(`/store/payment-collections`, {
|
||||
cart_id: cart.id,
|
||||
})
|
||||
await api.post(
|
||||
`/store/payment-collections`,
|
||||
{
|
||||
cart_id: cart.id,
|
||||
},
|
||||
storeHeaders
|
||||
)
|
||||
).data.payment_collection
|
||||
|
||||
await api.post(
|
||||
`/store/payment-collections/${paymentCollection.id}/payment-sessions`,
|
||||
{ provider_id: "pp_system_default" }
|
||||
{ provider_id: "pp_system_default" },
|
||||
storeHeaders
|
||||
)
|
||||
|
||||
// Adding a second payment session to ensure only one session gets created
|
||||
@@ -79,7 +92,8 @@ medusaIntegrationTestRunner({
|
||||
data: { payment_collection },
|
||||
} = await api.post(
|
||||
`/store/payment-collections/${paymentCollection.id}/payment-sessions`,
|
||||
{ provider_id: "pp_system_default" }
|
||||
{ provider_id: "pp_system_default" },
|
||||
storeHeaders
|
||||
)
|
||||
|
||||
expect(payment_collection.payment_sessions).toEqual([
|
||||
|
||||
@@ -37,7 +37,7 @@ medusaIntegrationTestRunner({
|
||||
beforeEach(async () => {
|
||||
container = getContainer()
|
||||
await createAdminUser(dbConnection, adminHeaders, container)
|
||||
order = await createOrderSeeder({ api })
|
||||
order = await createOrderSeeder({ api, container })
|
||||
|
||||
await api.post(
|
||||
`/admin/orders/${order.id}/fulfillments`,
|
||||
|
||||
@@ -8,6 +8,8 @@ import { medusaIntegrationTestRunner } from "medusa-test-utils"
|
||||
import {
|
||||
adminHeaders,
|
||||
createAdminUser,
|
||||
generatePublishableKey,
|
||||
generateStoreHeaders,
|
||||
} from "../../../../helpers/create-admin-user"
|
||||
import { getProductFixture } from "../../../../helpers/fixtures"
|
||||
|
||||
@@ -30,6 +32,8 @@ medusaIntegrationTestRunner({
|
||||
let variant4
|
||||
let inventoryItem1
|
||||
let inventoryItem2
|
||||
let storeHeaders
|
||||
let publishableKey
|
||||
|
||||
const createProducts = async (data) => {
|
||||
const response = await api.post(
|
||||
@@ -82,6 +86,8 @@ medusaIntegrationTestRunner({
|
||||
|
||||
beforeEach(async () => {
|
||||
appContainer = getContainer()
|
||||
publishableKey = await generatePublishableKey(appContainer)
|
||||
storeHeaders = generateStoreHeaders({ publishableKey })
|
||||
await createAdminUser(dbConnection, adminHeaders, appContainer)
|
||||
|
||||
const storeModule: IStoreModuleService = appContainer.resolve(
|
||||
@@ -104,7 +110,6 @@ medusaIntegrationTestRunner({
|
||||
})
|
||||
|
||||
describe("Get products based on publishable key", () => {
|
||||
let pubKey1
|
||||
let salesChannel1
|
||||
let salesChannel2
|
||||
|
||||
@@ -133,14 +138,6 @@ medusaIntegrationTestRunner({
|
||||
)
|
||||
).data.product
|
||||
|
||||
pubKey1 = (
|
||||
await api.post(
|
||||
"/admin/api-keys",
|
||||
{ title: "sample key", type: "publishable" },
|
||||
adminHeaders
|
||||
)
|
||||
).data.api_key
|
||||
|
||||
salesChannel1 = (
|
||||
await api.post(
|
||||
"/admin/sales-channels",
|
||||
@@ -184,7 +181,7 @@ medusaIntegrationTestRunner({
|
||||
|
||||
it("returns products from a specific channel associated with a publishable key", async () => {
|
||||
await api.post(
|
||||
`/admin/api-keys/${pubKey1.id}/sales-channels`,
|
||||
`/admin/api-keys/${publishableKey.id}/sales-channels`,
|
||||
{
|
||||
add: [salesChannel1.id],
|
||||
},
|
||||
@@ -194,7 +191,7 @@ medusaIntegrationTestRunner({
|
||||
const response = await api.get(`/store/products`, {
|
||||
headers: {
|
||||
...adminHeaders.headers,
|
||||
"x-publishable-api-key": pubKey1.token,
|
||||
"x-publishable-api-key": publishableKey.token,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -210,7 +207,7 @@ medusaIntegrationTestRunner({
|
||||
|
||||
it("returns products from multiples sales channels associated with a publishable key", async () => {
|
||||
await api.post(
|
||||
`/admin/api-keys/${pubKey1.id}/sales-channels`,
|
||||
`/admin/api-keys/${publishableKey.id}/sales-channels`,
|
||||
{
|
||||
add: [salesChannel1.id, salesChannel2.id],
|
||||
},
|
||||
@@ -220,7 +217,7 @@ medusaIntegrationTestRunner({
|
||||
const response = await api.get(`/store/products`, {
|
||||
headers: {
|
||||
...adminHeaders.headers,
|
||||
"x-publishable-api-key": pubKey1.token,
|
||||
"x-publishable-api-key": publishableKey.token,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -239,7 +236,7 @@ medusaIntegrationTestRunner({
|
||||
|
||||
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`,
|
||||
`/admin/api-keys/${publishableKey.id}/sales-channels`,
|
||||
{
|
||||
add: [salesChannel1.id, salesChannel2.id],
|
||||
},
|
||||
@@ -251,7 +248,7 @@ medusaIntegrationTestRunner({
|
||||
{
|
||||
headers: {
|
||||
...adminHeaders.headers,
|
||||
"x-publishable-api-key": pubKey1.token,
|
||||
"x-publishable-api-key": publishableKey.token,
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -266,41 +263,12 @@ medusaIntegrationTestRunner({
|
||||
)
|
||||
})
|
||||
|
||||
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,
|
||||
"x-publishable-api-key": publishableKey.token,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -322,7 +290,7 @@ medusaIntegrationTestRunner({
|
||||
|
||||
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`,
|
||||
`/admin/api-keys/${publishableKey.id}/sales-channels`,
|
||||
{
|
||||
add: [salesChannel1.id],
|
||||
},
|
||||
@@ -333,20 +301,20 @@ medusaIntegrationTestRunner({
|
||||
.get(`/store/products?sales_channel_id[]=${salesChannel2.id}`, {
|
||||
headers: {
|
||||
...adminHeaders.headers,
|
||||
"x-publishable-api-key": pubKey1.token,
|
||||
"x-publishable-api-key": publishableKey.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`
|
||||
`Requested sales channel is not part of the publishable key`
|
||||
)
|
||||
})
|
||||
|
||||
it("retrieve a product from a specific channel associated with a publishable key", async () => {
|
||||
await api.post(
|
||||
`/admin/api-keys/${pubKey1.id}/sales-channels`,
|
||||
`/admin/api-keys/${publishableKey.id}/sales-channels`,
|
||||
{
|
||||
add: [salesChannel1.id],
|
||||
},
|
||||
@@ -356,7 +324,7 @@ medusaIntegrationTestRunner({
|
||||
const response = await api.get(`/store/products/${product1.id}`, {
|
||||
headers: {
|
||||
...adminHeaders.headers,
|
||||
"x-publishable-api-key": pubKey1.token,
|
||||
"x-publishable-api-key": publishableKey.token,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -370,7 +338,7 @@ medusaIntegrationTestRunner({
|
||||
// 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`,
|
||||
`/admin/api-keys/${publishableKey.id}/sales-channels`,
|
||||
{
|
||||
add: [salesChannel1.id],
|
||||
},
|
||||
@@ -381,7 +349,7 @@ medusaIntegrationTestRunner({
|
||||
.get(`/store/products/${product2.id}`, {
|
||||
headers: {
|
||||
...adminHeaders.headers,
|
||||
"x-publishable-api-key": pubKey1.token,
|
||||
"x-publishable-api-key": publishableKey.token,
|
||||
},
|
||||
})
|
||||
.catch((e) => e)
|
||||
@@ -392,7 +360,7 @@ medusaIntegrationTestRunner({
|
||||
// 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`,
|
||||
`/admin/api-keys/${publishableKey.id}/sales-channels`,
|
||||
{
|
||||
add: [salesChannel1.id],
|
||||
},
|
||||
@@ -403,7 +371,7 @@ medusaIntegrationTestRunner({
|
||||
.get(`/store/variants/does-not-exist`, {
|
||||
headers: {
|
||||
...adminHeaders.headers,
|
||||
"x-publishable-api-key": pubKey1.token,
|
||||
"x-publishable-api-key": publishableKey.token,
|
||||
},
|
||||
})
|
||||
.catch((err) => {
|
||||
@@ -418,7 +386,7 @@ medusaIntegrationTestRunner({
|
||||
|
||||
it("should return 404 when the requested product doesn't exist", async () => {
|
||||
await api.post(
|
||||
`/admin/api-keys/${pubKey1.id}/sales-channels`,
|
||||
`/admin/api-keys/${publishableKey.id}/sales-channels`,
|
||||
{
|
||||
add: [salesChannel1.id],
|
||||
},
|
||||
@@ -429,7 +397,7 @@ medusaIntegrationTestRunner({
|
||||
.get(`/store/products/does-not-exist`, {
|
||||
headers: {
|
||||
...adminHeaders.headers,
|
||||
"x-publishable-api-key": pubKey1.token,
|
||||
"x-publishable-api-key": publishableKey.token,
|
||||
},
|
||||
})
|
||||
.catch((err) => {
|
||||
@@ -448,7 +416,7 @@ medusaIntegrationTestRunner({
|
||||
.get(`/store/products/${product1.id}`, {
|
||||
headers: {
|
||||
...adminHeaders.headers,
|
||||
"x-publishable-api-key": pubKey1.token,
|
||||
"x-publishable-api-key": publishableKey.token,
|
||||
},
|
||||
})
|
||||
.catch((err) => {
|
||||
@@ -461,7 +429,7 @@ medusaIntegrationTestRunner({
|
||||
.get(`/store/products/${product2.id}`, {
|
||||
headers: {
|
||||
...adminHeaders.headers,
|
||||
"x-publishable-api-key": pubKey1.token,
|
||||
"x-publishable-api-key": publishableKey.token,
|
||||
},
|
||||
})
|
||||
.catch((err) => {
|
||||
@@ -566,6 +534,12 @@ medusaIntegrationTestRunner({
|
||||
[product.id, product2.id, product3.id, product4.id]
|
||||
)
|
||||
|
||||
await api.post(
|
||||
`/admin/api-keys/${publishableKey.id}/sales-channels`,
|
||||
{ add: [defaultSalesChannel.id] },
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
const service = appContainer.resolve(ModuleRegistrationName.STORE)
|
||||
const [store] = await service.listStores()
|
||||
|
||||
@@ -583,7 +557,7 @@ medusaIntegrationTestRunner({
|
||||
})
|
||||
|
||||
it("should list all published products", async () => {
|
||||
let response = await api.get(`/store/products`)
|
||||
let response = await api.get(`/store/products`, storeHeaders)
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
expect(response.data.count).toEqual(3)
|
||||
@@ -601,7 +575,7 @@ medusaIntegrationTestRunner({
|
||||
])
|
||||
)
|
||||
|
||||
response = await api.get(`/store/products?q=uniquely`)
|
||||
response = await api.get(`/store/products?q=uniquely`, storeHeaders)
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
expect(response.data.count).toEqual(1)
|
||||
@@ -618,8 +592,15 @@ medusaIntegrationTestRunner({
|
||||
[product.id]
|
||||
)
|
||||
|
||||
await api.post(
|
||||
`/admin/api-keys/${publishableKey.id}/sales-channels`,
|
||||
{ add: [salesChannel.id] },
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
let response = await api.get(
|
||||
`/store/products?sales_channel_id[]=${salesChannel.id}`
|
||||
`/store/products?sales_channel_id[]=${salesChannel.id}`,
|
||||
storeHeaders
|
||||
)
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
@@ -643,7 +624,8 @@ medusaIntegrationTestRunner({
|
||||
)
|
||||
|
||||
const response = await api.get(
|
||||
`/store/products?category_id[]=${category.id}&category_id[]=${category2.id}`
|
||||
`/store/products?category_id[]=${category.id}&category_id[]=${category2.id}`,
|
||||
storeHeaders
|
||||
)
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
@@ -656,7 +638,7 @@ medusaIntegrationTestRunner({
|
||||
})
|
||||
|
||||
it("returns a list of ordered products by id ASC", async () => {
|
||||
const response = await api.get("/store/products?order=id")
|
||||
const response = await api.get("/store/products?order=id", storeHeaders)
|
||||
expect(response.status).toEqual(200)
|
||||
expect(response.data.products).toEqual(
|
||||
[product.id, product2.id, product3.id]
|
||||
@@ -666,7 +648,10 @@ medusaIntegrationTestRunner({
|
||||
})
|
||||
|
||||
it("returns a list of ordered products by id DESC", async () => {
|
||||
const response = await api.get("/store/products?order=-id")
|
||||
const response = await api.get(
|
||||
"/store/products?order=-id",
|
||||
storeHeaders
|
||||
)
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
expect(response.data.products).toEqual(
|
||||
@@ -678,7 +663,10 @@ medusaIntegrationTestRunner({
|
||||
|
||||
// TODO: This doesn't work currently, but worked in v1
|
||||
it.skip("returns a list of ordered products by variants title DESC", async () => {
|
||||
const response = await api.get("/store/products?order=-variants.title")
|
||||
const response = await api.get(
|
||||
"/store/products?order=-variants.title",
|
||||
storeHeaders
|
||||
)
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
expect(response.data.products).toEqual([
|
||||
@@ -690,7 +678,10 @@ medusaIntegrationTestRunner({
|
||||
|
||||
// TODO: This doesn't work currently, but worked in v1
|
||||
it.skip("returns a list of ordered products by variants title ASC", async () => {
|
||||
const response = await api.get("/store/products?order=variants.title")
|
||||
const response = await api.get(
|
||||
"/store/products?order=variants.title",
|
||||
storeHeaders
|
||||
)
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
expect(response.data.products).toEqual([
|
||||
@@ -703,7 +694,8 @@ medusaIntegrationTestRunner({
|
||||
// TODO: This doesn't work currently, but worked in v1
|
||||
it.skip("returns a list of ordered products by variants prices DESC", async () => {
|
||||
let response = await api.get(
|
||||
"/store/products?order=-variants.prices.amount"
|
||||
"/store/products?order=-variants.prices.amount",
|
||||
storeHeaders
|
||||
)
|
||||
})
|
||||
|
||||
@@ -711,14 +703,18 @@ medusaIntegrationTestRunner({
|
||||
it.skip("returns a list of ordered products by variants prices ASC", async () => {})
|
||||
|
||||
it("products contain only fields defined with `fields` param", async () => {
|
||||
const response = await api.get("/store/products?fields=handle")
|
||||
const response = await api.get(
|
||||
"/store/products?fields=handle",
|
||||
storeHeaders
|
||||
)
|
||||
expect(response.status).toEqual(200)
|
||||
expect(Object.keys(response.data.products[0])).toEqual(["handle", "id"])
|
||||
})
|
||||
|
||||
it("returns a list of products in collection", async () => {
|
||||
const response = await api.get(
|
||||
`/store/products?collection_id[]=${collection.id}`
|
||||
`/store/products?collection_id[]=${collection.id}`,
|
||||
storeHeaders
|
||||
)
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
@@ -730,7 +726,8 @@ medusaIntegrationTestRunner({
|
||||
|
||||
it("returns a list of products with a given tag", async () => {
|
||||
const response = await api.get(
|
||||
`/store/products?tag_id[]=${product.tags[0].id}`
|
||||
`/store/products?tag_id[]=${product.tags[0].id}`,
|
||||
storeHeaders
|
||||
)
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
@@ -743,7 +740,7 @@ medusaIntegrationTestRunner({
|
||||
// TODO: Not implemented yet
|
||||
it.skip("returns gift card product", async () => {
|
||||
const response = await api
|
||||
.get("/store/products?is_giftcard=true")
|
||||
.get("/store/products?is_giftcard=true", storeHeaders)
|
||||
.catch((err) => {
|
||||
console.log(err)
|
||||
})
|
||||
@@ -752,7 +749,7 @@ medusaIntegrationTestRunner({
|
||||
// TODO: Not implemented yet
|
||||
it.skip("returns non gift card products", async () => {
|
||||
const response = await api
|
||||
.get("/store/products?is_giftcard=false")
|
||||
.get("/store/products?is_giftcard=false", storeHeaders)
|
||||
.catch((err) => {
|
||||
console.log(err)
|
||||
})
|
||||
@@ -760,7 +757,8 @@ medusaIntegrationTestRunner({
|
||||
|
||||
it("returns a list of products in with a given handle", async () => {
|
||||
const response = await api.get(
|
||||
`/store/products?handle=${product.handle}`
|
||||
`/store/products?handle=${product.handle}`,
|
||||
storeHeaders
|
||||
)
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
@@ -773,7 +771,8 @@ medusaIntegrationTestRunner({
|
||||
it("returns a list of products filtered by variant options", async () => {
|
||||
const option = product.options.find((o) => o.title === "size")
|
||||
const response = await api.get(
|
||||
`/store/products?variants.options[option_id]=${option?.id}&variants.options[value]=large`
|
||||
`/store/products?variants.options[option_id]=${option?.id}&variants.options[value]=large`,
|
||||
storeHeaders
|
||||
)
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
@@ -807,6 +806,12 @@ medusaIntegrationTestRunner({
|
||||
|
||||
publishableKey1 = api1Res.data.api_key
|
||||
|
||||
await api.post(
|
||||
`/admin/api-keys/${publishableKey.id}/sales-channels`,
|
||||
{ add: [salesChannel1.id, salesChannel2.id] },
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
await api.post(
|
||||
`/admin/api-keys/${publishableKey1.id}/sales-channels`,
|
||||
{ add: [salesChannel1.id] },
|
||||
@@ -830,7 +835,10 @@ medusaIntegrationTestRunner({
|
||||
})
|
||||
|
||||
it("should list products by id", async () => {
|
||||
let response = await api.get(`/store/products?id[]=${product.id}`)
|
||||
let response = await api.get(
|
||||
`/store/products?id[]=${product.id}`,
|
||||
storeHeaders
|
||||
)
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
expect(response.data.count).toEqual(1)
|
||||
@@ -850,8 +858,8 @@ medusaIntegrationTestRunner({
|
||||
|
||||
expect(error.response.status).toEqual(400)
|
||||
expect(error.response.data).toEqual({
|
||||
message: `Publishable API key not found`,
|
||||
type: "invalid_data",
|
||||
message: `A valid publishable key is required to proceed with the request`,
|
||||
type: "not_allowed",
|
||||
})
|
||||
})
|
||||
|
||||
@@ -864,7 +872,7 @@ medusaIntegrationTestRunner({
|
||||
|
||||
expect(error.response.status).toEqual(400)
|
||||
expect(error.response.data).toEqual({
|
||||
message: `Requested sales channel is not part of the publishable key mappings`,
|
||||
message: `Requested sales channel is not part of the publishable key`,
|
||||
type: "invalid_data",
|
||||
})
|
||||
})
|
||||
@@ -878,7 +886,7 @@ medusaIntegrationTestRunner({
|
||||
|
||||
expect(error.response.status).toEqual(400)
|
||||
expect(error.response.data).toEqual({
|
||||
message: `Requested sales channel is not part of the publishable key mappings`,
|
||||
message: `Requested sales channel is not part of the publishable key`,
|
||||
type: "invalid_data",
|
||||
})
|
||||
})
|
||||
@@ -886,7 +894,10 @@ medusaIntegrationTestRunner({
|
||||
|
||||
it("should throw error when calculating prices without context", async () => {
|
||||
let error = await api
|
||||
.get(`/store/products?fields=*variants.calculated_price`)
|
||||
.get(
|
||||
`/store/products?fields=*variants.calculated_price`,
|
||||
storeHeaders
|
||||
)
|
||||
.catch((e) => e)
|
||||
|
||||
expect(error.response.status).toEqual(400)
|
||||
@@ -907,7 +918,8 @@ medusaIntegrationTestRunner({
|
||||
).data.region
|
||||
|
||||
let response = await api.get(
|
||||
`/store/products?fields=*variants.calculated_price®ion_id=${region.id}`
|
||||
`/store/products?fields=*variants.calculated_price®ion_id=${region.id}`,
|
||||
storeHeaders
|
||||
)
|
||||
|
||||
const expectation = expect.arrayContaining([
|
||||
@@ -957,7 +969,10 @@ medusaIntegrationTestRunner({
|
||||
expect(response.data.products).toEqual(expectation)
|
||||
|
||||
// with only region_id
|
||||
response = await api.get(`/store/products?region_id=${region.id}`)
|
||||
response = await api.get(
|
||||
`/store/products?region_id=${region.id}`,
|
||||
storeHeaders
|
||||
)
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
expect(response.data.products).toEqual(expectation)
|
||||
@@ -1137,6 +1152,12 @@ medusaIntegrationTestRunner({
|
||||
[product.id]
|
||||
)
|
||||
|
||||
await api.post(
|
||||
`/admin/api-keys/${publishableKey.id}/sales-channels`,
|
||||
{ add: [defaultSalesChannel.id] },
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
const service = appContainer.resolve(ModuleRegistrationName.STORE)
|
||||
const [store] = await service.listStores()
|
||||
|
||||
@@ -1154,7 +1175,10 @@ medusaIntegrationTestRunner({
|
||||
})
|
||||
|
||||
it("should retrieve product successfully", async () => {
|
||||
let response = await api.get(`/store/products/${product.id}`)
|
||||
let response = await api.get(
|
||||
`/store/products/${product.id}`,
|
||||
storeHeaders
|
||||
)
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
expect(response.data.product).toEqual(
|
||||
@@ -1185,7 +1209,8 @@ medusaIntegrationTestRunner({
|
||||
)
|
||||
|
||||
const response = await api.get(
|
||||
`/store/products/${product.id}?fields=*categories`
|
||||
`/store/products/${product.id}?fields=*categories`,
|
||||
storeHeaders
|
||||
)
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
@@ -1200,7 +1225,8 @@ medusaIntegrationTestRunner({
|
||||
it("should throw error when calculating prices without context", async () => {
|
||||
let error = await api
|
||||
.get(
|
||||
`/store/products/${product.id}?fields=*variants.calculated_price`
|
||||
`/store/products/${product.id}?fields=*variants.calculated_price`,
|
||||
storeHeaders
|
||||
)
|
||||
.catch((e) => e)
|
||||
|
||||
@@ -1222,7 +1248,8 @@ medusaIntegrationTestRunner({
|
||||
).data.region
|
||||
|
||||
let response = await api.get(
|
||||
`/store/products/${product.id}?fields=*variants.calculated_price®ion_id=${region.id}`
|
||||
`/store/products/${product.id}?fields=*variants.calculated_price®ion_id=${region.id}`,
|
||||
storeHeaders
|
||||
)
|
||||
|
||||
const expectation = expect.objectContaining({
|
||||
@@ -1270,7 +1297,8 @@ medusaIntegrationTestRunner({
|
||||
|
||||
// with only region_id
|
||||
response = await api.get(
|
||||
`/store/products/${product.id}?region_id=${region.id}`
|
||||
`/store/products/${product.id}?region_id=${region.id}`,
|
||||
storeHeaders
|
||||
)
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
@@ -1285,12 +1313,30 @@ medusaIntegrationTestRunner({
|
||||
let euCart
|
||||
|
||||
beforeEach(async () => {
|
||||
const salesChannel = (
|
||||
await api.post(
|
||||
"/admin/sales-channels",
|
||||
{
|
||||
name: "test name",
|
||||
description: "test description",
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
).data.sales_channel
|
||||
|
||||
await api.post(
|
||||
`/admin/api-keys/${publishableKey.id}/sales-channels`,
|
||||
{ add: [salesChannel.id] },
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
const store = (await api.get("/admin/stores", adminHeaders)).data
|
||||
.stores[0]
|
||||
if (store) {
|
||||
await api.post(
|
||||
`/admin/stores/${store.id}`,
|
||||
{
|
||||
default_sales_channel_id: salesChannel.id,
|
||||
supported_currencies: [
|
||||
{
|
||||
currency_code: "usd",
|
||||
@@ -1307,6 +1353,7 @@ medusaIntegrationTestRunner({
|
||||
await api.post(
|
||||
"/admin/stores",
|
||||
{
|
||||
default_sales_channel_id: salesChannel.id,
|
||||
name: "Test store",
|
||||
supported_currencies: [
|
||||
{
|
||||
@@ -1400,13 +1447,27 @@ medusaIntegrationTestRunner({
|
||||
product2 = (
|
||||
await api.post(
|
||||
"/admin/products",
|
||||
getProductFixture({ title: "test2", status: "published" }),
|
||||
getProductFixture({
|
||||
title: "test2",
|
||||
status: "published",
|
||||
}),
|
||||
adminHeaders
|
||||
)
|
||||
).data.product
|
||||
|
||||
euCart = (await api.post("/store/carts", { region_id: euRegion.id }))
|
||||
.data.cart
|
||||
await api.post(
|
||||
`/admin/sales-channels/${salesChannel.id}/products`,
|
||||
{ add: [product1.id, product2.id] },
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
euCart = (
|
||||
await api.post(
|
||||
"/store/carts",
|
||||
{ region_id: euRegion.id },
|
||||
storeHeaders
|
||||
)
|
||||
).data.cart
|
||||
|
||||
await api.post(
|
||||
`/admin/tax-regions`,
|
||||
@@ -1451,7 +1512,8 @@ medusaIntegrationTestRunner({
|
||||
it("should not return tax pricing if the context is not sufficient when listing products", async () => {
|
||||
const products = (
|
||||
await api.get(
|
||||
`/store/products?fields=id,*variants.calculated_price®ion_id=${usRegion.id}`
|
||||
`/store/products?fields=id,*variants.calculated_price®ion_id=${usRegion.id}`,
|
||||
storeHeaders
|
||||
)
|
||||
).data.products
|
||||
|
||||
@@ -1467,7 +1529,8 @@ medusaIntegrationTestRunner({
|
||||
it("should not return tax pricing if automatic taxes are off when listing products", async () => {
|
||||
const products = (
|
||||
await api.get(
|
||||
`/store/products?fields=id,*variants.calculated_price®ion_id=${usRegion.id}&country_code=us`
|
||||
`/store/products?fields=id,*variants.calculated_price®ion_id=${usRegion.id}&country_code=us`,
|
||||
storeHeaders
|
||||
)
|
||||
).data.products
|
||||
|
||||
@@ -1483,7 +1546,8 @@ medusaIntegrationTestRunner({
|
||||
it("should return prices with and without tax for a tax inclusive region when listing products", async () => {
|
||||
const products = (
|
||||
await api.get(
|
||||
`/store/products?fields=id,*variants.calculated_price®ion_id=${euRegion.id}&country_code=it`
|
||||
`/store/products?fields=id,*variants.calculated_price®ion_id=${euRegion.id}&country_code=it`,
|
||||
storeHeaders
|
||||
)
|
||||
).data.products
|
||||
|
||||
@@ -1524,7 +1588,8 @@ medusaIntegrationTestRunner({
|
||||
it("should return prices with and without tax for a tax exclusive region when listing products", async () => {
|
||||
const products = (
|
||||
await api.get(
|
||||
`/store/products?fields=id,*variants.calculated_price®ion_id=${dkRegion.id}&country_code=dk`
|
||||
`/store/products?fields=id,*variants.calculated_price®ion_id=${dkRegion.id}&country_code=dk`,
|
||||
storeHeaders
|
||||
)
|
||||
).data.products
|
||||
|
||||
@@ -1558,7 +1623,8 @@ medusaIntegrationTestRunner({
|
||||
it("should return prices with and without tax when the cart is available and a country is passed when listing products", async () => {
|
||||
const products = (
|
||||
await api.get(
|
||||
`/store/products?fields=id,*variants.calculated_price&cart_id=${euCart.id}&country_code=it`
|
||||
`/store/products?fields=id,*variants.calculated_price&cart_id=${euCart.id}&country_code=it`,
|
||||
storeHeaders
|
||||
)
|
||||
).data.products
|
||||
|
||||
@@ -1584,15 +1650,20 @@ medusaIntegrationTestRunner({
|
||||
})
|
||||
|
||||
it("should return prices with and without tax when the cart context is available when listing products", async () => {
|
||||
await api.post(`/store/carts/${euCart.id}`, {
|
||||
shipping_address: {
|
||||
country_code: "it",
|
||||
await api.post(
|
||||
`/store/carts/${euCart.id}`,
|
||||
{
|
||||
shipping_address: {
|
||||
country_code: "it",
|
||||
},
|
||||
},
|
||||
})
|
||||
storeHeaders
|
||||
)
|
||||
|
||||
const products = (
|
||||
await api.get(
|
||||
`/store/products?fields=id,*variants.calculated_price&cart_id=${euCart.id}`
|
||||
`/store/products?fields=id,*variants.calculated_price&cart_id=${euCart.id}`,
|
||||
storeHeaders
|
||||
)
|
||||
).data.products
|
||||
|
||||
@@ -1620,7 +1691,8 @@ medusaIntegrationTestRunner({
|
||||
it("should not return tax pricing if the context is not sufficient when fetching a single product", async () => {
|
||||
const product = (
|
||||
await api.get(
|
||||
`/store/products/${product1.id}?fields=id,*variants.calculated_price®ion_id=${usRegion.id}`
|
||||
`/store/products/${product1.id}?fields=id,*variants.calculated_price®ion_id=${usRegion.id}`,
|
||||
storeHeaders
|
||||
)
|
||||
).data.product
|
||||
|
||||
@@ -1635,7 +1707,8 @@ medusaIntegrationTestRunner({
|
||||
it("should return prices with and without tax for a tax inclusive region when fetching a single product", async () => {
|
||||
const product = (
|
||||
await api.get(
|
||||
`/store/products/${product1.id}?fields=id,*variants.calculated_price®ion_id=${euRegion.id}&country_code=it`
|
||||
`/store/products/${product1.id}?fields=id,*variants.calculated_price®ion_id=${euRegion.id}&country_code=it`,
|
||||
storeHeaders
|
||||
)
|
||||
).data.product
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ import { medusaIntegrationTestRunner } from "medusa-test-utils"
|
||||
import {
|
||||
adminHeaders,
|
||||
createAdminUser,
|
||||
generatePublishableKey,
|
||||
generateStoreHeaders,
|
||||
} from "../../../../helpers/create-admin-user"
|
||||
|
||||
jest.setTimeout(30000)
|
||||
@@ -12,10 +14,13 @@ medusaIntegrationTestRunner({
|
||||
let region1
|
||||
let region2
|
||||
let container
|
||||
let storeHeaders
|
||||
|
||||
beforeEach(async () => {
|
||||
container = getContainer()
|
||||
await createAdminUser(dbConnection, adminHeaders, container)
|
||||
const publishableKey = await generatePublishableKey(container)
|
||||
storeHeaders = generateStoreHeaders({ publishableKey })
|
||||
|
||||
region1 = (
|
||||
await api.post(
|
||||
@@ -111,7 +116,8 @@ medusaIntegrationTestRunner({
|
||||
)
|
||||
|
||||
let response = await api.get(
|
||||
`/store/regions/${region1.id}?fields=*payment_providers`
|
||||
`/store/regions/${region1.id}?fields=*payment_providers`,
|
||||
storeHeaders
|
||||
)
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
@@ -125,7 +131,8 @@ medusaIntegrationTestRunner({
|
||||
])
|
||||
|
||||
response = await api.get(
|
||||
`/store/regions/${region1.id}?fields=*payment_providers`
|
||||
`/store/regions/${region1.id}?fields=*payment_providers`,
|
||||
storeHeaders
|
||||
)
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
|
||||
Reference in New Issue
Block a user