TutorialsCourses

Test Hasura With Jest and Github Action Services

Introduction

Hasura permissions are great but in many cases can be complex. In order to truly test them you need to have a running instance, as well as a live DB. We'll setup the boilerplate for utilizing github action services to run Postgres and Hasura so that you can run Jest tests on a live instance running.

Setup Github Service

Github actions are defined by creating yaml files in .github/workflows. So we need to setup a file at .github/workflows/hasura-test.yml

First we need to setup the name and when this should run.

name: Hasura Tests

on:
  pull_request:
    branches:
      - main
    paths:
      - "hasura/**"

We call it our Hasura Tests, and when we open a pull request where anything in hasura has changed we will execute the test.

Next we need to setup the database and Hasura as github services.

jobs:
  build:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: kartoza/postgis:12.4
        env:
          POSTGRES_USER: postgres
          POSTGRES_PASS: postgrespassword
          POSTGRES_DBNAME: postgres
        options: --health-cmd pg_isready --health-interval 30s --health-retries 12
        ports:
          - 5432:5432
      hasura:
        image: hasura/graphql-engine:v2.0.7
        env:
          HASURA_GRAPHQL_DATABASE_URL: postgres://postgres:postgrespassword@postgres:5432/postgres
          HASURA_GRAPHQL_ENABLE_CONSOLE: "true"
          HASURA_GRAPHQL_DEV_MODE: "true"
          HASURA_GRAPHQL_LOG_LEVEL: debug
          HASURA_GRAPHQL_ENABLED_APIS: "metadata,graphql"
          HASURA_GRAPHQL_ENABLED_LOG_TYPES: startup, http-log, webhook-log, websocket-log, query-log
          HASURA_GRAPHQL_ADMIN_SECRET: admin_secret
        ports:
          - 8080:8080

We specify those under the services section. The postgres utilizes postgis, and has a healthcheck waiting for it to come up before moving onto Hasura. Additionally we need to expose it on 5432 so that our Hasura instance can connect to it.

Once the postgres service is healthy we setup a instance of Hasura, any other environment variables you will need to place under the env as well. Then expose Hasura on 8080 so our tests can talk to it.

The final bit are the steps. The github actions that need to run to do the final bits of setup, and actually run the tests.

The first few steps checkout the github repo, and then do a yarn install in our case we are just in the root, but you might have hasura tests in the hasura directory, or elsewhere.

Next we need to apply all of our metadata, migrations, and also our seeds. The metadata setups up the permissions, the migrations applies the database changes.

Then finally the seeds are SQL queries that we can use to insert data in our database. We'll cover how to create those in a later part.

Finally we run our tests.

steps:
  - uses: actions/checkout@v2
  - uses: c-hive/gha-yarn-cache@v2

  - name: Hasura CI/CD
    uses: browniefed/hasura-runner@master
    with:
      args: metadata apply
    env:
      PATH_TO_HASURA_PROJECT_ROOT: ./hasura
      HASURA_CLI_VERSION: v2.0.7
      HASURA_ENDPOINT: http://172.17.0.1:8080
      HASURA_ADMIN_SECRET: admin_secret

  - name: Hasura CI/CD
    uses: browniefed/hasura-runner@master
    with:
      args: migrate apply --database-name default
    env:
      PATH_TO_HASURA_PROJECT_ROOT: ./hasura
      HASURA_CLI_VERSION: v2.0.7
      HASURA_ENDPOINT: http://172.17.0.1:8080
      HASURA_ADMIN_SECRET: admin_secret

  - name: Hasura CI/CD
    uses: browniefed/hasura-runner@master
    with:
      args: metadata reload
    env:
      PATH_TO_HASURA_PROJECT_ROOT: ./hasura
      HASURA_CLI_VERSION: v2.0.7
      HASURA_ENDPOINT: http://172.17.0.1:8080
      HASURA_ADMIN_SECRET: admin_secret

  - name: Hasura CI/CD
    uses: browniefed/hasura-runner@master
    with:
      args: seeds apply --database-name default
    env:
      PATH_TO_HASURA_PROJECT_ROOT: ./hasura
      HASURA_CLI_VERSION: v2.0.7
      HASURA_ENDPOINT: http://172.17.0.1:8080
      HASURA_ADMIN_SECRET: admin_secret

  - name: Run unit tests
    run: yarn test:ci
    working-directory: hasura

