forked from mirrors/gotosocial
start working on struct validation for gtsmodel
This commit is contained in:
parent
ed46224573
commit
7c45403b24
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…
Reference in a new issue