Skip to content

Commit d9e9e03

Browse files
committed
Polish
1 parent 47c69ac commit d9e9e03

4 files changed

Lines changed: 83 additions & 2 deletions

File tree

client/db.go

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -442,8 +442,20 @@ type GQLExtensions struct {
442442
//
443443
// An empty value must be left out of the response, not sent as an empty object.
444444
// Sending `"extensions":{}` would change the shape of every response.
445+
//
446+
// This asks the encoder rather than checking each field, so a field added later is
447+
// covered without editing here. Checking fields by hand means the next field added is
448+
// silently dropped from every response until someone remembers to update this.
449+
//
450+
// A value the encoder cannot handle is treated as empty. The warning is then missing,
451+
// which is better than failing the whole response over a diagnostic.
445452
func (e *GQLExtensions) IsEmpty() bool {
446-
return e == nil || len(e.Warnings) == 0
453+
if e == nil {
454+
return true
455+
}
456+
457+
data, err := json.Marshal(e)
458+
return err != nil || string(data) == "{}"
447459
}
448460

449461
// GQLWarning describes something that happened during a request that worked. It is not
@@ -459,6 +471,13 @@ type GQLWarning struct {
459471
Message string `json:"message"`
460472

461473
// Detail holds values belonging to this warning. Optional.
474+
//
475+
// Everything here is sent to the caller and may also be logged, so do not put
476+
// secrets, credentials or identity material in it.
477+
//
478+
// Take care with counts and identifiers drawn from documents the caller is not
479+
// allowed to read. Saying how many documents were examined can tell the caller
480+
// about documents that access control hid from them.
462481
Detail map[string]any `json:"detail,omitempty"`
463482
}
464483

@@ -491,6 +510,11 @@ func (res *GQLResult) UnmarshalJSON(data []byte) error {
491510
}
492511
res.Data = out.Data
493512
res.Extensions = out.Extensions
513+
// A peer may send `"extensions":{}`, which decodes to a non nil empty value. Callers
514+
// are told the field is nil when there is nothing to report, so make that true.
515+
if res.Extensions.IsEmpty() {
516+
res.Extensions = nil
517+
}
494518
res.Errors = make([]error, len(out.Errors))
495519
for i, e := range out.Errors {
496520
res.Errors[i] = ReviveError(e.Message)

client/db_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,3 +139,42 @@ func TestGQLResultUnmarshal_WithoutExtensions_LeavesNil(t *testing.T) {
139139
require.Nil(t, output.Extensions)
140140
require.True(t, output.Extensions.IsEmpty())
141141
}
142+
143+
// The empty non nil slice is the case a field by field check gets wrong: the slice is
144+
// not the zero value, but it still encodes to nothing.
145+
func TestGQLExtensionsIsEmpty_WithEmptyWarningSlice_IsEmpty(t *testing.T) {
146+
extensions := &GQLExtensions{Warnings: []GQLWarning{}}
147+
148+
require.True(t, extensions.IsEmpty())
149+
}
150+
151+
func TestGQLExtensionsIsEmpty_WithNilReceiver_IsEmpty(t *testing.T) {
152+
var extensions *GQLExtensions
153+
154+
require.True(t, extensions.IsEmpty())
155+
}
156+
157+
func TestGQLExtensionsIsEmpty_WithAWarning_IsNotEmpty(t *testing.T) {
158+
extensions := &GQLExtensions{Warnings: []GQLWarning{{Code: "test_warning"}}}
159+
160+
require.False(t, extensions.IsEmpty())
161+
}
162+
163+
// A peer may send an empty extensions object. Callers are told the field is nil when
164+
// there is nothing to report, so decoding must make that true rather than hand back a
165+
// non nil value with nothing in it.
166+
func TestGQLResultUnmarshal_WithEmptyExtensions_LeavesNil(t *testing.T) {
167+
var output GQLResult
168+
err := json.Unmarshal([]byte(`{"data": null, "extensions": {}}`), &output)
169+
require.NoError(t, err)
170+
171+
require.Nil(t, output.Extensions)
172+
}
173+
174+
func TestGQLResultUnmarshal_WithOnlyUnknownExtensionKeys_LeavesNil(t *testing.T) {
175+
var output GQLResult
176+
err := json.Unmarshal([]byte(`{"data": null, "extensions": {"unknownKey": 1}}`), &output)
177+
require.NoError(t, err)
178+
179+
require.Nil(t, output.Extensions)
180+
}

internal/extensions/context.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package extensions
1818

1919
import (
2020
"context"
21+
"slices"
2122
"sync"
2223

2324
"github.com/sourcenetwork/defradb/client"
@@ -58,6 +59,9 @@ func AddWarning(ctx context.Context, warning client.GQLWarning) {
5859
}
5960

6061
// Collect returns the warnings recorded on the context, or nil if there are none.
62+
//
63+
// The returned slice is a copy. Handing out the accumulator's own slice would let a
64+
// later AddWarning change a result the caller is already holding.
6165
func Collect(ctx context.Context) *client.GQLExtensions {
6266
acc, ok := ctx.Value(accumulatorContextKey{}).(*accumulator)
6367
if !ok {
@@ -70,5 +74,5 @@ func Collect(ctx context.Context) *client.GQLExtensions {
7074
return nil
7175
}
7276

73-
return &client.GQLExtensions{Warnings: acc.warnings}
77+
return &client.GQLExtensions{Warnings: slices.Clone(acc.warnings)}
7478
}

internal/extensions/context_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,3 +82,17 @@ func TestAddWarning_FromMultipleGoroutines_RecordsAll(t *testing.T) {
8282

8383
require.Len(t, Collect(ctx).Warnings, 50)
8484
}
85+
86+
func TestCollect_ThenAddWarning_DoesNotChangeTheEarlierResult(t *testing.T) {
87+
ctx := WithAccumulator(context.Background())
88+
AddWarning(ctx, client.GQLWarning{Code: "first"})
89+
90+
collected := Collect(ctx)
91+
require.Len(t, collected.Warnings, 1)
92+
93+
AddWarning(ctx, client.GQLWarning{Code: "second"})
94+
95+
require.Len(t, collected.Warnings, 1)
96+
require.Equal(t, "first", collected.Warnings[0].Code)
97+
require.Len(t, Collect(ctx).Warnings, 2)
98+
}

0 commit comments

Comments
 (0)