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.
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
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" } }
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');
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); } }); });
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.