From f26cbc7db2a51748aa3106c718256a881d063f8c Mon Sep 17 00:00:00 2001 From: Jamie Tanna Date: Tue, 9 Jan 2024 11:05:35 +0000 Subject: [PATCH] Add tests for `nullable` type We have spun out a separate package, `oapi-codegen/nullable` as a step towards deepmap/oapi-codegen#1039. Until we have implemented #27, we cannot add an explicit type alias in this package, so we can at least add some tests to cover additional functionality and expectations that the package should have when interplaying with `oapi-codegen`. Co-authored-by: Sebastien Guilloux Co-authored-by: Ashutosh Kumar --- go.mod | 1 + go.sum | 2 + types/nullable_test.go | 444 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 447 insertions(+) create mode 100644 types/nullable_test.go diff --git a/go.mod b/go.mod index 8bcdd587..209b3f81 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/google/uuid v1.5.0 github.com/kataras/iris/v12 v12.2.6-0.20230908161203-24ba4e8933b9 github.com/labstack/echo/v4 v4.11.4 + github.com/oapi-codegen/nullable v1.0.0 github.com/stretchr/testify v1.8.4 ) diff --git a/go.sum b/go.sum index fa19a22a..b3711a89 100644 --- a/go.sum +++ b/go.sum @@ -122,6 +122,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/oapi-codegen/nullable v1.0.0 h1:FjV9h/GLxYxc3wSSFUafVMvi0lhrpvUcCbEkE04y0fw= +github.com/oapi-codegen/nullable v1.0.0/go.mod h1:KUZ3vUzkmEKY90ksAmit2+5juDIhIZhfDl+0PwOQlFY= github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0= github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= diff --git a/types/nullable_test.go b/types/nullable_test.go new file mode 100644 index 00000000..90d969a9 --- /dev/null +++ b/types/nullable_test.go @@ -0,0 +1,444 @@ +package types + +import ( + "encoding/json" + "testing" + + "github.com/oapi-codegen/nullable" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type SimpleStringNullableRequired struct { + // A required field must be `nullable.Nullable` + Name nullable.Nullable[string] `json:"name"` +} + +func TestSimpleStringNullableRequired(t *testing.T) { + type testCase struct { + name string + jsonInput []byte + wantNull bool + } + tests := []testCase{ + { + name: "simple object: set name to some non null value", + jsonInput: []byte(`{"name":"yolo"}`), + wantNull: false, + }, + + { + name: "simple object: set name to empty string value", + jsonInput: []byte(`{"name":""}`), + wantNull: false, + }, + + { + name: "simple object: set name to null value", + jsonInput: []byte(`{"name":null}`), + wantNull: true, + }, + + { + // For Nullable and required field there will not be a case when it is not + // provided in json payload but this test case exists just to explain + // the behaviour + name: "simple object: do not provide name in json data", + jsonInput: []byte(`{}`), + wantNull: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var obj SimpleStringNullableRequired + err := json.Unmarshal(tt.jsonInput, &obj) + require.NoError(t, err) + assert.Equalf(t, tt.wantNull, obj.Name.IsNull(), "IsNull()") + }) + } +} + +type SimpleStringNullableOptional struct { + // An optional field must be `nullable.Nullable` and have the JSON tag `omitempty` + Name nullable.Nullable[string] `json:"name,omitempty"` +} + +func TestSimpleStringNullableOptional(t *testing.T) { + type testCase struct { + name string + jsonInput []byte + wantNull bool + wantSpecified bool + } + tests := []testCase{ + { + name: "simple object: set name to some non null value", + jsonInput: []byte(`{"name":"yolo"}`), + wantNull: false, + wantSpecified: true, + }, + + { + name: "simple object: set name to empty string value", + jsonInput: []byte(`{"name":""}`), + wantNull: false, + wantSpecified: true, + }, + + { + name: "simple object: set name to null value", + jsonInput: []byte(`{"name":null}`), + wantNull: true, + wantSpecified: true, + }, + + { + name: "simple object: do not provide name in json data", + jsonInput: []byte(`{}`), + wantNull: false, + wantSpecified: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var obj SimpleStringNullableOptional + err := json.Unmarshal(tt.jsonInput, &obj) + require.NoError(t, err) + assert.Equalf(t, tt.wantNull, obj.Name.IsNull(), "IsNull()") + assert.Equalf(t, tt.wantSpecified, obj.Name.IsSpecified(), "IsSpecified") + }) + } +} + +type SimpleIntNullableRequired struct { + // Nullable type should be used for `nullable and required` fields. + ReplicaCount nullable.Nullable[int] `json:"replica_count"` +} + +func TestSimpleIntNullableRequired(t *testing.T) { + type testCase struct { + name string + jsonInput []byte + wantNull bool + } + tests := []testCase{ + { + name: "simple object: set name to some non null value", + jsonInput: []byte(`{"replica_count":1}`), + wantNull: false, + }, + + { + name: "simple object: set name to empty value", + jsonInput: []byte(`{"replica_count":0}`), + wantNull: false, + }, + + { + name: "simple object: set name to null value", + jsonInput: []byte(`{"replica_count":null}`), + wantNull: true, + }, + + { + // For Nullable and required field there will not be a case when it is not + // provided in json payload but this test case exists just to explain + // the behaviour + name: "simple object: do not provide name in json data", + jsonInput: []byte(`{}`), + wantNull: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var obj SimpleIntNullableRequired + err := json.Unmarshal(tt.jsonInput, &obj) + require.NoError(t, err) + assert.Equalf(t, tt.wantNull, obj.ReplicaCount.IsNull(), "IsNull()") + }) + } +} + +type SimpleIntNullableOptional struct { + ReplicaCount nullable.Nullable[int] `json:"replica_count,omitempty"` +} + +func TestSimpleIntNullableOptional(t *testing.T) { + type testCase struct { + name string + jsonInput []byte + wantNull bool + wantSpecified bool + } + tests := []testCase{ + { + name: "simple object: set name to some non null value", + jsonInput: []byte(`{"replica_count":1}`), + wantNull: false, + wantSpecified: true, + }, + + { + name: "simple object: set name to empty value", + jsonInput: []byte(`{"replica_count":0}`), + wantNull: false, + wantSpecified: true, + }, + + { + name: "simple object: set name to null value", + jsonInput: []byte(`{"replica_count":null}`), + wantNull: true, + wantSpecified: true, + }, + + { + name: "simple object: do not provide name in json data", + jsonInput: []byte(`{}`), + wantNull: false, + wantSpecified: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var obj SimpleIntNullableOptional + err := json.Unmarshal(tt.jsonInput, &obj) + require.NoError(t, err) + + assert.Equalf(t, tt.wantNull, obj.ReplicaCount.IsNull(), "IsNull()") + assert.Equalf(t, tt.wantSpecified, obj.ReplicaCount.IsSpecified(), "IsSpecified") + }) + } +} + +func TestNullableOptional_MarshalJSON(t *testing.T) { + type testCase struct { + name string + obj SimpleIntNullableOptional + want []byte + } + tests := []testCase{ + { + // When object is not set, and it is an optional type, it does not marshal + name: "when obj is explicitly not set", + want: []byte(`{}`), + }, + { + name: "when obj is explicitly set to a specific value", + obj: SimpleIntNullableOptional{ + ReplicaCount: nullable.NewNullableWithValue(5), + }, + want: []byte(`{"replica_count":5}`), + }, + { + name: "when obj is explicitly set to zero value", + obj: SimpleIntNullableOptional{ + ReplicaCount: nullable.NewNullableWithValue(0), + }, + want: []byte(`{"replica_count":0}`), + }, + { + name: "when obj is explicitly set to null value", + obj: SimpleIntNullableOptional{ + ReplicaCount: nullable.NewNullNullable[int](), + }, + want: []byte(`{"replica_count":null}`), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := json.Marshal(tt.obj) + require.NoError(t, err) + assert.Equalf(t, tt.want, got, "MarshalJSON()") + }) + } +} + +func TestNullableOptional_UnmarshalJSON(t *testing.T) { + type testCase struct { + name string + json []byte + assert func(t *testing.T, obj SimpleIntNullableOptional) + } + tests := []testCase{ + { + name: "when not provided", + json: []byte(`{}`), + assert: func(t *testing.T, obj SimpleIntNullableOptional) { + t.Helper() + + assert.Falsef(t, obj.ReplicaCount.IsSpecified(), "replica count should not be specified") + assert.Falsef(t, obj.ReplicaCount.IsNull(), "replica count should not be null") + }, + }, + + { + name: "when explicitly set to zero value", + json: []byte(`{"replica_count":0}`), + assert: func(t *testing.T, obj SimpleIntNullableOptional) { + t.Helper() + + assert.Truef(t, obj.ReplicaCount.IsSpecified(), "replica count should be specified") + assert.Falsef(t, obj.ReplicaCount.IsNull(), "replica count should not be null") + + actual, err := obj.ReplicaCount.Get() + require.NoError(t, err) + + assert.Equalf(t, 0, actual, "replica count value should be 0") + }, + }, + + { + name: "when explicitly set to null value", + json: []byte(`{"replica_count":null}`), + assert: func(t *testing.T, obj SimpleIntNullableOptional) { + t.Helper() + + assert.Truef(t, obj.ReplicaCount.IsSpecified(), "replica count should be set") + assert.Truef(t, obj.ReplicaCount.IsNull(), "replica count should be null") + }, + }, + + { + name: "when explicitly set to a specific value", + json: []byte(`{"replica_count":5}`), + assert: func(t *testing.T, obj SimpleIntNullableOptional) { + t.Helper() + + assert.Truef(t, obj.ReplicaCount.IsSpecified(), "replica count should not be null") + assert.Falsef(t, obj.ReplicaCount.IsNull(), "replica count should not be null") + + actual, err := obj.ReplicaCount.Get() + require.NoError(t, err) + + assert.Equalf(t, 5, actual, "replica count value should be 5") + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var obj SimpleIntNullableOptional + err := json.Unmarshal(tt.json, &obj) + require.NoError(t, err) + tt.assert(t, obj) + }) + } +} + +func TestNullableRequired_MarshalJSON(t *testing.T) { + type testCase struct { + name string + obj SimpleIntNullableRequired + want []byte + } + tests := []testCase{ + { + // When object is not set ( not provided in json ) + // it marshals to zero value. + // For Nullable and required field there will not be a case when it is not + // provided in json payload but this test case exists just to explain + // the behaviour + name: "when obj is explicitly not set", + want: []byte(`{"replica_count":0}`), + }, + { + name: "when obj is explicitly set to a specific value", + obj: SimpleIntNullableRequired{ + ReplicaCount: nullable.NewNullableWithValue(5), + }, + want: []byte(`{"replica_count":5}`), + }, + { + name: "when obj is explicitly set to zero value", + obj: SimpleIntNullableRequired{ + ReplicaCount: nullable.NewNullableWithValue(0), + }, + want: []byte(`{"replica_count":0}`), + }, + { + name: "when obj is explicitly set to null value", + obj: SimpleIntNullableRequired{ + ReplicaCount: nullable.NewNullNullable[int](), + }, + want: []byte(`{"replica_count":null}`), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := json.Marshal(tt.obj) + require.NoError(t, err) + assert.Equalf(t, tt.want, got, "MarshalJSON()") + }) + } +} + +func TestNullableRequired_UnmarshalJSON(t *testing.T) { + type testCase struct { + name string + json []byte + assert func(t *testing.T, obj SimpleIntNullableRequired) + } + tests := []testCase{ + { + // For Nullable and required field there will not be a case when it is not + // provided in json payload but this test case exists just to explain + // the behaviour + name: "when not provided", + json: []byte(`{}`), + assert: func(t *testing.T, obj SimpleIntNullableRequired) { + t.Helper() + + assert.Falsef(t, obj.ReplicaCount.IsNull(), "replica count should not be null") + }, + }, + + { + name: "when explicitly set to zero value", + json: []byte(`{"replica_count":0}`), + assert: func(t *testing.T, obj SimpleIntNullableRequired) { + t.Helper() + + assert.Falsef(t, obj.ReplicaCount.IsNull(), "replica count should not be null") + + actual, err := obj.ReplicaCount.Get() + require.NoError(t, err) + + assert.Equalf(t, 0, actual, "replica count value should be 0") + }, + }, + + { + name: "when explicitly set to null value", + json: []byte(`{"replica_count":null}`), + assert: func(t *testing.T, obj SimpleIntNullableRequired) { + t.Helper() + + assert.Truef(t, obj.ReplicaCount.IsNull(), "replica count should be null") + }, + }, + + { + name: "when explicitly set to a specific value", + json: []byte(`{"replica_count":5}`), + assert: func(t *testing.T, obj SimpleIntNullableRequired) { + t.Helper() + + assert.Falsef(t, obj.ReplicaCount.IsNull(), "replica count should not be null") + + actual, err := obj.ReplicaCount.Get() + require.NoError(t, err) + + assert.Equalf(t, 5, actual, "replica count value should be 5") + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var obj SimpleIntNullableRequired + err := json.Unmarshal(tt.json, &obj) + require.NoError(t, err) + + tt.assert(t, obj) + }) + } +}