Repository: seqsense/aws-iot-device-sdk-go
Pull request #213 (2021-02-20 00:34:34)

shadow: support metadata
#211 

codecov[bot]: # [Codecov](https://codecov.io/gh/seqsense/aws-iot-device-sdk-go/pull/213?src=pr&el=h1) Report
> Merging [#213](https://codecov.io/gh/seqsense/aws-iot-device-sdk-go/pull/213?src=pr&el=desc) (707d116) into [master](https://codecov.io/gh/seqsense/aws-iot-device-sdk-go/commit/736a443882c7b1d186aec10954e939d12ed824f7?el=desc) (736a443) will **increase** coverage by `0.22%`.
> The diff coverage is `62.74%`.

[![Impacted file tree graph](https://codecov.io/gh/seqsense/aws-iot-device-sdk-go/pull/213/graphs/tree.svg?width=650&height=150&src=pr&token=St5U6jzMqv)](https://codecov.io/gh/seqsense/aws-iot-device-sdk-go/pull/213?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #213      +/-   ##
==========================================
+ Coverage   58.19%   58.41%   +0.22%     
==========================================
  Files          27       28       +1     
  Lines        1239     1289      +50     
==========================================
+ Hits          721      753      +32     
- Misses        461      470       +9     
- Partials       57       66       +9     
```


| [Impacted Files](https://codecov.io/gh/seqsense/aws-iot-device-sdk-go/pull/213?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [shadow/state.go](https://codecov.io/gh/seqsense/aws-iot-device-sdk-go/pull/213/diff?src=pr&el=tree#diff-c2hhZG93L3N0YXRlLmdv) | `69.64% <53.33%> (-4.17%)` | :arrow_down: |
| [shadow/metadata.go](https://codecov.io/gh/seqsense/aws-iot-device-sdk-go/pull/213/diff?src=pr&el=tree#diff-c2hhZG93L21ldGFkYXRhLmdv) | `61.29% <61.29%> (ø)` | |
| [shadow/shadow.go](https://codecov.io/gh/seqsense/aws-iot-device-sdk-go/pull/213/diff?src=pr&el=tree#diff-c2hhZG93L3NoYWRvdy5nbw==) | `81.78% <100.00%> (+0.34%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/seqsense/aws-iot-device-sdk-go/pull/213?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/seqsense/aws-iot-device-sdk-go/pull/213?src=pr&el=footer). Last update [736a443...707d116](https://codecov.io/gh/seqsense/aws-iot-device-sdk-go/pull/213?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

at-wat: @pablocastilla could you try this branch?

Timestamp of `"attribute"` delta can be obtained by `document.Metadata.Delta["attribute"].(shadow.Metadata).Timestamp`.
<details>
<summary>example output of examples/shadow</summary>

```shell
$ cd go/src/github.com/seqsense/aws-iot-device-sdk-go/examples/shadow
$ go run . ${AWS_IOT_ENDPOINT} ${THING_NAME} ${SHADOW_NAME}
Active: <nil>
> update desire
delta:
  Value: 	1
  Struct: 	map[Values:[1 2]]
document:
  State: 
    Desired: 	map[Struct:map[Values:[1 2]] Value:1]
    Reported: 	map[]
    Delta: 	map[Struct:map[Values:[1 2]] Value:1]
  Metadata: 
    Desired: 	map[Struct:map[Values:[{Timestamp:1613810500} {Timestamp:1613810500}]] Value:{Timestamp:1613810500}]
    Reported: 	map[]
    Delta: 	map[Struct:map[Values:[{Timestamp:1613810500} {Timestamp:1613810500}]] Value:{Timestamp:1613810500}]
  Version: 	191
  Timestamp: 	1613810500
  ClientToken: 	

> update report
document:
  State: 
    Desired: 	map[Struct:map[Values:[1 2]] Value:1]
    Reported: 	map[Struct:map[Values:[1 2]] Value:2]
    Delta: 	map[Struct:map[Values:[1 2]] Value:1]
  Metadata: 
    Desired: 	map[Struct:map[Values:[{Timestamp:1613810500} {Timestamp:1613810500}]] Value:{Timestamp:1613810500}]
    Reported: 	map[Struct:map[Values:[{Timestamp:1613810501} {Timestamp:1613810501}]] Value:{Timestamp:1613810501}]
    Delta: 	map[Struct:map[Values:[{Timestamp:1613810500} {Timestamp:1613810500}]] Value:{Timestamp:1613810500}]
  Version: 	192
  Timestamp: 	1613810501
  ClientToken: 	
delta:
  Value: 	1

> get document
delta:
  Value: 	1
document:
  State: 
    Desired: 	map[Struct:map[Values:[1 2]] Value:1]
    Reported: 	map[Struct:map[Values:[1 2]] Value:2]
    Delta: 	map[Value:1]
  Metadata: 
    Desired: 	map[Struct:map[Values:[{Timestamp:1613810500} {Timestamp:1613810500}]] Value:{Timestamp:1613810500}]
    Reported: 	map[Struct:map[Values:[{Timestamp:1613810501} {Timestamp:1613810501}]] Value:{Timestamp:1613810501}]
    Delta: 	map[]
  Version: 	192
  Timestamp: 	1613810502
  ClientToken: 	

> remove one attribute from report
delta:
  Value: 	1
document:
  State: 
    Desired: 	map[Struct:map[Values:[1 2]] Value:1]
    Reported: 	map[Struct:map[Values:[1 2]]]
    Delta: 	map[Value:1]
  Metadata: 
    Desired: 	map[Struct:map[Values:[{Timestamp:1613810500} {Timestamp:1613810500}]] Value:{Timestamp:1613810500}]
    Reported: 	map[Struct:map[Values:[{Timestamp:1613810501} {Timestamp:1613810501}]] Value:{Timestamp:1613810504}]
    Delta: 	map[Value:{Timestamp:1613810500}]
  Version: 	193
  Timestamp: 	1613810504
  ClientToken: 	

> add an another attribute to report
document:
  State: 
    Desired: 	map[Struct:map[Values:[1 2]] Value:1]
    Reported: 	map[Struct:map[Values:[1 2]] Value2:1]
    Delta: 	map[Value:1]
  Metadata: 
    Desired: 	map[Struct:map[Values:[{Timestamp:1613810500} {Timestamp:1613810500}]] Value:{Timestamp:1613810500}]
    Reported: 	map[Struct:map[Values:[{Timestamp:1613810501} {Timestamp:1613810501}]] Value:{Timestamp:1613810504} Value2:{Timestamp:1613810505}]
    Delta: 	map[Value:{Timestamp:1613810500}]
  Version: 	194
  Timestamp: 	1613810505
  ClientToken: 	
delta:
  Value: 	1

> delete
document:
```

</details>
pablocastilla: Sure! I'll do it this Monday
pablocastilla: it works, now I can do a s.Get(ctx) and I have everything there. Just one question, does this call makes a request to AWS? Couldn't it pass the metadata info to the OnDelta event directly?
at-wat: `s.Get(ctx)` makes a request.
Latest thing document (including metadata) can be accessed by `s.Document()` without making a new request to AWS.

For example:
```go
s.OnDelta(func(delta map[string]interface{}) {
  val := delta["attribute"]
  meta := s.Document().Metadata.Delta["attribute"].(shadow.Metadata)
  ...
})
```
As I wrote [here](https://github.com/seqsense/aws-iot-device-sdk-go/issues/211#issuecomment-781922139), Thing Document returned by `Shadow.Document()` in `OnDelta` callback is guaranteed to be the version given by the delta event.
at-wat: BTW, I noticed that AWS IoT ommits Metadata.Delta from Get response, and `s.Document().Metadata.Delta` was cleared when calling `s.Get(ctx)`.

pablocastilla: perfect! Looking forward for the release, thanks so much for your work :)
at-wat: @seqsense/cloud_backend please take a look

Patch:
diff --git a/examples/shadow/main.go b/examples/shadow/main.go
index cf16291..75a01fe 100644
--- a/examples/shadow/main.go
+++ b/examples/shadow/main.go
@@ -97,7 +97,7 @@ func main() {
 		fmt.Printf("async error: %v\n", err)
 	})
 	s.OnDelta(func(delta map[string]interface{}) {
-		fmt.Printf("delta: %+v\n", delta)
+		fmt.Printf("delta:%s", prettyDump(delta))
 	})
 	cli.Handle(s)
 
@@ -106,28 +106,52 @@ func main() {
 	if err != nil {
 		panic(err)
 	}
-	fmt.Printf("document: %+v\n", doc)
+	fmt.Printf("document:%s", prettyDump(doc))
 
-	fmt.Print("> update report\n")
+	time.Sleep(time.Second)
+
+	fmt.Print("\n> update report\n")
 	doc, err = s.Report(ctx, sampleState{Value: 2, Struct: sampleStruct{Values: []int{1, 2}}})
 	if err != nil {
 		panic(err)
 	}
-	fmt.Printf("document: %+v\n", doc)
+	fmt.Printf("document:%s", prettyDump(doc))
+
+	time.Sleep(time.Second)
 
-	fmt.Print("> get document\n")
+	fmt.Print("\n> get document\n")
 	doc, err = s.Get(ctx)
 	if err != nil {
 		panic(err)
 	}
-	fmt.Printf("document: %+v\n", doc)
+	fmt.Printf("document:%s", prettyDump(doc))
+
+	time.Sleep(time.Second)
+
+	fmt.Print("\n> remove one attribute from report\n")
+	doc, err = s.Report(ctx, map[string]interface{}{"Value": nil})
+	if err != nil {
+		panic(err)
+	}
+	fmt.Printf("document:%s", prettyDump(doc))
+
+	time.Sleep(time.Second)
+
+	fmt.Print("\n> add an another attribute to report\n")
+	doc, err = s.Report(ctx, map[string]interface{}{"Value2": 1})
+	if err != nil {
+		panic(err)
+	}
+	fmt.Printf("document:%s", prettyDump(doc))
+
+	time.Sleep(time.Second)
 
-	fmt.Print("> delete\n")
+	fmt.Print("\n> delete\n")
 	err = s.Delete(ctx)
 	if err != nil {
 		panic(err)
 	}
-	fmt.Printf("document: %+v\n", s.Document())
+	fmt.Printf("document:%s\n\n", prettyDump(s.Document()))
 }
 
 type sampleStruct struct {
diff --git a/examples/shadow/shortprettydump.go b/examples/shadow/shortprettydump.go
new file mode 100644
index 0000000..2cfea5e
--- /dev/null
+++ b/examples/shadow/shortprettydump.go
@@ -0,0 +1,61 @@
+// Copyright 2021 SEQSENSE, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package main
+
+import (
+	"fmt"
+	"reflect"
+	"strings"
+)
+
+func prettyDump(d interface{}) string {
+	return prettyDumpImpl(reflect.ValueOf(d), 0)
+}
+
+func prettyDumpImpl(v reflect.Value, depth int) string {
+	if depth > 1 {
+		return fmt.Sprintf("\t%+v", v.Interface())
+	}
+	depth++
+	if v.Kind() == reflect.Ptr {
+		v = v.Elem()
+	}
+	indent := strings.Repeat("  ", depth)
+	var out string
+	switch v.Kind() {
+	case reflect.Slice:
+		out = "\n"
+		for i := 0; i < v.Len(); i++ {
+			out += indent + "- " + prettyDumpImpl(v.Index(i), depth) + "\n"
+		}
+	case reflect.Map:
+		out = "\n"
+		for _, k := range v.MapKeys() {
+			out += indent + k.String() + ": " + prettyDumpImpl(v.MapIndex(k), depth) + "\n"
+		}
+		return out
+	case reflect.Struct:
+		out = "\n"
+		for i := 0; i < v.NumField(); i++ {
+			out += indent + v.Type().Field(i).Name + ": " + prettyDumpImpl(v.Field(i), depth) + "\n"
+		}
+	default:
+		if !v.IsValid() {
+			return ""
+		}
+		return fmt.Sprintf("\t%+v", v.Interface())
+	}
+	return strings.Replace(out, "\n\n", "\n", -1)
+}
diff --git a/shadow/metadata.go b/shadow/metadata.go
new file mode 100644
index 0000000..003901b
--- /dev/null
+++ b/shadow/metadata.go
@@ -0,0 +1,86 @@
+// Copyright 2021 SEQSENSE, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package shadow
+
+import (
+	"encoding/json"
+	"regexp"
+)
+
+var regexLeafJSON = regexp.MustCompile(`^{[^{}]*}$`)
+
+// ThingStateMetadata represents Thing Shadow State metadata.
+type ThingStateMetadata struct {
+	Desired  NestedMetadata `json:"desired,omitempty"`
+	Reported NestedMetadata `json:"reported,omitempty"`
+	Delta    NestedMetadata `json:"delta,omitempty"`
+}
+
+// Metadata represents metadata of state attribute.
+type Metadata struct {
+	Timestamp int `json:"timestamp"`
+}
+
+// NestedMetadata is JSON unmarshaller for state metadata.
+type NestedMetadata map[string]interface{}
+
+func (n *NestedMetadata) UnmarshalJSON(b []byte) error {
+	if *n == nil {
+		*n = make(map[string]interface{})
+	}
+	j := make(map[string]json.RawMessage)
+	if err := json.Unmarshal(b, &j); err != nil {
+		return err
+	}
+	for k, v := range j {
+		v2, err := unmarshalMetadataImpl(v)
+		if err != nil {
+			return err
+		}
+		(*n)[k] = v2
+	}
+	return nil
+}
+
+func unmarshalMetadataImpl(b []byte) (interface{}, error) {
+	switch {
+	case b[0] == '[' && b[len(b)-1] == ']':
+		var v2 []json.RawMessage
+		if err := json.Unmarshal(b, &v2); err != nil {
+			return nil, err
+		}
+		var v5 []interface{}
+		for _, v3 := range v2 {
+			v4, err := unmarshalMetadataImpl(v3)
+			if err != nil {
+				return nil, err
+			}
+			v5 = append(v5, v4)
+		}
+		return v5, nil
+	case !regexLeafJSON.Match(b):
+		var v2 NestedMetadata
+		if err := json.Unmarshal(b, &v2); err != nil {
+			return nil, err
+		}
+		return v2, nil
+	default:
+		var v2 Metadata
+		if err := json.Unmarshal(b, &v2); err != nil {
+			return nil, err
+		}
+		return v2, nil
+	}
+}
diff --git a/shadow/shadow.go b/shadow/shadow.go
index cb59544..fb896fb 100644
--- a/shadow/shadow.go
+++ b/shadow/shadow.go
@@ -105,6 +105,8 @@ func (s *shadow) getAccepted(msg *mqtt.Message) {
 		return
 	}
 	s.mu.Lock()
+	// For some reason, AWS IoT omits Metadata.Delta from Get response. Keep previous Metadata.Delta.
+	doc.Metadata.Delta = s.doc.Metadata.Delta
 	s.doc = doc
 	s.mu.Unlock()
 	s.handleResponse(doc)
@@ -182,6 +184,11 @@ func New(ctx context.Context, cli awsiotdev.Device, opt ...Option) (Shadow, erro
 				Reported: map[string]interface{}{},
 				Delta:    map[string]interface{}{},
 			},
+			Metadata: ThingStateMetadata{
+				Desired:  map[string]interface{}{},
+				Reported: map[string]interface{}{},
+				Delta:    map[string]interface{}{},
+			},
 		},
 
 		chResps: make(map[string]chan interface{}),
diff --git a/shadow/state.go b/shadow/state.go
index 932d3a0..25dd9f9 100644
--- a/shadow/state.go
+++ b/shadow/state.go
@@ -51,10 +51,11 @@ type ThingState struct {
 
 // ThingDocument represents Thing Shadow Document.
 type ThingDocument struct {
-	State       ThingState `json:"state"`
-	Version     int        `json:"version,omitempty"`
-	Timestamp   int        `json:"timestamp,omitempty"`
-	ClientToken string     `json:"clientToken,omitempty"`
+	State       ThingState         `json:"state"`
+	Metadata    ThingStateMetadata `json:"metadata"`
+	Version     int                `json:"version,omitempty"`
+	Timestamp   int                `json:"timestamp,omitempty"`
+	ClientToken string             `json:"clientToken,omitempty"`
 }
 
 type thingStateRaw struct {
@@ -66,6 +67,7 @@ type thingStateRaw struct {
 
 type thingDocumentRaw struct {
 	State       thingStateRaw `json:"state"`
+	Metadata    thingStateRaw `json:"metadata"`
 	Version     int           `json:"version,omitempty"`
 	Timestamp   int           `json:"timestamp,omitempty"`
 	ClientToken string        `json:"clientToken,omitempty"`
@@ -73,6 +75,7 @@ type thingDocumentRaw struct {
 
 type thingDelta struct {
 	State     map[string]interface{} `json:"state"`
+	Metadata  NestedMetadata         `json:"metadata"`
 	Version   int                    `json:"version,omitempty"`
 	Timestamp int                    `json:"timestamp,omitempty"`
 }
@@ -87,9 +90,15 @@ func (s *ThingDocument) update(state *thingDocumentRaw) error {
 	if err := updateStateRaw(s.State.Desired, state.State.Desired); err != nil {
 		return ioterr.New(err, "updating desired state")
 	}
+	if err := updateStateMetadataRaw(s.Metadata.Desired, state.Metadata.Desired); err != nil {
+		return ioterr.New(err, "updating desired state")
+	}
 	if err := updateStateRaw(s.State.Reported, state.State.Reported); err != nil {
 		return ioterr.New(err, "updating reported state")
 	}
+	if err := updateStateMetadataRaw(s.Metadata.Reported, state.Metadata.Reported); err != nil {
+		return ioterr.New(err, "updating reported state")
+	}
 	return nil
 }
 
@@ -101,17 +110,25 @@ func (s *ThingDocument) updateDelta(state *thingDelta) bool {
 	s.Version = state.Version
 	s.Timestamp = state.Timestamp
 	s.State.Delta = state.State
+	s.Metadata.Delta = state.Metadata
 	return true
 }
 
-func updateStateRaw(state map[string]interface{}, update json.RawMessage) error {
+func updateStateRawCommon(state map[string]interface{}, update json.RawMessage) bool {
 	if !hasUpdate(update) {
-		return nil
+		return true
 	}
 	if update == nil {
 		for k := range state {
 			delete(state, k)
 		}
+		return true
+	}
+	return false
+}
+
+func updateStateRaw(state map[string]interface{}, update json.RawMessage) error {
+	if updateStateRawCommon(state, update) {
 		return nil
 	}
 	var u map[string]interface{}
@@ -121,6 +138,17 @@ func updateStateRaw(state map[string]interface{}, update json.RawMessage) error
 	return updateState(state, u)
 }
 
+func updateStateMetadataRaw(state map[string]interface{}, update json.RawMessage) error {
+	if updateStateRawCommon(state, update) {
+		return nil
+	}
+	var u NestedMetadata
+	if err := json.Unmarshal([]byte(update), &u); err != nil {
+		return ioterr.New(err, "unmarshaling update")
+	}
+	return updateState(state, u)
+}
+
 func updateState(state map[string]interface{}, update map[string]interface{}) error {
 	if len(update) == 0 {
 		for k := range state {


Test patch:
diff --git a/shadow/metadata_test.go b/shadow/metadata_test.go
new file mode 100644
index 0000000..5814d7c
--- /dev/null
+++ b/shadow/metadata_test.go
@@ -0,0 +1,43 @@
+// Copyright 2021 SEQSENSE, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package shadow
+
+import (
+	"encoding/json"
+	"reflect"
+	"testing"
+)
+
+func TestNestedMetadata(t *testing.T) {
+	const input = `{"attr1":{"timestamp":1234},"attr2":{"attr3":{"timestamp":1235}},"attr4":[{"timestamp":1236},{"timestamp":1237}]}`
+	var expected = NestedMetadata{
+		"attr1": Metadata{Timestamp: 1234},
+		"attr2": NestedMetadata{
+			"attr3": Metadata{Timestamp: 1235},
+		},
+		"attr4": []interface{}{
+			Metadata{Timestamp: 1236},
+			Metadata{Timestamp: 1237},
+		},
+	}
+
+	var v NestedMetadata
+	if err := json.Unmarshal([]byte(input), &v); err != nil {
+		t.Fatal(err)
+	}
+	if !reflect.DeepEqual(expected, v) {
+		t.Errorf("Expected:\n(%T) %+v\ngot:\n(%T) %+v", expected, expected, v, v)
+	}
+}
diff --git a/shadow/shadow_test.go b/shadow/shadow_test.go
index d9051d5..6b28bcf 100644
--- a/shadow/shadow_test.go
+++ b/shadow/shadow_test.go
@@ -181,7 +181,7 @@ func TestHandlers(t *testing.T) {
 				t.Error(err)
 			case delta := <-chDelta:
 				if !reflect.DeepEqual(expectedDoc.State.Delta, delta) {
-					t.Errorf("Expected delta: %v, got: %v",
+					t.Errorf("Expected delta:\n%+v\ngot:\n%+v",
 						expectedDoc.State.Delta, delta,
 					)
 				}
@@ -189,7 +189,7 @@ func TestHandlers(t *testing.T) {
 				t.Fatal("Timeout")
 			}
 			if !reflect.DeepEqual(expectedDoc, s.doc) {
-				t.Errorf("Expected state: %v, got: %v",
+				t.Errorf("Expected state:\n%v\ngot:\n%v",
 					expectedDoc, s.doc,
 				)
 			}
@@ -266,7 +266,7 @@ func TestHandlers(t *testing.T) {
 				t.Error(err)
 			case delta := <-chDelta:
 				if !reflect.DeepEqual(expectedDoc.State.Delta, delta) {
-					t.Errorf("Expected delta: %v, got: %v",
+					t.Errorf("Expected delta:\n%+v\ngot:\n%+v",
 						expectedDoc.State.Delta, delta,
 					)
 				}
@@ -274,7 +274,7 @@ func TestHandlers(t *testing.T) {
 				t.Fatal("Timeout")
 			}
 			if !reflect.DeepEqual(expectedDoc, s.doc) {
-				t.Errorf("Expected state: %v, got: %v",
+				t.Errorf("Expected state:\n%+v\ngot\n%+v",
 					expectedDoc, s.doc,
 				)
 			}
@@ -327,7 +327,7 @@ func TestHandlers(t *testing.T) {
 			default:
 			}
 			if !reflect.DeepEqual(expectedDoc, s.doc) {
-				t.Errorf("Expected state: %v, got: %v",
+				t.Errorf("Expected state:\n%+v\ngot:\n%+v",
 					expectedDoc, s.doc,
 				)
 			}
@@ -366,7 +366,7 @@ func TestHandlers(t *testing.T) {
 			)})
 
 			if !reflect.DeepEqual(expectedDoc, s.doc) {
-				t.Errorf("Expected state: %v, got: %v",
+				t.Errorf("Expected state:\n%+v\ngot:\n%+v",
 					expectedDoc, s.doc,
 				)
 			}
@@ -436,7 +436,7 @@ func TestOnDelta(t *testing.T) {
 	done := make(chan struct{})
 	s.OnDelta(func(delta map[string]interface{}) {
 		if !reflect.DeepEqual(expected, delta) {
-			t.Fatalf("Expected delta: %v, got: %v", expected, delta)
+			t.Fatalf("Expected delta:\n%+v\ngot:\n%+v", expected, delta)
 		}
 		close(done)
 	})
@@ -465,25 +465,46 @@ func TestOnDelta(t *testing.T) {
 
 func TestGet(t *testing.T) {
 	testCases := map[string]struct {
+		initial       *ThingDocument
 		response      interface{}
 		responseTopic string
 		expected      interface{}
 		err           error
 	}{
 		"Success": {
+			initial: &ThingDocument{
+				Version: 5,
+				State: ThingState{
+					Delta: map[string]interface{}{"key": "value2"},
+				},
+				Metadata: ThingStateMetadata{
+					Delta: map[string]interface{}{"key": Metadata{Timestamp: 1234}},
+				},
+			},
 			response: &ThingDocument{
 				Version: 5,
 				State: ThingState{
-					Desired:  map[string]interface{}{"key": "value"},
+					Desired:  map[string]interface{}{"key": "value2"},
 					Reported: map[string]interface{}{"key": "value"},
+					Delta:    map[string]interface{}{"key": "value2"},
+				},
+				Metadata: ThingStateMetadata{
+					Desired:  map[string]interface{}{"key": Metadata{Timestamp: 1234}},
+					Reported: map[string]interface{}{"key": Metadata{Timestamp: 1235}},
 				},
 			},
 			responseTopic: "get/accepted",
 			expected: &ThingDocument{
 				Version: 5,
 				State: ThingState{
-					Desired:  map[string]interface{}{"key": "value"},
+					Desired:  map[string]interface{}{"key": "value2"},
 					Reported: map[string]interface{}{"key": "value"},
+					Delta:    map[string]interface{}{"key": "value2"},
+				},
+				Metadata: ThingStateMetadata{
+					Desired:  NestedMetadata{"key": Metadata{Timestamp: 1234}},
+					Reported: NestedMetadata{"key": Metadata{Timestamp: 1235}},
+					Delta:    NestedMetadata{"key": Metadata{Timestamp: 1234}},
 				},
 			},
 		},
@@ -537,17 +558,20 @@ func TestGet(t *testing.T) {
 			if err != nil {
 				t.Fatal(err)
 			}
+			if testCase.initial != nil {
+				s.(*shadow).doc = testCase.initial
+			}
 			cli.Handle(s)
 
 			doc, err := s.Get(ctx)
 			if err != nil {
 				setClientToken(err, "")
 				if !reflect.DeepEqual(testCase.err, err) {
-					t.Fatalf("Expected error: %v, got: %v", testCase.err, err)
+					t.Fatalf("Expected error:\n%+v\ngot:\n%+v", testCase.err, err)
 				}
 			} else {
 				if !reflect.DeepEqual(testCase.expected, doc) {
-					t.Errorf("Expected document: %v, got: %v", testCase.expected, doc)
+					t.Errorf("Expected document:\n%+v\ngot:\n%+v", testCase.expected, doc)
 				}
 			}
 		})
@@ -569,6 +593,9 @@ func TestDesire(t *testing.T) {
 				State: ThingState{
 					Desired: map[string]interface{}{"key": "value"},
 				},
+				Metadata: ThingStateMetadata{
+					Desired: map[string]interface{}{"key": Metadata{Timestamp: 1234}},
+				},
 			},
 			responseTopic: "update/accepted",
 			expected: &ThingDocument{
@@ -578,6 +605,11 @@ func TestDesire(t *testing.T) {
 					Reported: map[string]interface{}{},
 					Delta:    map[string]interface{}{},
 				},
+				Metadata: ThingStateMetadata{
+					Desired:  NestedMetadata{"key": Metadata{Timestamp: 1234}},
+					Reported: NestedMetadata{},
+					Delta:    NestedMetadata{},
+				},
 			},
 		},
 		"Error": {
@@ -637,11 +669,11 @@ func TestDesire(t *testing.T) {
 			if err != nil {
 				setClientToken(err, "")
 				if !reflect.DeepEqual(testCase.err, err) {
-					t.Fatalf("Expected error: %v, got: %v", testCase.err, err)
+					t.Fatalf("Expected error:\n%+v\ngot:\n%+v", testCase.err, err)
 				}
 			} else {
 				if !reflect.DeepEqual(testCase.expected, doc) {
-					t.Errorf("Expected document: %v, got: %v", testCase.expected, doc)
+					t.Errorf("Expected document:\n%+v\ngot:\n%+v", testCase.expected, doc)
 				}
 			}
 		})
@@ -663,6 +695,9 @@ func TestReport(t *testing.T) {
 				State: ThingState{
 					Reported: map[string]interface{}{"key": "value"},
 				},
+				Metadata: ThingStateMetadata{
+					Reported: map[string]interface{}{"key": Metadata{Timestamp: 1234}},
+				},
 			},
 			responseTopic: "update/accepted",
 			expected: &ThingDocument{
@@ -672,6 +707,11 @@ func TestReport(t *testing.T) {
 					Desired:  map[string]interface{}{},
 					Delta:    map[string]interface{}{},
 				},
+				Metadata: ThingStateMetadata{
+					Reported: NestedMetadata{"key": Metadata{Timestamp: 1234}},
+					Desired:  NestedMetadata{},
+					Delta:    NestedMetadata{},
+				},
 			},
 		},
 		"Error": {
@@ -731,11 +771,11 @@ func TestReport(t *testing.T) {
 			if err != nil {
 				setClientToken(err, "")
 				if !reflect.DeepEqual(testCase.err, err) {
-					t.Fatalf("Expected error: %v, got: %v", testCase.err, err)
+					t.Fatalf("Expected error:\n%+v\ngot:\n%+v", testCase.err, err)
 				}
 			} else {
 				if !reflect.DeepEqual(testCase.expected, doc) {
-					t.Errorf("Expected document: %v, got: %v", testCase.expected, doc)
+					t.Errorf("Expected document:\n%+v\ngot:\n%+v", testCase.expected, doc)
 				}
 			}
 		})
@@ -808,7 +848,7 @@ func TestDelete(t *testing.T) {
 			if err != nil {
 				setClientToken(err, "")
 				if !reflect.DeepEqual(testCase.err, err) {
-					t.Fatalf("Expected error: %v, got: %v", testCase.err, err)
+					t.Fatalf("Expected error:\n%v\ngot:\n%v", testCase.err, err)
 				}
 			}
 		})