mirror of
https://github.com/open-policy-agent/opa.git
synced 2026-08-12 19:32:48 -06:00
f77322b3fb
Go 1.23 is no longer supported as per Go release policy. Changes: - Use Go v1.24.6 as the project SDK requirement - Apply lint fixes for Go 1.24 - Fix "non-constant format string in call" issues as seen in CI. Signed-off-by: Ville Vesilehto <ville@vesilehto.fi>
1353 lines
40 KiB
Go
1353 lines
40 KiB
Go
// Copyright 2016 The OPA Authors. All rights reserved.
|
|
// Use of this source code is governed by an Apache2
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package inmem
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"reflect"
|
|
"slices"
|
|
"testing"
|
|
|
|
"github.com/open-policy-agent/opa/internal/file/archive"
|
|
"github.com/open-policy-agent/opa/v1/ast"
|
|
"github.com/open-policy-agent/opa/v1/bundle"
|
|
storageerrors "github.com/open-policy-agent/opa/v1/storage/internal/errors"
|
|
|
|
"github.com/open-policy-agent/opa/v1/storage"
|
|
"github.com/open-policy-agent/opa/v1/util"
|
|
)
|
|
|
|
func TestInMemoryRead(t *testing.T) {
|
|
|
|
data := loadSmallTestData()
|
|
|
|
var tests = []struct {
|
|
path string
|
|
expected any
|
|
}{
|
|
{"/a/0", json.Number("1")},
|
|
{"/a/3", json.Number("4")},
|
|
{"/b/v1", "hello"},
|
|
{"/b/v2", "goodbye"},
|
|
{"/c/0/x/1", false},
|
|
{"/c/0/y/0", nil},
|
|
{"/c/0/y/1", json.Number("3.14159")},
|
|
{"/d/e/1", "baz"},
|
|
{"/d/e", []any{"bar", "baz"}},
|
|
{"/c/0/z", map[string]any{"p": true, "q": false}},
|
|
{"/a/0/beef", storageerrors.NewNotFoundError(storage.MustParsePath("/a/0/beef"))},
|
|
{"/d/100", storageerrors.NewNotFoundError(storage.MustParsePath("/d/100"))},
|
|
{"/dead/beef", storageerrors.NewNotFoundError(storage.MustParsePath("/dead/beef"))},
|
|
{"/a/str", storageerrors.NewNotFoundErrorWithHint(storage.MustParsePath("/a/str"), storageerrors.ArrayIndexTypeMsg)},
|
|
{"/a/100", storageerrors.NewNotFoundErrorWithHint(storage.MustParsePath("/a/100"), storageerrors.OutOfRangeMsg)},
|
|
{"/a/-1", storageerrors.NewNotFoundErrorWithHint(storage.MustParsePath("/a/-1"), storageerrors.OutOfRangeMsg)},
|
|
}
|
|
|
|
store := NewFromObject(data)
|
|
ctx := t.Context()
|
|
|
|
for idx, tc := range tests {
|
|
result, err := storage.ReadOne(ctx, store, storage.MustParsePath(tc.path))
|
|
switch e := tc.expected.(type) {
|
|
case error:
|
|
if err == nil {
|
|
t.Errorf("Test case %d: expected error for %v but got %v", idx+1, tc.path, result)
|
|
} else if !reflect.DeepEqual(err, tc.expected) {
|
|
t.Errorf("Test case %d: unexpected error for %v: %v, expected: %v", idx+1, tc.path, err, e)
|
|
}
|
|
default:
|
|
if err != nil {
|
|
t.Errorf("Test case %d: expected success for %v but got %v", idx+1, tc.path, err)
|
|
}
|
|
if !reflect.DeepEqual(result, tc.expected) {
|
|
t.Errorf("Test case %d: expected %f but got %f", idx+1, tc.expected, result)
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
func TestInMemoryReadAst(t *testing.T) {
|
|
|
|
data := loadSmallTestData()
|
|
|
|
var tests = []struct {
|
|
path string
|
|
expected any
|
|
}{
|
|
{"/a/0", ast.Number("1")},
|
|
{"/a/3", ast.Number("4")},
|
|
{"/b/v1", ast.String("hello")},
|
|
{"/b/v2", ast.String("goodbye")},
|
|
{"/c/0/x/1", ast.Boolean(false)},
|
|
{"/c/0/y/0", ast.Null{}},
|
|
{"/c/0/y/1", ast.Number("3.14159")},
|
|
{"/d/e/1", ast.String("baz")},
|
|
{"/d/e", ast.NewArray(ast.StringTerm("bar"), ast.StringTerm("baz"))},
|
|
{"/c/0/z", ast.NewObject(ast.Item(ast.StringTerm("p"), ast.BooleanTerm(true)), ast.Item(ast.StringTerm("q"), ast.BooleanTerm(false)))},
|
|
{"/a/0/beef", storageerrors.NewNotFoundError(storage.MustParsePath("/a/0/beef"))},
|
|
{"/d/100", storageerrors.NewNotFoundError(storage.MustParsePath("/d/100"))},
|
|
{"/dead/beef", storageerrors.NewNotFoundError(storage.MustParsePath("/dead/beef"))},
|
|
{"/a/str", storageerrors.NewNotFoundErrorWithHint(storage.MustParsePath("/a/str"), storageerrors.ArrayIndexTypeMsg)},
|
|
{"/a/100", storageerrors.NewNotFoundErrorWithHint(storage.MustParsePath("/a/100"), storageerrors.OutOfRangeMsg)},
|
|
{"/a/-1", storageerrors.NewNotFoundErrorWithHint(storage.MustParsePath("/a/-1"), storageerrors.OutOfRangeMsg)},
|
|
}
|
|
|
|
store := NewFromObjectWithOpts(data, OptReturnASTValuesOnRead(true))
|
|
ctx := t.Context()
|
|
|
|
for idx, tc := range tests {
|
|
result, err := storage.ReadOne(ctx, store, storage.MustParsePath(tc.path))
|
|
switch e := tc.expected.(type) {
|
|
case error:
|
|
if err == nil {
|
|
t.Errorf("Test case %d: expected error for %v but got %v", idx+1, tc.path, result)
|
|
} else if !reflect.DeepEqual(err, tc.expected) {
|
|
t.Errorf("Test case %d: unexpected error for %v: %v, expected: %v", idx+1, tc.path, err, e)
|
|
}
|
|
default:
|
|
if err != nil {
|
|
t.Errorf("Test case %d: expected success for %v but got %v", idx+1, tc.path, err)
|
|
}
|
|
if ast.Compare(result, tc.expected) != 0 {
|
|
t.Errorf("Test case %d: expected %f but got %f", idx+1, tc.expected, result)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestInMemoryWrite(t *testing.T) {
|
|
readValueType := []struct {
|
|
note string
|
|
ast bool
|
|
}{
|
|
{"raw", false},
|
|
{"ast", true},
|
|
}
|
|
|
|
for _, rvt := range readValueType {
|
|
t.Run(rvt.note, func(t *testing.T) {
|
|
tests := []struct {
|
|
note string
|
|
op string
|
|
path string
|
|
value string
|
|
expected error
|
|
getPath string
|
|
getExpected any
|
|
}{
|
|
{"add root", "add", "/", `{"a": [1]}`, nil, "/", `{"a": [1]}`},
|
|
{"add", "add", "/newroot", `{"a": [[1]]}`, nil, "/newroot", `{"a": [[1]]}`},
|
|
{"add arr", "add", "/a/1", `"x"`, nil, "/a", `[1,"x",2,3,4]`},
|
|
{"add arr/arr", "add", "/h/1/2", `"x"`, nil, "/h", `[[1,2,3], [2,3,"x",4]]`},
|
|
{"add obj/arr", "add", "/d/e/1", `"x"`, nil, "/d", `{"e": ["bar", "x", "baz"]}`},
|
|
{"add obj", "add", "/b/vNew", `"x"`, nil, "/b", `{"v1": "hello", "v2": "goodbye", "vNew": "x"}`},
|
|
{"add obj (existing)", "add", "/b/v2", `"x"`, nil, "/b", `{"v1": "hello", "v2": "x"}`},
|
|
|
|
{"append arr", "add", "/a/-", `"x"`, nil, "/a", `[1,2,3,4,"x"]`},
|
|
{"append arr-2", "add", "/a/4", `"x"`, nil, "/a", `[1,2,3,4,"x"]`},
|
|
{"append obj/arr", "add", `/c/0/x/-`, `"x"`, nil, "/c/0/x", `[true,false,"foo","x"]`},
|
|
{"append obj/arr-2", "add", `/c/0/x/3`, `"x"`, nil, "/c/0/x", `[true,false,"foo","x"]`},
|
|
{"append arr/arr", "add", `/h/0/-`, `"x"`, nil, `/h/0/3`, `"x"`},
|
|
{"append arr/arr-2", "add", `/h/0/3`, `"x"`, nil, `/h/0/3`, `"x"`},
|
|
{"append err", "remove", "/c/0/x/-", "", invalidPatchError("/c/0/x/-: invalid patch path"), "", nil},
|
|
{"append err-2", "replace", "/c/0/x/-", "", invalidPatchError("/c/0/x/-: invalid patch path"), "", nil},
|
|
|
|
{"remove", "remove", "/a", "", nil, "/a", storageerrors.NewNotFoundError(storage.MustParsePath("/a"))},
|
|
{"remove arr", "remove", "/a/1", "", nil, "/a", "[1,3,4]"},
|
|
{"remove obj/arr", "remove", "/c/0/x/1", "", nil, "/c/0/x", `[true,"foo"]`},
|
|
{"remove arr/arr", "remove", "/h/0/1", "", nil, "/h/0", "[1,3]"},
|
|
{"remove obj", "remove", "/b/v2", "", nil, "/b", `{"v1": "hello"}`},
|
|
|
|
{"replace root", "replace", "/", `{"a": [1]}`, nil, "/", `{"a": [1]}`},
|
|
{"replace", "replace", "/a", "1", nil, "/a", "1"},
|
|
{"replace obj", "replace", "/b/v1", "1", nil, "/b", `{"v1": 1, "v2": "goodbye"}`},
|
|
{"replace array", "replace", "/a/1", "999", nil, "/a", "[1,999,3,4]"},
|
|
|
|
{"err: bad root type", "add", "/", "[1,2,3]", invalidPatchError(rootMustBeObjectMsg), "", nil},
|
|
{"err: remove root", "remove", "/", "", invalidPatchError(rootCannotBeRemovedMsg), "", nil},
|
|
{"err: add arr (non-integer)", "add", "/a/foo", "1", storageerrors.NewNotFoundErrorWithHint(storage.MustParsePath("/a/foo"), storageerrors.ArrayIndexTypeMsg), "", nil},
|
|
{"err: add arr (non-integer)", "add", "/a/3.14", "1", storageerrors.NewNotFoundErrorWithHint(storage.MustParsePath("/a/3.14"), storageerrors.ArrayIndexTypeMsg), "", nil},
|
|
{"err: add arr (out of range)", "add", "/a/5", "1", storageerrors.NewNotFoundErrorWithHint(storage.MustParsePath("/a/5"), storageerrors.OutOfRangeMsg), "", nil},
|
|
{"err: add arr (out of range)", "add", "/a/-1", "1", storageerrors.NewNotFoundErrorWithHint(storage.MustParsePath("/a/-1"), storageerrors.OutOfRangeMsg), "", nil},
|
|
{"err: add arr (missing root)", "add", "/dead/beef/0", "1", storageerrors.NewNotFoundError(storage.MustParsePath("/dead/beef/0")), "", nil},
|
|
{"err: add non-coll", "add", "/a/1/2", "1", storageerrors.NewNotFoundError(storage.MustParsePath("/a/1/2")), "", nil},
|
|
{"err: append (missing)", "add", `/dead/beef/-`, "1", storageerrors.NewNotFoundError(storage.MustParsePath("/dead/beef/-")), "", nil},
|
|
{"err: append obj/arr", "add", `/c/0/deadbeef/-`, `"x"`, storageerrors.NewNotFoundError(storage.MustParsePath("/c/0/deadbeef/-")), "", nil},
|
|
{"err: append arr/arr (out of range)", "add", `/h/9999/-`, `"x"`, storageerrors.NewNotFoundErrorWithHint(storage.MustParsePath("/h/9999/-"), storageerrors.OutOfRangeMsg), "", nil},
|
|
{"err: append append+add", "add", `/a/-/b/-`, `"x"`, storageerrors.NewNotFoundErrorWithHint(storage.MustParsePath(`/a/-/b/-`), storageerrors.ArrayIndexTypeMsg), "", nil},
|
|
{"err: append arr/arr (non-array)", "add", `/b/v1/-`, "1", storageerrors.NewNotFoundError(storage.MustParsePath("/b/v1/-")), "", nil},
|
|
{"err: remove missing", "remove", "/dead/beef/0", "", storageerrors.NewNotFoundError(storage.MustParsePath("/dead/beef/0")), "", nil},
|
|
{"err: remove obj (missing)", "remove", "/b/deadbeef", "", storageerrors.NewNotFoundError(storage.MustParsePath("/b/deadbeef")), "", nil},
|
|
{"err: replace root (missing)", "replace", "/deadbeef", "1", storageerrors.NewNotFoundError(storage.MustParsePath("/deadbeef")), "", nil},
|
|
{"err: replace missing", "replace", "/dead/beef/1", "1", storageerrors.NewNotFoundError(storage.MustParsePath("/dead/beef/1")), "", nil},
|
|
}
|
|
|
|
ctx := t.Context()
|
|
|
|
for i, tc := range tests {
|
|
data := loadSmallTestData()
|
|
store := NewFromObjectWithOpts(data, OptReturnASTValuesOnRead(rvt.ast))
|
|
|
|
// Perform patch and check result
|
|
value := loadExpectedSortedResult(tc.value)
|
|
|
|
var op storage.PatchOp
|
|
switch tc.op {
|
|
case "add":
|
|
op = storage.AddOp
|
|
case "remove":
|
|
op = storage.RemoveOp
|
|
case "replace":
|
|
op = storage.ReplaceOp
|
|
default:
|
|
panic(fmt.Sprintf("illegal value: %v", tc.op))
|
|
}
|
|
|
|
err := storage.WriteOne(ctx, store, op, storage.MustParsePath(tc.path), value)
|
|
if tc.expected == nil {
|
|
if err != nil {
|
|
t.Errorf("Test case %d (%v): unexpected patch error: %v", i+1, tc.note, err)
|
|
continue
|
|
}
|
|
} else {
|
|
if err == nil {
|
|
t.Errorf("Test case %d (%v): expected patch error, but got nil instead", i+1, tc.note)
|
|
continue
|
|
}
|
|
if err.Error() != tc.expected.Error() {
|
|
t.Errorf("Test case %d (%v): expected patch error %v but got: %v", i+1, tc.note, tc.expected, err)
|
|
continue
|
|
}
|
|
}
|
|
|
|
if tc.getPath == "" {
|
|
continue
|
|
}
|
|
|
|
// Perform get and verify result
|
|
result, err := storage.ReadOne(ctx, store, storage.MustParsePath(tc.getPath))
|
|
switch expected := tc.getExpected.(type) {
|
|
case error:
|
|
if err == nil {
|
|
t.Errorf("Test case %d (%v): expected get error but got: %v", i+1, tc.note, result)
|
|
continue
|
|
}
|
|
if err.Error() != expected.Error() {
|
|
t.Errorf("Test case %d (%v): expected get error %v but got: %v", i+1, tc.note, expected, err)
|
|
continue
|
|
}
|
|
case string:
|
|
if err != nil {
|
|
t.Errorf("Test case %d (%v): unexpected get error: %v", i+1, tc.note, err)
|
|
continue
|
|
}
|
|
|
|
if rvt.ast {
|
|
e := ast.MustParseTerm(expected)
|
|
|
|
if ast.Compare(result, e.Value) != 0 {
|
|
t.Errorf("Test case %d (%v): expected get result %v but got: %v", i+1, tc.note, e, result)
|
|
}
|
|
} else {
|
|
e := loadExpectedResult(expected)
|
|
|
|
if !reflect.DeepEqual(result, e) {
|
|
t.Errorf("Test case %d (%v): expected get result %v but got: %v", i+1, tc.note, e, result)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestInMemoryWriteOfStruct(t *testing.T) {
|
|
type B struct {
|
|
Bar int `json:"bar"`
|
|
}
|
|
|
|
type A struct {
|
|
Foo *B `json:"foo"`
|
|
}
|
|
|
|
cases := map[string]struct {
|
|
value any
|
|
expected string
|
|
}{
|
|
"nested struct": {A{&B{10}}, `{"foo": {"bar": 10 } }`},
|
|
"pointer to nested struct": {&A{&B{10}}, `{"foo": {"bar": 10 } }`},
|
|
"pointer to pointer to nested struct": {
|
|
func() any {
|
|
a := &A{&B{10}}
|
|
return &a
|
|
}(), `{"foo": {"bar": 10 } }`},
|
|
}
|
|
|
|
for name, tc := range cases {
|
|
t.Run(name, func(t *testing.T) {
|
|
store := New()
|
|
ctx := t.Context()
|
|
|
|
err := storage.WriteOne(ctx, store, storage.AddOp, storage.MustParsePath("/x"), tc.value)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
actual, err := storage.ReadOne(ctx, store, storage.MustParsePath("/x"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
expected := loadExpectedSortedResult(tc.expected)
|
|
if !reflect.DeepEqual(expected, actual) {
|
|
t.Errorf("expected %v, got %v", tc.expected, actual)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestInMemoryWriteOfStructAst(t *testing.T) {
|
|
type B struct {
|
|
Bar int `json:"bar"`
|
|
}
|
|
|
|
type A struct {
|
|
Foo *B `json:"foo"`
|
|
}
|
|
|
|
cases := map[string]struct {
|
|
value any
|
|
expected string
|
|
}{
|
|
"nested struct": {A{&B{10}}, `{"foo": {"bar": 10 } }`},
|
|
"pointer to nested struct": {&A{&B{10}}, `{"foo": {"bar": 10 } }`},
|
|
"pointer to pointer to nested struct": {
|
|
func() any {
|
|
a := &A{&B{10}}
|
|
return &a
|
|
}(), `{"foo": {"bar": 10 } }`},
|
|
}
|
|
|
|
for name, tc := range cases {
|
|
t.Run(name, func(t *testing.T) {
|
|
store := NewWithOpts(OptReturnASTValuesOnRead(true))
|
|
ctx := t.Context()
|
|
|
|
// Written non-AST values are expected to be converted to AST values
|
|
err := storage.WriteOne(ctx, store, storage.AddOp, storage.MustParsePath("/x"), tc.value)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
actual, err := storage.ReadOne(ctx, store, storage.MustParsePath("/x"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// We expect the result to be an AST value
|
|
expected := ast.MustParseTerm(tc.expected)
|
|
if ast.Compare(expected.Value, actual) != 0 {
|
|
t.Errorf("expected %v, got %v", tc.expected, actual)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestInMemoryTxnMultipleWrites(t *testing.T) {
|
|
|
|
ctx := t.Context()
|
|
store := NewFromObject(loadSmallTestData())
|
|
txn := storage.NewTransactionOrDie(ctx, store, storage.WriteParams)
|
|
|
|
// Perform a sequence of writes and then verify the read results are the
|
|
// same for the writer during the transaction and the reader after the
|
|
// commit.
|
|
writes := []struct {
|
|
op storage.PatchOp
|
|
path string
|
|
value string
|
|
}{
|
|
{storage.AddOp, "/a/-", "[]"},
|
|
{storage.AddOp, "/a/4/-", "1"},
|
|
{storage.AddOp, "/a/4/-", "2"},
|
|
{storage.AddOp, "/a/4/2", "3"},
|
|
{storage.AddOp, "/b/foo", "{}"},
|
|
{storage.AddOp, "/b/foo/bar", "{}"},
|
|
{storage.AddOp, "/b/foo/bar/baz", "1"},
|
|
{storage.AddOp, "/arr", "[]"},
|
|
{storage.AddOp, "/arr/-", "1"},
|
|
{storage.AddOp, "/arr/0", "2"},
|
|
{storage.AddOp, "/arr/2", "3"},
|
|
{storage.AddOp, "/c/0/x/-", "0"},
|
|
{storage.AddOp, "/_", "null"}, // introduce new txn.log head
|
|
{storage.AddOp, "/c/0", `"new c[0]"`},
|
|
{storage.AddOp, "/c/1", `"new c[1]"`},
|
|
{storage.AddOp, "/_head", "1"},
|
|
{storage.AddOp, "/_head", "2"}, // invalidate the txn.log head
|
|
{storage.AddOp, "/d/f", `{"g": {"h": 0}}`},
|
|
{storage.AddOp, "/d/f/g/i", `{"j": 1}`},
|
|
}
|
|
|
|
reads := []struct {
|
|
path string
|
|
expected string
|
|
}{
|
|
{"/a", `[1,2,3,4,[1,2,3]]`},
|
|
{"/b/foo", `{"bar": {"baz": 1}}`},
|
|
{"/arr", `[2,1,3]`},
|
|
{"/c/0", `"new c[0]"`},
|
|
{"/c/1", `"new c[1]"`},
|
|
{"/d/f", `{"g": {"h": 0, "i": {"j": 1}}}`},
|
|
{"/d", `{"e": ["bar", "baz"], "f": {"g":{"h": 0, "i": {"j": 1}}}}`},
|
|
{"/h/1/2", "4"},
|
|
}
|
|
|
|
for _, w := range writes {
|
|
var jsn any
|
|
if w.value != "" {
|
|
jsn = util.MustUnmarshalJSON([]byte(w.value))
|
|
}
|
|
if err := store.Write(ctx, txn, w.op, storage.MustParsePath(w.path), jsn); err != nil {
|
|
t.Fatalf("Unexpected write error on %v: %v", w, err)
|
|
}
|
|
}
|
|
|
|
for _, r := range reads {
|
|
jsn := util.MustUnmarshalJSON([]byte(r.expected))
|
|
result, err := store.Read(ctx, txn, storage.MustParsePath(r.path))
|
|
if err != nil || !reflect.DeepEqual(jsn, result) {
|
|
t.Fatalf("Expected writer's read %v to be %v but got: %v (err: %v)", r.path, jsn, result, err)
|
|
}
|
|
}
|
|
|
|
if err := store.Commit(ctx, txn); err != nil {
|
|
t.Fatalf("Unexpected commit error: %v", err)
|
|
}
|
|
|
|
txn = storage.NewTransactionOrDie(ctx, store)
|
|
|
|
for _, r := range reads {
|
|
jsn := util.MustUnmarshalJSON([]byte(r.expected))
|
|
result, err := store.Read(ctx, txn, storage.MustParsePath(r.path))
|
|
if err != nil || !reflect.DeepEqual(jsn, result) {
|
|
t.Fatalf("Expected reader's read %v to be %v but got: %v (err: %v)", r.path, jsn, result, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestInMemoryTxnMultipleWritesAst(t *testing.T) {
|
|
|
|
ctx := t.Context()
|
|
store := NewFromObjectWithOpts(loadSmallTestData(), OptReturnASTValuesOnRead(true))
|
|
txn := storage.NewTransactionOrDie(ctx, store, storage.WriteParams)
|
|
|
|
// Perform a sequence of writes and then verify the read results are the
|
|
// same for the writer during the transaction and the reader after the
|
|
// commit.
|
|
writes := []struct {
|
|
op storage.PatchOp
|
|
path string
|
|
value string
|
|
}{
|
|
{storage.AddOp, "/a/-", "[]"},
|
|
{storage.AddOp, "/a/4/-", "1"},
|
|
{storage.AddOp, "/a/4/-", "2"},
|
|
{storage.AddOp, "/a/4/2", "3"},
|
|
{storage.AddOp, "/b/foo", "{}"},
|
|
{storage.AddOp, "/b/foo/bar", "{}"},
|
|
{storage.AddOp, "/b/foo/bar/baz", "1"},
|
|
{storage.AddOp, "/arr", "[]"},
|
|
{storage.AddOp, "/arr/-", "1"},
|
|
{storage.AddOp, "/arr/0", "2"},
|
|
{storage.AddOp, "/arr/2", "3"},
|
|
{storage.AddOp, "/c/0/x/-", "0"},
|
|
{storage.AddOp, "/_", "null"}, // introduce new txn.log head
|
|
{storage.AddOp, "/c/0", `"new c[0]"`},
|
|
{storage.AddOp, "/c/1", `"new c[1]"`},
|
|
{storage.AddOp, "/_head", "1"},
|
|
{storage.AddOp, "/_head", "2"}, // invalidate the txn.log head
|
|
{storage.AddOp, "/d/f", `{"g": {"h": 0}}`},
|
|
{storage.AddOp, "/d/f/g/i", `{"j": 1}`},
|
|
}
|
|
|
|
reads := []struct {
|
|
path string
|
|
expected string
|
|
}{
|
|
{"/a", `[1,2,3,4,[1,2,3]]`},
|
|
{"/b/foo", `{"bar": {"baz": 1}}`},
|
|
{"/arr", `[2,1,3]`},
|
|
{"/c/0", `"new c[0]"`},
|
|
{"/c/1", `"new c[1]"`},
|
|
{"/d/f", `{"g": {"h": 0, "i": {"j": 1}}}`},
|
|
{"/d", `{"e": ["bar", "baz"], "f": {"g":{"h": 0, "i": {"j": 1}}}}`},
|
|
{"/h/1/2", "4"},
|
|
}
|
|
|
|
for _, w := range writes {
|
|
var jsn any
|
|
if w.value != "" {
|
|
jsn = util.MustUnmarshalJSON([]byte(w.value))
|
|
}
|
|
if err := store.Write(ctx, txn, w.op, storage.MustParsePath(w.path), jsn); err != nil {
|
|
t.Fatalf("Unexpected write error on %v: %v", w, err)
|
|
}
|
|
}
|
|
|
|
for _, r := range reads {
|
|
exp := ast.MustParseTerm(r.expected)
|
|
result, err := store.Read(ctx, txn, storage.MustParsePath(r.path))
|
|
if err != nil || ast.Compare(exp.Value, result) != 0 {
|
|
t.Fatalf("Expected writer's read %v to be %v but got: %v (err: %v)", r.path, exp, result, err)
|
|
}
|
|
}
|
|
|
|
if err := store.Commit(ctx, txn); err != nil {
|
|
t.Fatalf("Unexpected commit error: %v", err)
|
|
}
|
|
|
|
txn = storage.NewTransactionOrDie(ctx, store)
|
|
|
|
for _, r := range reads {
|
|
exp := ast.MustParseTerm(r.expected)
|
|
result, err := store.Read(ctx, txn, storage.MustParsePath(r.path))
|
|
if err != nil || ast.Compare(exp.Value, result) != 0 {
|
|
t.Fatalf("Expected reader's read %v to be %v but got: %v (err: %v)", r.path, exp, result, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTruncateNoExistingPath(t *testing.T) {
|
|
cases := []struct {
|
|
note string
|
|
ast bool
|
|
}{
|
|
{"raw", false},
|
|
{"ast", true},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
ctx := t.Context()
|
|
store := NewFromObjectWithOpts(map[string]any{}, OptReturnASTValuesOnRead(tc.ast))
|
|
txn := storage.NewTransactionOrDie(ctx, store, storage.WriteParams)
|
|
|
|
var archiveFiles = map[string]string{
|
|
"/a/b/c/data.json": "[1,2,3]",
|
|
}
|
|
|
|
files := make([][2]string, 0, len(archiveFiles))
|
|
for name, content := range archiveFiles {
|
|
files = append(files, [2]string{name, content})
|
|
}
|
|
|
|
buf := archive.MustWriteTarGz(files)
|
|
b, err := bundle.NewReader(buf).WithLazyLoadingMode(true).Read()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
iterator := bundle.NewIterator(b.Raw)
|
|
|
|
params := storage.WriteParams
|
|
params.BasePaths = []string{""}
|
|
|
|
err = store.Truncate(ctx, txn, params, iterator)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected truncate error: %v", err)
|
|
}
|
|
|
|
if err := store.Commit(ctx, txn); err != nil {
|
|
t.Fatalf("Unexpected commit error: %v", err)
|
|
}
|
|
|
|
txn = storage.NewTransactionOrDie(ctx, store)
|
|
|
|
actual, err := store.Read(ctx, txn, storage.MustParsePath("/"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
expected := `
|
|
{
|
|
"a": {
|
|
"b": {
|
|
"c": [1,2,3]
|
|
}
|
|
}
|
|
}
|
|
`
|
|
if tc.ast {
|
|
exp := ast.MustParseTerm(expected)
|
|
|
|
if ast.Compare(exp.Value, actual) != 0 {
|
|
t.Fatalf("Expected reader's read to be %v but got: %v", exp, actual)
|
|
}
|
|
} else {
|
|
jsn := util.MustUnmarshalJSON([]byte(expected))
|
|
|
|
if !reflect.DeepEqual(jsn, actual) {
|
|
t.Fatalf("Expected reader's read to be %v but got: %v", jsn, actual)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTruncate(t *testing.T) {
|
|
ctx := t.Context()
|
|
store := NewFromObject(map[string]any{})
|
|
txn := storage.NewTransactionOrDie(ctx, store, storage.WriteParams)
|
|
|
|
var archiveFiles = map[string]string{
|
|
"/a/b/c/data.json": "[1,2,3]",
|
|
"/a/b/d/data.json": "true",
|
|
"/data.json": `{"x": {"y": true}, "a": {"b": {"z": true}}}`,
|
|
"/a/b/y/data.yaml": `foo: 1`,
|
|
"/policy.rego": "package foo\n p = 1",
|
|
"/roles/policy.rego": "package bar\n p = 1",
|
|
}
|
|
|
|
files := make([][2]string, 0, len(archiveFiles))
|
|
for name, content := range archiveFiles {
|
|
files = append(files, [2]string{name, content})
|
|
}
|
|
|
|
buf := archive.MustWriteTarGz(files)
|
|
b, err := bundle.NewReader(buf).WithLazyLoadingMode(true).Read()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
iterator := bundle.NewIterator(b.Raw)
|
|
|
|
params := storage.WriteParams
|
|
params.BasePaths = []string{""}
|
|
|
|
err = store.Truncate(ctx, txn, params, iterator)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected truncate error: %v", err)
|
|
}
|
|
|
|
if err := store.Commit(ctx, txn); err != nil {
|
|
t.Fatalf("Unexpected commit error: %v", err)
|
|
}
|
|
|
|
txn = storage.NewTransactionOrDie(ctx, store)
|
|
|
|
actual, err := store.Read(ctx, txn, storage.MustParsePath("/"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
expected := `
|
|
{
|
|
"a": {
|
|
"b": {
|
|
"c": [1,2,3],
|
|
"d": true,
|
|
"y": {
|
|
"foo": 1
|
|
},
|
|
"z": true
|
|
}
|
|
},
|
|
"x": {
|
|
"y": true
|
|
}
|
|
}
|
|
`
|
|
jsn := util.MustUnmarshalJSON([]byte(expected))
|
|
|
|
if !reflect.DeepEqual(jsn, actual) {
|
|
t.Fatalf("Expected reader's read to be %v but got: %v", jsn, actual)
|
|
}
|
|
|
|
store.Abort(ctx, txn)
|
|
|
|
txn = storage.NewTransactionOrDie(ctx, store)
|
|
ids, err := store.ListPolicies(ctx, txn)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
expectedIDs := map[string]struct{}{"policy.rego": {}, "roles/policy.rego": {}}
|
|
|
|
for _, id := range ids {
|
|
if _, ok := expectedIDs[id]; !ok {
|
|
t.Fatalf("Expected list policies to contain %v but got: %v", id, expectedIDs)
|
|
}
|
|
}
|
|
|
|
bs, err := store.GetPolicy(ctx, txn, "policy.rego")
|
|
expectedBytes := []byte("package foo\n p = 1")
|
|
if err != nil || !bytes.Equal(expectedBytes, bs) {
|
|
t.Fatalf("Expected get policy to return %v but got: %v (err: %v)", expectedBytes, bs, err)
|
|
}
|
|
|
|
bs, err = store.GetPolicy(ctx, txn, "roles/policy.rego")
|
|
expectedBytes = []byte("package bar\n p = 1")
|
|
if err != nil || !bytes.Equal(expectedBytes, bs) {
|
|
t.Fatalf("Expected get policy to return %v but got: %v (err: %v)", expectedBytes, bs, err)
|
|
}
|
|
}
|
|
|
|
func TestTruncateAst(t *testing.T) {
|
|
ctx := t.Context()
|
|
store := NewFromObjectWithOpts(map[string]any{}, OptReturnASTValuesOnRead(true))
|
|
txn := storage.NewTransactionOrDie(ctx, store, storage.WriteParams)
|
|
|
|
var archiveFiles = map[string]string{
|
|
"/a/b/c/data.json": "[1,2,3]",
|
|
"/a/b/d/data.json": "true",
|
|
"/data.json": `{"x": {"y": true}, "a": {"b": {"z": true}}}`,
|
|
"/a/b/y/data.yaml": `foo: 1`,
|
|
"/policy.rego": "package foo\n p = 1",
|
|
"/roles/policy.rego": "package bar\n p = 1",
|
|
}
|
|
|
|
files := make([][2]string, 0, len(archiveFiles))
|
|
for name, content := range archiveFiles {
|
|
files = append(files, [2]string{name, content})
|
|
}
|
|
|
|
buf := archive.MustWriteTarGz(files)
|
|
b, err := bundle.NewReader(buf).WithLazyLoadingMode(true).Read()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
iterator := bundle.NewIterator(b.Raw)
|
|
|
|
params := storage.WriteParams
|
|
params.BasePaths = []string{""}
|
|
|
|
err = store.Truncate(ctx, txn, params, iterator)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected truncate error: %v", err)
|
|
}
|
|
|
|
if err := store.Commit(ctx, txn); err != nil {
|
|
t.Fatalf("Unexpected commit error: %v", err)
|
|
}
|
|
|
|
txn = storage.NewTransactionOrDie(ctx, store)
|
|
|
|
actual, err := store.Read(ctx, txn, storage.MustParsePath("/"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
expected := `
|
|
{
|
|
"a": {
|
|
"b": {
|
|
"c": [1,2,3],
|
|
"d": true,
|
|
"y": {
|
|
"foo": 1
|
|
},
|
|
"z": true
|
|
}
|
|
},
|
|
"x": {
|
|
"y": true
|
|
}
|
|
}
|
|
`
|
|
exp := ast.MustParseTerm(expected)
|
|
|
|
if ast.Compare(exp.Value, actual) != 0 {
|
|
t.Fatalf("Expected reader's read to be %v but got: %v", exp, actual)
|
|
}
|
|
|
|
store.Abort(ctx, txn)
|
|
|
|
txn = storage.NewTransactionOrDie(ctx, store)
|
|
ids, err := store.ListPolicies(ctx, txn)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
expectedIDs := map[string]struct{}{"policy.rego": {}, "roles/policy.rego": {}}
|
|
|
|
for _, id := range ids {
|
|
if _, ok := expectedIDs[id]; !ok {
|
|
t.Fatalf("Expected list policies to contain %v but got: %v", id, expectedIDs)
|
|
}
|
|
}
|
|
|
|
bs, err := store.GetPolicy(ctx, txn, "policy.rego")
|
|
expectedBytes := []byte("package foo\n p = 1")
|
|
if err != nil || !bytes.Equal(expectedBytes, bs) {
|
|
t.Fatalf("Expected get policy to return %v but got: %v (err: %v)", expectedBytes, bs, err)
|
|
}
|
|
|
|
bs, err = store.GetPolicy(ctx, txn, "roles/policy.rego")
|
|
expectedBytes = []byte("package bar\n p = 1")
|
|
if err != nil || !bytes.Equal(expectedBytes, bs) {
|
|
t.Fatalf("Expected get policy to return %v but got: %v (err: %v)", expectedBytes, bs, err)
|
|
}
|
|
}
|
|
|
|
func TestTruncateDataMergeError(t *testing.T) {
|
|
cases := []struct {
|
|
note string
|
|
ast bool
|
|
}{
|
|
{"raw", false},
|
|
{"ast", true},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
ctx := t.Context()
|
|
store := NewFromObjectWithOpts(map[string]any{}, OptReturnASTValuesOnRead(tc.ast))
|
|
txn := storage.NewTransactionOrDie(ctx, store, storage.WriteParams)
|
|
|
|
var archiveFiles = map[string]string{
|
|
"/a/b/data.json": `{"c": "foo"}`,
|
|
"/data.json": `{"a": {"b": {"c": "bar"}}}`,
|
|
}
|
|
|
|
files := make([][2]string, 0, len(archiveFiles))
|
|
for name, content := range archiveFiles {
|
|
files = append(files, [2]string{name, content})
|
|
}
|
|
|
|
buf := archive.MustWriteTarGz(files)
|
|
b, err := bundle.NewReader(buf).WithLazyLoadingMode(true).Read()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
iterator := bundle.NewIterator(b.Raw)
|
|
|
|
err = store.Truncate(ctx, txn, storage.WriteParams, iterator)
|
|
if err == nil {
|
|
t.Fatal("Expected truncate error but got nil")
|
|
}
|
|
|
|
expected := "failed to insert data file from path a/b"
|
|
if err.Error() != expected {
|
|
t.Fatalf("Expected error %v but got %v", expected, err.Error())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTruncateBadRootWrite(t *testing.T) {
|
|
cases := []struct {
|
|
note string
|
|
ast bool
|
|
}{
|
|
{"raw", false},
|
|
{"ast", true},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
ctx := t.Context()
|
|
store := NewFromObjectWithOpts(map[string]any{}, OptReturnASTValuesOnRead(tc.ast))
|
|
txn := storage.NewTransactionOrDie(ctx, store, storage.WriteParams)
|
|
|
|
var archiveFiles = map[string]string{
|
|
"/a/b/d/data.json": "true",
|
|
"/data.json": "[1,2,3]",
|
|
"/roles/policy.rego": "package bar\n p = 1",
|
|
}
|
|
|
|
files := make([][2]string, 0, len(archiveFiles))
|
|
for name, content := range archiveFiles {
|
|
files = append(files, [2]string{name, content})
|
|
}
|
|
|
|
buf := archive.MustWriteTarGz(files)
|
|
b, err := bundle.NewReader(buf).WithLazyLoadingMode(true).Read()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
iterator := bundle.NewIterator(b.Raw)
|
|
|
|
err = store.Truncate(ctx, txn, storage.WriteParams, iterator)
|
|
if err == nil {
|
|
t.Fatal("Expected truncate error but got nil")
|
|
}
|
|
|
|
expected := "storage_invalid_patch_error: root must be object"
|
|
if err.Error() != expected {
|
|
t.Fatalf("Expected error %v but got %v", expected, err.Error())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestInMemoryTxnWriteFailures(t *testing.T) {
|
|
cases := []struct {
|
|
note string
|
|
ast bool
|
|
}{
|
|
{"raw", false},
|
|
{"ast", true},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
ctx := t.Context()
|
|
store := NewFromObjectWithOpts(loadSmallTestData(), OptReturnASTValuesOnRead(tc.ast))
|
|
txn := storage.NewTransactionOrDie(ctx, store, storage.WriteParams)
|
|
|
|
writes := []struct {
|
|
op storage.PatchOp
|
|
path string
|
|
value string
|
|
errCode string
|
|
}{
|
|
{storage.RemoveOp, "/c/0/y", "", ""},
|
|
{storage.RemoveOp, "/c/0/y", "", storage.NotFoundErr},
|
|
{storage.ReplaceOp, "/c/0/y/0", "", storage.NotFoundErr},
|
|
{storage.AddOp, "/new", `{"foo": "bar"}`, ""},
|
|
{storage.AddOp, "/a/0/beef", "", storage.NotFoundErr},
|
|
{storage.AddOp, "/arr", `[1,2,3]`, ""},
|
|
{storage.AddOp, "/arr/0/foo", "", storage.NotFoundErr},
|
|
{storage.AddOp, "/arr/4", "", storage.NotFoundErr},
|
|
}
|
|
|
|
for _, w := range writes {
|
|
var jsn any
|
|
if w.value != "" {
|
|
jsn = util.MustUnmarshalJSON([]byte(w.value))
|
|
}
|
|
err := store.Write(ctx, txn, w.op, storage.MustParsePath(w.path), jsn)
|
|
if (w.errCode == "" && err != nil) || (err == nil && w.errCode != "") {
|
|
t.Fatalf("Expected errCode %q but got: %v", w.errCode, err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestInMemoryTxnReadFailures(t *testing.T) {
|
|
cases := []struct {
|
|
note string
|
|
ast bool
|
|
}{
|
|
{"raw", false},
|
|
{"ast", true},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
ctx := t.Context()
|
|
store := NewFromObjectWithOpts(loadSmallTestData(), OptReturnASTValuesOnRead(tc.ast))
|
|
txn := storage.NewTransactionOrDie(ctx, store, storage.WriteParams)
|
|
|
|
if err := store.Write(ctx, txn, storage.RemoveOp, storage.MustParsePath("/c/0/y"), nil); err != nil {
|
|
t.Fatalf("Unexpected write error: %v", err)
|
|
}
|
|
|
|
if result, err := store.Read(ctx, txn, storage.MustParsePath("/c/0/y/0")); !storage.IsNotFound(err) {
|
|
t.Fatalf("Expected NotFoundErr for /c/0/y/0 but got: %v (err: %v)", result, err)
|
|
}
|
|
|
|
if result, err := store.Read(ctx, txn, storage.MustParsePath("/c/0/y")); !storage.IsNotFound(err) {
|
|
t.Fatalf("Expected NotFoundErr for /c/0/y but got: %v (err: %v)", result, err)
|
|
}
|
|
|
|
if result, err := store.Read(ctx, txn, storage.MustParsePath("/a/0/beef")); !storage.IsNotFound(err) {
|
|
t.Fatalf("Expected NotFoundErr for /c/0/y but got: %v (err: %v)", result, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestInMemoryTxnBadWrite(t *testing.T) {
|
|
ctx := t.Context()
|
|
store := NewFromObject(loadSmallTestData())
|
|
txn := storage.NewTransactionOrDie(ctx, store)
|
|
if err := store.Write(ctx, txn, storage.RemoveOp, storage.MustParsePath("/a"), nil); !storage.IsInvalidTransaction(err) {
|
|
t.Fatalf("Expected InvalidTransactionErr but got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestInMemoryTxnPolicies(t *testing.T) {
|
|
|
|
ctx := t.Context()
|
|
store := New()
|
|
|
|
txn := storage.NewTransactionOrDie(ctx, store, storage.WriteParams)
|
|
|
|
if err := store.UpsertPolicy(ctx, txn, "test", []byte("package test")); err != nil {
|
|
t.Fatalf("Unexpected error on policy insert: %v", err)
|
|
}
|
|
|
|
if err := store.Commit(ctx, txn); err != nil {
|
|
t.Fatalf("Unexpected commit error: %v", err)
|
|
}
|
|
|
|
txn = storage.NewTransactionOrDie(ctx, store, storage.WriteParams)
|
|
|
|
if err := store.UpsertPolicy(ctx, txn, "test", []byte("package test\nimport data.foo")); err != nil {
|
|
t.Fatalf("Unexpected error on policy insert/update: %v", err)
|
|
}
|
|
|
|
ids, err := store.ListPolicies(ctx, txn)
|
|
expectedIDs := []string{"test"}
|
|
if err != nil || !slices.Equal(expectedIDs, ids) {
|
|
t.Fatalf("Expected list policies to return %v but got: %v (err: %v)", expectedIDs, ids, err)
|
|
}
|
|
|
|
bs, err := store.GetPolicy(ctx, txn, "test")
|
|
expectedBytes := []byte("package test\nimport data.foo")
|
|
if err != nil || !bytes.Equal(expectedBytes, bs) {
|
|
t.Fatalf("Expected get policy to return %v but got: %v (err: %v)", expectedBytes, bs, err)
|
|
}
|
|
|
|
if err := store.DeletePolicy(ctx, txn, "test"); err != nil {
|
|
t.Fatalf("Unexpected delete policy error: %v", err)
|
|
}
|
|
|
|
if err := store.UpsertPolicy(ctx, txn, "test2", []byte("package test2")); err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
|
|
ids, err = store.ListPolicies(ctx, txn)
|
|
expectedIDs = []string{"test2"}
|
|
if err != nil || !slices.Equal(expectedIDs, ids) {
|
|
t.Fatalf("Expected list policies to return %v but got: %v (err: %v)", expectedIDs, ids, err)
|
|
}
|
|
|
|
bs, err = store.GetPolicy(ctx, txn, "test2")
|
|
expectedBytes = []byte("package test2")
|
|
if err != nil || !bytes.Equal(expectedBytes, bs) {
|
|
t.Fatalf("Expected get policy to return %v but got: %v (err: %v)", expectedBytes, bs, err)
|
|
}
|
|
|
|
if exist, err := store.GetPolicy(ctx, txn, "test"); !storage.IsNotFound(err) {
|
|
t.Fatalf("Expected NotFoundErr for test but got: %v (err: %v)", exist, err)
|
|
}
|
|
|
|
store.Abort(ctx, txn)
|
|
|
|
txn = storage.NewTransactionOrDie(ctx, store)
|
|
ids, err = store.ListPolicies(ctx, txn)
|
|
expectedIDs = []string{"test"}
|
|
if err != nil || !slices.Equal(expectedIDs, ids) {
|
|
t.Fatalf("Expected list policies to return %v but got: %v (err: %v)", expectedIDs, ids, err)
|
|
}
|
|
|
|
if exist, err := store.GetPolicy(ctx, txn, "test2"); !storage.IsNotFound(err) {
|
|
t.Fatalf("Expected NotFoundErr for test2 but got: %v (err: %v)", exist, err)
|
|
}
|
|
|
|
if err := store.DeletePolicy(ctx, txn, "test"); !storage.IsInvalidTransaction(err) {
|
|
t.Fatalf("Expected InvalidTransactionErr for test but got: %v", err)
|
|
}
|
|
|
|
store.Abort(ctx, txn)
|
|
|
|
txn = storage.NewTransactionOrDie(ctx, store, storage.WriteParams)
|
|
|
|
if err := store.DeletePolicy(ctx, txn, "test"); err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
|
|
if err := store.Commit(ctx, txn); err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
|
|
txn = storage.NewTransactionOrDie(ctx, store)
|
|
|
|
if ids, err := store.ListPolicies(ctx, txn); err != nil || len(ids) != 0 {
|
|
t.Fatalf("Expected list policies to be empty but got: %v (err: %v)", ids, err)
|
|
}
|
|
|
|
}
|
|
|
|
func TestInMemoryTriggers(t *testing.T) {
|
|
cases := []struct {
|
|
note string
|
|
ast bool
|
|
}{
|
|
{"raw", false},
|
|
{"ast", true},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
ctx := t.Context()
|
|
store := NewFromObjectWithOpts(loadSmallTestData(), OptReturnASTValuesOnRead(tc.ast))
|
|
writeTxn := storage.NewTransactionOrDie(ctx, store, storage.WriteParams)
|
|
readTxn := storage.NewTransactionOrDie(ctx, store)
|
|
|
|
_, err := store.Register(ctx, readTxn, storage.TriggerConfig{
|
|
OnCommit: func(context.Context, storage.Transaction, storage.TriggerEvent) {},
|
|
})
|
|
|
|
if err == nil || !storage.IsInvalidTransaction(err) {
|
|
t.Fatalf("Expected transaction error: %v", err)
|
|
}
|
|
|
|
store.Abort(ctx, readTxn)
|
|
|
|
var event storage.TriggerEvent
|
|
modifiedPath := storage.MustParsePath("/a")
|
|
expectedValue := "hello"
|
|
|
|
_, err = store.Register(ctx, writeTxn, storage.TriggerConfig{
|
|
OnCommit: func(ctx context.Context, txn storage.Transaction, evt storage.TriggerEvent) {
|
|
result, err := store.Read(ctx, txn, modifiedPath)
|
|
if tc.ast {
|
|
expAstValue := ast.String(expectedValue)
|
|
if err != nil || ast.Compare(expAstValue, result) != 0 {
|
|
t.Fatalf("Expected result to be %v for trigger read but got: %v (err: %v)", expectedValue, result, err)
|
|
}
|
|
} else if err != nil || !reflect.DeepEqual(result, expectedValue) {
|
|
t.Fatalf("Expected result to be %v for trigger read but got: %v (err: %v)", expectedValue, result, err)
|
|
}
|
|
event = evt
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Failed to register callback: %v", err)
|
|
}
|
|
|
|
if err := store.Write(ctx, writeTxn, storage.ReplaceOp, modifiedPath, expectedValue); err != nil {
|
|
t.Fatalf("Unexpected write error: %v", err)
|
|
}
|
|
|
|
id := "test"
|
|
data := []byte("package abc")
|
|
if err := store.UpsertPolicy(ctx, writeTxn, id, data); err != nil {
|
|
t.Fatalf("Unexpected upsert error: %v", err)
|
|
}
|
|
|
|
if err := store.Commit(ctx, writeTxn); err != nil {
|
|
t.Fatalf("Unexpected commit error: %v", err)
|
|
}
|
|
|
|
if event.IsZero() || !event.PolicyChanged() || !event.DataChanged() {
|
|
t.Fatalf("Expected policy and data change but got: %v", event)
|
|
}
|
|
|
|
expData := storage.DataEvent{Path: modifiedPath, Data: expectedValue, Removed: false}
|
|
if d := event.Data[0]; !reflect.DeepEqual(expData, d) {
|
|
t.Fatalf("Expected data event %v, got %v", expData, d)
|
|
}
|
|
|
|
expPolicy := storage.PolicyEvent{ID: id, Data: data, Removed: false}
|
|
if p := event.Policy[0]; !reflect.DeepEqual(expPolicy, p) {
|
|
t.Fatalf("Expected policy event %v, got %v", expPolicy, p)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestInMemoryTriggersUnregister(t *testing.T) {
|
|
ctx := t.Context()
|
|
store := NewFromObject(loadSmallTestData())
|
|
writeTxn := storage.NewTransactionOrDie(ctx, store, storage.WriteParams)
|
|
modifiedPath := storage.MustParsePath("/a")
|
|
expectedValue := "hello"
|
|
|
|
var called bool
|
|
_, err := store.Register(ctx, writeTxn, storage.TriggerConfig{
|
|
OnCommit: func(_ context.Context, _ storage.Transaction, evt storage.TriggerEvent) {
|
|
if !evt.IsZero() {
|
|
called = true
|
|
}
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Failed to register callback: %v", err)
|
|
}
|
|
|
|
handle, err := store.Register(ctx, writeTxn, storage.TriggerConfig{
|
|
OnCommit: func(_ context.Context, _ storage.Transaction, evt storage.TriggerEvent) {
|
|
if !evt.IsZero() {
|
|
t.Fatalf("Callback should have been unregistered")
|
|
}
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Failed to register callback: %v", err)
|
|
}
|
|
|
|
if err := store.Commit(ctx, writeTxn); err != nil {
|
|
t.Fatalf("Unexpected commit error: %v", err)
|
|
}
|
|
|
|
writeTxn = storage.NewTransactionOrDie(ctx, store, storage.WriteParams)
|
|
if err := store.Write(ctx, writeTxn, storage.AddOp, modifiedPath, expectedValue); err != nil {
|
|
t.Fatalf("Failed to write to store: %v", err)
|
|
}
|
|
handle.Unregister(ctx, writeTxn)
|
|
|
|
if err := store.Commit(ctx, writeTxn); err != nil {
|
|
t.Fatalf("Unexpected commit error: %v", err)
|
|
}
|
|
|
|
if !called {
|
|
t.Fatal("Registered callback was not called")
|
|
}
|
|
}
|
|
|
|
func TestInMemoryContext(t *testing.T) {
|
|
|
|
ctx := t.Context()
|
|
store := New()
|
|
params := storage.WriteParams
|
|
params.Context = storage.NewContext()
|
|
params.Context.Put("foo", "bar")
|
|
|
|
txn, err := store.NewTransaction(ctx, params)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
_, err = store.Register(ctx, txn, storage.TriggerConfig{
|
|
OnCommit: func(_ context.Context, _ storage.Transaction, event storage.TriggerEvent) {
|
|
if event.Context.Get("foo") != "bar" {
|
|
t.Fatalf("Expected foo/bar in context but got: %+v", event.Context)
|
|
} else if event.Context.Get("deadbeef") != nil {
|
|
t.Fatalf("Got unexpected deadbeef value in context: %+v", event.Context)
|
|
}
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if err := store.Commit(ctx, txn); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
}
|
|
|
|
func loadExpectedResult(input string) any {
|
|
if len(input) == 0 {
|
|
return nil
|
|
}
|
|
var data any
|
|
if err := util.UnmarshalJSON([]byte(input), &data); err != nil {
|
|
panic(err)
|
|
}
|
|
return data
|
|
}
|
|
|
|
func loadExpectedSortedResult(input string) any {
|
|
data := loadExpectedResult(input)
|
|
switch data := data.(type) {
|
|
case []any:
|
|
return data
|
|
default:
|
|
return data
|
|
}
|
|
}
|
|
|
|
func loadSmallTestData() map[string]any {
|
|
var data map[string]any
|
|
err := util.UnmarshalJSON([]byte(`{
|
|
"a": [1,2,3,4],
|
|
"b": {
|
|
"v1": "hello",
|
|
"v2": "goodbye"
|
|
},
|
|
"c": [{
|
|
"x": [true, false, "foo"],
|
|
"y": [null, 3.14159],
|
|
"z": {"p": true, "q": false}
|
|
}],
|
|
"d": {
|
|
"e": ["bar", "baz"]
|
|
},
|
|
"g": {
|
|
"a": [1, 0, 0, 0],
|
|
"b": [0, 2, 0, 0],
|
|
"c": [0, 0, 0, 4]
|
|
},
|
|
"h": [
|
|
[1,2,3],
|
|
[2,3,4]
|
|
]
|
|
}`), &data)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return data
|
|
}
|
|
|
|
func TestOptRoundTripOnWrite(t *testing.T) {
|
|
validObject := map[string]string{"foo": "bar"}
|
|
|
|
// self-referential objects are not serializable to JSON.
|
|
invalidObject := map[string]any{}
|
|
invalidObject["foo"] = invalidObject
|
|
|
|
tests := []struct {
|
|
name string
|
|
opts []Opt
|
|
obj any
|
|
wantErr bool
|
|
}{{
|
|
name: "success on valid object no Opts",
|
|
opts: nil,
|
|
obj: validObject,
|
|
wantErr: false,
|
|
}, {
|
|
name: "success on valid object round trip enabled",
|
|
opts: []Opt{OptRoundTripOnWrite(true)},
|
|
obj: validObject,
|
|
wantErr: false,
|
|
}, {
|
|
name: "success on valid object round trip disabled",
|
|
opts: []Opt{OptRoundTripOnWrite(false)},
|
|
obj: validObject,
|
|
wantErr: false,
|
|
}, {
|
|
// Ensure the setting defaults to "true".
|
|
name: "failure on invalid object no Opts",
|
|
opts: nil,
|
|
obj: invalidObject,
|
|
wantErr: true,
|
|
}, {
|
|
name: "failure on invalid object round trip enabled",
|
|
opts: []Opt{OptRoundTripOnWrite(true)},
|
|
obj: invalidObject,
|
|
wantErr: true,
|
|
}, {
|
|
// While this represents a bad use case, it's how we know the round-tripping
|
|
// has been disabled.
|
|
name: "success on invalid object round trip disabled",
|
|
opts: []Opt{OptRoundTripOnWrite(false)},
|
|
obj: invalidObject,
|
|
wantErr: false,
|
|
}}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
db := NewWithOpts(tt.opts...)
|
|
ctx := t.Context()
|
|
|
|
txn, err := db.NewTransaction(ctx, storage.WriteParams)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
err = db.Write(ctx, txn, storage.AddOp, []string{"data"}, tt.obj)
|
|
if tt.wantErr && err == nil {
|
|
t.Fatal("got Write error = nil, want error")
|
|
} else if !tt.wantErr && err != nil {
|
|
t.Fatalf("got Write error, want nil")
|
|
}
|
|
})
|
|
}
|
|
}
|