# Example: Write Integration Tests for API Routes

In this chapter, you'll learn how to write integration tests for API routes using [medusaIntegrationTestRunner](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/integration-tests) from Medusa's Testing Framework.

### Prerequisites

- [Testing Tools Setup](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools)

## Test a GET API Route

Consider the following API route created at `src/api/custom/route.ts`:

```ts title="src/api/custom/route.ts"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
){
  res.json({
    message: "Hello, World!",
  })
}
```

To write an integration test that tests this API route, create the file `integration-tests/http/custom-routes.spec.ts` with the following content:

```ts title="integration-tests/http/custom-routes.spec.ts" highlights={getHighlights}
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"

medusaIntegrationTestRunner({
  testSuite: ({ api, getContainer }) => {
    describe("Custom endpoints", () => {
      describe("GET /custom", () => {
        it("returns correct message", async () => {
          const response = await api.get(
            `/custom`
          )
  
          expect(response.status).toEqual(200)
          expect(response.data).toHaveProperty("message")
          expect(response.data.message).toEqual("Hello, World!")
        })
      })
    })
  },
})

jest.setTimeout(60 * 1000)
```

You use the `medusaIntegrationTestRunner` to write your tests.

You add a single test that sends a `GET` request to `/custom` using the `api.get` method. For the test to pass, the response is expected to:

- Have a code status `200`,
- Have a `message` property in the returned data.
- Have the value of the `message` property equal to `Hello, World!`.

### Run Tests

Run the following command to run your tests:

```bash npm2yarn
npm run test:integration:http
```

If you don't have a `test:integration:http` script in `package.json`, refer to the [Medusa Testing Tools chapter](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools#add-test-commands).

This runs your Medusa application and runs the tests available under the `src/integrations/http` directory.

### Jest Timeout

Since your tests connect to the database and perform actions that require more time than the typical tests, make sure to increase the timeout in your test:

```ts title="integration-tests/http/custom-routes.spec.ts"
// in your test's file
jest.setTimeout(60 * 1000)
```

***

## Test a POST API Route

Suppose you have a `blog` module whose main service extends the service factory, and that has the following model:

```ts title="src/modules/blog/models/my-custom.ts"
import { model } from "@medusajs/framework/utils"

const Post = model.define("post", {
  id: model.id().primaryKey(),
  name: model.text(),
})

export default Post
```

And consider that the file `src/api/custom/route.ts` defines another route handler for `POST` requests:

```ts title="src/api/custom/route.ts"
// other imports...
import BlogModuleService from "../../../modules/blog/service"
import { BLOG_MODULE } from "../../../modules/blog"

// ...

export async function POST(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const blogModuleService: BlogModuleService = req.scope.resolve(
    BLOG_MODULE
  )

  const post = await blogModuleService.createPosts(
    req.body
  )

  res.json({
    post,
  })
}
```

This API route creates a new record of `Post`.

To write tests for this API route, add the following at the end of the `testSuite` function in `integration-tests/http/custom-routes.spec.ts`:

```ts title="integration-tests/http/custom-routes.spec.ts" highlights={postHighlights}
// other imports...
import BlogModuleService from "../../src/modules/blog/service"

medusaIntegrationTestRunner({
  testSuite: ({ api, getContainer }) => {
    describe("Custom endpoints", () => {
      // other tests...

      describe("POST /custom", () => {
        const id = "1"

        it("Creates my custom", async () => {
  
          const response = await api.post(
            `/custom`,
            {
              id,
              name: "Test",
            }
          )
  
          expect(response.status).toEqual(200)
          expect(response.data).toHaveProperty("post")
          expect(response.data.post).toEqual({
            id,
            name: "Test",
            created_at: expect.any(String),
            updated_at: expect.any(String),
          })
        })
      })
    })
  },
})
```

This adds a test for the `POST /custom` API route. It uses `api.post` to send the POST request. The `api.post` method accepts as a second parameter the data to pass in the request body.

