mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2024-11-22 11:46:40 +00:00
start working on struct validation for gtsmodel
This commit is contained in:
parent
7d193de25f
commit
d2276fc553
|
@ -20,19 +20,14 @@
|
||||||
|
|
||||||
import "time"
|
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 {
|
type StatusMute struct {
|
||||||
// id of this mute in the database
|
ID string `validate:"required,ulid" bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database
|
||||||
ID string `bun:"type:CHAR(26),pk,notnull,unique"`
|
CreatedAt time.Time `validate:"required" bun:",nullzero,notnull,default:current_timestamp"` // when was item created
|
||||||
// when was this mute created
|
AccountID string `validate:"required,ulid" bun:"type:CHAR(26),nullzero,notnull"` // id of the account that created ('did') the mute
|
||||||
CreatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"`
|
Account *Account `validate:"-" bun:"rel:belongs-to"` // pointer to the account specified by accountID
|
||||||
// id of the account that created ('did') the mute
|
TargetAccountID string `validate:"required,ulid" bun:"type:CHAR(26),nullzero,notnull"` // id the account owning the muted status (can be the same as accountID)
|
||||||
AccountID string `bun:"type:CHAR(26),notnull"`
|
TargetAccount *Account `validate:"-" bun:"rel:belongs-to"` // pointer to the account specified by targetAccountID
|
||||||
Account *Account `bun:"rel:belongs-to"`
|
StatusID string `validate:"required,ulid" bun:"type:CHAR(26),nullzero,notnull"` // database id of the status that has been muted
|
||||||
// id the account owning the muted status (can be the same as accountID)
|
Status *Status `validate:"-" bun:"rel:belongs-to"` // pointer to the muted status specified by statusID
|
||||||
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"`
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,24 +20,15 @@
|
||||||
|
|
||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
// Tag represents a hashtag for gathering public statuses together
|
// Tag represents a hashtag for gathering public statuses together.
|
||||||
type Tag struct {
|
type Tag struct {
|
||||||
// id of this tag in the database
|
ID string `validate:"required,ulid" bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database
|
||||||
ID string `bun:",unique,type:CHAR(26),pk,notnull"`
|
CreatedAt time.Time `validate:"required" bun:",nullzero,notnull,default:current_timestamp"` // when was item created
|
||||||
// Href of this tag, eg https://example.org/tags/somehashtag
|
UpdatedAt time.Time `validate:"required" bun:",nullzero,notnull,default:current_timestamp"` // when was item last updated
|
||||||
URL string `bun:",nullzero"`
|
URL string `validate:"required,url" bun:",nullzero,notnull"` // Href of this tag, eg https://example.org/tags/somehashtag
|
||||||
// name of this tag -- the tag without the hash part
|
Name string `validate:"required" bun:",unique,nullzero,notnull"` // name of this tag -- the tag without the hash part
|
||||||
Name string `bun:",unique,notnull"`
|
FirstSeenFromAccountID string `validate:"ulid" bun:"type:CHAR(26),nullzero"` // Which account ID is the first one we saw using this tag?
|
||||||
// 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?
|
||||||
FirstSeenFromAccountID string `bun:"type:CHAR(26),nullzero"`
|
Listable bool `validate:"-" bun:",nullzero,notnull,default:true"` // can our instance users look up this tag?
|
||||||
// when was this tag created
|
LastStatusAt time.Time `validate:"required" bun:",nullzero,notnull,default:current_timestamp"` // when was this tag last used?
|
||||||
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"`
|
|
||||||
}
|
}
|
||||||
|
|
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 @@
|
||||||
// User represents an actual human user of gotosocial. Note, this is a LOCAL gotosocial user, not a remote account.
|
// 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.
|
// To cross reference this local user with their account (which can be local or remote), use the AccountID field.
|
||||||
type User struct {
|
type User struct {
|
||||||
/*
|
ID string `validate:"required,ulid" bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database
|
||||||
BASIC INFO
|
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
|
EncryptedOTPSecret string `validate:"-" bun:",nullzero"`
|
||||||
ID string `bun:"type:CHAR(26),pk,notnull,unique"`
|
EncryptedOTPSecretIv string `validate:"-" bun:",nullzero"`
|
||||||
// confirmed email address for this user, this should be unique -- only one email address registered per instance, multiple users per email are not supported
|
EncryptedOTPSecretSalt string `validate:"-" bun:",nullzero"`
|
||||||
Email string `bun:"default:null,unique,nullzero"`
|
OTPRequiredForLogin bool `validate:"-" bun:",nullzero"`
|
||||||
// The id of the local gtsmodel.Account entry for this user, if it exists (unconfirmed users don't have an account yet)
|
OTPBackupCodes []string `validate:"-" bun:",nullzero"`
|
||||||
AccountID string `bun:"type:CHAR(26),unique,nullzero"`
|
ConsumedTimestamp int `validate:"-" bun:",nullzero"`
|
||||||
Account *Account `bun:"rel:belongs-to"`
|
RememberToken string `validate:"-" bun:",nullzero"`
|
||||||
// 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
|
SignInToken string `validate:"-" bun:",nullzero"`
|
||||||
EncryptedPassword string `bun:",notnull"`
|
SignInTokenSentAt time.Time `validate:"-" bun:",nullzero"`
|
||||||
|
WebauthnID string `validate:"-" bun:",nullzero"`
|
||||||
/*
|
|
||||||
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"`
|
|
||||||
}
|
}
|
||||||
|
|
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 @@
|
||||||
|
|
||||||
const randomRange = 631152381 // ~20 years in seconds
|
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.
|
// NewULID returns a new ULID string using the current time, or an error if something goes wrong.
|
||||||
func NewULID() (string, error) {
|
func NewULID() (string, error) {
|
||||||
newUlid, err := ulid.New(ulid.Timestamp(time.Now()), rand.Reader)
|
newUlid, err := ulid.New(ulid.Timestamp(time.Now()), rand.Reader)
|
||||||
|
|
|
@ -90,6 +90,7 @@
|
||||||
followPathRegex = regexp.MustCompile(followPathRegexString)
|
followPathRegex = regexp.MustCompile(followPathRegexString)
|
||||||
|
|
||||||
ulidRegexString = `[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}`
|
ulidRegexString = `[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}`
|
||||||
|
ulidRegex = regexp.MustCompile(fmt.Sprintf(`^%s$`, ulidRegexString))
|
||||||
|
|
||||||
likedPathRegexString = fmt.Sprintf(`^/?%s/(%s)/%s$`, UsersPath, usernameRegexString, LikedPath)
|
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
|
// 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
|
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