From d2276fc553332477740e2c320d51a9cbc3cf2585 Mon Sep 17 00:00:00 2001 From: tsmethurst Date: Sun, 29 Aug 2021 16:52:23 +0200 Subject: [PATCH] start working on struct validation for gtsmodel --- internal/gtsmodel/statusmute.go | 23 ++--- internal/gtsmodel/tag.go | 29 +++---- internal/gtsmodel/tag_test.go | 92 ++++++++++++++++++++ internal/gtsmodel/user.go | 132 +++++++++-------------------- internal/gtsmodel/user_test.go | 106 +++++++++++++++++++++++ internal/gtsmodel/validate.go | 78 +++++++++++++++++ internal/gtsmodel/validate_test.go | 64 ++++++++++++++ internal/id/ulid.go | 2 + internal/util/regexes.go | 1 + internal/util/validation.go | 5 ++ 10 files changed, 407 insertions(+), 125 deletions(-) create mode 100644 internal/gtsmodel/tag_test.go create mode 100644 internal/gtsmodel/user_test.go create mode 100644 internal/gtsmodel/validate.go create mode 100644 internal/gtsmodel/validate_test.go diff --git a/internal/gtsmodel/statusmute.go b/internal/gtsmodel/statusmute.go index 56a792ab4..90eb41bdb 100644 --- a/internal/gtsmodel/statusmute.go +++ b/internal/gtsmodel/statusmute.go @@ -20,19 +20,14 @@ package gtsmodel import "time" -// StatusMute refers to one account having muted the status of another account or its own +// StatusMute refers to one account having muted the status of another account or its own. type StatusMute struct { - // id of this mute in the database - ID string `bun:"type:CHAR(26),pk,notnull,unique"` - // when was this mute created - CreatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"` - // id of the account that created ('did') the mute - AccountID string `bun:"type:CHAR(26),notnull"` - Account *Account `bun:"rel:belongs-to"` - // id the account owning the muted status (can be the same as accountID) - TargetAccountID string `bun:"type:CHAR(26),notnull"` - TargetAccount *Account `bun:"rel:belongs-to"` - // database id of the status that has been muted - StatusID string `bun:"type:CHAR(26),notnull"` - Status *Status `bun:"rel:belongs-to"` + ID string `validate:"required,ulid" bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database + CreatedAt time.Time `validate:"required" bun:",nullzero,notnull,default:current_timestamp"` // when was item created + AccountID string `validate:"required,ulid" bun:"type:CHAR(26),nullzero,notnull"` // id of the account that created ('did') the mute + Account *Account `validate:"-" bun:"rel:belongs-to"` // pointer to the account specified by accountID + TargetAccountID string `validate:"required,ulid" bun:"type:CHAR(26),nullzero,notnull"` // id the account owning the muted status (can be the same as accountID) + TargetAccount *Account `validate:"-" bun:"rel:belongs-to"` // pointer to the account specified by targetAccountID + StatusID string `validate:"required,ulid" bun:"type:CHAR(26),nullzero,notnull"` // database id of the status that has been muted + Status *Status `validate:"-" bun:"rel:belongs-to"` // pointer to the muted status specified by statusID } diff --git a/internal/gtsmodel/tag.go b/internal/gtsmodel/tag.go index d4be0b66c..359f9a91c 100644 --- a/internal/gtsmodel/tag.go +++ b/internal/gtsmodel/tag.go @@ -20,24 +20,15 @@ package gtsmodel import "time" -// Tag represents a hashtag for gathering public statuses together +// Tag represents a hashtag for gathering public statuses together. type Tag struct { - // id of this tag in the database - ID string `bun:",unique,type:CHAR(26),pk,notnull"` - // Href of this tag, eg https://example.org/tags/somehashtag - URL string `bun:",nullzero"` - // name of this tag -- the tag without the hash part - Name string `bun:",unique,notnull"` - // Which account ID is the first one we saw using this tag? - FirstSeenFromAccountID string `bun:"type:CHAR(26),nullzero"` - // when was this tag created - CreatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"` - // when was this tag last updated - UpdatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"` - // can our instance users use this tag? - Useable bool `bun:",notnull,default:true"` - // can our instance users look up this tag? - Listable bool `bun:",notnull,default:true"` - // when was this tag last used? - LastStatusAt time.Time `bun:",nullzero"` + ID string `validate:"required,ulid" bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database + CreatedAt time.Time `validate:"required" bun:",nullzero,notnull,default:current_timestamp"` // when was item created + UpdatedAt time.Time `validate:"required" bun:",nullzero,notnull,default:current_timestamp"` // when was item last updated + URL string `validate:"required,url" bun:",nullzero,notnull"` // Href of this tag, eg https://example.org/tags/somehashtag + Name string `validate:"required" bun:",unique,nullzero,notnull"` // name of this tag -- the tag without the hash part + FirstSeenFromAccountID string `validate:"ulid" bun:"type:CHAR(26),nullzero"` // Which account ID is the first one we saw using this tag? + Useable bool `validate:"-" bun:",nullzero,notnull,default:true"` // can our instance users use this tag? + Listable bool `validate:"-" bun:",nullzero,notnull,default:true"` // can our instance users look up this tag? + LastStatusAt time.Time `validate:"required" bun:",nullzero,notnull,default:current_timestamp"` // when was this tag last used? } diff --git a/internal/gtsmodel/tag_test.go b/internal/gtsmodel/tag_test.go new file mode 100644 index 000000000..baafe55bd --- /dev/null +++ b/internal/gtsmodel/tag_test.go @@ -0,0 +1,92 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package gtsmodel_test + +import ( + "testing" + "time" + + "github.com/stretchr/testify/suite" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +) + +func happyTag() *gtsmodel.Tag { + return >smodel.Tag{ + ID: "01FE91RJR88PSEEE30EV35QR8N", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + URL: "https://example.org/tags/some_tag", + Name: "some_tag", + FirstSeenFromAccountID: "01FE91SR5P2GW06K3AJ98P72MT", + Useable: true, + Listable: true, + LastStatusAt: time.Now(), + } +} + +type TagValidateTestSuite struct { + suite.Suite +} + +func (suite *TagValidateTestSuite) TestValidateTagHappyPath() { + // no problem here + t := happyTag() + err := gtsmodel.ValidateStruct(*t) + suite.NoError(err) +} + +func (suite *TagValidateTestSuite) TestValidateTagNoName() { + t := happyTag() + t.Name = "" + + err := gtsmodel.ValidateStruct(*t) + suite.EqualError(err, "Key: 'Tag.Name' Error:Field validation for 'Name' failed on the 'required' tag") +} + +func (suite *TagValidateTestSuite) TestValidateTagBadURL() { + t := happyTag() + + t.URL = "" + err := gtsmodel.ValidateStruct(*t) + suite.EqualError(err, "Key: 'Tag.URL' Error:Field validation for 'URL' failed on the 'required' tag") + + t.URL = "no-schema.com" + err = gtsmodel.ValidateStruct(*t) + suite.EqualError(err, "Key: 'Tag.URL' Error:Field validation for 'URL' failed on the 'url' tag") + + t.URL = "justastring" + err = gtsmodel.ValidateStruct(*t) + suite.EqualError(err, "Key: 'Tag.URL' Error:Field validation for 'URL' failed on the 'url' tag") + + t.URL = "https://aaa\n\n\naaaaaaaa" + err = gtsmodel.ValidateStruct(*t) + suite.EqualError(err, "Key: 'Tag.URL' Error:Field validation for 'URL' failed on the 'url' tag") +} + +func (suite *TagValidateTestSuite) TestValidateTagNoFirstSeenFromAccountID() { + t := happyTag() + t.FirstSeenFromAccountID = "" + + err := gtsmodel.ValidateStruct(*t) + suite.NoError(err) +} + +func TestTagValidateTestSuite(t *testing.T) { + suite.Run(t, new(TagValidateTestSuite)) +} diff --git a/internal/gtsmodel/user.go b/internal/gtsmodel/user.go index c36d75c8c..27089763d 100644 --- a/internal/gtsmodel/user.go +++ b/internal/gtsmodel/user.go @@ -26,97 +26,45 @@ import ( // User represents an actual human user of gotosocial. Note, this is a LOCAL gotosocial user, not a remote account. // To cross reference this local user with their account (which can be local or remote), use the AccountID field. type User struct { - /* - BASIC INFO - */ + ID string `validate:"required,ulid" bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database + CreatedAt time.Time `validate:"required" bun:",nullzero,notnull,default:current_timestamp"` // when was item created + UpdatedAt time.Time `validate:"required" bun:",nullzero,notnull,default:current_timestamp"` // when was item last updated + Email string `validate:"required_with=ConfirmedAt" bun:",nullzero,notnull,unique"` // confirmed email address for this user, this should be unique -- only one email address registered per instance, multiple users per email are not supported + AccountID string `validate:"required,ulid" bun:"type:CHAR(26),nullzero,notnull,unique"` // The id of the local gtsmodel.Account entry for this user. + Account *Account `validate:"-" bun:"rel:belongs-to"` // Pointer to the account of this user that corresponds to AccountID. + EncryptedPassword string `validate:"required" bun:",nullzero,notnull"` // The encrypted password of this user, generated using https://pkg.go.dev/golang.org/x/crypto/bcrypt#GenerateFromPassword. A salt is included so we're safe against 🌈 tables. + SignUpIP net.IP `validate:"-" bun:",nullzero"` // From what IP was this user created? + CurrentSignInAt time.Time `validate:"-" bun:",nullzero"` // When did the user sign in with their current session. + CurrentSignInIP net.IP `validate:"-" bun:",nullzero"` // What's the most recent IP of this user + LastSignInAt time.Time `validate:"-" bun:",nullzero"` // When did this user last sign in? + LastSignInIP net.IP `validate:"-" bun:",nullzero"` // What's the previous IP of this user? + SignInCount int `validate:"-" bun:",nullzero,notnull,default:0"` // How many times has this user signed in? + InviteID string `validate:"ulid" bun:"type:CHAR(26),nullzero"` // id of the user who invited this user (who let this joker in?) + ChosenLanguages []string `validate:"-" bun:",nullzero"` // What languages does this user want to see? + FilteredLanguages []string `validate:"-" bun:",nullzero"` // What languages does this user not want to see? + Locale string `validate:"-" bun:",nullzero"` // In what timezone/locale is this user located? + CreatedByApplicationID string `validate:"ulid" bun:"type:CHAR(26),nullzero,notnull"` // Which application id created this user? See gtsmodel.Application + CreatedByApplication *Application `validate:"-" bun:"rel:belongs-to"` // Pointer to the application corresponding to createdbyapplicationID. + LastEmailedAt time.Time `validate:"-" bun:",nullzero"` // When was this user last contacted by email. + ConfirmationToken string `validate:"required_with=ConfirmationSentAt" bun:",nullzero"` // What confirmation token did we send this user/what are we expecting back? + ConfirmationSentAt time.Time `validate:"required_with=ConfirmationToken" bun:",nullzero"` // When did we send email confirmation to this user? + ConfirmedAt time.Time `validate:"required_with=Email" bun:",nullzero"` // When did the user confirm their email address + UnconfirmedEmail string `validate:"required_without=Email" bun:",nullzero"` // Email address that hasn't yet been confirmed + Moderator bool `validate:"-" bun:",nullzero,notnull,default:false"` // Is this user a moderator? + Admin bool `validate:"-" bun:",nullzero,notnull,default:false"` // Is this user an admin? + Disabled bool `validate:"-" bun:",nullzero,notnull,default:false"` // Is this user disabled from posting? + Approved bool `validate:"-" bun:",nullzero,notnull,default:false"` // Has this user been approved by a moderator? + ResetPasswordToken string `validate:"required_with=ResetPasswordSentAt" bun:",nullzero"` // The generated token that the user can use to reset their password + ResetPasswordSentAt time.Time `validate:"required_with=ResetPasswordToken" bun:",nullzero"` // When did we email the user their reset-password email? - // id of this user in the local database; the end-user will never need to know this, it's strictly internal - ID string `bun:"type:CHAR(26),pk,notnull,unique"` - // confirmed email address for this user, this should be unique -- only one email address registered per instance, multiple users per email are not supported - Email string `bun:"default:null,unique,nullzero"` - // The id of the local gtsmodel.Account entry for this user, if it exists (unconfirmed users don't have an account yet) - AccountID string `bun:"type:CHAR(26),unique,nullzero"` - Account *Account `bun:"rel:belongs-to"` - // The encrypted password of this user, generated using https://pkg.go.dev/golang.org/x/crypto/bcrypt#GenerateFromPassword. A salt is included so we're safe against 🌈 tables - EncryptedPassword string `bun:",notnull"` - - /* - USER METADATA - */ - - // When was this user created? - CreatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"` - // From what IP was this user created? - SignUpIP net.IP `bun:",nullzero"` - // When was this user updated (eg., password changed, email address changed)? - UpdatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"` - // When did this user sign in for their current session? - CurrentSignInAt time.Time `bun:",nullzero"` - // What's the most recent IP of this user - CurrentSignInIP net.IP `bun:",nullzero"` - // When did this user last sign in? - LastSignInAt time.Time `bun:",nullzero"` - // What's the previous IP of this user? - LastSignInIP net.IP `bun:",nullzero"` - // How many times has this user signed in? - SignInCount int - // id of the user who invited this user (who let this guy in?) - InviteID string `bun:"type:CHAR(26),nullzero"` - // What languages does this user want to see? - ChosenLanguages []string - // What languages does this user not want to see? - FilteredLanguages []string - // In what timezone/locale is this user located? - Locale string `bun:",nullzero"` - // Which application id created this user? See gtsmodel.Application - CreatedByApplicationID string `bun:"type:CHAR(26),nullzero"` - CreatedByApplication *Application `bun:"rel:belongs-to"` - // When did we last contact this user - LastEmailedAt time.Time `bun:",nullzero"` - - /* - USER CONFIRMATION - */ - - // What confirmation token did we send this user/what are we expecting back? - ConfirmationToken string `bun:",nullzero"` - // When did the user confirm their email address - ConfirmedAt time.Time `bun:",nullzero"` - // When did we send email confirmation to this user? - ConfirmationSentAt time.Time `bun:",nullzero"` - // Email address that hasn't yet been confirmed - UnconfirmedEmail string `bun:",nullzero"` - - /* - ACL FLAGS - */ - - // Is this user a moderator? - Moderator bool - // Is this user an admin? - Admin bool - // Is this user disabled from posting? - Disabled bool - // Has this user been approved by a moderator? - Approved bool - - /* - USER SECURITY - */ - - // The generated token that the user can use to reset their password - ResetPasswordToken string `bun:",nullzero"` - // When did we email the user their reset-password email? - ResetPasswordSentAt time.Time `bun:",nullzero"` - - EncryptedOTPSecret string `bun:",nullzero"` - EncryptedOTPSecretIv string `bun:",nullzero"` - EncryptedOTPSecretSalt string `bun:",nullzero"` - OTPRequiredForLogin bool - OTPBackupCodes []string - ConsumedTimestamp int - RememberToken string `bun:",nullzero"` - SignInToken string `bun:",nullzero"` - SignInTokenSentAt time.Time `bun:",nullzero"` - WebauthnID string `bun:",nullzero"` + EncryptedOTPSecret string `validate:"-" bun:",nullzero"` + EncryptedOTPSecretIv string `validate:"-" bun:",nullzero"` + EncryptedOTPSecretSalt string `validate:"-" bun:",nullzero"` + OTPRequiredForLogin bool `validate:"-" bun:",nullzero"` + OTPBackupCodes []string `validate:"-" bun:",nullzero"` + ConsumedTimestamp int `validate:"-" bun:",nullzero"` + RememberToken string `validate:"-" bun:",nullzero"` + SignInToken string `validate:"-" bun:",nullzero"` + SignInTokenSentAt time.Time `validate:"-" bun:",nullzero"` + WebauthnID string `validate:"-" bun:",nullzero"` } diff --git a/internal/gtsmodel/user_test.go b/internal/gtsmodel/user_test.go new file mode 100644 index 000000000..c1a9bf849 --- /dev/null +++ b/internal/gtsmodel/user_test.go @@ -0,0 +1,106 @@ +package gtsmodel_test + +import ( + "net" + "testing" + "time" + + "github.com/stretchr/testify/suite" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +) + +func happyUser() *gtsmodel.User { + return >smodel.User{ + ID: "01FE8TTK9F34BR0KG7639AJQTX", + Email: "whatever@example.org", + AccountID: "01FE8TWA7CN8J7237K5DFS1RY5", + Account: nil, + EncryptedPassword: "$2y$10$tkRapNGW.RWkEuCMWdgArunABFvsPGRvFQY3OibfSJo0RDL3z8WfC", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + SignUpIP: net.ParseIP("128.64.32.16"), + CurrentSignInAt: time.Now(), + CurrentSignInIP: net.ParseIP("128.64.32.16"), + LastSignInAt: time.Now(), + LastSignInIP: net.ParseIP("128.64.32.16"), + SignInCount: 0, + InviteID: "", + ChosenLanguages: []string{}, + FilteredLanguages: []string{}, + Locale: "en", + CreatedByApplicationID: "01FE8Y5EHMWCA1MHMTNHRVZ1X4", + CreatedByApplication: nil, + LastEmailedAt: time.Now(), + ConfirmationToken: "", + ConfirmedAt: time.Now(), + ConfirmationSentAt: time.Now(), + UnconfirmedEmail: "", + Moderator: false, + Admin: false, + Disabled: false, + Approved: true, + } +} + +type UserValidateTestSuite struct { + suite.Suite +} + +func (suite *UserValidateTestSuite) TestValidateUserHappyPath() { + // no problem here + u := happyUser() + err := gtsmodel.ValidateStruct(*u) + suite.NoError(err) +} + +func (suite *UserValidateTestSuite) TestValidateUserNoID() { + // user has no id set + u := happyUser() + u.ID = "" + + err := gtsmodel.ValidateStruct(*u) + suite.EqualError(err, "Key: 'User.ID' Error:Field validation for 'ID' failed on the 'required' tag") +} + +func (suite *UserValidateTestSuite) TestValidateUserNoEmail() { + // user has no email or unconfirmed email set + u := happyUser() + u.Email = "" + + err := gtsmodel.ValidateStruct(*u) + suite.EqualError(err, "Key: 'User.Email' Error:Field validation for 'Email' failed on the 'required_with' tag\nKey: 'User.UnconfirmedEmail' Error:Field validation for 'UnconfirmedEmail' failed on the 'required_without' tag") +} + +func (suite *UserValidateTestSuite) TestValidateUserOnlyUnconfirmedEmail() { + // user has only UnconfirmedEmail but ConfirmedAt is set + u := happyUser() + u.Email = "" + u.UnconfirmedEmail = "whatever@example.org" + + err := gtsmodel.ValidateStruct(*u) + suite.EqualError(err, "Key: 'User.Email' Error:Field validation for 'Email' failed on the 'required_with' tag") +} + +func (suite *UserValidateTestSuite) TestValidateUserOnlyUnconfirmedEmailOK() { + // user has only UnconfirmedEmail and ConfirmedAt is not set + u := happyUser() + u.Email = "" + u.UnconfirmedEmail = "whatever@example.org" + u.ConfirmedAt = time.Time{} + + err := gtsmodel.ValidateStruct(*u) + suite.NoError(err) +} + +func (suite *UserValidateTestSuite) TestValidateUserNoConfirmedAt() { + // user has Email but no ConfirmedAt + u := happyUser() + u.ConfirmedAt = time.Time{} + + err := gtsmodel.ValidateStruct(*u) + suite.EqualError(err, "Key: 'User.ConfirmedAt' Error:Field validation for 'ConfirmedAt' failed on the 'required_with' tag") +} + +func TestUserValidateTestSuite(t *testing.T) { + suite.Run(t, new(UserValidateTestSuite)) +} diff --git a/internal/gtsmodel/validate.go b/internal/gtsmodel/validate.go new file mode 100644 index 000000000..720aad463 --- /dev/null +++ b/internal/gtsmodel/validate.go @@ -0,0 +1,78 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package gtsmodel + +import ( + "reflect" + + "github.com/go-playground/validator/v10" + "github.com/superseriousbusiness/gotosocial/internal/util" +) + +var v *validator.Validate + +const ( + PointerValidationPanic = "validate function was passed pointer" + InvalidValidationPanic = "validate function was passed invalid item" +) + +var ulidValidator = func(fl validator.FieldLevel) bool { + value, kind, _ := fl.ExtractType(fl.Field()) + + if kind != reflect.String { + return false + } + + // we want either an empty string, or a proper ULID, nothing else + // if the string is empty, the `required` tag will take care of it so we don't need to worry about it here + s := value.String() + if len(s) == 0 { + return true + } + return util.ValidateULID(s) +} + +func init() { + v = validator.New() + v.RegisterValidation("ulid", ulidValidator) +} + +func ValidateStruct(s interface{}) error { + switch reflect.ValueOf(s).Kind() { + case reflect.Invalid: + panic(InvalidValidationPanic) + case reflect.Ptr: + panic(PointerValidationPanic) + } + + err := v.Struct(s) + return processValidationError(err) +} + +func processValidationError(err error) error { + if err == nil { + return nil + } + + if ive, ok := err.(*validator.InvalidValidationError); ok { + panic(ive) + } + + return err.(validator.ValidationErrors) +} diff --git a/internal/gtsmodel/validate_test.go b/internal/gtsmodel/validate_test.go new file mode 100644 index 000000000..7200522bc --- /dev/null +++ b/internal/gtsmodel/validate_test.go @@ -0,0 +1,64 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package gtsmodel_test + +import ( + "testing" + + "github.com/stretchr/testify/suite" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +) + +type ValidateTestSuite struct { + suite.Suite +} + +func (suite *ValidateTestSuite) TestValidatePointer() { + var nilUser *gtsmodel.User + suite.PanicsWithValue(gtsmodel.PointerValidationPanic, func() { + gtsmodel.ValidateStruct(nilUser) + }) +} + +func (suite *ValidateTestSuite) TestValidateNil() { + suite.PanicsWithValue(gtsmodel.InvalidValidationPanic, func() { + gtsmodel.ValidateStruct(nil) + }) +} + +func (suite *ValidateTestSuite) TestValidateWeirdULID() { + type a struct { + ID bool `validate:"required,ulid"` + } + + err := gtsmodel.ValidateStruct(a{ID: true}) + suite.Error(err) +} + +func (suite *ValidateTestSuite) TestValidateNotStruct() { + type aaaaaaa string + aaaaaa := aaaaaaa("aaaa") + suite.Panics(func() { + gtsmodel.ValidateStruct(aaaaaa) + }) +} + +func TestValidateTestSuite(t *testing.T) { + suite.Run(t, new(ValidateTestSuite)) +} diff --git a/internal/id/ulid.go b/internal/id/ulid.go index b488ddfc4..f9fbd4d88 100644 --- a/internal/id/ulid.go +++ b/internal/id/ulid.go @@ -10,6 +10,8 @@ import ( const randomRange = 631152381 // ~20 years in seconds +type ULID string + // NewULID returns a new ULID string using the current time, or an error if something goes wrong. func NewULID() (string, error) { newUlid, err := ulid.New(ulid.Timestamp(time.Now()), rand.Reader) diff --git a/internal/util/regexes.go b/internal/util/regexes.go index 88212fc43..d3a42d1fc 100644 --- a/internal/util/regexes.go +++ b/internal/util/regexes.go @@ -90,6 +90,7 @@ var ( followPathRegex = regexp.MustCompile(followPathRegexString) ulidRegexString = `[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}` + ulidRegex = regexp.MustCompile(fmt.Sprintf(`^%s$`, ulidRegexString)) likedPathRegexString = fmt.Sprintf(`^/?%s/(%s)/%s$`, UsersPath, usernameRegexString, LikedPath) // likedPathRegex parses a path that validates and captures the username part from eg /users/example_username/liked diff --git a/internal/util/validation.go b/internal/util/validation.go index 446f7a70e..aa25ccd16 100644 --- a/internal/util/validation.go +++ b/internal/util/validation.go @@ -171,3 +171,8 @@ func ValidateSiteTerms(t string) error { return nil } + +// ValidateULID returns true if the passed string is a valid ULID. +func ValidateULID(i string) bool { + return ulidRegex.MatchString(i) +}