The test passes if the response has:

- Status code `200`.
- A `post` property in its data.
- Its `id` and `name` match the ones provided to the request.

### Tear Down Created Record

To ensure consistency in the database for the rest of the tests after the above test is executed, utilize [Jest's setup and teardown hooks](https://jestjs.io/docs/setup-teardown) to delete the created record.

Use the `getContainer` function passed as a parameter to the `testSuite` function to resolve a service and use it for setup or teardown purposes

So, add an `afterAll` hook in the `describe` block for `POST /custom`:

```ts title="integration-tests/http/custom-routes.spec.ts"
// other imports...
import BlogModuleService from "../../src/modules/blog/service"
import { BLOG_MODULE } from "../../src/modules/blog"

medusaIntegrationTestRunner({
  testSuite: ({ api, getContainer }) => {
    describe("Custom endpoints", () => {
      // other tests...

      describe("POST /custom", () => {
        // ...
        afterAll(() => async () => {
          const blogModuleService: BlogModuleService = getContainer().resolve(
            BLOG_MODULE
          )

          await blogModuleService.deletePosts(id)
        })
      })
    })
  },
})
```

The `afterAll` hook resolves the `BlogModuleService` and use its `deletePosts` to delete the record created by the test.

***

## Test a DELETE API Route

Consider a `/custom/:id` API route created at `src/api/custom/[id]/route.ts`:

```ts title="src/api/custom/[id]/route.ts"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import BlogModuleService from "../../../modules/blog/service"
import { BLOG_MODULE } from "../../../modules/blog"

export async function DELETE(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const blogModuleService: BlogModuleService = req.scope.resolve(
    BLOG_MODULE
  )

  await blogModuleService.deletePosts(req.params.id)

  res.json({
    success: true,
  })
}
```

This API route accepts an ID path parameter, and uses the `BlogModuleService` to delete a `Post` record by that ID.

To add tests for this API route, add the following to `integration-tests/http/custom-routes.spec.ts`:

```ts title="integration-tests/http/custom-routes.spec.ts" highlights={deleteHighlights}
medusaIntegrationTestRunner({
  testSuite: ({ api, getContainer }) => {
    describe("Custom endpoints", () => {
      // ...

      describe("DELETE /custom/:id", () => {
        const id = "1"

        beforeAll(() => async () => {
          const blogModuleService: BlogModuleService = getContainer().resolve(
            BLOG_MODULE
          )

          await blogModuleService.createPosts({
            id,
            name: "Test",
          })
        })

        it("Deletes my custom", async () => {
          const response = await api.delete(
            `/custom/${id}`
          )

          expect(response.status).toEqual(200)
          expect(response.data).toHaveProperty("success")
          expect(response.data.success).toBeTruthy()
        })
      })
    })
  },
})
```

This adds a new test for the `DELETE /custom/:id` API route. You use the `beforeAll` hook to create a `Post` record using the `BlogModuleService`.

In the test, you use the `api.delete` method to send a `DELETE` request to `/custom/:id`. The test passes if the response:

- Has a `200` status code.
- Has a `success` property in its data.
- The `success` property's value is true.

***

## Pass Headers in Test Requests

Some requests require passing headers. For example, all routes prefixed with `/store` must pass a publishable API key in the header.

The `get`, `post`, and `delete` methods accept an optional third parameter that you can pass a `headers` property to, whose value is an object of headers to pass in the request.

### Pass Publishable API Key

For example, to pass a publishable API key in the header for a request to a `/store` route:

```ts title="integration-tests/http/custom-routes.spec.ts" highlights={headersHighlights}
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
import { ApiKeyDTO } from "@medusajs/framework/types"
import { createApiKeysWorkflow } from "@medusajs/medusa/core-flows"

medusaIntegrationTestRunner({
  testSuite: ({ api, getContainer }) => {
    describe("Custom endpoints", () => {
      let pak: ApiKeyDTO
      beforeAll(async () => {
        pak = (await createApiKeysWorkflow(getContainer()).run({
          input: {
            api_keys: [
              {
                type: "publishable",
                title: "Test Key",
                created_by: "",
              },
            ],
          },
        })).result[0]
      })
      describe("GET /custom", () => {
        it("returns correct message", async () => {
          const response = await api.get(
            `/store/custom`,
            {
              headers: {
                "x-publishable-api-key": pak.token,
              },
            }
          )
  
          expect(response.status).toEqual(200)
          expect(response.data).toHaveProperty("message")
          expect(response.data.message).toEqual("Hello, World!")
        })
      })
    })
  },
})

jest.setTimeout(60 * 1000)
```

In your test suit, you add a `beforeAll` hook to create a publishable API key before the tests run. To create the API key, you can use the `createApiKeysWorkflow` or the [API Key Module's service](https://docs.medusajs.com/resources/commerce-modules/api-key).

Then, in the test, you pass an object as the last parameter to `api.get` with a `headers` property. The `headers` property is an object with the key `x-publishable-api-key` and the value of the API key's token.

### Send Authenticated Requests

If your custom route is accessible by authenticated users only, such as routes prefixed by `/admin` or `/store/customers/me`, you can create a test customer or user, generate a JWT token for them, and pass the token in the request's Authorization header.

First, install the `jsonwebtoken` package to generate JWT tokens:

```bash npm2yarn
npm install jsonwebtoken
```

Then, use it in your test to create a user or customer, generate a JWT token for them, and pass the token in the request's headers:

For custom actor types, you only need to change the `actorType` value in the `jwt.sign` method.

### Admin User

```ts title="integration-tests/http/custom-routes.spec.ts" highlights={adminHighlights}
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
import jwt from "jsonwebtoken"

medusaIntegrationTestRunner({
  testSuite: ({ api, getContainer }) => {
    describe("Custom endpoints", () => {
      describe("GET /custom", () => {
        const headers: Record<string, string> = {
        }
        beforeEach(async () => {
          const container = getContainer()
          
          const authModuleService = container.resolve("auth")
          const userModuleService = container.resolve("user")
          
          const user = await userModuleService.createUsers({
            email: "admin@medusa.js",
            
          })
          const authIdentity = await authModuleService.createAuthIdentities({
            provider_identities: [
              {
                provider: "emailpass",
                entity_id: "admin@medusa.js",
                provider_metadata: {
                  password: "supersecret",
                },
              },
            ],
            app_metadata: {
              user_id: user.id,
            },
          })
  
          const token = jwt.sign(
            {
              actor_id: user.id,
              actor_type: "user",
              auth_identity_id: authIdentity.id,
            },
            "supersecret",
            {
              expiresIn: "1d",
            }
          )
        
          headers["authorization"] = `Bearer ${token}`
        })
        it("returns correct message", async () => {
          const response = await api.get(
            `/admin/custom`,
            { headers }
          )
  
          expect(response.status).toEqual(200)
        })
      })
    })
  },
})

jest.setTimeout(60 * 1000)
```

### Customer User

```ts title="integration-tests/http/custom-routes.spec.ts" highlights={customerHighlights}
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
import { ApiKeyDTO } from "@medusajs/framework/types"
import jwt from "jsonwebtoken"
import { createApiKeysWorkflow } from "@medusajs/medusa/core-flows"

medusaIntegrationTestRunner({
  testSuite: ({ api, getContainer }) => {
    describe("Custom endpoints", () => {
      describe("GET /custom", () => {
        const headers: Record<string, string> = {
        }
        beforeEach(async () => {
          const container = getContainer()
          
          const authModuleService = container.resolve("auth")
          const customerModuleService = container.resolve("customer")
          
          const customer = await customerModuleService.createCustomers({
            email: "admin@medusa.js",
            
          })
          const authIdentity = await authModuleService.createAuthIdentities({
            provider_identities: [
              {
                provider: "emailpass",
                entity_id: "customer@medusa.js",
                provider_metadata: {
                  password: "supersecret",
                },
              },
            ],
            app_metadata: {
              user_id: customer.id,
            },
          })
  
          const token = jwt.sign(
            {
              actor_id: customer.id,
              actor_type: "customer",
              auth_identity_id: authIdentity.id,
            },
            "supersecret",
            {
              expiresIn: "1d",
            }
          )
        
          headers["authorization"] = `Bearer ${token}`


          const pak = (await createApiKeysWorkflow(getContainer()).run({
            input: {
              api_keys: [
                {
                  type: "publishable",
                  title: "Test Key",
                  created_by: "",
                },
              ],
            },
          })).result[0]

          headers["x-publishable-api-key"] = pak.token
        })
        it("returns correct message", async () => {
          const response = await api.get(
            `/store/customers/me/custom`,
            { headers }
          )
  
          expect(response.status).toEqual(200)
        })
      })
    })
  },
})

jest.setTimeout(60 * 1000)
```

In the test suite, you add a `beforeEach` hook that creates a user or customer, an auth identity, and generates a JWT token for them. The JWT token is then set in the `Authorization` header of the request.

You also create and pass a publishable API key in the header for the customer as it's required for requests to `/store` routes. Learn more in [this section](#pass-publishable-api-key).

***

## Upload Files in Test Requests

If your API route requires uploading a file, you can use the `form-data` library to create a `FormData` object, then pass the form data headers in the request.

First, run the following command to install the `form-data` package:

```bash npm2yarn
npm install form-data
```

Then, use it in your test to upload files:

```ts title="integration-tests/http/custom-routes.spec.ts"
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
import FormData from "form-data"

medusaIntegrationTestRunner({
  testSuite: ({ api, getContainer }) => {
    describe("Custom endpoints", () => {
      describe("GET /custom", () => {
        it("upload file", async () => {
          const form = new FormData()
          form.append("files", Buffer.from("content 1"), "image1.jpg")
          form.append("files", Buffer.from("content 2"), "image2.jpg")

          const response = await api.post(`/custom`, form, {
            headers: form.getHeaders(),
          })
  
          expect(response.status).toEqual(200)
          expect(response.data).toHaveProperty("files")
          expect(response.data.files).toEqual(
            expect.arrayContaining([
              expect.objectContaining({
                id: expect.any(String),
                url: expect.any(String),
              }),
            ])
          )
        })
      })
    })
  },
})

jest.setTimeout(60 * 1000)
```

You don't have to actually upload a file, you use the `form.append` method to append to a `files` field in the form data object, and you pass random content using the `Buffer.from` method.

Then, you pass to the `api.post` method the form data object as a second parameter, and an object with the `headers` property set to the form data object's headers as a third parameter.

If you're passing authentication or other headers, you can pass both the form data headers and the authentication headers in the same object:

```ts title="integration-tests/http/custom-routes.spec.ts"
const response = await api.post(`/custom`, form, {
  headers: {
    ...form.getHeaders(),
    ...authHeaders,
  },
})
```

***

## Write Tests for Feature-Flagged API Routes

If your API route is hidden behind a \[feature flag], you can use the `env` option of `medusaIntegrationTestRunner` its feature flag.

For example:

```ts title="integration-tests/http/custom-routes.spec.ts"
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"

medusaIntegrationTestRunner({
  env: {
    FF_BLOG_FEATURE: true,
  },
  testSuite: ({ api, getContainer }) => {
    // TODO write tests...
  },
})
```

The `env` option accepts an object of environment variables to set for the test suite.


---

The best way to deploy Medusa is through Medusa Cloud where you get autoscaling production infrastructure fine tuned for Medusa. Create an account by signing up at cloud.medusajs.com/signup.
