From d6926857a0dfd7772409c5f98c56e7587d8133ea Mon Sep 17 00:00:00 2001 From: anjankow Date: Mon, 3 Apr 2023 07:10:13 +0000 Subject: [PATCH 1/9] get fixture insertable fields with reflection --- internal/test/fixtures.go | 35 ++-- internal/util/struct.go | 57 +++++++ internal/util/struct_test.go | 149 ++++++++++++++++++ .../TestGetFieldsImplementingSuccess.golden | 68 ++++++++ 4 files changed, 291 insertions(+), 18 deletions(-) create mode 100644 internal/util/struct.go create mode 100644 internal/util/struct_test.go create mode 100644 test/testdata/snapshots/TestGetFieldsImplementingSuccess.golden diff --git a/internal/test/fixtures.go b/internal/test/fixtures.go index 4385bb36..f0408f8d 100644 --- a/internal/test/fixtures.go +++ b/internal/test/fixtures.go @@ -2,9 +2,11 @@ package test import ( "context" + "fmt" "time" "allaboutapps.dev/aw/go-starter/internal/models" + "allaboutapps.dev/aw/go-starter/internal/util" "github.com/volatiletech/null/v8" "github.com/volatiletech/sqlboiler/v4/boil" ) @@ -19,7 +21,8 @@ type Insertable interface { Insert(ctx context.Context, exec boil.ContextExecutor, columns boil.Columns) error } -// FixtureMap represents the main definition which fixtures are available though Fixtures() +// The main definition which fixtures are available through Fixtures(). +// Mind the declaration order! The fields get inserted exactly in the order they are declared. type FixtureMap struct { User1 *models.User User1AppUserProfile *models.AppUserProfile @@ -135,22 +138,18 @@ func Fixtures() FixtureMap { // Inserts defines the order in which the fixtures will be inserted // into the test database func Inserts() []Insertable { - fixtures := Fixtures() - - return []Insertable{ - fixtures.User1, - fixtures.User1AppUserProfile, - fixtures.User1AccessToken1, - fixtures.User1RefreshToken1, - fixtures.User2, - fixtures.User2AppUserProfile, - fixtures.User2AccessToken1, - fixtures.User2RefreshToken1, - fixtures.UserDeactivated, - fixtures.UserDeactivatedAppUserProfile, - fixtures.UserDeactivatedAccessToken1, - fixtures.UserDeactivatedRefreshToken1, - fixtures.User1PushToken, - fixtures.User1PushTokenAPN, + fix := Fixtures() + insertableIfc := (*Insertable)(nil) + insertsAsInterface, err := util.GetFieldsImplementing(&fix, insertableIfc) + if err != nil { + panic(fmt.Errorf("failed to get insertable fixture fields: %w", err)) } + + // TODO: could be improved with generics + inserts := make([]Insertable, 0, len(insertsAsInterface)) + for _, object := range insertsAsInterface { + inserts = append(inserts, object.(Insertable)) + } + + return inserts } diff --git a/internal/util/struct.go b/internal/util/struct.go new file mode 100644 index 00000000..6aec7cc4 --- /dev/null +++ b/internal/util/struct.go @@ -0,0 +1,57 @@ +package util + +import ( + "errors" + "reflect" +) + +// GetFieldsImplementing returns all fields of a struct implementing a certain interface. +// Parameter structPtr must be a pointer to a struct. +// Parameter interfaceObject must be given as a pointer to an interface, +// for example (*Insertable)(nil), where Insertable is an interface name. +func GetFieldsImplementing(structPtr interface{}, interfaceObject interface{}) ([]interface{}, error) { + + // Verify if structPtr is a pointer to a struct + inputParamStructType := reflect.TypeOf(structPtr) + if inputParamStructType == nil || + inputParamStructType.Kind() != reflect.Ptr || + inputParamStructType.Elem().Kind() != reflect.Struct { + return nil, errors.New("invalid input structPtr param: should be a pointer to a struct") + } + + inputParamIfcType := reflect.TypeOf(interfaceObject) + // Verify if interfaceObject is a pointer to an interface + if inputParamIfcType == nil || + inputParamIfcType.Kind() != reflect.Ptr || + inputParamIfcType.Elem().Kind() != reflect.Interface { + + return nil, errors.New("invalid input interfaceObject param: should be a pointer to an interface") + } + + // We need the type, not the pointer to it. + // By using Elem() we can get the value pointed by the pointer. + interfaceType := inputParamIfcType.Elem() + structType := inputParamStructType.Elem() + + structValue := reflect.ValueOf(structPtr).Elem() + + retFields := make([]interface{}, 0) + + // Getting the VisibleFields returns all public fields in the struct + for i, field := range reflect.VisibleFields(structType) { + + // Check the field type, should be a pointer to a struct + if field.Type.Kind() != reflect.Ptr || field.Type.Elem().Kind() != reflect.Struct { + continue + } + + // Check if the field type implements the interface and can be exported. + // Interface() can be called only on exportable fields. + if field.Type.Implements(interfaceType) && field.IsExported() { + // Great, we can add it to the return slice + retFields = append(retFields, structValue.Field(i).Interface()) + } + } + + return retFields, nil +} diff --git a/internal/util/struct_test.go b/internal/util/struct_test.go new file mode 100644 index 00000000..20c3fcdf --- /dev/null +++ b/internal/util/struct_test.go @@ -0,0 +1,149 @@ +package util_test + +import ( + "context" + "testing" + + "allaboutapps.dev/aw/go-starter/internal/models" + "allaboutapps.dev/aw/go-starter/internal/test" + "allaboutapps.dev/aw/go-starter/internal/util" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/volatiletech/sqlboiler/v4/boil" +) + +type insertable interface { + Insert(ctx context.Context, exec boil.ContextExecutor, columns boil.Columns) error +} + +type testStructEmpty struct { +} + +type testStructPrivateFiled struct { + privateUser *models.User +} + +type testStructPrimitives struct { + X int + Y string +} + +type testStructFixture struct { + User1 *models.User + User2 *models.User + User1AppUserProfile *models.AppUserProfile + User1AccessToken1 *models.AccessToken + + X int + Y string + privateUser *models.User +} + +func TestGetFieldsImplementingInvalidInput(t *testing.T) { + + _, err := util.GetFieldsImplementing(nil, nil) + assert.Error(t, err) + + // invalid interfaceObject input param, must be a pointer to an interface + _, err = util.GetFieldsImplementing(&testStructEmpty{}, nil) + assert.Error(t, err) + assert.Contains(t, err.Error(), "interfaceObject") + _, err = util.GetFieldsImplementing(&testStructEmpty{}, testStructEmpty{}) + assert.Error(t, err) + assert.Contains(t, err.Error(), "interfaceObject") + _, err = util.GetFieldsImplementing(&testStructEmpty{}, &testStructEmpty{}) + assert.Error(t, err) + assert.Contains(t, err.Error(), "interfaceObject") + _, err = util.GetFieldsImplementing(&testStructEmpty{}, (insertable)(nil)) + assert.Error(t, err) + assert.Contains(t, err.Error(), "interfaceObject") + + // invalid structPtr input param, must be a pointer to a struct + _, err = util.GetFieldsImplementing(testStructEmpty{}, (*insertable)(nil)) + assert.Error(t, err) + assert.Contains(t, err.Error(), "structPtr") + _, err = util.GetFieldsImplementing((*insertable)(nil), (*insertable)(nil)) + assert.Error(t, err) + assert.Contains(t, err.Error(), "structPtr") + _, err = util.GetFieldsImplementing([]*testStructEmpty{}, (*insertable)(nil)) + assert.Error(t, err) + assert.Contains(t, err.Error(), "structPtr") +} + +func TestGetFieldsImplementingNoFields(t *testing.T) { + // No fields returned from empty structs + structEmptyFields, err := util.GetFieldsImplementing(&testStructEmpty{}, (*insertable)(nil)) + assert.NoError(t, err) + assert.Empty(t, structEmptyFields) + + // No fields returned from structs with only private fields + structPrivate := testStructPrivateFiled{privateUser: &models.User{ID: "bfc9d3be-a13c-4790-befb-573c9a5b11a4"}} + structPrivateFields, err := util.GetFieldsImplementing(&structPrivate, (*insertable)(nil)) + assert.NoError(t, err) + assert.Empty(t, structPrivateFields) + + // No fields returned if struct fields are primitive + structPrimitive := testStructPrimitives{X: 12, Y: "y"} + structPrimitiveFields, err := util.GetFieldsImplementing(&structPrimitive, (*insertable)(nil)) + assert.NoError(t, err) + assert.Empty(t, structPrimitiveFields) + + // No fieds returned if an interface is not matching + type notMatchedInterface interface { + // columns param missing + Insert(ctx context.Context, exec boil.ContextExecutor) error + } + fix := testStructFixture{ + User1: &models.User{ID: "bfc9d3be-a13c-4790-befb-573c9a5b11a4"}, + } + fixFields, err := util.GetFieldsImplementing(&fix, (*notMatchedInterface)(nil)) + assert.NoError(t, err) + assert.Empty(t, fixFields) +} + +func TestGetFieldsImplementingSuccess(t *testing.T) { + // Struct not initialized + // It's a responsibility of a user to make sure that the fields are not nil before using them. + structNotInitialized := testStructFixture{} + structNotInitializedFields, err := util.GetFieldsImplementing(&structNotInitialized, (*insertable)(nil)) + assert.NoError(t, err) + assert.Equal(t, 4, len(structNotInitializedFields)) + for _, f := range structNotInitializedFields { + object, ok := f.(insertable) + require.True(t, ok) + assert.Nil(t, object) + } + + // Struct initialized + fix := testStructFixture{ + privateUser: &models.User{ID: "bfc9d3be-a13c-4790-befb-573c9a5b11a4"}, + User1: &models.User{ID: "9e16c597-2491-45bb-89ca-775b6e07f51d"}, + User2: &models.User{ID: "52028fd6-e299-4d36-8bba-21fe4713ffcd"}, + User1AppUserProfile: &models.AppUserProfile{UserID: "9e16c597-2491-45bb-89ca-775b6e07f51d"}, + User1AccessToken1: &models.AccessToken{UserID: "9e16c597-2491-45bb-89ca-775b6e07f51d"}, + X: 12, + Y: "y", + } + + insertableFields, err := util.GetFieldsImplementing(&fix, (*insertable)(nil)) + assert.NoError(t, err) + assert.Equal(t, 4, len(insertableFields)) + test.Snapshoter.Save(t, insertableFields) + + for _, f := range insertableFields { + _, ok := f.(insertable) + require.True(t, ok) + } + + type upsertable interface { + Upsert(ctx context.Context, exec boil.ContextExecutor, updateOnConflict bool, conflictColumns []string, updateColumns, insertColumns boil.Columns) error + } + upsertableFields, err := util.GetFieldsImplementing(&fix, (*upsertable)(nil)) + assert.NoError(t, err) + // there should be equal number of fields implementing Insertable and Upsertable interface + assert.Equal(t, len(insertableFields), len(upsertableFields)) + for _, f := range upsertableFields { + _, ok := f.(upsertable) + require.True(t, ok) + } +} diff --git a/test/testdata/snapshots/TestGetFieldsImplementingSuccess.golden b/test/testdata/snapshots/TestGetFieldsImplementingSuccess.golden new file mode 100644 index 00000000..29d547b0 --- /dev/null +++ b/test/testdata/snapshots/TestGetFieldsImplementingSuccess.golden @@ -0,0 +1,68 @@ +([]interface {}) (len=4) { + (*models.User)({ + ID: (string) (len=36) "9e16c597-2491-45bb-89ca-775b6e07f51d", + Username: (null.String) { + String: (string) "", + Valid: (bool) false + }, + Password: (null.String) { + String: (string) "", + Valid: (bool) false + }, + IsActive: (bool) false, + Scopes: (types.StringArray) , + LastAuthenticatedAt: (null.Time) { + Time: (time.Time) 0001-01-01 00:00:00 +0000 UTC, + Valid: (bool) false + }, + CreatedAt: (time.Time) 0001-01-01 00:00:00 +0000 UTC, + UpdatedAt: (time.Time) 0001-01-01 00:00:00 +0000 UTC, + R: (*models.userR)(), + L: (models.userL) { + } + }), + (*models.User)({ + ID: (string) (len=36) "52028fd6-e299-4d36-8bba-21fe4713ffcd", + Username: (null.String) { + String: (string) "", + Valid: (bool) false + }, + Password: (null.String) { + String: (string) "", + Valid: (bool) false + }, + IsActive: (bool) false, + Scopes: (types.StringArray) , + LastAuthenticatedAt: (null.Time) { + Time: (time.Time) 0001-01-01 00:00:00 +0000 UTC, + Valid: (bool) false + }, + CreatedAt: (time.Time) 0001-01-01 00:00:00 +0000 UTC, + UpdatedAt: (time.Time) 0001-01-01 00:00:00 +0000 UTC, + R: (*models.userR)(), + L: (models.userL) { + } + }), + (*models.AppUserProfile)({ + UserID: (string) (len=36) "9e16c597-2491-45bb-89ca-775b6e07f51d", + LegalAcceptedAt: (null.Time) { + Time: (time.Time) 0001-01-01 00:00:00 +0000 UTC, + Valid: (bool) false + }, + CreatedAt: (time.Time) 0001-01-01 00:00:00 +0000 UTC, + UpdatedAt: (time.Time) 0001-01-01 00:00:00 +0000 UTC, + R: (*models.appUserProfileR)(), + L: (models.appUserProfileL) { + } + }), + (*models.AccessToken)({ + Token: (string) "", + ValidUntil: (time.Time) 0001-01-01 00:00:00 +0000 UTC, + UserID: (string) (len=36) "9e16c597-2491-45bb-89ca-775b6e07f51d", + CreatedAt: (time.Time) 0001-01-01 00:00:00 +0000 UTC, + UpdatedAt: (time.Time) 0001-01-01 00:00:00 +0000 UTC, + R: (*models.accessTokenR)(), + L: (models.accessTokenL) { + } + }) +} From 097b71b058d39fa8310d54e688d09b33c5f032c3 Mon Sep 17 00:00:00 2001 From: Anna Jankowska Date: Mon, 3 Apr 2023 10:58:03 +0200 Subject: [PATCH 2/9] fix trivy CVE-2022-41723 vulnerability --- go.mod | 6 +++--- go.sum | 11 +++++++---- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index 80f0072a..b31b5546 100644 --- a/go.mod +++ b/go.mod @@ -33,8 +33,8 @@ require ( github.com/volatiletech/sqlboiler/v4 v4.13.0 github.com/volatiletech/strmangle v0.0.4 golang.org/x/crypto v0.3.0 - golang.org/x/sys v0.2.0 - golang.org/x/text v0.4.0 + golang.org/x/sys v0.5.0 + golang.org/x/text v0.7.0 google.golang.org/api v0.103.0 ) @@ -107,7 +107,7 @@ require ( github.com/volatiletech/inflect v0.0.1 // indirect go.mongodb.org/mongo-driver v1.11.0 // indirect go.opencensus.io v0.24.0 // indirect - golang.org/x/net v0.2.0 // indirect + golang.org/x/net v0.7.0 // indirect golang.org/x/oauth2 v0.2.0 // indirect golang.org/x/time v0.2.0 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect diff --git a/go.sum b/go.sum index 08a8d2fd..b317ec96 100644 --- a/go.sum +++ b/go.sum @@ -856,8 +856,9 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -971,12 +972,13 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.2.0 h1:z85xZCsEl7bi/KwbNADeBYoOP0++7W1ipu+aGnpwzRM= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -986,8 +988,9 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= From 612f60600212e412370ad9a4631b610feee64dcb Mon Sep 17 00:00:00 2001 From: anjankow Date: Fri, 7 Apr 2023 11:41:06 +0200 Subject: [PATCH 3/9] Use generics --- internal/test/fixtures.go | 8 +------- internal/util/struct.go | 6 +++--- internal/util/struct_test.go | 32 +++++++++++--------------------- 3 files changed, 15 insertions(+), 31 deletions(-) diff --git a/internal/test/fixtures.go b/internal/test/fixtures.go index f0408f8d..e30769ef 100644 --- a/internal/test/fixtures.go +++ b/internal/test/fixtures.go @@ -140,16 +140,10 @@ func Fixtures() FixtureMap { func Inserts() []Insertable { fix := Fixtures() insertableIfc := (*Insertable)(nil) - insertsAsInterface, err := util.GetFieldsImplementing(&fix, insertableIfc) + inserts, err := util.GetFieldsImplementing(&fix, insertableIfc) if err != nil { panic(fmt.Errorf("failed to get insertable fixture fields: %w", err)) } - // TODO: could be improved with generics - inserts := make([]Insertable, 0, len(insertsAsInterface)) - for _, object := range insertsAsInterface { - inserts = append(inserts, object.(Insertable)) - } - return inserts } diff --git a/internal/util/struct.go b/internal/util/struct.go index 6aec7cc4..d0fe4d51 100644 --- a/internal/util/struct.go +++ b/internal/util/struct.go @@ -9,7 +9,7 @@ import ( // Parameter structPtr must be a pointer to a struct. // Parameter interfaceObject must be given as a pointer to an interface, // for example (*Insertable)(nil), where Insertable is an interface name. -func GetFieldsImplementing(structPtr interface{}, interfaceObject interface{}) ([]interface{}, error) { +func GetFieldsImplementing[T any](structPtr interface{}, interfaceObject *T) ([]T, error) { // Verify if structPtr is a pointer to a struct inputParamStructType := reflect.TypeOf(structPtr) @@ -35,7 +35,7 @@ func GetFieldsImplementing(structPtr interface{}, interfaceObject interface{}) ( structValue := reflect.ValueOf(structPtr).Elem() - retFields := make([]interface{}, 0) + retFields := make([]T, 0) // Getting the VisibleFields returns all public fields in the struct for i, field := range reflect.VisibleFields(structType) { @@ -49,7 +49,7 @@ func GetFieldsImplementing(structPtr interface{}, interfaceObject interface{}) ( // Interface() can be called only on exportable fields. if field.Type.Implements(interfaceType) && field.IsExported() { // Great, we can add it to the return slice - retFields = append(retFields, structValue.Field(i).Interface()) + retFields = append(retFields, structValue.Field(i).Interface().(T)) } } diff --git a/internal/util/struct_test.go b/internal/util/struct_test.go index 20c3fcdf..f7485b31 100644 --- a/internal/util/struct_test.go +++ b/internal/util/struct_test.go @@ -5,10 +5,8 @@ import ( "testing" "allaboutapps.dev/aw/go-starter/internal/models" - "allaboutapps.dev/aw/go-starter/internal/test" "allaboutapps.dev/aw/go-starter/internal/util" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" "github.com/volatiletech/sqlboiler/v4/boil" ) @@ -41,20 +39,14 @@ type testStructFixture struct { func TestGetFieldsImplementingInvalidInput(t *testing.T) { - _, err := util.GetFieldsImplementing(nil, nil) - assert.Error(t, err) - // invalid interfaceObject input param, must be a pointer to an interface - _, err = util.GetFieldsImplementing(&testStructEmpty{}, nil) - assert.Error(t, err) - assert.Contains(t, err.Error(), "interfaceObject") - _, err = util.GetFieldsImplementing(&testStructEmpty{}, testStructEmpty{}) - assert.Error(t, err) - assert.Contains(t, err.Error(), "interfaceObject") - _, err = util.GetFieldsImplementing(&testStructEmpty{}, &testStructEmpty{}) + // pointer to a struct + _, err := util.GetFieldsImplementing(&testStructEmpty{}, &testStructEmpty{}) assert.Error(t, err) assert.Contains(t, err.Error(), "interfaceObject") - _, err = util.GetFieldsImplementing(&testStructEmpty{}, (insertable)(nil)) + // pointer to a pointer to an interface + interfaceObjPtr := (*insertable)(nil) + _, err = util.GetFieldsImplementing(&testStructEmpty{}, &interfaceObjPtr) assert.Error(t, err) assert.Contains(t, err.Error(), "interfaceObject") @@ -109,9 +101,8 @@ func TestGetFieldsImplementingSuccess(t *testing.T) { assert.NoError(t, err) assert.Equal(t, 4, len(structNotInitializedFields)) for _, f := range structNotInitializedFields { - object, ok := f.(insertable) - require.True(t, ok) - assert.Nil(t, object) + assert.Nil(t, f) + assert.Implements(t, (*insertable)(nil), f) } // Struct initialized @@ -128,11 +119,10 @@ func TestGetFieldsImplementingSuccess(t *testing.T) { insertableFields, err := util.GetFieldsImplementing(&fix, (*insertable)(nil)) assert.NoError(t, err) assert.Equal(t, 4, len(insertableFields)) - test.Snapshoter.Save(t, insertableFields) for _, f := range insertableFields { - _, ok := f.(insertable) - require.True(t, ok) + assert.NotNil(t, f) + assert.Implements(t, (*insertable)(nil), f) } type upsertable interface { @@ -143,7 +133,7 @@ func TestGetFieldsImplementingSuccess(t *testing.T) { // there should be equal number of fields implementing Insertable and Upsertable interface assert.Equal(t, len(insertableFields), len(upsertableFields)) for _, f := range upsertableFields { - _, ok := f.(upsertable) - require.True(t, ok) + assert.NotNil(t, f) + assert.Implements(t, (*upsertable)(nil), f) } } From b7d1424034c89fa486ddea31f44f73074045013c Mon Sep 17 00:00:00 2001 From: anjankow Date: Fri, 7 Apr 2023 11:41:06 +0200 Subject: [PATCH 4/9] Remove fixture dependency in struct tests --- internal/util/struct.go | 40 ++++++-- internal/util/struct_test.go | 193 +++++++++++++++++++++++------------ 2 files changed, 161 insertions(+), 72 deletions(-) diff --git a/internal/util/struct.go b/internal/util/struct.go index d0fe4d51..5165f8d7 100644 --- a/internal/util/struct.go +++ b/internal/util/struct.go @@ -6,6 +6,8 @@ import ( ) // GetFieldsImplementing returns all fields of a struct implementing a certain interface. +// Returned fields are pointers to a type or interface objects. +// // Parameter structPtr must be a pointer to a struct. // Parameter interfaceObject must be given as a pointer to an interface, // for example (*Insertable)(nil), where Insertable is an interface name. @@ -40,16 +42,40 @@ func GetFieldsImplementing[T any](structPtr interface{}, interfaceObject *T) ([] // Getting the VisibleFields returns all public fields in the struct for i, field := range reflect.VisibleFields(structType) { - // Check the field type, should be a pointer to a struct - if field.Type.Kind() != reflect.Ptr || field.Type.Elem().Kind() != reflect.Struct { + // Check if the field can be exported. + // Interface() can be called only on exportable fields. + if !field.IsExported() { continue } - // Check if the field type implements the interface and can be exported. - // Interface() can be called only on exportable fields. - if field.Type.Implements(interfaceType) && field.IsExported() { - // Great, we can add it to the return slice - retFields = append(retFields, structValue.Field(i).Interface().(T)) + fieldValue := structValue.Field(i) + + // Depending on the field type, different checks apply. + switch field.Type.Kind() { + + case reflect.Pointer: + + // Let's check if it implements the interface. + if field.Type.Implements(interfaceType) { + // Great, we can add it to the return slice + retFields = append(retFields, fieldValue.Interface().(T)) + } + + case reflect.Interface: + // If it's an interface, make sure it's not nil. + if fieldValue.IsNil() { + continue + } + + // Now we can check if it's the same interface. + if field.Type.Implements(interfaceType) { + // Great, we can add it to the return slice + retFields = append(retFields, fieldValue.Interface().(T)) + } + + default: + // We can skip any other cases. + continue } } diff --git a/internal/util/struct_test.go b/internal/util/struct_test.go index f7485b31..eb7a95fd 100644 --- a/internal/util/struct_test.go +++ b/internal/util/struct_test.go @@ -1,139 +1,202 @@ package util_test import ( - "context" + "bytes" + "io" + "net" + "strings" "testing" - "allaboutapps.dev/aw/go-starter/internal/models" "allaboutapps.dev/aw/go-starter/internal/util" + "github.com/go-openapi/swag" "github.com/stretchr/testify/assert" - "github.com/volatiletech/sqlboiler/v4/boil" ) -type insertable interface { - Insert(ctx context.Context, exec boil.ContextExecutor, columns boil.Columns) error +type readInterface interface { + Read(p []byte) (n int, err error) } -type testStructEmpty struct { -} - -type testStructPrivateFiled struct { - privateUser *models.User -} - -type testStructPrimitives struct { - X int - Y string +type writeInterface interface { + WriteTo(w io.Writer) (n int64, err error) } -type testStructFixture struct { - User1 *models.User - User2 *models.User - User1AppUserProfile *models.AppUserProfile - User1AccessToken1 *models.AccessToken +type testStruct struct { + // satisfy only readInterface + LimitedReader *io.LimitedReader + Reader io.Reader - X int - Y string - privateUser *models.User + // satisfy both readInterface and writeInterface + Buffer1 *bytes.Buffer + Buffer2 *bytes.Buffer + NetBuffer *net.Buffers } func TestGetFieldsImplementingInvalidInput(t *testing.T) { - // invalid interfaceObject input param, must be a pointer to an interface - // pointer to a struct + // Invalid interfaceObject input param, must be a pointer to an interface + // Pointer to a struct _, err := util.GetFieldsImplementing(&testStructEmpty{}, &testStructEmpty{}) assert.Error(t, err) assert.Contains(t, err.Error(), "interfaceObject") - // pointer to a pointer to an interface - interfaceObjPtr := (*insertable)(nil) + // Pointer to a pointer to an interface + interfaceObjPtr := (*readInterface)(nil) _, err = util.GetFieldsImplementing(&testStructEmpty{}, &interfaceObjPtr) assert.Error(t, err) assert.Contains(t, err.Error(), "interfaceObject") - // invalid structPtr input param, must be a pointer to a struct - _, err = util.GetFieldsImplementing(testStructEmpty{}, (*insertable)(nil)) + // Invalid structPtr input param, must be a pointer to a struct + _, err = util.GetFieldsImplementing(testStructEmpty{}, (*readInterface)(nil)) assert.Error(t, err) assert.Contains(t, err.Error(), "structPtr") - _, err = util.GetFieldsImplementing((*insertable)(nil), (*insertable)(nil)) + _, err = util.GetFieldsImplementing((*readInterface)(nil), (*readInterface)(nil)) assert.Error(t, err) assert.Contains(t, err.Error(), "structPtr") - _, err = util.GetFieldsImplementing([]*testStructEmpty{}, (*insertable)(nil)) + _, err = util.GetFieldsImplementing([]*testStructEmpty{}, (*readInterface)(nil)) assert.Error(t, err) assert.Contains(t, err.Error(), "structPtr") } func TestGetFieldsImplementingNoFields(t *testing.T) { // No fields returned from empty structs - structEmptyFields, err := util.GetFieldsImplementing(&testStructEmpty{}, (*insertable)(nil)) + structEmptyFields, err := util.GetFieldsImplementing(&testStructEmpty{}, (*readInterface)(nil)) assert.NoError(t, err) assert.Empty(t, structEmptyFields) // No fields returned from structs with only private fields - structPrivate := testStructPrivateFiled{privateUser: &models.User{ID: "bfc9d3be-a13c-4790-befb-573c9a5b11a4"}} - structPrivateFields, err := util.GetFieldsImplementing(&structPrivate, (*insertable)(nil)) + structPrivate := testStructPrivateFiled{privateMember: bytes.NewBufferString("my content")} + structPrivateFields, err := util.GetFieldsImplementing(&structPrivate, (*readInterface)(nil)) assert.NoError(t, err) assert.Empty(t, structPrivateFields) // No fields returned if struct fields are primitive - structPrimitive := testStructPrimitives{X: 12, Y: "y"} - structPrimitiveFields, err := util.GetFieldsImplementing(&structPrimitive, (*insertable)(nil)) + structPrimitive := testStructPrimitives{X: 12, Y: "y", XPtr: swag.Int(15), YPtr: swag.String("YPtr")} + structPrimitiveFields, err := util.GetFieldsImplementing(&structPrimitive, (*readInterface)(nil)) assert.NoError(t, err) assert.Empty(t, structPrimitiveFields) + // No fields returned if struct fields are structs (not pointer to a struct) + structMemberStruct := testStructMemberStruct{Member: *bytes.NewBufferString("my content")} + structMemberStructFields, err := util.GetFieldsImplementing(&structMemberStruct, (*readInterface)(nil)) + assert.NoError(t, err) + assert.Empty(t, structMemberStructFields) + // No fieds returned if an interface is not matching type notMatchedInterface interface { - // columns param missing - Insert(ctx context.Context, exec boil.ContextExecutor) error + Read(p []byte) (n int, err error, additional []string) } - fix := testStructFixture{ - User1: &models.User{ID: "bfc9d3be-a13c-4790-befb-573c9a5b11a4"}, + testStructObj := testStruct{} + testStructFields, err := util.GetFieldsImplementing(&testStructObj, (*notMatchedInterface)(nil)) + assert.NoError(t, err) + assert.Empty(t, testStructFields) +} + +func TestGetFieldsImplementingMemberStructPointer(t *testing.T) { + content := "runs all day and never walks" + testStructObj := testStructMemberStructPtr{ + Member: bytes.NewBufferString(content), } - fixFields, err := util.GetFieldsImplementing(&fix, (*notMatchedInterface)(nil)) + fields, err := util.GetFieldsImplementing(&testStructObj, (*readInterface)(nil)) assert.NoError(t, err) - assert.Empty(t, fixFields) + assert.Len(t, fields, 1) + + output := make([]byte, len(content)) + n, err := fields[0].Read(output) + assert.Equal(t, len(content), n) + assert.Equal(t, content, string(output)) +} + +func TestGetFieldsImplementingMemberInterface(t *testing.T) { + content := "it has a bed and never sleeps" + testStructObj := testStructMemberInterface{ + Member: bytes.NewBufferString(content), + } + fields, err := util.GetFieldsImplementing(&testStructObj, (*readInterface)(nil)) + assert.NoError(t, err) + assert.Len(t, fields, 1) + + output := make([]byte, len(content)) + n, err := fields[0].Read(output) + assert.Equal(t, len(content), n) + assert.Equal(t, content, string(output)) } func TestGetFieldsImplementingSuccess(t *testing.T) { // Struct not initialized // It's a responsibility of a user to make sure that the fields are not nil before using them. - structNotInitialized := testStructFixture{} - structNotInitializedFields, err := util.GetFieldsImplementing(&structNotInitialized, (*insertable)(nil)) + structNotInitialized := testStruct{} + structNotInitializedFields, err := util.GetFieldsImplementing(&structNotInitialized, (*readInterface)(nil)) assert.NoError(t, err) + // There are 4 pointer members of the testStruct satisfying the interface. + // Nil interface members are not returned. assert.Equal(t, 4, len(structNotInitializedFields)) for _, f := range structNotInitializedFields { assert.Nil(t, f) - assert.Implements(t, (*insertable)(nil), f) + assert.Implements(t, (*readInterface)(nil), f) } // Struct initialized - fix := testStructFixture{ - privateUser: &models.User{ID: "bfc9d3be-a13c-4790-befb-573c9a5b11a4"}, - User1: &models.User{ID: "9e16c597-2491-45bb-89ca-775b6e07f51d"}, - User2: &models.User{ID: "52028fd6-e299-4d36-8bba-21fe4713ffcd"}, - User1AppUserProfile: &models.AppUserProfile{UserID: "9e16c597-2491-45bb-89ca-775b6e07f51d"}, - User1AccessToken1: &models.AccessToken{UserID: "9e16c597-2491-45bb-89ca-775b6e07f51d"}, - X: 12, - Y: "y", + testStructObj := testStruct{ + // satisfy only readInterface + LimitedReader: &io.LimitedReader{N: 100}, + Reader: strings.NewReader("did you know that"), + // satisfy both readInterface and writeInterface + Buffer1: bytes.NewBufferString("there are rats with"), + Buffer2: bytes.NewBufferString("human BRAIN cells transplanted"), + NetBuffer: &net.Buffers{[]byte{0x19}}, } - insertableFields, err := util.GetFieldsImplementing(&fix, (*insertable)(nil)) + // Fields implementing readInterface + readInterfaceFields, err := util.GetFieldsImplementing(&testStructObj, (*readInterface)(nil)) assert.NoError(t, err) - assert.Equal(t, 4, len(insertableFields)) + assert.Equal(t, 5, len(readInterfaceFields)) - for _, f := range insertableFields { + for _, f := range readInterfaceFields { assert.NotNil(t, f) - assert.Implements(t, (*insertable)(nil), f) + assert.Implements(t, (*readInterface)(nil), f) } - type upsertable interface { - Upsert(ctx context.Context, exec boil.ContextExecutor, updateOnConflict bool, conflictColumns []string, updateColumns, insertColumns boil.Columns) error - } - upsertableFields, err := util.GetFieldsImplementing(&fix, (*upsertable)(nil)) + // Fields implementing writeInterface + writeInterfaceFields, err := util.GetFieldsImplementing(&testStructObj, (*writeInterface)(nil)) assert.NoError(t, err) - // there should be equal number of fields implementing Insertable and Upsertable interface - assert.Equal(t, len(insertableFields), len(upsertableFields)) - for _, f := range upsertableFields { + assert.Equal(t, 3, len(writeInterfaceFields)) + for _, f := range writeInterfaceFields { assert.NotNil(t, f) - assert.Implements(t, (*upsertable)(nil), f) + assert.Implements(t, (*writeInterface)(nil), f) } + + type readWriteInterface interface { + readInterface + writeInterface + } + readWriteInterfaceFields, err := util.GetFieldsImplementing(&testStructObj, (*readWriteInterface)(nil)) + assert.NoError(t, err) + // All members implementing writeInterface implement readInterface too + assert.Equal(t, 3, len(readWriteInterfaceFields)) + +} + +type testStructEmpty struct { +} + +type testStructPrivateFiled struct { + privateMember *bytes.Buffer +} + +type testStructPrimitives struct { + X int + Y string + XPtr *int + YPtr *string +} + +type testStructMemberStruct struct { + Member bytes.Buffer +} + +type testStructMemberStructPtr struct { + Member *bytes.Buffer +} + +type testStructMemberInterface struct { + Member io.Reader } From 43511a69092b6dce7cdb7710abc7a3112943167d Mon Sep 17 00:00:00 2001 From: anjankow Date: Fri, 7 Apr 2023 11:41:06 +0200 Subject: [PATCH 5/9] Use GetFieldsImplementing with Upsertable --- internal/data/fixtures.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/internal/data/fixtures.go b/internal/data/fixtures.go index 128a6a5b..935134b8 100644 --- a/internal/data/fixtures.go +++ b/internal/data/fixtures.go @@ -2,7 +2,9 @@ package data import ( "context" + "fmt" + "allaboutapps.dev/aw/go-starter/internal/util" "github.com/volatiletech/sqlboiler/v4/boil" ) @@ -14,6 +16,7 @@ type Upsertable interface { Upsert(ctx context.Context, exec boil.ContextExecutor, updateOnConflict bool, conflictColumns []string, updateColumns, insertColumns boil.Columns) error } +// Mind the declaration order! The fields get upserted exactly in the order they are declared. type FixtureMap struct{} func Fixtures() FixtureMap { @@ -21,5 +24,12 @@ func Fixtures() FixtureMap { } func Upserts() []Upsertable { - return []Upsertable{} + fix := Fixtures() + upsertableIfc := (*Upsertable)(nil) + upserts, err := util.GetFieldsImplementing(&fix, upsertableIfc) + if err != nil { + panic(fmt.Errorf("failed to get upsertable fixture fields: %w", err)) + } + + return upserts } From 2b4baa1167e35307808f04331db417a8d8dc18f2 Mon Sep 17 00:00:00 2001 From: anjankow Date: Fri, 7 Apr 2023 11:41:06 +0200 Subject: [PATCH 6/9] Fix linter --- internal/util/struct_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/util/struct_test.go b/internal/util/struct_test.go index eb7a95fd..745d69c0 100644 --- a/internal/util/struct_test.go +++ b/internal/util/struct_test.go @@ -101,6 +101,7 @@ func TestGetFieldsImplementingMemberStructPointer(t *testing.T) { output := make([]byte, len(content)) n, err := fields[0].Read(output) + assert.NoError(t, err) assert.Equal(t, len(content), n) assert.Equal(t, content, string(output)) } @@ -116,6 +117,7 @@ func TestGetFieldsImplementingMemberInterface(t *testing.T) { output := make([]byte, len(content)) n, err := fields[0].Read(output) + assert.NoError(t, err) assert.Equal(t, len(content), n) assert.Equal(t, content, string(output)) } From db64e27978a03a4c8f45b4b9d11ce64bd6c2c056 Mon Sep 17 00:00:00 2001 From: anjankow Date: Fri, 7 Apr 2023 11:45:01 +0200 Subject: [PATCH 7/9] Remove not needed test snapshot --- .../TestGetFieldsImplementingSuccess.golden | 68 ------------------- 1 file changed, 68 deletions(-) delete mode 100644 test/testdata/snapshots/TestGetFieldsImplementingSuccess.golden diff --git a/test/testdata/snapshots/TestGetFieldsImplementingSuccess.golden b/test/testdata/snapshots/TestGetFieldsImplementingSuccess.golden deleted file mode 100644 index 29d547b0..00000000 --- a/test/testdata/snapshots/TestGetFieldsImplementingSuccess.golden +++ /dev/null @@ -1,68 +0,0 @@ -([]interface {}) (len=4) { - (*models.User)({ - ID: (string) (len=36) "9e16c597-2491-45bb-89ca-775b6e07f51d", - Username: (null.String) { - String: (string) "", - Valid: (bool) false - }, - Password: (null.String) { - String: (string) "", - Valid: (bool) false - }, - IsActive: (bool) false, - Scopes: (types.StringArray) , - LastAuthenticatedAt: (null.Time) { - Time: (time.Time) 0001-01-01 00:00:00 +0000 UTC, - Valid: (bool) false - }, - CreatedAt: (time.Time) 0001-01-01 00:00:00 +0000 UTC, - UpdatedAt: (time.Time) 0001-01-01 00:00:00 +0000 UTC, - R: (*models.userR)(), - L: (models.userL) { - } - }), - (*models.User)({ - ID: (string) (len=36) "52028fd6-e299-4d36-8bba-21fe4713ffcd", - Username: (null.String) { - String: (string) "", - Valid: (bool) false - }, - Password: (null.String) { - String: (string) "", - Valid: (bool) false - }, - IsActive: (bool) false, - Scopes: (types.StringArray) , - LastAuthenticatedAt: (null.Time) { - Time: (time.Time) 0001-01-01 00:00:00 +0000 UTC, - Valid: (bool) false - }, - CreatedAt: (time.Time) 0001-01-01 00:00:00 +0000 UTC, - UpdatedAt: (time.Time) 0001-01-01 00:00:00 +0000 UTC, - R: (*models.userR)(), - L: (models.userL) { - } - }), - (*models.AppUserProfile)({ - UserID: (string) (len=36) "9e16c597-2491-45bb-89ca-775b6e07f51d", - LegalAcceptedAt: (null.Time) { - Time: (time.Time) 0001-01-01 00:00:00 +0000 UTC, - Valid: (bool) false - }, - CreatedAt: (time.Time) 0001-01-01 00:00:00 +0000 UTC, - UpdatedAt: (time.Time) 0001-01-01 00:00:00 +0000 UTC, - R: (*models.appUserProfileR)(), - L: (models.appUserProfileL) { - } - }), - (*models.AccessToken)({ - Token: (string) "", - ValidUntil: (time.Time) 0001-01-01 00:00:00 +0000 UTC, - UserID: (string) (len=36) "9e16c597-2491-45bb-89ca-775b6e07f51d", - CreatedAt: (time.Time) 0001-01-01 00:00:00 +0000 UTC, - UpdatedAt: (time.Time) 0001-01-01 00:00:00 +0000 UTC, - R: (*models.accessTokenR)(), - L: (models.accessTokenL) { - } - }) -} From 37608166060f1dc43d5df115af5c0ecb605470b8 Mon Sep 17 00:00:00 2001 From: anjankow Date: Wed, 19 Apr 2023 12:54:05 +0000 Subject: [PATCH 8/9] fix CVE-2022-41723 vulnerability --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ccb98e9..4b474472 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,7 @@ - Minor: [Bump golang.org/x/crypto from v0.0.0-20220411220226-7b82a4e95df4 to 0.3.0](https://cs.opensource.google/go/x/crypto) - Minor: [Bump golang.org/x/sys from v0.0.0-20220412211240-33da011f77ad to 0.2.0](https://cs.opensource.google/go/x/sys) - Minor: [Bump golang.org/x/text from 0.3.7 to 0.4.0](https://cs.opensource.google/go/x/text) (Fixing CVE-2022-32149) + - Minor: [Bump golang.org/x/net from 0.2.0 to 0.7.0](https://cs.opensource.google/go/x/net) (Fixing CVE-2022-41723) - Minor: [Bump google.golang.org/api from 0.74.0 to 0.103.0](https://github.com/googleapis/google-api-go-client/compare/v0.80.0...v0.103.0) ## 2022-09-13 From 7ef261e577f3c7b60125525e4494795a9c04ea06 Mon Sep 17 00:00:00 2001 From: anjankow Date: Thu, 20 Apr 2023 10:26:06 +0000 Subject: [PATCH 9/9] Update changelog --- CHANGELOG.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b474472..77797545 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,9 +20,10 @@ - Major: Upgrade distroless app image from base-debian10 to base-debian11 - Major: Dockerfile is now build to support amd64 and arm64 architecture - Improve speed of `make swagger` when dealing with many files in `/api` by generating to a docker volume instead of the host filesystem, rsyncing only to changes into `/internal/types`. Furthermore split our swagger type generation and validation into two separate make targets, that can run concurrently (requires `./docker-helper.sh --rebuild`). - - Note that `/app/api/tmp`, `/app/tmp` and `/app/bin` are now baked by proper docker volumes when using our `docker-compose.yml`/`./docker-helper.sh --up`. You **cannot** remove these directories directly inside the container (but its contents) and you can also no longer see its files on your host machine directly! + - Note that `/app/api/tmp`, `/app/tmp` and `/app/bin` are now baked by proper docker volumes when using our `docker-compose.yml`/`./docker-helper.sh --up`. You **cannot** remove these directories directly inside the container (but its contents) and you can also no longer see its files on your host machine directly! - Fix `make check-gen-dirs` false positives hidden files. - Allow to trace/benchmark `Makefile` targets execution by using a custom shell wrapper for make execution. See `SHELL` and `.SHELLFLAGS` within `Makefile` and the custom `rksh` script in the root working directory. Usage: `MAKE_TRACE_TIME=true make ` +- Minor: add `GetFieldsImplementing` to utils and use it to easier add new fixture fields. - `go.mod` changes: - Minor: [Bump github.com/BurntSushi/toml from 1.1.0 to 1.2.1](https://github.com/BurntSushi/toml/releases/tag/v1.2.1) - Minor: [Bump github.com/gabriel-vasile/mimetype from 1.4.0 to 1.4.1](https://github.com/gabriel-vasile/mimetype/releases/tag/v1.4.1) @@ -77,7 +78,7 @@ - `go.mod` changes: - Major: [Bump `github.com/rubenv/sql-migrate` from v0.0.0-20210614095031-55d5740dbbcc to v1.1.1](https://github.com/rubenv/sql-migrate/compare/55d5740dbbccbaa4934009263b37ba52d837241f...v1.1.1) (though this should not lead to any major changes) - Minor: [Bump github.com/volatiletech/sqlboiler/v4 from 4.6.0 to v4.9.2](https://github.com/volatiletech/sqlboiler/blob/v4.9.2/CHANGELOG.md#v492---2022-04-11) (your generated model might slightly change, minor changes). - - Note that v5 will prefer wrapping errors (e.g. `sql.ErrNoRows`) to retain the stack trace, thus it's about time for us to start to enforce proper `errors.Is` checks in our codebase (see above). + - Note that v5 will prefer wrapping errors (e.g. `sql.ErrNoRows`) to retain the stack trace, thus it's about time for us to start to enforce proper `errors.Is` checks in our codebase (see above). - Minor: [#178: Bump github.com/labstack/echo/v4 from 4.6.1 to 4.7.2](https://github.com/allaboutapps/go-starter/pull/178) (support for HEAD method query params binding, minor changes). - Minor: [#160: Bump github.com/rs/zerolog from 1.25.0 to 1.26.1](https://github.com/allaboutapps/go-starter/pull/160) (minor changes). - Minor: [#179: Bump github.com/nicksnyder/go-i18n/v2 from 2.1.2 to 2.2.0](https://github.com/allaboutapps/go-starter/pull/179) (minor changes). @@ -97,7 +98,7 @@ - This does not require a development container restart. - We override the env within the app process through `config.DefaultServiceConfigFromEnv()`, so this does not mess with the actual container ENV. - See `.env.local.sample` for further instructions to use this. - - Note that `.env.local` is **NEVER automatically** applied during **test runs**. If you really need that, use the specialized `test.DotEnvLoadLocalOrSkipTest` helper before loading up your server within that very test! This ensures that this test is automatically skipped if the `.env.local` file is no longer available. + - Note that `.env.local` is **NEVER automatically** applied during **test runs**. If you really need that, use the specialized `test.DotEnvLoadLocalOrSkipTest` helper before loading up your server within that very test! This ensures that this test is automatically skipped if the `.env.local` file is no longer available. - VSCode windows closes now explicitly stop Docker containers via [`shutdownAction: "stopCompose"`](https://code.visualstudio.com/docs/remote/devcontainerjson-reference) within `.devcontainer.json`. - Use `./docker-helper --halt` or other `docker` or `docker-compose` management commands to do this explicitly instead. - Drone CI specific (minor): Fix multiline ENV variables were messing up our `.hostenv` for `docker run` command testing of the final image. @@ -122,9 +123,9 @@ - **BREAKING** Username format change in auth handlers - Added the `util.ToUsernameFormat` helper function, which will **lowercase** and **trim whitespaces**. We use it to format usernames in the login, register, and forgot-password handlers. - - This prevents user duplication (e.g. two accounts registered with the same email address with different casing) and - - cases where users would inadvertently register with specific casing or a trailing whitespace after their username, and subsequently struggle to log into their account. - - **This effectively locks existing users whose username contains uppercase characters and/or whitespaces out of their accounts.** + - This prevents user duplication (e.g. two accounts registered with the same email address with different casing) and + - cases where users would inadvertently register with specific casing or a trailing whitespace after their username, and subsequently struggle to log into their account. + - **This effectively locks existing users whose username contains uppercase characters and/or whitespaces out of their accounts.** - Before rolling out this change, check whether any existing users are affected and migrate their usernames to a format that is compatible with this change. - Be aware that this may cause conflicts in regard to the uniqueness constraint of usernames and therefore need to be resolved manually, which is why we are not including a database migration to automatically migrate existing usernames to the new format. - For more information and a possible manual database migration flow please see this special WIKI page: https://github.com/allaboutapps/go-starter/wiki/2022-02-28 @@ -134,7 +135,7 @@ ### Changed - Changed order of make targets in the `make swagger` pipeline. `make swagger-lint-ref-siblings` will now run after `make swagger-concat`, always linting the current version of our swagger file. This helps avoid errors regarding an invalid `swagger.yml` when resolving merge conflicts as those are often resolved by running `make swagger` and generating a fresh `swagger.yml`. - + ## 2022-02-02 ### Changed @@ -226,7 +227,7 @@ ### Changed -- **Hotfix**: We will pin the `Dockerfile` development and builder stage to `golang:1.16.7-buster` (+ `-buster`) for now, as currently the [new debian bullseye release within the go official docker images](https://github.com/docker-library/golang/commit/48a7371ed6055a97a10adb0b75756192ad5f1c97) breaks some tooling. The upgrade to debian bullseye and Go 1.17 will happen ~simultaneously~ **separately** within go-starter in the following weeks. +- **Hotfix**: We will pin the `Dockerfile` development and builder stage to `golang:1.16.7-buster` (+ `-buster`) for now, as currently the [new debian bullseye release within the go official docker images](https://github.com/docker-library/golang/commit/48a7371ed6055a97a10adb0b75756192ad5f1c97) breaks some tooling. The upgrade to debian bullseye and Go 1.17 will happen ~simultaneously~ **separately** within go-starter in the following weeks. ## 2021-08-16