diff --git a/internal/module/account/account_test.go b/internal/module/account/account_test.go index e84348e63..60dfd5bef 100644 --- a/internal/module/account/account_test.go +++ b/internal/module/account/account_test.go @@ -20,6 +20,9 @@ package account import ( "context" + "encoding/json" + "fmt" + "io/ioutil" "net/http" "net/http/httptest" "net/url" @@ -27,62 +30,49 @@ import ( "time" "github.com/gin-gonic/gin" + "github.com/google/uuid" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/db/model" "github.com/superseriousbusiness/gotosocial/internal/oauth" + "github.com/superseriousbusiness/gotosocial/pkg/mastotypes" "github.com/superseriousbusiness/oauth2/v4" + "github.com/superseriousbusiness/oauth2/v4/models" oauthmodels "github.com/superseriousbusiness/oauth2/v4/models" - "github.com/sirupsen/logrus" - "github.com/stretchr/testify/suite" + "golang.org/x/crypto/bcrypt" ) type AccountTestSuite struct { suite.Suite - log *logrus.Logger - testAccountLocal *model.Account - testAccountRemote *model.Account - testUser *model.User - testApplication *model.Application - testToken oauth2.TokenInfo - db db.DB - accountModule *accountModule + config *config.Config + log *logrus.Logger + testAccountLocal *model.Account + testAccountRemote *model.Account + testUser *model.User + testApplication *model.Application + testToken oauth2.TokenInfo + mockOauthServer *oauth.MockServer + db db.DB + accountModule *accountModule + newUserFormHappyPath url.Values } +/* + TEST INFRASTRUCTURE +*/ + // SetupSuite sets some variables on the suite that we can use as consts (more or less) throughout func (suite *AccountTestSuite) SetupSuite() { + // some of our subsequent entities need a log so create this here log := logrus.New() log.SetLevel(logrus.TraceLevel) suite.log = log - c := config.Empty() - c.DBConfig = &config.DBConfig{ - Type: "postgres", - Address: "localhost", - Port: 5432, - User: "postgres", - Password: "postgres", - Database: "postgres", - ApplicationName: "gotosocial", - } - c.AccountsConfig = &config.AccountsConfig{ - OpenRegistration: true, - RequireApproval: true, - ReasonRequired: true, - } - - database, err := db.New(context.Background(), c, log) - if err != nil { - suite.FailNow(err.Error()) - } - suite.db = database - - suite.accountModule = &accountModule{ - config: c, - db: database, - log: log, - } - + // can use this test application throughout suite.testApplication = &model.Application{ ID: "weeweeeeeeeeeeeeee", Name: "a test application", @@ -94,6 +84,7 @@ func (suite *AccountTestSuite) SetupSuite() { VapidKey: "aaaaaa-aaaaaaaa-aaaaaaaaaaa", } + // can use this test token throughout suite.testToken = &oauthmodels.Token{ ClientID: "a-known-client-id", RedirectURI: "http://localhost:8080", @@ -103,97 +94,48 @@ func (suite *AccountTestSuite) SetupSuite() { CodeExpiresIn: time.Duration(10 * time.Minute), } - // encryptedPassword, err := bcrypt.GenerateFromPassword([]byte("password"), bcrypt.DefaultCost) - // if err != nil { - // logrus.Panicf("error encrypting user pass: %s", err) - // } + // Direct config to local postgres instance + c := config.Empty() + c.DBConfig = &config.DBConfig{ + Type: "postgres", + Address: "localhost", + Port: 5432, + User: "postgres", + Password: "postgres", + Database: "postgres", + ApplicationName: "gotosocial", + } + // Default accountsconfig + c.AccountsConfig = &config.AccountsConfig{ + OpenRegistration: true, + RequireApproval: true, + ReasonRequired: true, + } + suite.config = c - // localAvatar, err := url.Parse("https://localhost:8080/media/aaaaaaaaa.png") - // if err != nil { - // logrus.Panicf("error parsing localavatar url: %s", err) - // } - // localHeader, err := url.Parse("https://localhost:8080/media/ffffffffff.png") - // if err != nil { - // logrus.Panicf("error parsing localheader url: %s", err) - // } + // use an actual database for this, because it's just easier than mocking one out + database, err := db.New(context.Background(), c, log) + if err != nil { + suite.FailNow(err.Error()) + } + suite.db = database - // acctID := uuid.NewString() - // suite.testAccountLocal = &model.Account{ - // ID: acctID, - // Username: "local_account_of_some_kind", - // AvatarRemoteURL: localAvatar, - // HeaderRemoteURL: localHeader, - // DisplayName: "michael caine", - // Fields: []model.Field{ - // { - // Name: "come and ave a go", - // Value: "if you think you're hard enough", - // }, - // { - // Name: "website", - // Value: "https://imdb.com", - // VerifiedAt: time.Now(), - // }, - // }, - // Note: "My name is Michael Caine and i'm a local user.", - // Discoverable: true, - // } + // we need to mock the oauth server because account creation needs it to create a new token + suite.mockOauthServer = &oauth.MockServer{} + suite.mockOauthServer.On("GenerateUserAccessToken", suite.testToken, suite.testApplication.ClientSecret, mock.AnythingOfType("string")).Run(func(args mock.Arguments) { + l := suite.log.WithField("func", "GenerateUserAccessToken") + token := args.Get(0).(oauth2.TokenInfo) + l.Infof("received token %+v", token) + clientSecret := args.Get(1).(string) + l.Infof("received clientSecret %+v", clientSecret) + userID := args.Get(2).(string) + l.Infof("received userID %+v", userID) + }).Return(&models.Token{ + Code: "we're authorized now!", + }, nil) - // avatarURL, err := url.Parse("http://example.org/accounts/avatars/000/207/122/original/089-1098-09.png") - // if err != nil { - // logrus.Panicf("error parsing avatarURL: %s", err) - // } - - // headerURL, err := url.Parse("http://example.org/accounts/headers/000/207/122/original/111111111111.png") - // if err != nil { - // logrus.Panicf("error parsing avatarURL: %s", err) - // } - // suite.testAccountRemote = &model.Account{ - // ID: uuid.NewString(), - // Username: "neato_bombeato", - // Domain: "example.org", - - // AvatarFileName: "avatar.png", - // AvatarContentType: "image/png", - // AvatarFileSize: 1024, - // AvatarUpdatedAt: time.Now(), - // AvatarRemoteURL: avatarURL, - - // HeaderFileName: "avatar.png", - // HeaderContentType: "image/png", - // HeaderFileSize: 1024, - // HeaderUpdatedAt: time.Now(), - // HeaderRemoteURL: headerURL, - - // DisplayName: "one cool dude 420", - // Fields: []model.Field{ - // { - // Name: "pronouns", - // Value: "he/they", - // }, - // { - // Name: "website", - // Value: "https://imcool.edu", - // VerifiedAt: time.Now(), - // }, - // }, - // Note: "
I'm cool as heck!
", - // Discoverable: true, - // URI: "https://example.org/users/neato_bombeato", - // URL: "https://example.org/@neato_bombeato", - // LastWebfingeredAt: time.Now(), - // InboxURL: "https://example.org/users/neato_bombeato/inbox", - // OutboxURL: "https://example.org/users/neato_bombeato/outbox", - // SharedInboxURL: "https://example.org/inbox", - // FollowersURL: "https://example.org/users/neato_bombeato/followers", - // FeaturedCollectionURL: "https://example.org/users/neato_bombeato/collections/featured", - // } - // suite.testUser = &model.User{ - // ID: uuid.NewString(), - // EncryptedPassword: string(encryptedPassword), - // Email: "user@example.org", - // AccountID: acctID, - // } + // and finally here's the thing we're actually testing! + suite.accountModule = New(suite.config, suite.db, suite.mockOauthServer, suite.log).(*accountModule) } func (suite *AccountTestSuite) TearDownSuite() { @@ -218,6 +160,16 @@ func (suite *AccountTestSuite) SetupTest() { logrus.Panicf("db connection error: %s", err) } } + + // form to submit for happy path account create requests -- this will be changed inside tests so it's better to set it before each test + suite.newUserFormHappyPath = url.Values{ + "reason": []string{"a very good reason that's at least 40 characters i swear"}, + "username": []string{"test_user"}, + "email": []string{"user@example.org"}, + "password": []string{"very-strong-password"}, + "agreement": []string{"true"}, + "locale": []string{"en"}, + } } // TearDownTest drops tables to make sure there's no data in the db @@ -237,24 +189,86 @@ func (suite *AccountTestSuite) TearDownTest() { } } -func (suite *AccountTestSuite) TestAccountCreatePOSTHandler() { - // TODO: figure out how to test this properly +/* + ACTUAL TESTS +*/ + +// TestAccountCreatePOSTHandlerSuccessful checks the happy path for an account creation request: all the fields provided are valid, +// and at the end of it a new user and account should be added into the database. +// +// This is the handler served at /api/v1/accounts as POST +func (suite *AccountTestSuite) TestAccountCreatePOSTHandlerSuccessful() { + + // setup recorder := httptest.NewRecorder() - recorder.Header().Set("X-Forwarded-For", "127.0.0.1") - recorder.Header().Set("Content-Type", "application/json") ctx, _ := gin.CreateTestContext(recorder) ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplication) ctx.Set(oauth.SessionAuthorizedToken, suite.testToken) - ctx.Request = httptest.NewRequest(http.MethodPost, "http://localhost:8080/api/v1/accounts", nil) - ctx.Request.Form = url.Values{ - "reason": []string{"a very good reason that's at least 40 characters i swear"}, - "username": []string{"test_user"}, - "email": []string{"user@example.org"}, - "password": []string{"very-strong-password"}, - "agreement": []string{"true"}, - "locale": []string{"en"}, - } + ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", basePath), nil) // the endpoint we're hitting + ctx.Request.Form = suite.newUserFormHappyPath suite.accountModule.accountCreatePOSTHandler(ctx) + + // check response + + // 1. we should have OK from our call to the function + suite.EqualValues(http.StatusOK, recorder.Code) + + // 2. we should have a token in the result body + result := recorder.Result() + defer result.Body.Close() + b, err := ioutil.ReadAll(result.Body) + assert.NoError(suite.T(), err) + t := &mastotypes.Token{} + err = json.Unmarshal(b, t) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), "we're authorized now!", t.AccessToken) + + // check new account + + // 1. we should be able to get the new account from the db + acct := &model.Account{} + err = suite.db.GetWhere("username", "test_user", acct) + assert.NoError(suite.T(), err) + assert.NotNil(suite.T(), acct) + // 2. reason should be set + assert.Equal(suite.T(), suite.newUserFormHappyPath.Get("reason"), acct.Reason) + // 3. display name should be equal to username by default + assert.Equal(suite.T(), suite.newUserFormHappyPath.Get("username"), acct.DisplayName) + // 4. domain should be nil because this is a local account + assert.Nil(suite.T(), nil, acct.Domain) + // 5. id should be set and parseable as a uuid + assert.NotNil(suite.T(), acct.ID) + _, err = uuid.Parse(acct.ID) + assert.Nil(suite.T(), err) + // 6. private and public key should be set + assert.NotNil(suite.T(), acct.PrivateKey) + assert.NotNil(suite.T(), acct.PublicKey) + + // check new user + + // 1. we should be able to get the new user from the db + usr := &model.User{} + err = suite.db.GetWhere("unconfirmed_email", suite.newUserFormHappyPath.Get("email"), usr) + assert.Nil(suite.T(), err) + assert.NotNil(suite.T(), usr) + + // 2. user should have account id set to account we got above + assert.Equal(suite.T(), acct.ID, usr.AccountID) + + // 3. id should be set and parseable as a uuid + assert.NotNil(suite.T(), usr.ID) + _, err = uuid.Parse(usr.ID) + assert.Nil(suite.T(), err) + + // 4. locale should be equal to what we requested + assert.Equal(suite.T(), suite.newUserFormHappyPath.Get("locale"), usr.Locale) + + // 5. created by application id should be equal to the app id + assert.Equal(suite.T(), suite.testApplication.ID, usr.CreatedByApplicationID) + + // 6. password should be matcheable to what we set above + err = bcrypt.CompareHashAndPassword([]byte(usr.EncryptedPassword), []byte(suite.newUserFormHappyPath.Get("password"))) + assert.Nil(suite.T(), err) } func TestAccountTestSuite(t *testing.T) {