The full github action workflow looks like this.

name: Hasura Tests

on:
  pull_request:
    branches:
      - main
    paths:
      - "hasura/**"
jobs:
  build:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: kartoza/postgis:12.4
        env:
          POSTGRES_USER: postgres
          POSTGRES_PASS: postgrespassword
          POSTGRES_DBNAME: postgres
        options: --health-cmd pg_isready --health-interval 30s --health-retries 12
        ports:
          - 5432:5432
      hasura:
        image: hasura/graphql-engine:v2.0.7
        env:
          HASURA_GRAPHQL_DATABASE_URL: postgres://postgres:postgrespassword@postgres:5432/postgres
          HASURA_GRAPHQL_ENABLE_CONSOLE: "true"
          HASURA_GRAPHQL_DEV_MODE: "true"
          HASURA_GRAPHQL_LOG_LEVEL: debug
          HASURA_GRAPHQL_ENABLED_APIS: "metadata,graphql"
          HASURA_GRAPHQL_ENABLED_LOG_TYPES: startup, http-log, webhook-log, websocket-log, query-log
          HASURA_GRAPHQL_ADMIN_SECRET: admin_secret
        ports:
          - 8080:8080
    steps:
      - uses: actions/checkout@v2

      - uses: c-hive/gha-yarn-cache@v2
        with:
          directory: hasura

      - name: Hasura CI/CD
        uses: browniefed/hasura-runner@master
        with:
          args: metadata apply
        env:
          PATH_TO_HASURA_PROJECT_ROOT: ./hasura
          HASURA_CLI_VERSION: v2.0.7
          HASURA_ENDPOINT: http://172.17.0.1:8080
          HASURA_ADMIN_SECRET: admin_secret

      - name: Hasura CI/CD
        uses: browniefed/hasura-runner@master
        with:
          args: migrate apply --database-name default
        env:
          PATH_TO_HASURA_PROJECT_ROOT: ./hasura
          HASURA_CLI_VERSION: v2.0.7
          HASURA_ENDPOINT: http://172.17.0.1:8080
          HASURA_ADMIN_SECRET: admin_secret

      - name: Hasura CI/CD
        uses: browniefed/hasura-runner@master
        with:
          args: metadata reload
        env:
          PATH_TO_HASURA_PROJECT_ROOT: ./hasura
          HASURA_CLI_VERSION: v2.0.7
          HASURA_ENDPOINT: http://172.17.0.1:8080
          HASURA_ADMIN_SECRET: admin_secret

      - name: Hasura CI/CD
        uses: browniefed/hasura-runner@master
        with:
          args: seeds apply --database-name default
        env:
          PATH_TO_HASURA_PROJECT_ROOT: ./hasura
          HASURA_CLI_VERSION: v2.0.7
          HASURA_ENDPOINT: http://172.17.0.1:8080
          HASURA_ADMIN_SECRET: admin_secret

      - name: Run unit tests
        run: yarn test:ci
        working-directory: hasura

Setup Jest and Tests

First off we need to install jest so run yarn add jest.

We also need to setup some tests to actually run, the default folder structure for tests in jest is __tests__

If you have a complicated authentication flow or anything it doesn't matter. Utilizing the hasura admins secret we can setup tests to execute with any role and hasura claims that is necessary. Alternatively if you did want to or can generate a token that actually can be passed to Hasura that will be better in ensuring your Hasura is executing correctly.

In order to test locally we need to refer to localhost, however when running in a container that is running on github actions we need to use 172.17.0.1. So we need to swap an env variable to point to local, or 172.17.0.1 which we do using HASURA_ENDPOINT. We will also add isomorphic-fetch so we can make calls to query Hasura.

