mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-10-31 14:32:24 -05:00 
			
		
		
		
	start working on struct validation for gtsmodel
This commit is contained in:
		
					parent
					
						
							
								53507ac2a3
							
						
					
				
			
			
				commit
				
					
						8aa72f995f
					
				
			
		
					 10 changed files with 407 additions and 125 deletions
				
			
		|  | @ -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 | ||||
| } | ||||
|  |  | |||
|  | @ -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? | ||||
| } | ||||
|  |  | |||
							
								
								
									
										92
									
								
								internal/gtsmodel/tag_test.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								internal/gtsmodel/tag_test.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -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 <http://www.gnu.org/licenses/>. | ||||
| */ | ||||
| 
 | ||||
| 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)) | ||||
| } | ||||
|  | @ -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"` | ||||
| } | ||||
|  |  | |||
							
								
								
									
										106
									
								
								internal/gtsmodel/user_test.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								internal/gtsmodel/user_test.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -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)) | ||||
| } | ||||
							
								
								
									
										78
									
								
								internal/gtsmodel/validate.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								internal/gtsmodel/validate.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -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 <http://www.gnu.org/licenses/>. | ||||
| */ | ||||
| 
 | ||||
| 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) | ||||
| } | ||||
							
								
								
									
										64
									
								
								internal/gtsmodel/validate_test.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								internal/gtsmodel/validate_test.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -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 <http://www.gnu.org/licenses/>. | ||||
| */ | ||||
| 
 | ||||
| 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)) | ||||
| } | ||||
|  | @ -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) | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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) | ||||
| } | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue