safing-portmaster/service/netquery/orm/decoder_test.go

570 lines
11 KiB
Go

package orm
import (
"bytes"
"context"
"encoding/json"
"reflect"
"testing"
"time"
"github.com/stretchr/testify/assert"
"zombiezen.com/go/sqlite"
)
type testStmt struct {
columns []string
values []interface{}
types []sqlite.ColumnType
}
func (ts testStmt) ColumnCount() int { return len(ts.columns) }
func (ts testStmt) ColumnName(i int) string { return ts.columns[i] }
func (ts testStmt) ColumnBool(i int) bool { return ts.values[i].(bool) } //nolint:forcetypeassert
func (ts testStmt) ColumnText(i int) string { return ts.values[i].(string) } //nolint:forcetypeassert
func (ts testStmt) ColumnFloat(i int) float64 { return ts.values[i].(float64) } //nolint:forcetypeassert
func (ts testStmt) ColumnInt(i int) int { return ts.values[i].(int) } //nolint:forcetypeassert
func (ts testStmt) ColumnReader(i int) *bytes.Reader { return bytes.NewReader(ts.values[i].([]byte)) } //nolint:forcetypeassert
func (ts testStmt) ColumnType(i int) sqlite.ColumnType { return ts.types[i] }
// Compile time check.
var _ Stmt = new(testStmt)
type exampleFieldTypes struct {
S string
I int
F float64
B bool
}
type examplePointerTypes struct {
S *string
I *int
F *float64
B *bool
}
type exampleStructTags struct {
S string `sqlite:"col_string"`
I int `sqlite:"col_int"`
}
type exampleIntConv struct {
I8 int8
I16 int16
I32 int32
I64 int64
I int
}
type exampleBlobTypes struct {
B []byte
}
type exampleJSONRawTypes struct {
B json.RawMessage
}
type exampleTimeTypes struct {
T time.Time
TP *time.Time
}
type exampleInterface struct {
I interface{}
IP *interface{}
}
func (ett *exampleTimeTypes) Equal(other interface{}) bool {
oett, ok := other.(*exampleTimeTypes)
if !ok {
return false
}
return ett.T.Equal(oett.T) && (ett.TP != nil && oett.TP != nil && ett.TP.Equal(*oett.TP)) || (ett.TP == nil && oett.TP == nil)
}
type myInt int
type exampleTimeNano struct {
T time.Time `sqlite:",unixnano"`
}
func (etn *exampleTimeNano) Equal(other interface{}) bool {
oetn, ok := other.(*exampleTimeNano)
if !ok {
return false
}
return etn.T.Equal(oetn.T)
}
func TestDecoder(t *testing.T) { //nolint:maintidx,tparallel
t.Parallel()
ctx := context.TODO()
refTime := time.Date(2022, time.February, 15, 9, 51, 0, 0, time.UTC)
cases := []struct {
Desc string
Stmt testStmt
ColumnDef []ColumnDef
Result interface{}
Expected interface{}
}{
{
"Decoding into nil is not allowed",
testStmt{
columns: nil,
values: nil,
types: nil,
},
nil,
nil,
nil,
},
{
"Decoding into basic types",
testStmt{
columns: []string{"S", "I", "F", "B"},
types: []sqlite.ColumnType{
sqlite.TypeText,
sqlite.TypeInteger,
sqlite.TypeFloat,
sqlite.TypeInteger,
},
values: []interface{}{
"string value",
1,
1.2,
true,
},
},
nil,
&exampleFieldTypes{},
&exampleFieldTypes{
S: "string value",
I: 1,
F: 1.2,
B: true,
},
},
{
"Decoding into basic types with different order",
testStmt{
columns: []string{"I", "S", "B", "F"},
types: []sqlite.ColumnType{
sqlite.TypeInteger,
sqlite.TypeText,
sqlite.TypeInteger,
sqlite.TypeFloat,
},
values: []interface{}{
1,
"string value",
true,
1.2,
},
},
nil,
&exampleFieldTypes{},
&exampleFieldTypes{
S: "string value",
I: 1,
F: 1.2,
B: true,
},
},
{
"Decoding into basic types with missing values",
testStmt{
columns: []string{"F", "B"},
types: []sqlite.ColumnType{
sqlite.TypeFloat,
sqlite.TypeInteger,
},
values: []interface{}{
1.2,
true,
},
},
nil,
&exampleFieldTypes{},
&exampleFieldTypes{
F: 1.2,
B: true,
},
},
{
"Decoding into pointer types",
testStmt{
columns: []string{"S", "I", "F", "B"},
types: []sqlite.ColumnType{
sqlite.TypeText,
sqlite.TypeInteger,
sqlite.TypeFloat,
sqlite.TypeInteger,
},
values: []interface{}{
"string value",
1,
1.2,
true,
},
},
nil,
&examplePointerTypes{},
func() interface{} {
s := "string value"
i := 1
f := 1.2
b := true
return &examplePointerTypes{
S: &s,
I: &i,
F: &f,
B: &b,
}
},
},
{
"Decoding into pointer types with missing values",
testStmt{
columns: []string{"S", "B"},
types: []sqlite.ColumnType{
sqlite.TypeText,
sqlite.TypeInteger,
sqlite.TypeFloat,
sqlite.TypeInteger,
},
values: []interface{}{
"string value",
true,
},
},
nil,
&examplePointerTypes{},
func() interface{} {
s := "string value"
b := true
return &examplePointerTypes{
S: &s,
B: &b,
}
},
},
{
"Decoding into fields with struct tags",
testStmt{
columns: []string{"col_string", "col_int"},
types: []sqlite.ColumnType{
sqlite.TypeText,
sqlite.TypeInteger,
},
values: []interface{}{
"string value",
1,
},
},
nil,
&exampleStructTags{},
&exampleStructTags{
S: "string value",
I: 1,
},
},
{
"Decoding into correct int type",
testStmt{
columns: []string{"I8", "I16", "I32", "I64", "I"},
types: []sqlite.ColumnType{
sqlite.TypeInteger,
sqlite.TypeInteger,
sqlite.TypeInteger,
sqlite.TypeInteger,
sqlite.TypeInteger,
},
values: []interface{}{
1,
1,
1,
1,
1,
},
},
nil,
&exampleIntConv{},
&exampleIntConv{
1, 1, 1, 1, 1,
},
},
{
"Handling NULL values for basic types",
testStmt{
columns: []string{"S", "I", "F"},
types: []sqlite.ColumnType{
sqlite.TypeNull,
sqlite.TypeNull,
sqlite.TypeFloat,
},
values: []interface{}{
// we use nil here but actually that does not matter
nil,
nil,
1.0,
},
},
nil,
&exampleFieldTypes{},
&exampleFieldTypes{
F: 1.0,
},
},
{
"Handling NULL values for pointer types",
testStmt{
columns: []string{"S", "I", "F"},
types: []sqlite.ColumnType{
sqlite.TypeNull,
sqlite.TypeNull,
sqlite.TypeFloat,
},
values: []interface{}{
// we use nil here but actually that does not matter
nil,
nil,
1.0,
},
},
nil,
&examplePointerTypes{},
func() interface{} {
f := 1.0
return &examplePointerTypes{F: &f}
},
},
{
"Handling blob types",
testStmt{
columns: []string{"B"},
types: []sqlite.ColumnType{
sqlite.TypeBlob,
},
values: []interface{}{
([]byte)("hello world"),
},
},
nil,
&exampleBlobTypes{},
&exampleBlobTypes{
B: ([]byte)("hello world"),
},
},
{
"Handling blob types as json.RawMessage",
testStmt{
columns: []string{"B"},
types: []sqlite.ColumnType{
sqlite.TypeBlob,
},
values: []interface{}{
([]byte)("hello world"),
},
},
nil,
&exampleJSONRawTypes{},
&exampleJSONRawTypes{
B: (json.RawMessage)("hello world"),
},
},
{
"Handling time.Time and pointers to it",
testStmt{
columns: []string{"T", "TP"},
types: []sqlite.ColumnType{
sqlite.TypeInteger,
sqlite.TypeInteger,
},
values: []interface{}{
int(refTime.Unix()),
int(refTime.Unix()),
},
},
nil,
&exampleTimeTypes{},
&exampleTimeTypes{
T: refTime,
TP: &refTime,
},
},
{
"Handling time.Time in nano-second resolution (struct tags)",
testStmt{
columns: []string{"T", "TP"},
types: []sqlite.ColumnType{
sqlite.TypeInteger,
sqlite.TypeInteger,
},
values: []interface{}{
int(refTime.UnixNano()),
int(refTime.UnixNano()),
},
},
nil,
&exampleTimeNano{},
&exampleTimeNano{
T: refTime,
},
},
{
"Decoding into interface",
testStmt{
columns: []string{"I", "IP"},
types: []sqlite.ColumnType{
sqlite.TypeText,
sqlite.TypeText,
},
values: []interface{}{
"value1",
"value2",
},
},
nil,
&exampleInterface{},
func() interface{} {
var x interface{} = "value2"
return &exampleInterface{
I: "value1",
IP: &x,
}
},
},
{
"Decoding into map[string]interface{}",
testStmt{
columns: []string{"I", "F", "S", "B"},
types: []sqlite.ColumnType{
sqlite.TypeInteger,
sqlite.TypeFloat,
sqlite.TypeText,
sqlite.TypeBlob,
},
values: []interface{}{
1,
1.1,
"string value",
[]byte("blob value"),
},
},
nil,
new(map[string]interface{}),
&map[string]interface{}{
"I": 1,
"F": 1.1,
"S": "string value",
"B": []byte("blob value"),
},
},
{
"Decoding using type-hints",
testStmt{
columns: []string{"B", "T"},
types: []sqlite.ColumnType{
sqlite.TypeInteger,
sqlite.TypeText,
},
values: []interface{}{
true,
refTime.Format(SqliteTimeFormat),
},
},
[]ColumnDef{
{
Name: "B",
Type: sqlite.TypeInteger,
GoType: reflect.TypeOf(true),
},
{
Name: "T",
Type: sqlite.TypeText,
GoType: reflect.TypeOf(time.Time{}),
IsTime: true,
},
},
new(map[string]interface{}),
&map[string]interface{}{
"B": true,
"T": refTime,
},
},
{
"Decoding into type aliases",
testStmt{
columns: []string{"B"},
types: []sqlite.ColumnType{
sqlite.TypeBlob,
},
values: []interface{}{
[]byte(`{"foo": "bar}`),
},
},
[]ColumnDef{
{
Name: "B",
Type: sqlite.TypeBlob,
GoType: reflect.TypeOf(json.RawMessage(`{"foo": "bar}`)),
},
},
new(map[string]interface{}),
&map[string]interface{}{
"B": json.RawMessage(`{"foo": "bar}`),
},
},
{
"Decoding into type aliases #2",
testStmt{
columns: []string{"I"},
types: []sqlite.ColumnType{sqlite.TypeInteger},
values: []interface{}{
10,
},
},
[]ColumnDef{
{
Name: "I",
Type: sqlite.TypeInteger,
GoType: reflect.TypeOf(myInt(0)),
},
},
new(map[string]interface{}),
&map[string]interface{}{
"I": myInt(10),
},
},
}
for idx := range cases { //nolint:paralleltest
c := cases[idx]
t.Run(c.Desc, func(t *testing.T) {
// log.Println(c.Desc)
err := DecodeStmt(ctx, &TableSchema{Columns: c.ColumnDef}, c.Stmt, c.Result, DefaultDecodeConfig)
if fn, ok := c.Expected.(func() interface{}); ok {
c.Expected = fn()
}
if c.Expected == nil {
assert.Error(t, err, c.Desc)
} else {
assert.NoError(t, err, c.Desc)
if equaler, ok := c.Expected.(interface{ Equal(x interface{}) bool }); ok {
assert.True(t, equaler.Equal(c.Result))
} else {
assert.Equal(t, c.Expected, c.Result)
}
}
})
}
}