2018-03-21 18:20:17 +00:00
|
|
|
package state
|
|
|
|
|
|
|
|
import (
|
2018-04-07 00:13:22 +00:00
|
|
|
"reflect"
|
2018-03-21 18:20:17 +00:00
|
|
|
"testing"
|
2018-04-07 00:13:22 +00:00
|
|
|
"time"
|
2018-03-21 18:20:17 +00:00
|
|
|
|
|
|
|
"github.com/hashicorp/consul/agent/connect"
|
|
|
|
"github.com/hashicorp/consul/agent/structs"
|
|
|
|
"github.com/hashicorp/go-memdb"
|
2018-04-07 00:13:22 +00:00
|
|
|
"github.com/pascaldekloe/goe/verify"
|
2018-03-21 18:20:17 +00:00
|
|
|
"github.com/stretchr/testify/assert"
|
|
|
|
)
|
|
|
|
|
2018-04-07 00:13:22 +00:00
|
|
|
func TestStore_CAConfig(t *testing.T) {
|
|
|
|
s := testStateStore(t)
|
|
|
|
|
|
|
|
expected := &structs.CAConfiguration{
|
|
|
|
Provider: "consul",
|
|
|
|
Config: map[string]interface{}{
|
|
|
|
"PrivateKey": "asdf",
|
|
|
|
"RootCert": "qwer",
|
|
|
|
"RotationPeriod": 90 * 24 * time.Hour,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := s.CASetConfig(0, expected); err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
|
2019-08-19 18:03:03 +00:00
|
|
|
idx, config, err := s.CAConfig(nil)
|
2018-04-07 00:13:22 +00:00
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
if idx != 0 {
|
|
|
|
t.Fatalf("bad: %d", idx)
|
|
|
|
}
|
|
|
|
if !reflect.DeepEqual(expected, config) {
|
|
|
|
t.Fatalf("bad: %#v, %#v", expected, config)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestStore_CAConfigCAS(t *testing.T) {
|
|
|
|
s := testStateStore(t)
|
|
|
|
|
|
|
|
expected := &structs.CAConfiguration{
|
|
|
|
Provider: "consul",
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := s.CASetConfig(0, expected); err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
// Do an extra operation to move the index up by 1 for the
|
|
|
|
// check-and-set operation after this
|
|
|
|
if err := s.CASetConfig(1, expected); err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Do a CAS with an index lower than the entry
|
|
|
|
ok, err := s.CACheckAndSetConfig(2, 0, &structs.CAConfiguration{
|
|
|
|
Provider: "static",
|
|
|
|
})
|
|
|
|
if ok || err != nil {
|
|
|
|
t.Fatalf("expected (false, nil), got: (%v, %#v)", ok, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check that the index is untouched and the entry
|
|
|
|
// has not been updated.
|
2019-08-19 18:03:03 +00:00
|
|
|
idx, config, err := s.CAConfig(nil)
|
2018-04-07 00:13:22 +00:00
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
if idx != 1 {
|
|
|
|
t.Fatalf("bad: %d", idx)
|
|
|
|
}
|
|
|
|
if config.Provider != "consul" {
|
|
|
|
t.Fatalf("bad: %#v", config)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Do another CAS, this time with the correct index
|
|
|
|
ok, err = s.CACheckAndSetConfig(2, 1, &structs.CAConfiguration{
|
|
|
|
Provider: "static",
|
|
|
|
})
|
|
|
|
if !ok || err != nil {
|
|
|
|
t.Fatalf("expected (true, nil), got: (%v, %#v)", ok, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Make sure the config was updated
|
2019-08-19 18:03:03 +00:00
|
|
|
idx, config, err = s.CAConfig(nil)
|
2018-04-07 00:13:22 +00:00
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
if idx != 2 {
|
|
|
|
t.Fatalf("bad: %d", idx)
|
|
|
|
}
|
|
|
|
if config.Provider != "static" {
|
|
|
|
t.Fatalf("bad: %#v", config)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestStore_CAConfig_Snapshot_Restore(t *testing.T) {
|
|
|
|
s := testStateStore(t)
|
|
|
|
before := &structs.CAConfiguration{
|
|
|
|
Provider: "consul",
|
|
|
|
Config: map[string]interface{}{
|
|
|
|
"PrivateKey": "asdf",
|
|
|
|
"RootCert": "qwer",
|
|
|
|
"RotationPeriod": 90 * 24 * time.Hour,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
if err := s.CASetConfig(99, before); err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
snap := s.Snapshot()
|
|
|
|
defer snap.Close()
|
|
|
|
|
|
|
|
after := &structs.CAConfiguration{
|
|
|
|
Provider: "static",
|
|
|
|
Config: map[string]interface{}{},
|
|
|
|
}
|
|
|
|
if err := s.CASetConfig(100, after); err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
snapped, err := snap.CAConfig()
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("err: %s", err)
|
|
|
|
}
|
|
|
|
verify.Values(t, "", before, snapped)
|
|
|
|
|
|
|
|
s2 := testStateStore(t)
|
|
|
|
restore := s2.Restore()
|
|
|
|
if err := restore.CAConfig(snapped); err != nil {
|
|
|
|
t.Fatalf("err: %s", err)
|
|
|
|
}
|
|
|
|
restore.Commit()
|
|
|
|
|
2019-08-19 18:03:03 +00:00
|
|
|
idx, res, err := s2.CAConfig(nil)
|
2018-04-07 00:13:22 +00:00
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("err: %s", err)
|
|
|
|
}
|
|
|
|
if idx != 99 {
|
|
|
|
t.Fatalf("bad index: %d", idx)
|
|
|
|
}
|
|
|
|
verify.Values(t, "", before, res)
|
|
|
|
}
|
|
|
|
|
2018-12-06 02:27:20 +00:00
|
|
|
// Make sure we handle the case of a leftover blank CA config that
|
|
|
|
// got stuck in a snapshot, as in https://github.com/hashicorp/consul/issues/4954
|
|
|
|
func TestStore_CAConfig_Snapshot_Restore_BlankConfig(t *testing.T) {
|
|
|
|
s := testStateStore(t)
|
|
|
|
before := &structs.CAConfiguration{}
|
|
|
|
if err := s.CASetConfig(99, before); err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
snap := s.Snapshot()
|
|
|
|
defer snap.Close()
|
|
|
|
|
|
|
|
snapped, err := snap.CAConfig()
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("err: %s", err)
|
|
|
|
}
|
|
|
|
verify.Values(t, "", before, snapped)
|
|
|
|
|
|
|
|
s2 := testStateStore(t)
|
|
|
|
restore := s2.Restore()
|
|
|
|
if err := restore.CAConfig(snapped); err != nil {
|
|
|
|
t.Fatalf("err: %s", err)
|
|
|
|
}
|
|
|
|
restore.Commit()
|
|
|
|
|
2019-08-19 18:03:03 +00:00
|
|
|
idx, result, err := s2.CAConfig(nil)
|
2018-12-06 02:27:20 +00:00
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("err: %s", err)
|
|
|
|
}
|
|
|
|
if idx != 0 {
|
|
|
|
t.Fatalf("bad index: %d", idx)
|
|
|
|
}
|
|
|
|
if result != nil {
|
|
|
|
t.Fatalf("should be nil: %v", result)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-03-21 18:20:17 +00:00
|
|
|
func TestStore_CARootSetList(t *testing.T) {
|
|
|
|
assert := assert.New(t)
|
|
|
|
s := testStateStore(t)
|
|
|
|
|
|
|
|
// Call list to populate the watch set
|
|
|
|
ws := memdb.NewWatchSet()
|
|
|
|
_, _, err := s.CARoots(ws)
|
|
|
|
assert.Nil(err)
|
|
|
|
|
|
|
|
// Build a valid value
|
|
|
|
ca1 := connect.TestCA(t, nil)
|
|
|
|
|
|
|
|
// Set
|
|
|
|
ok, err := s.CARootSetCAS(1, 0, []*structs.CARoot{ca1})
|
|
|
|
assert.Nil(err)
|
|
|
|
assert.True(ok)
|
|
|
|
|
|
|
|
// Make sure the index got updated.
|
|
|
|
assert.Equal(s.maxIndex(caRootTableName), uint64(1))
|
|
|
|
assert.True(watchFired(ws), "watch fired")
|
|
|
|
|
|
|
|
// Read it back out and verify it.
|
|
|
|
expected := *ca1
|
|
|
|
expected.RaftIndex = structs.RaftIndex{
|
|
|
|
CreateIndex: 1,
|
|
|
|
ModifyIndex: 1,
|
|
|
|
}
|
|
|
|
|
|
|
|
ws = memdb.NewWatchSet()
|
|
|
|
_, roots, err := s.CARoots(ws)
|
|
|
|
assert.Nil(err)
|
|
|
|
assert.Len(roots, 1)
|
|
|
|
actual := roots[0]
|
|
|
|
assert.Equal(&expected, actual)
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestStore_CARootSet_emptyID(t *testing.T) {
|
|
|
|
assert := assert.New(t)
|
|
|
|
s := testStateStore(t)
|
|
|
|
|
|
|
|
// Call list to populate the watch set
|
|
|
|
ws := memdb.NewWatchSet()
|
|
|
|
_, _, err := s.CARoots(ws)
|
|
|
|
assert.Nil(err)
|
|
|
|
|
|
|
|
// Build a valid value
|
|
|
|
ca1 := connect.TestCA(t, nil)
|
|
|
|
ca1.ID = ""
|
|
|
|
|
|
|
|
// Set
|
|
|
|
ok, err := s.CARootSetCAS(1, 0, []*structs.CARoot{ca1})
|
|
|
|
assert.NotNil(err)
|
|
|
|
assert.Contains(err.Error(), ErrMissingCARootID.Error())
|
|
|
|
assert.False(ok)
|
|
|
|
|
|
|
|
// Make sure the index got updated.
|
|
|
|
assert.Equal(s.maxIndex(caRootTableName), uint64(0))
|
|
|
|
assert.False(watchFired(ws), "watch fired")
|
|
|
|
|
|
|
|
// Read it back out and verify it.
|
|
|
|
ws = memdb.NewWatchSet()
|
|
|
|
_, roots, err := s.CARoots(ws)
|
|
|
|
assert.Nil(err)
|
|
|
|
assert.Len(roots, 0)
|
|
|
|
}
|
|
|
|
|
2018-03-27 03:38:39 +00:00
|
|
|
func TestStore_CARootSet_noActive(t *testing.T) {
|
|
|
|
assert := assert.New(t)
|
|
|
|
s := testStateStore(t)
|
|
|
|
|
|
|
|
// Call list to populate the watch set
|
|
|
|
ws := memdb.NewWatchSet()
|
|
|
|
_, _, err := s.CARoots(ws)
|
|
|
|
assert.Nil(err)
|
|
|
|
|
|
|
|
// Build a valid value
|
|
|
|
ca1 := connect.TestCA(t, nil)
|
|
|
|
ca1.Active = false
|
|
|
|
ca2 := connect.TestCA(t, nil)
|
|
|
|
ca2.Active = false
|
|
|
|
|
|
|
|
// Set
|
|
|
|
ok, err := s.CARootSetCAS(1, 0, []*structs.CARoot{ca1, ca2})
|
|
|
|
assert.NotNil(err)
|
|
|
|
assert.Contains(err.Error(), "exactly one active")
|
|
|
|
assert.False(ok)
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestStore_CARootSet_multipleActive(t *testing.T) {
|
|
|
|
assert := assert.New(t)
|
|
|
|
s := testStateStore(t)
|
|
|
|
|
|
|
|
// Call list to populate the watch set
|
|
|
|
ws := memdb.NewWatchSet()
|
|
|
|
_, _, err := s.CARoots(ws)
|
|
|
|
assert.Nil(err)
|
|
|
|
|
|
|
|
// Build a valid value
|
|
|
|
ca1 := connect.TestCA(t, nil)
|
|
|
|
ca2 := connect.TestCA(t, nil)
|
|
|
|
|
|
|
|
// Set
|
|
|
|
ok, err := s.CARootSetCAS(1, 0, []*structs.CARoot{ca1, ca2})
|
|
|
|
assert.NotNil(err)
|
|
|
|
assert.Contains(err.Error(), "exactly one active")
|
|
|
|
assert.False(ok)
|
|
|
|
}
|
|
|
|
|
2018-03-21 18:20:17 +00:00
|
|
|
func TestStore_CARootActive_valid(t *testing.T) {
|
|
|
|
assert := assert.New(t)
|
|
|
|
s := testStateStore(t)
|
|
|
|
|
|
|
|
// Build a valid value
|
|
|
|
ca1 := connect.TestCA(t, nil)
|
|
|
|
ca1.Active = false
|
|
|
|
ca2 := connect.TestCA(t, nil)
|
|
|
|
ca3 := connect.TestCA(t, nil)
|
|
|
|
ca3.Active = false
|
|
|
|
|
|
|
|
// Set
|
|
|
|
ok, err := s.CARootSetCAS(1, 0, []*structs.CARoot{ca1, ca2, ca3})
|
|
|
|
assert.Nil(err)
|
|
|
|
assert.True(ok)
|
|
|
|
|
|
|
|
// Query
|
|
|
|
ws := memdb.NewWatchSet()
|
|
|
|
idx, res, err := s.CARootActive(ws)
|
|
|
|
assert.Equal(idx, uint64(1))
|
|
|
|
assert.Nil(err)
|
|
|
|
assert.NotNil(res)
|
|
|
|
assert.Equal(ca2.ID, res.ID)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Test that querying the active CA returns the correct value.
|
|
|
|
func TestStore_CARootActive_none(t *testing.T) {
|
|
|
|
assert := assert.New(t)
|
|
|
|
s := testStateStore(t)
|
|
|
|
|
|
|
|
// Querying with no results returns nil.
|
|
|
|
ws := memdb.NewWatchSet()
|
|
|
|
idx, res, err := s.CARootActive(ws)
|
|
|
|
assert.Equal(idx, uint64(0))
|
|
|
|
assert.Nil(res)
|
|
|
|
assert.Nil(err)
|
|
|
|
}
|
|
|
|
|
2018-03-21 18:33:19 +00:00
|
|
|
func TestStore_CARoot_Snapshot_Restore(t *testing.T) {
|
2018-03-21 18:20:17 +00:00
|
|
|
assert := assert.New(t)
|
|
|
|
s := testStateStore(t)
|
|
|
|
|
|
|
|
// Create some intentions.
|
2018-03-21 18:33:19 +00:00
|
|
|
roots := structs.CARoots{
|
|
|
|
connect.TestCA(t, nil),
|
|
|
|
connect.TestCA(t, nil),
|
|
|
|
connect.TestCA(t, nil),
|
|
|
|
}
|
|
|
|
for _, r := range roots[1:] {
|
|
|
|
r.Active = false
|
2018-03-21 18:20:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Force the sort order of the UUIDs before we create them so the
|
|
|
|
// order is deterministic.
|
|
|
|
id := testUUID()
|
2018-03-21 18:33:19 +00:00
|
|
|
roots[0].ID = "a" + id[1:]
|
|
|
|
roots[1].ID = "b" + id[1:]
|
|
|
|
roots[2].ID = "c" + id[1:]
|
2018-03-21 18:20:17 +00:00
|
|
|
|
|
|
|
// Now create
|
2018-03-21 18:33:19 +00:00
|
|
|
ok, err := s.CARootSetCAS(1, 0, roots)
|
|
|
|
assert.Nil(err)
|
|
|
|
assert.True(ok)
|
2018-03-21 18:20:17 +00:00
|
|
|
|
|
|
|
// Snapshot the queries.
|
|
|
|
snap := s.Snapshot()
|
|
|
|
defer snap.Close()
|
|
|
|
|
|
|
|
// Alter the real state store.
|
2018-03-21 18:33:19 +00:00
|
|
|
ok, err = s.CARootSetCAS(2, 1, roots[:1])
|
|
|
|
assert.Nil(err)
|
|
|
|
assert.True(ok)
|
2018-03-21 18:20:17 +00:00
|
|
|
|
|
|
|
// Verify the snapshot.
|
2018-03-21 18:33:19 +00:00
|
|
|
assert.Equal(snap.LastIndex(), uint64(1))
|
|
|
|
dump, err := snap.CARoots()
|
2018-03-21 18:20:17 +00:00
|
|
|
assert.Nil(err)
|
2018-03-21 18:33:19 +00:00
|
|
|
assert.Equal(roots, dump)
|
2018-03-21 18:20:17 +00:00
|
|
|
|
|
|
|
// Restore the values into a new state store.
|
|
|
|
func() {
|
|
|
|
s := testStateStore(t)
|
|
|
|
restore := s.Restore()
|
2018-03-21 18:33:19 +00:00
|
|
|
for _, r := range dump {
|
|
|
|
assert.Nil(restore.CARoot(r))
|
2018-03-21 18:20:17 +00:00
|
|
|
}
|
|
|
|
restore.Commit()
|
|
|
|
|
|
|
|
// Read the restored values back out and verify that they match.
|
2018-03-21 18:33:19 +00:00
|
|
|
idx, actual, err := s.CARoots(nil)
|
2018-03-21 18:20:17 +00:00
|
|
|
assert.Nil(err)
|
2018-03-21 18:33:19 +00:00
|
|
|
assert.Equal(idx, uint64(2))
|
|
|
|
assert.Equal(roots, actual)
|
2018-03-21 18:20:17 +00:00
|
|
|
}()
|
|
|
|
}
|
2018-04-27 06:28:27 +00:00
|
|
|
|
|
|
|
func TestStore_CABuiltinProvider(t *testing.T) {
|
|
|
|
assert := assert.New(t)
|
|
|
|
s := testStateStore(t)
|
|
|
|
|
|
|
|
{
|
|
|
|
expected := &structs.CAConsulProviderState{
|
2018-05-04 23:01:38 +00:00
|
|
|
ID: "foo",
|
|
|
|
PrivateKey: "a",
|
|
|
|
RootCert: "b",
|
2018-04-27 06:28:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
ok, err := s.CASetProviderState(0, expected)
|
|
|
|
assert.NoError(err)
|
|
|
|
assert.True(ok)
|
|
|
|
|
|
|
|
idx, state, err := s.CAProviderState(expected.ID)
|
|
|
|
assert.NoError(err)
|
|
|
|
assert.Equal(idx, uint64(0))
|
|
|
|
assert.Equal(expected, state)
|
|
|
|
}
|
|
|
|
|
|
|
|
{
|
|
|
|
expected := &structs.CAConsulProviderState{
|
2018-05-04 23:01:38 +00:00
|
|
|
ID: "bar",
|
|
|
|
PrivateKey: "c",
|
|
|
|
RootCert: "d",
|
2018-04-27 06:28:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
ok, err := s.CASetProviderState(1, expected)
|
|
|
|
assert.NoError(err)
|
|
|
|
assert.True(ok)
|
|
|
|
|
|
|
|
idx, state, err := s.CAProviderState(expected.ID)
|
|
|
|
assert.NoError(err)
|
|
|
|
assert.Equal(idx, uint64(1))
|
|
|
|
assert.Equal(expected, state)
|
|
|
|
}
|
2020-01-09 15:32:19 +00:00
|
|
|
|
|
|
|
{
|
|
|
|
// Since we've already written to the builtin provider table the serial
|
|
|
|
// numbers will initialize from the max index of the provider table.
|
|
|
|
// That's why this first serial is 2 and not 1.
|
|
|
|
sn, err := s.CAIncrementProviderSerialNumber()
|
|
|
|
assert.NoError(err)
|
|
|
|
assert.Equal(uint64(2), sn)
|
|
|
|
|
|
|
|
sn, err = s.CAIncrementProviderSerialNumber()
|
|
|
|
assert.NoError(err)
|
|
|
|
assert.Equal(uint64(3), sn)
|
|
|
|
|
|
|
|
sn, err = s.CAIncrementProviderSerialNumber()
|
|
|
|
assert.NoError(err)
|
|
|
|
assert.Equal(uint64(4), sn)
|
|
|
|
}
|
2018-04-27 06:28:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func TestStore_CABuiltinProvider_Snapshot_Restore(t *testing.T) {
|
|
|
|
assert := assert.New(t)
|
|
|
|
s := testStateStore(t)
|
|
|
|
|
|
|
|
// Create multiple state entries.
|
|
|
|
before := []*structs.CAConsulProviderState{
|
|
|
|
{
|
2018-05-04 23:01:38 +00:00
|
|
|
ID: "bar",
|
|
|
|
PrivateKey: "y",
|
|
|
|
RootCert: "z",
|
2018-04-27 06:28:27 +00:00
|
|
|
},
|
|
|
|
{
|
2018-05-04 23:01:38 +00:00
|
|
|
ID: "foo",
|
|
|
|
PrivateKey: "a",
|
|
|
|
RootCert: "b",
|
2018-04-27 06:28:27 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
for i, state := range before {
|
|
|
|
ok, err := s.CASetProviderState(uint64(98+i), state)
|
|
|
|
assert.NoError(err)
|
|
|
|
assert.True(ok)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Take a snapshot.
|
|
|
|
snap := s.Snapshot()
|
|
|
|
defer snap.Close()
|
|
|
|
|
|
|
|
// Modify the state store.
|
|
|
|
after := &structs.CAConsulProviderState{
|
2018-05-04 23:01:38 +00:00
|
|
|
ID: "foo",
|
|
|
|
PrivateKey: "c",
|
|
|
|
RootCert: "d",
|
2018-04-27 06:28:27 +00:00
|
|
|
}
|
|
|
|
ok, err := s.CASetProviderState(100, after)
|
|
|
|
assert.NoError(err)
|
|
|
|
assert.True(ok)
|
|
|
|
|
|
|
|
snapped, err := snap.CAProviderState()
|
|
|
|
assert.NoError(err)
|
|
|
|
assert.Equal(before, snapped)
|
|
|
|
|
|
|
|
// Restore onto a new state store.
|
|
|
|
s2 := testStateStore(t)
|
|
|
|
restore := s2.Restore()
|
|
|
|
for _, entry := range snapped {
|
|
|
|
assert.NoError(restore.CAProviderState(entry))
|
|
|
|
}
|
|
|
|
restore.Commit()
|
|
|
|
|
|
|
|
// Verify the restored values match those from before the snapshot.
|
|
|
|
for _, state := range before {
|
|
|
|
idx, res, err := s2.CAProviderState(state.ID)
|
|
|
|
assert.NoError(err)
|
|
|
|
assert.Equal(idx, uint64(99))
|
|
|
|
assert.Equal(state, res)
|
|
|
|
}
|
|
|
|
}
|