# GqlCase
GqlCase is a comprehensive GraphQL testing library designed specifically for Absinthe projects. It provides powerful macros and utilities to easily test GraphQL queries and mutations with support for authentication, custom headers, and import resolution.
## Key Features
- **Easy GraphQL testing** - Load GraphQL documents from files or strings with simple macros
- **Authentication support** - Seamless JWT bearer token integration for authenticated queries
- **Flexible header management** - Configure default headers with override capabilities
- **Import resolution** - Support for `#import` statements in GraphQL files
- **Security by design** - Built-in validation and size limits for safe operation
## Installation
Add `gql_case` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:gql_case, "~> 0.5"}
]
end
```
## Basic Setup
To use GqlCase in your tests, first create a configuration module:
```elixir
defmodule MyApp.GqlCase do
use GqlCase,
gql_path: "/graphql",
jwt_bearer_fn: &MyApp.Guardian.encode_and_sign/1,
default_headers: [
{"x-app-version", "1.0.0"},
{"x-client-type", "test"}
]
end
```
Then use it in your test modules:
```elixir
defmodule MyApp.UserQueryTest do
use ExUnit.Case
use MyApp.GqlCase
@endpoint MyApp.Endpoint
load_gql_file("queries/GetUser.gql")
test "fetches user data" do
result = query_gql(variables: %{id: "123"})
assert %{"data" => %{"user" => %{"id" => "123"}}} = result
end
end
```
## Core Configuration
The `use GqlCase` directive accepts several required and optional parameters:
### Required Parameters
- **`gql_path`** - The endpoint path for GraphQL requests (e.g., `"/graphql"`)
- **`jwt_bearer_fn`** - Function to generate JWT tokens for authentication (arity 1)
### Optional Parameters
- **`default_headers`** - List of default headers applied to all requests
```elixir
defmodule MyApp.GqlCase do
use GqlCase,
gql_path: "/api/graphql",
jwt_bearer_fn: &MyApp.Auth.create_token/1,
default_headers: [
{"accept", "application/json"},
{"x-api-version", "v1"}
]
end
```
## Loading GraphQL Documents
GqlCase provides two ways to load GraphQL documents into your test modules:
### Loading from Files
Use `load_gql_file/1` to load GraphQL documents from external files:
```elixir
defmodule MyApp.ProductTest do
use MyApp.GqlCase
# Load from relative path
load_gql_file("queries/GetProducts.gql")
test "gets products" do
result = query_gql()
assert %{"data" => %{"products" => products}} = result
end
end
```
### Loading from Strings
Use `load_gql_string/1` for inline GraphQL queries:
```elixir
defmodule MyApp.SimpleTest do
use MyApp.GqlCase
load_gql_string """
query GetHello {
hello
}
"""
test "says hello" do
result = query_gql()
assert %{"data" => %{"hello" => "Hello, World!"}} = result
end
end
```
### Import System
GqlCase supports GraphQL import statements for modular query organization:
```graphql
# fragments/UserFields.gql
fragment UserFields on User {
id
name
email
}
# queries/GetUser.gql
#import "fragments/UserFields.gql"
query GetUser($id: ID!) {
user(id: $id) {
...UserFields
}
}
```
## Basic Query Execution
Execute loaded GraphQL documents using the `query_gql/1` macro:
### Simple Queries
```elixir
result = query_gql()
assert %{"data" => %{"hello" => "Hello, World!"}} = result
```
### Queries with Variables
```elixir
result = query_gql(variables: %{id: "123", name: "John"})
assert %{"data" => %{"user" => %{"id" => "123"}}} = result
```
## Authentication Features
GqlCase seamlessly integrates with JWT-based authentication systems:
### Authenticated Queries
Pass a `current_user` to automatically generate and include JWT tokens:
```elixir
user = %{id: "123", email: "user@example.com"}
result = query_gql(current_user: user)
```
### JWT Bearer Function
The JWT bearer function should accept a user struct or map and return `{:ok, token, claims}`:
```elixir
defmodule MyApp.Auth do
def create_token(user) do
Guardian.encode_and_sign(user, %{}, ttl: {1, :hour})
end
end
```
## Header Management
GqlCase provides a flexible header management system with multiple levels of configuration:
### Default Headers
Set default headers that apply to all requests in your configuration:
```elixir
use GqlCase,
gql_path: "/graphql",
jwt_bearer_fn: &MyApp.Auth.create_token/1,
default_headers: [
{"x-app-version", "1.0.0"},
{"accept-language", "en-US"}
]
```
### Module-Specific Headers
Override or add headers for specific test modules:
```elixir
defmodule MyApp.AdminTest do
use ExUnit.Case
use MyApp.GqlCase, headers: [
{"x-admin-role", "super"},
{"x-feature-flag", "admin-panel"}
]
load_gql_string "query { adminData }"
test "accesses admin data" do
result = query_gql()
# Request includes both default headers and module-specific headers
end
end
```
### Runtime Header Overrides
Add or override headers for individual queries:
```elixir
result = query_gql(
variables: %{id: "123"},
headers: [
{"x-request-id", "abc-123"},
{"x-app-version", "2.0.0"} # Overrides default
]
)
```
### Header Priority System
Headers are merged with the following priority (highest to lowest):
1. **Runtime headers** (passed to `query_gql/1`)
2. **Authorization header** (generated from `current_user`)
3. **Module-specific headers** (from `use MyApp.GqlCase, headers: [...]`)
4. **Default headers** (from configuration)
5. **Built-in headers** (`{"content-type", "application/json"}`)
## Advanced Usage
### Error Handling Patterns
GqlCase validates GraphQL documents and provides detailed error information:
```elixir
# File not found
load_gql_file("nonexistent.gql") # Raises LoaderError
# Invalid GraphQL syntax
load_gql_string "query { invalid syntax }" # Raises ParseError
# Missing import
load_gql_file("query_with_missing_import.gql") # Raises ImportError
```
### Import Resolution
Imports are resolved relative to the importing file's directory:
```
project/
├── test/
│ └── queries/
│ ├── fragments/
│ │ └── UserFields.gql
│ └── GetUser.gql
```
```graphql
# In GetUser.gql:
#import "fragments/UserFields.gql"
```
### Unicode Support
GqlCase fully supports Unicode in GraphQL documents:
```elixir
load_gql_string """
query GetGreeting($name: String!) {
greeting(name: $name)
}
"""
result = query_gql(variables: %{name: "José"})
```
### Security Constraints
Built-in security measures include:
- **File size limits** - Query strings limited to 10MB
- **Null byte detection** - Prevents null byte injection
- **Path validation** - Ensures safe file path resolution
## API Reference
### Macros
#### `load_gql_file(file_path)`
Loads a GraphQL document from a file path relative to the calling module.
- **Arguments**: `file_path` (string) - Path to the GraphQL file
- **Raises**: `LoaderError` if file cannot be read, `ParseError` if GraphQL is invalid
#### `load_gql_string(query_string)`
Loads a GraphQL document from an inline string.
- **Arguments**: `query_string` (string) - GraphQL query/mutation string
- **Raises**: `ParseError` if GraphQL is invalid
#### `query_gql(opts \\ [])`
Executes the loaded GraphQL document against the configured endpoint.
- **Options**:
- `variables` - Map of GraphQL variables (default: `%{}`)
- `current_user` - User map for JWT authentication (default: `nil`)
- `headers` - Additional request headers (default: `[]`)
- **Returns**: Decoded JSON response from GraphQL endpoint
### Error Types
#### `GqlCase.SetupError`
Raised when GqlCase is configured incorrectly:
- `:missing_path` - No `gql_path` provided
- `:missing_jwt_bearer_fn` - No JWT function provided
- `:invalid_jwt_bearer_fn` - JWT function has wrong arity
- `:double_declaration` - Multiple `load_gql_*` calls in same module
#### `GqlCase.GqlLoader.LoaderError`
Raised when files cannot be loaded:
- Contains `path` and `reason` for debugging
#### `GqlCase.GqlLoader.ImportError`
Raised when imported files cannot be found:
- Contains `path` and `parent` file information
#### `GqlCase.GqlLoader.ParseError`
Raised when GraphQL documents are invalid:
- Contains `path`, error message, and line number
---
For more examples and advanced usage patterns, see the [test suite](test/) in this repository.