Skip to content

Commit

Permalink
Add more integration tests (#2)
Browse files Browse the repository at this point in the history
Tests all operators, JSONB and error cases against actual Postgres.

Co-authored-by: Koen Bollen <[email protected]>
  • Loading branch information
erikdubbelboer and koenbollen authored May 3, 2024
1 parent debe1f2 commit f804715
Show file tree
Hide file tree
Showing 2 changed files with 297 additions and 0 deletions.
264 changes: 264 additions & 0 deletions integration/postgres_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package integration

import (
"context"
"errors"
"reflect"
"strings"
"testing"

"github.com/lib/pq"
Expand Down Expand Up @@ -217,3 +219,265 @@ func TestIntegration_InAny_PGX(t *testing.T) {
t.Fatalf("expected [3, 4, 5, 6, 7, 8, 9, 10], got %v", ids)
}
}

func TestIntegration_BasicOperators(t *testing.T) {
db := setupPQ(t)

createPlayersTable(t, db)

tests := []struct {
name string
input string
expectedPlayers []int
expectedError error
}{
{
`$gt`,
`{"level": {"$gt": 50}}`,
[]int{6, 7, 8, 9, 10},
nil,
},
{
`$gte`,
`{"level": {"$gte": 50}}`,
[]int{5, 6, 7, 8, 9, 10},
nil,
},
{
`$lt`,
`{"level": {"$lt": 50}}`,
[]int{1, 2, 3, 4},
nil,
},
{
`$lte`,
`{"level": {"$lte": 50}}`,
[]int{1, 2, 3, 4, 5},
nil,
},
{
`$eq`,
`{"name": "Alice"}`,
[]int{1},
nil,
},
{
`$ne`,
`{"name": {"$eq": "Alice"}}`,
[]int{1},
nil,
},
{
`$ne`,
`{"name": {"$ne": "Alice"}}`,
[]int{2, 3, 4, 5, 6, 7, 8, 9, 10},
nil,
},
{
`$regex`,
`{"name": {"$regex": "a.k$"}}`,
[]int{6, 8, 10},
nil,
},
{
`unknown column`,
`{"foobar": "admin"}`,
nil,
errors.New("pq: column \"foobar\" does not exist"),
},
{
`invalid value`,
`{"level": "town1"}`, // Level is an integer column, but the value is a string.
nil,
errors.New("pq: invalid input syntax for type integer: \"town1\""),
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := filter.NewConverter(filter.WithArrayDriver(pq.Array))
where, values, err := c.Convert([]byte(tt.input))
if err != nil {
t.Fatal(err)
}

rows, err := db.Query(`
SELECT id
FROM players
WHERE `+where+`;
`, values...)
if err != nil {
if tt.expectedError == nil {
t.Fatalf("unexpected error: %v", err)
} else if !strings.Contains(err.Error(), tt.expectedError.Error()) {
t.Fatalf("expected error %q, got %q", tt.expectedError, err)
}
return
}
defer rows.Close()
players := []int{}
for rows.Next() {
var id int
if err := rows.Scan(&id); err != nil {
t.Fatal(err)
}
players = append(players, id)
}

if !reflect.DeepEqual(players, tt.expectedPlayers) {
t.Fatalf("%q expected %v, got %v (where clause used: %q)", tt.input, tt.expectedPlayers, players, where)
}
})
}

for op := range filter.BasicOperatorMap {
found := false
for _, tt := range tests {
if strings.Contains(tt.input, op) {
found = true
break
}
}
if !found {
t.Fatalf("operator %q is not tested", op)
}
}
}

