gdub01

gdub01

I’m not sure if this is the correct way to test an API call in with Absinthe. The code works, but I’m not sure if this is what I should be doing for each query?

  @account %{email: "hey@you.com", password: "herewego"}

  setup do
    {:ok, account} = AccountResolver.create(@account, %{})
    {:ok, token} = AccountResolver.login(@account, %{})
    {:ok, %{token: token.token, id: account.id}}
  end

  test "Logged in user should be able to see their email", info do

    queryDoc = %{
      "operationName" => "account",
      "query" => "query account { account (id: #{info.id}) { email } }",
      "variables" => "{}"
    }

    conn = info.conn
      |> put_req_header("authorization", "Bearer #{info.token}")
      |> Map.put(:host, "localhost:4001")
      |> Map.put(:body_params, queryDoc)
      |> post("/api")

    assert conn.state == :sent
    assert conn.status == 200
    assert String.contains?(conn.resp_body, "hey@you.com")
  end

And in a second, not related question, I am transforming changeset errors into a string to be passed back to Absinthe on error:

  defmodule Graphqlapi.ChangesetErrors do

  def handle_changeset_errors(errors) do
    Enum.map(errors, fn {field, detail} ->
      "#{field} " <> render_detail(detail)
    end)
      |> Enum.join
  end

  def render_detail({message, values}) do
    Enum.reduce values, message, fn {k, v}, acc ->
      String.replace(acc, "%{#{k}}", to_string(v))
    end
  end

  def render_detail(message) do
    message
  end
end

So that in my graphql resolver I have this:

  def create(params, _info) do
    case Auth.register(params) do
      {:ok, account} -> {:ok, account}
      {:error, changeset} -> {:error, ChangesetErrors.handle_changeset_errors(changeset.errors)}
    end
  end

With the idea being I don’t have to write error code twice. Is that a good plan or a bad one?

Showing Posts 1 to 10

benwilson512

benwilson512

Author of Craft GraphQL APIs in Elixir with Absinthe

Hey there, I’m glad you’re using this library!

Let’s take each of your questions in turn.

Testing

The overall approach here is a very common way to do integration level testing. You may also find that the functions you build to handle the resolvers need unit testing if they’re more complex, but often complexity there gets extracted into service type functions that you’d want to unit test anyway.

A couple of things however would make the testing code you have a bit more idiomatic. Most of these things are phoenix conn test related, there isn’t much absinthe specific about this method of testing.

  • use |> post("/api", query_doc) instead of directly setting :body_params
  • parse the response body into JSON to look for the value you want instead of using String.contains?. This is particularly important because GraphQL does not use HTTP status codes to indicate errors that may happen on a given field. So for example suppose your account field returned an error "No account found for email hey@you.com". Your tests would actually pass right now, but clearly there’d be an error.

There’s also a few minor things about the testing here that are a bit confusing. Where does the conn part of info.conn come from? What content type header is being set? Why is the :host value being set? Are you using phoenix or just bare plug.

Error handling

It’s definitely common to want to handle changeset errors in a generic way, and definitely noto something you want to have to call explicitly in your resolvers over and over again.

The best solution at the moment is to build a wrapper function that handles this possible return value from the resolver function its wrapping. Here’s an example:

# in your field
resolve handle_errors(&SomeResolver.function/2)

# in your schema module somewhere, or imported thereinto
def handle_errors(fun) do
  fn source, args, info ->
    case Absinthe.Resolution.call(fun, source, args, info) do
      {:error, %Ecto.Changeset{} = changeset} -> format_changeset(changeset)
      val -> val
    end
  end
end

Now all that you have to do is wrap resolvers where you want changesets to be handled in a handle_errors call and you’re good to go. Having to still manually place handle_errors throughout your schema is a bit of an annoyance as well, and so we’re working to finalize a middleware pattern that will let you apply this pattern in an even more generic way. Until then, wrapper functions are the way to go.

11
Post #1
gdub01

gdub01 OP

Thanks for the answers @benwilson512! They were both very helpful =)

I’ll use that wrapper function and I’m going to have another go at that test to clean up the naming problems and confusing code. Thanks for pointing those things out as I wasn’t sure how far off I was from the right track.

gdub01

gdub01 OP

Okay - I’ve revised the test. Here’s what it looks like now if anyone is interested. Thanks again @benwilson512

  @account %{email: "hey@you.com", password: "herewego"}

  setup do
    {:ok, account} = AccountResolver.create(@account, %{})
    {:ok, token} = AccountResolver.login(@account, %{})

    conn = build_conn()
        |> put_req_header("authorization", "Bearer #{token.token}")
        |> put_req_header("content-type", "application/json")

    {:ok, %{conn: conn, id: account.id}}
  end

  defp query_skeleton(query, query_name) do
    %{
      "operationName" => "#{query_name}",
      "query" => "query #{query_name} #{query}",
      "variables" => "{}"
    }
  end

  test "Logged in user should be able to see their email", context do
    query = """
      {
        account (id: #{context.id}) {
          email
        }
      }
    """

    res = context.conn
        |> post("/api", query_skeleton(query, "account"))

    assert json_response(res, 200)["data"]["account"]["email"] == "hey@you.com"
  end

  test "Logged in user should not be able to see their password", context do
    query = """
      {
        account (id: #{context.id}) {
          password
        }
      }
    """

    res = context.conn
        |> post("/api", query_skeleton(query, "account"))

    assert hd(json_response(res, 400)["errors"])["message"]
      == "Cannot query field \"password\" on type \"Account\"."
  end