{
  "scripts": {
    "test": "HASURA_ENDPOINT=http://localhost:8080/v1/graphql jest",
    "test:ci": "HASURA_ENDPOINT=http://172.17.0.1:8080/v1/graphql jest"
  },
  "dependencies": {
    "isomorphic-fetch": "^3.0.0",
    "jest": "^27.0.6"
  }
}

Seeds

Next up to prime the database we need to create some seeds. This will also give us some predictable data. To create this I went into the hasura console on my local environment. I did some basic inserts and then ran the command below.

hasura seed create users_seed --from-table users --database-name default

This command will grab that table, and export it to the seeds directory. You can specify 1 or more tables comma delimited, and then what database and in our case it's the default database.

It dumps something like this. Some SQL inserts.

SET check_function_bodies = false;
INSERT INTO public.users (id, username) VALUES ('e4182c07-70b1-4638-a11e-979889a80593', 'browniefed');
INSERT INTO public.users (id, username) VALUES ('0aee3b94-0d3b-4073-b17e-04722ffcd991', 'cool_guy');

Creating the Test

With our infrastructure setup for github actions, Jest installed, and some seeds create we need to write some tests.

Lets create __tests__/user.test.ts file. __tests__ is just a default folder structure that Jest will look for.

We'll import require("isomorphic-fetch"); so we can call fetch.

it("should be to get self user", async () => {
  expect.assertions(2);
  try {
    const response = await fetch(process.env.HASURA_ENDPOINT, {
      method: "POST",
      headers: {
        "x-hasura-admin-secret": "admin_secret",
        "x-hasura-role": "user",
        "x-hasura-user-id": "e4182c07-70b1-4638-a11e-979889a80593",
      },
      body: JSON.stringify({
        query: `query GetUser {
            users {
              id
              username
            }
          }
          `,
        variables: {},
      }),
    });
    const { data } = await response.json();

    expect(data.users.length).toEqual(1);
    expect(data.users[0].id).toEqual("e4182c07-70b1-4638-a11e-979889a80593");
  } catch (error) {
    console.log(error);
  }
});

The important parts of this test, are that one we specify expect.assertions(2);. If your test errors out for some reason, there will be no assertions and will pass. Expecting 2 assertions means that if anything happens the test will fail.

We do a fetch and setup our headers to be

{
  "x-hasura-admin-secret": "admin_secret",
  "x-hasura-role": "user",
  "x-hasura-user-id": "e4182c07-70b1-4638-a11e-979889a80593"
}

This is how we can emulate being a specific user, without having to worry about whatever authentication system you are using.

Finally the query

body: JSON.stringify({
  query: `query GetUser {
            users {
              id
              username
            }
          }
          `,
  variables: {},
});

The structure here is how you would make any request to a GraphQL server. You provide it a query and any variables. In our case we are just loading users.

Once we execute this we do our assertions.

const { data } = await response.json();

expect(data.users.length).toEqual(1);
expect(data.users[0].id).toEqual("e4182c07-70b1-4638-a11e-979889a80593");

We just make sure that yes we only loaded 1 user based on our permissions and that the user id is equal to the user that we made the request with.

The full test put together.

require("isomorphic-fetch");

describe("User Tests", () => {
  it("should be to get self user", async () => {
    expect.assertions(2);
    try {
      const response = await fetch(process.env.HASURA_ENDPOINT, {
        method: "POST",
        headers: {
          "x-hasura-admin-secret": "admin_secret",
          "x-hasura-role": "user",
          "x-hasura-user-id": "e4182c07-70b1-4638-a11e-979889a80593",
        },
        body: JSON.stringify({
          query: `query GetUser {
            users {
              id
              username
            }
          }
          `,
          variables: {},
        }),
      });
      const { data } = await response.json();

      expect(data.users.length).toEqual(1);
      expect(data.users[0].id).toEqual("e4182c07-70b1-4638-a11e-979889a80593");
    } catch (error) {
      console.log(error);
    }
  });
});

Ending

You can now have real Hasura tests against your permissions, and Postgres using Github Actions. These final tests are a bit verbose, but they are for demonstrations. Typically you would have a little more structure, and less boilerplate around these. I generally like to use GraphQL Code Generator to create clients and then I can have type-checked tests, and queries pre-built that my app actually uses.