Pārlūkot izejas kodu

util/syspolicy/setting: make setting.RawItem JSON-marshallable

We add setting.RawValue, a new type that facilitates unmarshalling JSON numbers and arrays
as uint64 and []string (instead of float64 and []any) for policy setting values.
We then use it to make setting.RawItem JSON-marshallable and update the tests.

Updates #12687

Signed-off-by: Nick Khyl <[email protected]>
Nick Khyl 1 gadu atpakaļ
vecāks
revīzija
2a2228f97b

+ 1 - 1
types/opt/value.go

@@ -36,7 +36,7 @@ func ValueOf[T any](v T) Value[T] {
 }
 
 // String implements [fmt.Stringer].
-func (o *Value[T]) String() string {
+func (o Value[T]) String() string {
 	if !o.set {
 		return fmt.Sprintf("(empty[%T])", o.value)
 	}

+ 109 - 14
util/syspolicy/setting/raw_item.go

@@ -5,7 +5,11 @@ package setting
 
 import (
 	"fmt"
+	"reflect"
 
+	jsonv2 "github.com/go-json-experiment/json"
+	"github.com/go-json-experiment/json/jsontext"
+	"tailscale.com/types/opt"
 	"tailscale.com/types/structs"
 )
 
@@ -17,10 +21,15 @@ import (
 // or converted from strings, these setting types predate the typed policy
 // hierarchies, and must be supported at this layer.
 type RawItem struct {
-	_      structs.Incomparable
-	value  any
-	err    *ErrorText
-	origin *Origin // or nil
+	_    structs.Incomparable
+	data rawItemJSON
+}
+
+// rawItemJSON holds JSON-marshallable data for [RawItem].
+type rawItemJSON struct {
+	Value  RawValue   `json:",omitzero"`
+	Error  *ErrorText `json:",omitzero"` // or nil
+	Origin *Origin    `json:",omitzero"` // or nil
 }
 
 // RawItemOf returns a [RawItem] with the specified value.
@@ -30,20 +39,20 @@ func RawItemOf(value any) RawItem {
 
 // RawItemWith returns a [RawItem] with the specified value, error and origin.
 func RawItemWith(value any, err *ErrorText, origin *Origin) RawItem {
-	return RawItem{value: value, err: err, origin: origin}
+	return RawItem{data: rawItemJSON{Value: RawValue{opt.ValueOf(value)}, Error: err, Origin: origin}}
 }
 
 // Value returns the value of the policy setting, or nil if the policy setting
 // is not configured, or an error occurred while reading it.
 func (i RawItem) Value() any {
-	return i.value
+	return i.data.Value.Get()
 }
 
 // Error returns the error that occurred when reading the policy setting,
 // or nil if no error occurred.
 func (i RawItem) Error() error {
-	if i.err != nil {
-		return i.err
+	if i.data.Error != nil {
+		return i.data.Error
 	}
 	return nil
 }
@@ -51,17 +60,103 @@ func (i RawItem) Error() error {
 // Origin returns an optional [Origin] indicating where the policy setting is
 // configured.
 func (i RawItem) Origin() *Origin {
-	return i.origin
+	return i.data.Origin
 }
 
 // String implements [fmt.Stringer].
 func (i RawItem) String() string {
 	var suffix string
-	if i.origin != nil {
-		suffix = fmt.Sprintf(" - {%v}", i.origin)
+	if i.data.Origin != nil {
+		suffix = fmt.Sprintf(" - {%v}", i.data.Origin)
+	}
+	if i.data.Error != nil {
+		return fmt.Sprintf("Error{%q}%s", i.data.Error.Error(), suffix)
+	}
+	return fmt.Sprintf("%v%s", i.data.Value.Value, suffix)
+}
+
+// MarshalJSONV2 implements [jsonv2.MarshalerV2].
+func (i RawItem) MarshalJSONV2(out *jsontext.Encoder, opts jsonv2.Options) error {
+	return jsonv2.MarshalEncode(out, &i.data, opts)
+}
+
+// UnmarshalJSONV2 implements [jsonv2.UnmarshalerV2].
+func (i *RawItem) UnmarshalJSONV2(in *jsontext.Decoder, opts jsonv2.Options) error {
+	return jsonv2.UnmarshalDecode(in, &i.data, opts)
+}
+
+// MarshalJSON implements [json.Marshaler].
+func (i RawItem) MarshalJSON() ([]byte, error) {
+	return jsonv2.Marshal(i) // uses MarshalJSONV2
+}
+
+// UnmarshalJSON implements [json.Unmarshaler].
+func (i *RawItem) UnmarshalJSON(b []byte) error {
+	return jsonv2.Unmarshal(b, i) // uses UnmarshalJSONV2
+}
+
+// RawValue represents a raw policy setting value read from a policy store.
+// It is JSON-marshallable and facilitates unmarshalling of JSON values
+// into corresponding policy setting types, with special handling for JSON numbers
+// (unmarshalled as float64) and JSON string arrays (unmarshalled as []string).
+// See also [RawValue.UnmarshalJSONV2].
+type RawValue struct {
+	opt.Value[any]
+}
+
+// RawValueType is a constraint that permits raw setting value types.
+type RawValueType interface {
+	bool | uint64 | string | []string
+}
+
+// RawValueOf returns a new [RawValue] holding the specified value.
+func RawValueOf[T RawValueType](v T) RawValue {
+	return RawValue{opt.ValueOf[any](v)}
+}
+
+// MarshalJSONV2 implements [jsonv2.MarshalerV2].
+func (v RawValue) MarshalJSONV2(out *jsontext.Encoder, opts jsonv2.Options) error {
+	return jsonv2.MarshalEncode(out, v.Value, opts)
+}
+
+// UnmarshalJSONV2 implements [jsonv2.UnmarshalerV2] by attempting to unmarshal
+// a JSON value as one of the supported policy setting value types (bool, string, uint64, or []string),
+// based on the JSON value type. It fails if the JSON value is an object, if it's a JSON number that
+// cannot be represented as a uint64, or if a JSON array contains anything other than strings.
+func (v *RawValue) UnmarshalJSONV2(in *jsontext.Decoder, opts jsonv2.Options) error {
+	var valPtr any
+	switch k := in.PeekKind(); k {
+	case 't', 'f':
+		valPtr = new(bool)
+	case '"':
+		valPtr = new(string)
+	case '0':
+		valPtr = new(uint64) // unmarshal JSON numbers as uint64
+	case '[', 'n':
+		valPtr = new([]string) // unmarshal arrays as string slices
+	case '{':
+		return fmt.Errorf("unexpected token: %v", k)
+	default:
+		panic("unreachable")
 	}
-	if i.err != nil {
-		return fmt.Sprintf("Error{%q}%s", i.err.Error(), suffix)
+	if err := jsonv2.UnmarshalDecode(in, valPtr, opts); err != nil {
+		v.Value.Clear()
+		return err
 	}
-	return fmt.Sprintf("%v%s", i.value, suffix)
+	value := reflect.ValueOf(valPtr).Elem().Interface()
+	v.Value = opt.ValueOf(value)
+	return nil
+}
+
+// MarshalJSON implements [json.Marshaler].
+func (v RawValue) MarshalJSON() ([]byte, error) {
+	return jsonv2.Marshal(v) // uses MarshalJSONV2
 }
+
+// UnmarshalJSON implements [json.Unmarshaler].
+func (v *RawValue) UnmarshalJSON(b []byte) error {
+	return jsonv2.Unmarshal(b, v) // uses UnmarshalJSONV2
+}
+
+// RawValues is a map of keyed setting values that can be read from a JSON.
+type RawValues map[Key]RawValue

+ 101 - 0
util/syspolicy/setting/raw_item_test.go

@@ -0,0 +1,101 @@
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package setting
+
+import (
+	"math"
+	"reflect"
+	"strconv"
+	"testing"
+
+	jsonv2 "github.com/go-json-experiment/json"
+)
+
+func TestMarshalUnmarshalRawValue(t *testing.T) {
+	tests := []struct {
+		name    string
+		json    string
+		want    RawValue
+		wantErr bool
+	}{
+		{
+			name: "Bool/True",
+			json: `true`,
+			want: RawValueOf(true),
+		},
+		{
+			name: "Bool/False",
+			json: `false`,
+			want: RawValueOf(false),
+		},
+		{
+			name: "String/Empty",
+			json: `""`,
+			want: RawValueOf(""),
+		},
+		{
+			name: "String/NonEmpty",
+			json: `"Test"`,
+			want: RawValueOf("Test"),
+		},
+		{
+			name: "StringSlice/Null",
+			json: `null`,
+			want: RawValueOf([]string(nil)),
+		},
+		{
+			name: "StringSlice/Empty",
+			json: `[]`,
+			want: RawValueOf([]string{}),
+		},
+		{
+			name: "StringSlice/NonEmpty",
+			json: `["A", "B", "C"]`,
+			want: RawValueOf([]string{"A", "B", "C"}),
+		},
+		{
+			name:    "StringSlice/NonStrings",
+			json:    `[1, 2, 3]`,
+			wantErr: true,
+		},
+		{
+			name: "Number/Integer/0",
+			json: `0`,
+			want: RawValueOf(uint64(0)),
+		},
+		{
+			name: "Number/Integer/1",
+			json: `1`,
+			want: RawValueOf(uint64(1)),
+		},
+		{
+			name: "Number/Integer/MaxUInt64",
+			json: strconv.FormatUint(math.MaxUint64, 10),
+			want: RawValueOf(uint64(math.MaxUint64)),
+		},
+		{
+			name:    "Number/Integer/Negative",
+			json:    `-1`,
+			wantErr: true,
+		},
+		{
+			name:    "Object",
+			json:    `{}`,
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			var got RawValue
+			gotErr := jsonv2.Unmarshal([]byte(tt.json), &got)
+			if (gotErr != nil) != tt.wantErr {
+				t.Fatalf("Error: got %v; want %v", gotErr, tt.wantErr)
+			}
+
+			if !tt.wantErr && !reflect.DeepEqual(got, tt.want) {
+				t.Fatalf("Value: got %v; want %v", got, tt.want)
+			}
+		})
+	}
+}

+ 125 - 126
util/syspolicy/setting/snapshot_test.go

@@ -30,134 +30,134 @@ func TestMergeSnapshots(t *testing.T) {
 			name: "first-nil",
 			s1:   nil,
 			s2: NewSnapshot(map[Key]RawItem{
-				"Setting1": {value: 123},
-				"Setting2": {value: "String"},
-				"Setting3": {value: true},
+				"Setting1": RawItemOf(123),
+				"Setting2": RawItemOf("String"),
+				"Setting3": RawItemOf(true),
 			}),
 			want: NewSnapshot(map[Key]RawItem{
-				"Setting1": {value: 123},
-				"Setting2": {value: "String"},
-				"Setting3": {value: true},
+				"Setting1": RawItemOf(123),
+				"Setting2": RawItemOf("String"),
+				"Setting3": RawItemOf(true),
 			}),
 		},
 		{
 			name: "first-empty",
 			s1:   NewSnapshot(map[Key]RawItem{}),
 			s2: NewSnapshot(map[Key]RawItem{
-				"Setting1": {value: 123},
-				"Setting2": {value: "String"},
-				"Setting3": {value: false},
+				"Setting1": RawItemOf(123),
+				"Setting2": RawItemOf("String"),
+				"Setting3": RawItemOf(false),
 			}),
 			want: NewSnapshot(map[Key]RawItem{
-				"Setting1": {value: 123},
-				"Setting2": {value: "String"},
-				"Setting3": {value: false},
+				"Setting1": RawItemOf(123),
+				"Setting2": RawItemOf("String"),
+				"Setting3": RawItemOf(false),
 			}),
 		},
 		{
 			name: "second-nil",
 			s1: NewSnapshot(map[Key]RawItem{
-				"Setting1": {value: 123},
-				"Setting2": {value: "String"},
-				"Setting3": {value: true},
+				"Setting1": RawItemOf(123),
+				"Setting2": RawItemOf("String"),
+				"Setting3": RawItemOf(true),
 			}),
 			s2: nil,
 			want: NewSnapshot(map[Key]RawItem{
-				"Setting1": {value: 123},
-				"Setting2": {value: "String"},
-				"Setting3": {value: true},
+				"Setting1": RawItemOf(123),
+				"Setting2": RawItemOf("String"),
+				"Setting3": RawItemOf(true),
 			}),
 		},
 		{
 			name: "second-empty",
 			s1: NewSnapshot(map[Key]RawItem{
-				"Setting1": {value: 123},
-				"Setting2": {value: "String"},
-				"Setting3": {value: false},
+				"Setting1": RawItemOf(123),
+				"Setting2": RawItemOf("String"),
+				"Setting3": RawItemOf(false),
 			}),
 			s2: NewSnapshot(map[Key]RawItem{}),
 			want: NewSnapshot(map[Key]RawItem{
-				"Setting1": {value: 123},
-				"Setting2": {value: "String"},
-				"Setting3": {value: false},
+				"Setting1": RawItemOf(123),
+				"Setting2": RawItemOf("String"),
+				"Setting3": RawItemOf(false),
 			}),
 		},
 		{
 			name: "no-conflicts",
 			s1: NewSnapshot(map[Key]RawItem{
-				"Setting1": {value: 123},
-				"Setting2": {value: "String"},
-				"Setting3": {value: false},
+				"Setting1": RawItemOf(123),
+				"Setting2": RawItemOf("String"),
+				"Setting3": RawItemOf(false),
 			}),
 			s2: NewSnapshot(map[Key]RawItem{
-				"Setting4": {value: 2 * time.Hour},
-				"Setting5": {value: VisibleByPolicy},
-				"Setting6": {value: ShowChoiceByPolicy},
+				"Setting4": RawItemOf(2 * time.Hour),
+				"Setting5": RawItemOf(VisibleByPolicy),
+				"Setting6": RawItemOf(ShowChoiceByPolicy),
 			}),
 			want: NewSnapshot(map[Key]RawItem{
-				"Setting1": {value: 123},
-				"Setting2": {value: "String"},
-				"Setting3": {value: false},
-				"Setting4": {value: 2 * time.Hour},
-				"Setting5": {value: VisibleByPolicy},
-				"Setting6": {value: ShowChoiceByPolicy},
+				"Setting1": RawItemOf(123),
+				"Setting2": RawItemOf("String"),
+				"Setting3": RawItemOf(false),
+				"Setting4": RawItemOf(2 * time.Hour),
+				"Setting5": RawItemOf(VisibleByPolicy),
+				"Setting6": RawItemOf(ShowChoiceByPolicy),
 			}),
 		},
 		{
 			name: "with-conflicts",
 			s1: NewSnapshot(map[Key]RawItem{
-				"Setting1": {value: 123},
-				"Setting2": {value: "String"},
-				"Setting3": {value: true},
+				"Setting1": RawItemOf(123),
+				"Setting2": RawItemOf("String"),
+				"Setting3": RawItemOf(true),
 			}),
 			s2: NewSnapshot(map[Key]RawItem{
-				"Setting1": {value: 456},
-				"Setting3": {value: false},
-				"Setting4": {value: 2 * time.Hour},
+				"Setting1": RawItemOf(456),
+				"Setting3": RawItemOf(false),
+				"Setting4": RawItemOf(2 * time.Hour),
 			}),
 			want: NewSnapshot(map[Key]RawItem{
-				"Setting1": {value: 456},
-				"Setting2": {value: "String"},
-				"Setting3": {value: false},
-				"Setting4": {value: 2 * time.Hour},
+				"Setting1": RawItemOf(456),
+				"Setting2": RawItemOf("String"),
+				"Setting3": RawItemOf(false),
+				"Setting4": RawItemOf(2 * time.Hour),
 			}),
 		},
 		{
 			name: "with-scope-first-wins",
 			s1: NewSnapshot(map[Key]RawItem{
-				"Setting1": {value: 123},
-				"Setting2": {value: "String"},
-				"Setting3": {value: true},
+				"Setting1": RawItemOf(123),
+				"Setting2": RawItemOf("String"),
+				"Setting3": RawItemOf(true),
 			}, DeviceScope),
 			s2: NewSnapshot(map[Key]RawItem{
-				"Setting1": {value: 456},
-				"Setting3": {value: false},
-				"Setting4": {value: 2 * time.Hour},
+				"Setting1": RawItemOf(456),
+				"Setting3": RawItemOf(false),
+				"Setting4": RawItemOf(2 * time.Hour),
 			}, CurrentUserScope),
 			want: NewSnapshot(map[Key]RawItem{
-				"Setting1": {value: 123},
-				"Setting2": {value: "String"},
-				"Setting3": {value: true},
-				"Setting4": {value: 2 * time.Hour},
+				"Setting1": RawItemOf(123),
+				"Setting2": RawItemOf("String"),
+				"Setting3": RawItemOf(true),
+				"Setting4": RawItemOf(2 * time.Hour),
 			}, CurrentUserScope),
 		},
 		{
 			name: "with-scope-second-wins",
 			s1: NewSnapshot(map[Key]RawItem{
-				"Setting1": {value: 123},
-				"Setting2": {value: "String"},
-				"Setting3": {value: true},
+				"Setting1": RawItemOf(123),
+				"Setting2": RawItemOf("String"),
+				"Setting3": RawItemOf(true),
 			}, CurrentUserScope),
 			s2: NewSnapshot(map[Key]RawItem{
-				"Setting1": {value: 456},
-				"Setting3": {value: false},
-				"Setting4": {value: 2 * time.Hour},
+				"Setting1": RawItemOf(456),
+				"Setting3": RawItemOf(false),
+				"Setting4": RawItemOf(2 * time.Hour),
 			}, DeviceScope),
 			want: NewSnapshot(map[Key]RawItem{
-				"Setting1": {value: 456},
-				"Setting2": {value: "String"},
-				"Setting3": {value: false},
-				"Setting4": {value: 2 * time.Hour},
+				"Setting1": RawItemOf(456),
+				"Setting2": RawItemOf("String"),
+				"Setting3": RawItemOf(false),
+				"Setting4": RawItemOf(2 * time.Hour),
 			}, CurrentUserScope),
 		},
 		{
@@ -170,28 +170,27 @@ func TestMergeSnapshots(t *testing.T) {
 			name: "with-scope-first-empty",
 			s1:   NewSnapshot(map[Key]RawItem{}, CurrentUserScope),
 			s2: NewSnapshot(map[Key]RawItem{
-				"Setting1": {value: 123},
-				"Setting2": {value: "String"},
-				"Setting3": {value: true}},
-				DeviceScope, NewNamedOrigin("TestPolicy", DeviceScope)),
+				"Setting1": RawItemOf(123),
+				"Setting2": RawItemOf("String"),
+				"Setting3": RawItemOf(true)}, DeviceScope, NewNamedOrigin("TestPolicy", DeviceScope)),
 			want: NewSnapshot(map[Key]RawItem{
-				"Setting1": {value: 123},
-				"Setting2": {value: "String"},
-				"Setting3": {value: true},
+				"Setting1": RawItemOf(123),
+				"Setting2": RawItemOf("String"),
+				"Setting3": RawItemOf(true),
 			}, CurrentUserScope, NewNamedOrigin("TestPolicy", DeviceScope)),
 		},
 		{
 			name: "with-scope-second-empty",
 			s1: NewSnapshot(map[Key]RawItem{
-				"Setting1": {value: 123},
-				"Setting2": {value: "String"},
-				"Setting3": {value: true},
+				"Setting1": RawItemOf(123),
+				"Setting2": RawItemOf("String"),
+				"Setting3": RawItemOf(true),
 			}, CurrentUserScope),
 			s2: NewSnapshot(map[Key]RawItem{}),
 			want: NewSnapshot(map[Key]RawItem{
-				"Setting1": {value: 123},
-				"Setting2": {value: "String"},
-				"Setting3": {value: true},
+				"Setting1": RawItemOf(123),
+				"Setting2": RawItemOf("String"),
+				"Setting3": RawItemOf(true),
 			}, CurrentUserScope),
 		},
 	}
@@ -244,9 +243,9 @@ func TestSnapshotEqual(t *testing.T) {
 			name: "first-nil",
 			s1:   nil,
 			s2: NewSnapshot(map[Key]RawItem{
-				"Setting1": {value: 123},
-				"Setting2": {value: "String"},
-				"Setting3": {value: false},
+				"Setting1": RawItemOf(123),
+				"Setting2": RawItemOf("String"),
+				"Setting3": RawItemOf(false),
 			}),
 			wantEqual:      false,
 			wantEqualItems: false,
@@ -255,9 +254,9 @@ func TestSnapshotEqual(t *testing.T) {
 			name: "first-empty",
 			s1:   NewSnapshot(map[Key]RawItem{}),
 			s2: NewSnapshot(map[Key]RawItem{
-				"Setting1": {value: 123},
-				"Setting2": {value: "String"},
-				"Setting3": {value: false},
+				"Setting1": RawItemOf(123),
+				"Setting2": RawItemOf("String"),
+				"Setting3": RawItemOf(false),
 			}),
 			wantEqual:      false,
 			wantEqualItems: false,
@@ -265,9 +264,9 @@ func TestSnapshotEqual(t *testing.T) {
 		{
 			name: "second-nil",
 			s1: NewSnapshot(map[Key]RawItem{
-				"Setting1": {value: 123},
-				"Setting2": {value: "String"},
-				"Setting3": {value: true},
+				"Setting1": RawItemOf(123),
+				"Setting2": RawItemOf("String"),
+				"Setting3": RawItemOf(true),
 			}),
 			s2:             nil,
 			wantEqual:      false,
@@ -276,9 +275,9 @@ func TestSnapshotEqual(t *testing.T) {
 		{
 			name: "second-empty",
 			s1: NewSnapshot(map[Key]RawItem{
-				"Setting1": {value: 123},
-				"Setting2": {value: "String"},
-				"Setting3": {value: false},
+				"Setting1": RawItemOf(123),
+				"Setting2": RawItemOf("String"),
+				"Setting3": RawItemOf(false),
 			}),
 			s2:             NewSnapshot(map[Key]RawItem{}),
 			wantEqual:      false,
@@ -287,14 +286,14 @@ func TestSnapshotEqual(t *testing.T) {
 		{
 			name: "same-items-same-order-no-scope",
 			s1: NewSnapshot(map[Key]RawItem{
-				"Setting1": {value: 123},
-				"Setting2": {value: "String"},
-				"Setting3": {value: false},
+				"Setting1": RawItemOf(123),
+				"Setting2": RawItemOf("String"),
+				"Setting3": RawItemOf(false),
 			}),
 			s2: NewSnapshot(map[Key]RawItem{
-				"Setting1": {value: 123},
-				"Setting2": {value: "String"},
-				"Setting3": {value: false},
+				"Setting1": RawItemOf(123),
+				"Setting2": RawItemOf("String"),
+				"Setting3": RawItemOf(false),
 			}),
 			wantEqual:      true,
 			wantEqualItems: true,
@@ -302,14 +301,14 @@ func TestSnapshotEqual(t *testing.T) {
 		{
 			name: "same-items-same-order-same-scope",
 			s1: NewSnapshot(map[Key]RawItem{
-				"Setting1": {value: 123},
-				"Setting2": {value: "String"},
-				"Setting3": {value: false},
+				"Setting1": RawItemOf(123),
+				"Setting2": RawItemOf("String"),
+				"Setting3": RawItemOf(false),
 			}, DeviceScope),
 			s2: NewSnapshot(map[Key]RawItem{
-				"Setting1": {value: 123},
-				"Setting2": {value: "String"},
-				"Setting3": {value: false},
+				"Setting1": RawItemOf(123),
+				"Setting2": RawItemOf("String"),
+				"Setting3": RawItemOf(false),
 			}, DeviceScope),
 			wantEqual:      true,
 			wantEqualItems: true,
@@ -317,14 +316,14 @@ func TestSnapshotEqual(t *testing.T) {
 		{
 			name: "same-items-different-order-same-scope",
 			s1: NewSnapshot(map[Key]RawItem{
-				"Setting1": {value: 123},
-				"Setting2": {value: "String"},
-				"Setting3": {value: false},
+				"Setting1": RawItemOf(123),
+				"Setting2": RawItemOf("String"),
+				"Setting3": RawItemOf(false),
 			}, DeviceScope),
 			s2: NewSnapshot(map[Key]RawItem{
-				"Setting3": {value: false},
-				"Setting1": {value: 123},
-				"Setting2": {value: "String"},
+				"Setting3": RawItemOf(false),
+				"Setting1": RawItemOf(123),
+				"Setting2": RawItemOf("String"),
 			}, DeviceScope),
 			wantEqual:      true,
 			wantEqualItems: true,
@@ -332,14 +331,14 @@ func TestSnapshotEqual(t *testing.T) {
 		{
 			name: "same-items-same-order-different-scope",
 			s1: NewSnapshot(map[Key]RawItem{
-				"Setting1": {value: 123},
-				"Setting2": {value: "String"},
-				"Setting3": {value: false},
+				"Setting1": RawItemOf(123),
+				"Setting2": RawItemOf("String"),
+				"Setting3": RawItemOf(false),
 			}, DeviceScope),
 			s2: NewSnapshot(map[Key]RawItem{
-				"Setting1": {value: 123},
-				"Setting2": {value: "String"},
-				"Setting3": {value: false},
+				"Setting1": RawItemOf(123),
+				"Setting2": RawItemOf("String"),
+				"Setting3": RawItemOf(false),
 			}, CurrentUserScope),
 			wantEqual:      false,
 			wantEqualItems: true,
@@ -347,14 +346,14 @@ func TestSnapshotEqual(t *testing.T) {
 		{
 			name: "different-items-same-scope",
 			s1: NewSnapshot(map[Key]RawItem{
-				"Setting1": {value: 123},
-				"Setting2": {value: "String"},
-				"Setting3": {value: false},
+				"Setting1": RawItemOf(123),
+				"Setting2": RawItemOf("String"),
+				"Setting3": RawItemOf(false),
 			}, DeviceScope),
 			s2: NewSnapshot(map[Key]RawItem{
-				"Setting4": {value: 2 * time.Hour},
-				"Setting5": {value: VisibleByPolicy},
-				"Setting6": {value: ShowChoiceByPolicy},
+				"Setting4": RawItemOf(2 * time.Hour),
+				"Setting5": RawItemOf(VisibleByPolicy),
+				"Setting6": RawItemOf(ShowChoiceByPolicy),
 			}, DeviceScope),
 			wantEqual:      false,
 			wantEqualItems: false,
@@ -401,9 +400,9 @@ func TestSnapshotString(t *testing.T) {
 		{
 			name: "non-empty",
 			snapshot: NewSnapshot(map[Key]RawItem{
-				"Setting1": {value: 2 * time.Hour},
-				"Setting2": {value: VisibleByPolicy},
-				"Setting3": {value: ShowChoiceByPolicy},
+				"Setting1": RawItemOf(2 * time.Hour),
+				"Setting2": RawItemOf(VisibleByPolicy),
+				"Setting3": RawItemOf(ShowChoiceByPolicy),
 			}, NewNamedOrigin("Test Policy", DeviceScope)),
 			wantString: `{Test Policy (Device)}
 Setting1 = 2h0m0s
@@ -413,14 +412,14 @@ Setting3 = user-decides`,
 		{
 			name: "non-empty-with-item-origin",
 			snapshot: NewSnapshot(map[Key]RawItem{
-				"Setting1": {value: 42, origin: NewNamedOrigin("Test Policy", DeviceScope)},
+				"Setting1": RawItemWith(42, nil, NewNamedOrigin("Test Policy", DeviceScope)),
 			}),
 			wantString: `Setting1 = 42 - {Test Policy (Device)}`,
 		},
 		{
 			name: "non-empty-with-item-error",
 			snapshot: NewSnapshot(map[Key]RawItem{
-				"Setting1": {err: NewErrorText("bang!")},
+				"Setting1": RawItemWith(nil, NewErrorText("bang!"), nil),
 			}),
 			wantString: `Setting1 = Error{"bang!"}`,
 		},