cultofmetatron

cultofmetatron

this solution worked well for me based on brian’s answer

  def handle_errors(fun) do
    fn source, args, info ->
      case Absinthe.Resolution.call(fun, source, args, info) do
        {:error, %Ecto.Changeset{} = changeset} -> format_changeset(changeset)
        val -> val
      end
    end
  end

  def format_changeset(changeset) do
    #{:error, [email: {"has already been taken", []}]}
    errors = changeset.errors
      |> Enum.map(fn({key, {value, context}}) -> 
           [message: "#{key} #{value}", details: context]
         end)
    {:error, errors}
  end
tosbourn

tosbourn

I just wanted to say thanks to @gdub01 for asking about this and the answers that came from it. I am incredibly new to Elixir, GraphQL, and Absinthe and this helped a lot.

I’ve written up what I did in order to test both queries and mutations. Testing Absinthe with ExUnit.

The main difference is that the query param you send needs to look slightly different, these are the two helpers I came up with based on what was shared here before;

def mutation_skeleton(query) do
  %{
    "operationName" => "",
    "query" => "#{query}",
    "variables" => ""
  }
end

def query_skeleton(query, query_name) do
  %{
    "operationName" => "#{query_name}",
    "query" => "query #{query_name} #{query}",
    "variables" => "{}"
  }
end
law

law

This is cool :slight_smile:

I havn’t written any integration tests at the phoenix level for my graphql endpoint so this’ll be helpful when I get there.

Self Plug: I test my queries one level down using Kronky

So like

query = "{ user(id: \"1\")
  {
    firstName
    lastName
    age
  }
}
"
{:ok, %{data: data}} = evaluate_graphql(query)

user_fields = %{first_name: :string, last_name: string, age: integer}
expected = %User{first_name: "Lilo", last_name: "Pelekai", age: 6}
%{"user" => result} = data
assert_equivalent_graphql(expected, result, user_fields)
benwilson512

benwilson512

Author of Craft GraphQL APIs in Elixir with Absinthe

I highly recommend making use of variables within test, at least for any complicated input. It means you don’t have to worry about escaping stuff within the query string.

aptinio

aptinio

First of all, thank you @benwilson512 and the Absinthe team for your work!

What do you think about bypassing conn using Absinthe.run like so?:

test "query for a post by its ID" do
  {:ok, result} =
    """
    {
      post(id: 42) {
        title
        body
      }
    }
    """
    |> Absinthe.run(Blog.Schema)

  assert result == %{
    data: %{
      "post" => %{
        "title" => "Lorem ipsum",
        "body" => "Lorem ipsum dolor sit amet, consectetur adipiscing elit"
      }
    }
  }
end
benwilson512

benwilson512

Author of Craft GraphQL APIs in Elixir with Absinthe

It’s an option, but I’m not sure it really brings anything to the table. If you want something more like a unit test I’d focus on unit testing whatever business logic function gets called within specific resolvers. For integration level tests this leaves out stuff that can be important like authorization, or any custom phases specified on the plug.

aptinio

aptinio

Good points. Thank you so much!

— All posts loaded —

Where Next? Top

Trending in Questions Top

stjefim
Hello! Suppose you are building workflow (order / task / payment) processing system with the following requirements: Each workflow con...
New
Blokh
Hey guys, I’ve got a huge CSV ( around 10 GB ) that needs to be processed hourly Do you guys have any suggestions what is the best prac...
New
roeland
Kia ora, We have been using elixir-google-api to connect to Google Drive. However, with the updates to Tesla due to CVEs this is now bro...
New
kszambelanczyk
Hello! Could someone please give me a help/sample code, how to delete a file from s3 using waffle/waffle_ecto from Phoenix app. I creat...
New
Onor.io
I have what I’ve heard referred to as a “lookup table” in my database. This is a way of assigning codes to common values. One common lo...
New
jaybe78
Hello, I’m developing a online persistent chat system (what’s app) like using elixir/dynamodb/aws for a mobile app(flutter). The diffic...
New
Trolleger
What approach to take when sending live updates to “random” users Hi! I have a question, I have a little chat app, and when I create a DM...
New

Other Trending Topics Top

garrison
Hobbes is a low-level distributed database for the Elixir programming language. Hobbes provides a simple, safe, and scalable storage lay...
New
mcass19
ExRatatui lets you cook up rich terminal UIs in Elixir, powered by Rust’s ratatui via Rustler NIFs. Build interactive terminal applicatio...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge &amp; Solve. They are GUI (Emerge) and State management (S...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New
wintermeyer
There are three potential reasons for members of this forum to have a look at https://vutuv.de You are tired or annoyed of LinkedIn. Yo...
New
aseigo
ICal is a library for interacting with iCalendar data. It parses iCalendars into typed Elixir structs via ICal.from_ics, and can prepare ...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews