Skip to content

Commit

Permalink
[Tavern] YAML Tests (#178)
Browse files Browse the repository at this point in the history
* Enabled defining tavern tests using YAML

* Added a few test cases

* added mutation tests

* added simple query tests

* Updated docs
  • Loading branch information
KCarretto authored Apr 12, 2023
1 parent ceab700 commit c1d9ef9
Show file tree
Hide file tree
Showing 29 changed files with 865 additions and 1 deletion.
14 changes: 13 additions & 1 deletion docs/_docs/dev-guide/tavern.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ If you'd like to explore the Graph API and try out some queries, head to the `/g
2. Run `go generate ./...`
3. Implement generated the generated mutation resolver method in `tavern/graphql/mutation.resolvers.go`
* Depending on the mutation you're trying to implement, a one liner such as `return r.client.<NAME>.Create().SetInput(input).Save(ctx)` might be sufficient
4. Please write a unit test for your new mutation in `<NAME>_test.go` <3
4. Please write a unit test for your new mutation by defining YAML test cases in a new `testdata/mutations` subdirectory with your mutations name (e.g. `tavern/graphql/testdata/mutations/mymutation/SomeTest.yml`)

### Code Generation Reference

Expand All @@ -161,6 +161,18 @@ If you'd like to explore the Graph API and try out some queries, head to the `/g
* `tavern/graphql/schema/scalars.graphql` defines scalar GraphQL types that can be used to help with Go bindings (See [gqlgen docs](https://gqlgen.com/reference/scalars/) for more info)
* `tavern/graphql/schema/inputs.graphql` defines custom GraphQL inputs that can be used with your mutations (e.g. outside of the default auto-generated CRUD inputs)

### YAML Test Reference

|Field|Description|Required|
|-----|-----------|--------|
|state| SQL queries that define the initial db state before the query is run.| no |
|requestor| Holds information about the authenticated context making the query. | no |
|requestor.session_token| Session token corresponding to the user for authentication. You may create a user with a predetermined session token using the `state` field. | no |
|query| GraphQL query or mutation to be executed | yes |
|variables| A map of variables that will be passed with your GraphQL Query to the server | no |
|expected| A map that defines the expected response that the server should return | no |
|expected_error| An expected message that should be included in the query when it fails | no |

### Resources

* [Relay Documentation](https://relay.dev/graphql/connections.htm)
Expand Down
138 changes: 138 additions & 0 deletions tavern/graphql/api_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package graphql_test

import (
"database/sql"
"encoding/json"
"net/http"
"os"
"path/filepath"
"strings"
"testing"
"time"

"github.com/kcarretto/realm/tavern/auth"
"github.com/kcarretto/realm/tavern/ent/enttest"
"github.com/kcarretto/realm/tavern/graphql"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/99designs/gqlgen/client"
"github.com/99designs/gqlgen/graphql/handler"
_ "github.com/mattn/go-sqlite3"
"gopkg.in/yaml.v3"
)

type testCase struct {
State string `yaml:"state"`
Requestor struct {
SessionToken string `yaml:"session_token"`
} `yaml:"requestor"`
Query string `yaml:"query"`
Variables map[string]any `yaml:"variables"`
Expected map[string]any `yaml:"expected"`
ExpectedError string `yaml:"expected_error"`
}

func runTestCase(t *testing.T, path string) {
// TestDB Config
var (
driverName = "sqlite3"
dataSourceName = "file:ent?mode=memory&cache=shared&_fk=1"
)

// Read Test Case
tcBytes, err := os.ReadFile(path)
require.NoError(t, err, "failed to read test case %q", path)

// Parse Test Case
var tc testCase
yamlErr := yaml.Unmarshal(tcBytes, &tc)
require.NoError(t, yamlErr, "failed to parse test case %q", path)

// Marshal expected result to JSON
expectedJSON, err := json.Marshal(tc.Expected)
require.NoError(t, err)

// Ent Client
graph := enttest.Open(t, driverName, dataSourceName, enttest.WithOptions())
defer graph.Close()

// Initial DB State
db, err := sql.Open(driverName, dataSourceName)
require.NoError(t, err, "failed to open test db")
defer db.Close()
_, dbErr := db.Exec(tc.State)
require.NoError(t, dbErr, "failed to setup test db state")

// Server
srv := auth.Middleware(handler.NewDefaultServer(graphql.NewSchema(graph)), graph)
gqlClient := client.New(srv)

var opts []client.Option

// Variables
for key, val := range tc.Variables {
opts = append(opts, client.Var(key, val))
}

// Requestor
if tc.Requestor.SessionToken != "" {
opts = append(opts, client.AddCookie(&http.Cookie{
Name: auth.SessionCookieName,
Value: tc.Requestor.SessionToken,
Expires: time.Now().Add(24 * time.Hour),
}))
}

// Make Request
resp := new(map[string]any)
queryErr := gqlClient.Post(tc.Query, resp, opts...)

// Handle Expected Errors
if tc.ExpectedError != "" {
assert.ErrorContains(t, queryErr, tc.ExpectedError)
return
}
require.NoError(t, queryErr, "query failed with error")

// Marshal response to JSON
respJSON, err := json.Marshal(resp)
require.NoError(t, err, "failed to marshal response to JSON")

// Assert the result is as expected
assert.Equal(t, string(expectedJSON), string(respJSON), "response does not match expected result")
}

// TestAPI finds and runs all test cases defined in the testdata directory.
func TestAPI(t *testing.T) {
runTestsInDir(t, "testdata")
}

func runTestsInDir(t *testing.T, root string) {
files, err := os.ReadDir(root)
require.NoError(t, err)

for _, f := range files {
// Derive relative path
path := filepath.Join(root, f.Name())

// Recurse in subdirectories by grouping them into sub-tests
if f.IsDir() {
t.Run(filepath.Base(f.Name()), func(t *testing.T) {
runTestsInDir(t, path)
})
continue
}

// Skip files that are not test case files
if filepath.Ext(path) != ".yml" {
continue
}

// Run test case
testName := filepath.Base(strings.TrimSuffix(path, ".yml"))
t.Run(testName, func(t *testing.T) {
runTestCase(t, path)
})
}
}
23 changes: 23 additions & 0 deletions tavern/graphql/testdata/example.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Example Test Case

# Create a user in the DB before the test case runs
state: |
INSERT INTO `users` (id,oauth_id,photo_url,name,session_token,is_activated,is_admin)
VALUES (5,"test_oauth_id","https://photos.com","test","secretToken",true,true);
# Authenticate as that user (via session_token)
requestor:
session_token: secretToken

# Query all existing users
query: |
query Users {
users {
id
}
}
# Ensure the GraphQL query returns exactly one user, the initial one we created
expected:
users:
- id: "5"
20 changes: 20 additions & 0 deletions tavern/graphql/testdata/mutations/claimTasks/ExistingSession.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
state: |
INSERT INTO `sessions` (id, name, identifier)
VALUES (1337,"delightful-lich","EXISTING-SESSION");
query: |
mutation ExistingSession($input: ClaimTasksInput!) {
claimTasks(input: $input) {
id
}
}
variables:
input:
principal: root
hostname: some-machine
hostPlatform: Linux
sessionIdentifier: EXISTING-SESSION
hostIdentifier: MY-HOST
agentIdentifier: COOL-TEST

expected:
claimTasks: []
36 changes: 36 additions & 0 deletions tavern/graphql/testdata/mutations/claimTasks/Filters.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
state: |
INSERT INTO `sessions` (id, name, identifier)
VALUES (1337,"delightful-lich","EXISTING-SESSION");
INSERT INTO `sessions` (id, name, identifier)
VALUES (1338,"bad-boi","BAD-SESSION");
INSERT INTO `tomes` (id, name, description, eldritch, hash, created_at, last_modified_at)
VALUES (2000,"Test Tome","Used in a unit test :D","print('Hello World!')","abcdefg","2023-03-04 14:51:13","2023-03-04 14:51:13");
INSERT INTO `jobs` (id, job_tome, name, created_at, last_modified_at)
VALUES (7000,2000,"Test Job","2023-03-04 14:51:13","2023-03-04 14:51:13");
INSERT INTO `tasks` (id, task_session, job_tasks, created_at, last_modified_at)
VALUES (8000,1337,7000,"2023-03-04 14:51:13","2023-03-04 14:51:13");
INSERT INTO `tasks` (id, task_session, job_tasks, created_at, last_modified_at)
VALUES (8001,1337,7000,"2023-03-04 14:51:13","2023-03-04 14:51:13");
INSERT INTO `tasks` (id, task_session, job_tasks, created_at, last_modified_at)
VALUES (8002,1338,7000,"2023-03-04 14:51:13","2023-03-04 14:51:13");
INSERT INTO `tasks` (id, task_session, job_tasks, created_at, last_modified_at, claimed_at)
VALUES (8003,1337,7000,"2023-03-04 14:51:13","2023-03-04 14:51:13","2023-03-04 14:51:13");
query: |
mutation Filters($input: ClaimTasksInput!) {
claimTasks(input: $input) {
id
}
}
variables:
input:
principal: root
hostname: some-machine
hostPlatform: Linux
sessionIdentifier: EXISTING-SESSION
hostIdentifier: MY-HOST
agentIdentifier: COOL-TEST

expected:
claimTasks:
- id: "8000"
- id: "8001"
30 changes: 30 additions & 0 deletions tavern/graphql/testdata/mutations/claimTasks/MultiTask.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
state: |
INSERT INTO `sessions` (id, name, identifier)
VALUES (1337,"delightful-lich","EXISTING-SESSION");
INSERT INTO `tomes` (id, name, description, eldritch, hash, created_at, last_modified_at)
VALUES (2000,"Test Tome","Used in a unit test :D","print('Hello World!')","abcdefg","2023-03-04 14:51:13","2023-03-04 14:51:13");
INSERT INTO `jobs` (id, job_tome, name, created_at, last_modified_at)
VALUES (7000,2000,"Test Job","2023-03-04 14:51:13","2023-03-04 14:51:13");
INSERT INTO `tasks` (id, task_session, job_tasks, created_at, last_modified_at)
VALUES (8000,1337,7000,"2023-03-04 14:51:13","2023-03-04 14:51:13");
INSERT INTO `tasks` (id, task_session, job_tasks, created_at, last_modified_at)
VALUES (8001,1337,7000,"2023-03-04 14:51:13","2023-03-04 14:51:13");
query: |
mutation MutiTask($input: ClaimTasksInput!) {
claimTasks(input: $input) {
id
}
}
variables:
input:
principal: root
hostname: some-machine
hostPlatform: Linux
sessionIdentifier: EXISTING-SESSION
hostIdentifier: MY-HOST
agentIdentifier: COOL-TEST

expected:
claimTasks:
- id: "8000"
- id: "8001"
17 changes: 17 additions & 0 deletions tavern/graphql/testdata/mutations/claimTasks/NewSession.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
query: |
mutation NewSession($input: ClaimTasksInput!) {
claimTasks(input: $input) {
id
}
}
variables:
input:
principal: root
hostname: some-machine
hostPlatform: Linux
sessionIdentifier: SESSION-IDENTIFIER
hostIdentifier: MY-HOST
agentIdentifier: COOL-TEST

expected:
claimTasks: []
27 changes: 27 additions & 0 deletions tavern/graphql/testdata/mutations/claimTasks/OneTask.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
state: |
INSERT INTO `sessions` (id, name, identifier)
VALUES (1337,"delightful-lich","EXISTING-SESSION");
INSERT INTO `tomes` (id, name, description, eldritch, hash, created_at, last_modified_at)
VALUES (2000,"Test Tome","Used in a unit test :D","print('Hello World!')","abcdefg","2023-03-04 14:51:13","2023-03-04 14:51:13");
INSERT INTO `jobs` (id, job_tome, name, created_at, last_modified_at)
VALUES (7000,2000,"Test Job","2023-03-04 14:51:13","2023-03-04 14:51:13");
INSERT INTO `tasks` (id, task_session, job_tasks, created_at, last_modified_at)
VALUES (8000,1337,7000,"2023-03-04 14:51:13","2023-03-04 14:51:13");
query: |
mutation OneTask($input: ClaimTasksInput!) {
claimTasks(input: $input) {
id
}
}
variables:
input:
principal: root
hostname: some-machine
hostPlatform: Linux
sessionIdentifier: EXISTING-SESSION
hostIdentifier: MY-HOST
agentIdentifier: COOL-TEST

expected:
claimTasks:
- id: "8000"
45 changes: 45 additions & 0 deletions tavern/graphql/testdata/mutations/createJob/NoFiles.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
state: |
INSERT INTO `users` (id,oauth_id,photo_url,name,session_token,is_activated,is_admin)
VALUES (5,"test_oauth_id","https://photos.com","test","secretToken",true,true);
INSERT INTO `sessions` (id, name, identifier)
VALUES (1337,"delightful-lich","ABCDEFG-123456");
INSERT INTO `tomes` (id, name, description, eldritch, hash, created_at, last_modified_at)
VALUES (2000,"Test Tome","Used in a unit test :D", "print('Hello World!')", "abcdefg", "2023-03-04 14:51:13", "2023-03-04 14:51:13");
requestor:
session_token: secretToken
query: |
mutation CreateJobWithNoFiles($sessionIDs: [ID!]!, $input: CreateJobInput!) {
createJob(sessionIDs:$sessionIDs, input:$input) {
name
tome {
id
name
}
tasks {
session {
id
}
job {
name
}
}
}
}
variables:
sessionIDs:
- 1337
input:
name: "WonderfulJob"
tomeID: "2000"

expected:
createJob:
name: "WonderfulJob"
tome:
id: "2000"
name: "Test Tome"
tasks:
- session:
id: "1337"
job:
name: "WonderfulJob"
Loading

0 comments on commit c1d9ef9

Please sign in to comment.