func TestIntegration_NestedJSONB(t *testing.T) {
db := setupPQ(t)

createPlayersTable(t, db)

tests := []struct {
name string
input string
expectedPlayers []int
}{
{
"jsonb equals",
`{"guild_id": 20}`,
[]int{1, 2},
},
{
"jsonb regex",
`{"pet": {"$regex": "^.{3}$"}}`,
[]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
},
{
"excemption column",
`{"name": "Alice"}`,
[]int{1},
},
{
"unknown column",
`{"foobar": "admin"}`,
[]int{}, // Will always default to the jsonb column and return no results since it doesn't exist.
},
{
"invalid value",
`{"guild_id": "dragon_slayers"}`, // Guild ID only contains integer values in the test data.
[]int{},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := filter.NewConverter(filter.WithArrayDriver(pq.Array), filter.WithNestedJSONB("metadata", "name", "level", "class"))
where, values, err := c.Convert([]byte(tt.input))
if err != nil {
t.Fatal(err)
}

rows, err := db.Query(`
SELECT id
FROM players
WHERE `+where+`;
`, values...)
if err != nil {
t.Fatal(err)
}
defer rows.Close()
players := []int{}
for rows.Next() {
var id int
if err := rows.Scan(&id); err != nil {
t.Fatal(err)
}
players = append(players, id)
}

if !reflect.DeepEqual(players, tt.expectedPlayers) {
t.Fatalf("%q expected %v, got %v (where clause used: %q)", tt.input, tt.expectedPlayers, players, where)
}
})
}
}

func TestIntegration_Logic(t *testing.T) {
db := setupPQ(t)

createPlayersTable(t, db)

tests := []struct {
name string
input string
expectedPlayers []int
}{
{
"basic or",
`{"$or": [{"level": {"$gt": 50}}, {"pet": "dog"}]}`,
[]int{1, 3, 5, 6, 7, 8, 9, 10},
},
{
// (mages and (ends with E or ends with K)) or (dog owners and (guild in (50, 20)))
"complex triple nested",
`{"$or": [
{"$and": [
{"class": "mage"},
{"$or": [
{"name": {"$regex": "e$"}},
{"name": {"$regex": "k$"}}
]}
]},
{"$and": [
{"pet": "dog"},
{"guild_id": {"$in": [50, 20]}}
]}
]}`,
[]int{1, 5, 7, 8},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := filter.NewConverter(filter.WithArrayDriver(pq.Array), filter.WithNestedJSONB("metadata", "name", "level", "class"))
where, values, err := c.Convert([]byte(tt.input))
if err != nil {
t.Fatal(err)
}

rows, err := db.Query(`
SELECT id
FROM players
WHERE `+where+`;
`, values...)
if err != nil {
t.Fatal(err)
}
defer rows.Close()
players := []int{}
for rows.Next() {
var id int
if err := rows.Scan(&id); err != nil {
t.Fatal(err)
}
players = append(players, id)
}

if !reflect.DeepEqual(players, tt.expectedPlayers) {
t.Fatalf("%q expected %v, got %v (where clause used: %q)", tt.input, tt.expectedPlayers, players, where)
}
})
}
}
33 changes: 33 additions & 0 deletions integration/setup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,36 @@ func setupDatabase(t *testing.T, connect func(string) error) {
}
})
}

// createPlayersTable create a players table with 10 players.
func createPlayersTable(t *testing.T, db *sql.DB) {
t.Helper()

if _, err := db.Exec(`
CREATE TABLE players (
"id" serial PRIMARY KEY,
"name" text,
"metadata" jsonb,
"level" int,
"class" text
);
`); err != nil {
t.Fatal(err)
}
if _, err := db.Exec(`
INSERT INTO players ("id", "name", "metadata", "level", "class")
VALUES
(1, 'Alice', '{"guild_id": 20, "pet": "dog"}', 10, 'warrior'),
(2, 'Bob', '{"guild_id": 20, "pet": "cat"}', 20, 'mage'),
(3, 'Charlie', '{"guild_id": 30, "pet": "dog"}', 30, 'rogue'),
(4, 'David', '{"guild_id": 30, "pet": "cat"}', 40, 'warrior'),
(5, 'Eve', '{"guild_id": 40, "pet": "dog"}', 50, 'mage'),
(6, 'Frank', '{"guild_id": 40, "pet": "cat"}', 60, 'rogue'),
(7, 'Grace', '{"guild_id": 50, "pet": "dog"}', 70, 'warrior'),
(8, 'Hank', '{"guild_id": 50, "pet": "cat"}', 80, 'mage'),
(9, 'Ivy', '{"guild_id": 60, "pet": "dog"}', 90, 'rogue'),
(10, 'Jack', '{"guild_id": 60, "pet": "cat"}', 100, 'warrior')
`); err != nil {
t.Fatal(err)
}
}

0 comments on commit f804715

Please sign in to comment.