2019-05-16 10:39:25 -04:00
|
|
|
package tests
|
|
|
|
|
|
|
|
import (
|
2019-06-26 20:21:00 -08:00
|
|
|
"bytes"
|
|
|
|
"context"
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
2019-06-27 04:48:18 -08:00
|
|
|
"io/ioutil"
|
2019-05-16 10:39:25 -04:00
|
|
|
"net/http"
|
2019-06-26 20:21:00 -08:00
|
|
|
"net/http/httptest"
|
2019-05-16 10:39:25 -04:00
|
|
|
"os"
|
2019-06-26 20:21:00 -08:00
|
|
|
"strings"
|
2019-05-16 10:39:25 -04:00
|
|
|
"testing"
|
|
|
|
"time"
|
|
|
|
|
2019-06-27 04:48:18 -08:00
|
|
|
"geeks-accelerator/oss/saas-starter-kit/example-project/cmd/web-api/handlers"
|
2019-06-26 20:21:00 -08:00
|
|
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/account"
|
2019-06-27 04:48:18 -08:00
|
|
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth"
|
|
|
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/tests"
|
|
|
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web"
|
2019-06-26 20:21:00 -08:00
|
|
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/signup"
|
2019-06-27 04:48:18 -08:00
|
|
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/user"
|
2019-06-26 20:21:00 -08:00
|
|
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/user_account"
|
2019-06-27 04:48:18 -08:00
|
|
|
"github.com/google/go-cmp/cmp"
|
2019-06-26 20:21:00 -08:00
|
|
|
"github.com/iancoleman/strcase"
|
|
|
|
"github.com/pborman/uuid"
|
2019-06-27 04:48:18 -08:00
|
|
|
"github.com/pkg/errors"
|
2019-05-16 10:39:25 -04:00
|
|
|
)
|
|
|
|
|
|
|
|
var a http.Handler
|
|
|
|
var test *tests.Test
|
2019-06-27 04:48:18 -08:00
|
|
|
var authenticator *auth.Authenticator
|
2019-05-16 10:39:25 -04:00
|
|
|
|
|
|
|
// Information about the users we have created for testing.
|
2019-06-26 01:16:57 -08:00
|
|
|
type roleTest struct {
|
2019-06-27 04:48:18 -08:00
|
|
|
Role string
|
|
|
|
Token user.Token
|
|
|
|
Claims auth.Claims
|
|
|
|
User mockUser
|
|
|
|
Account *account.Account
|
|
|
|
ForbiddenUser mockUser
|
|
|
|
ForbiddenAccount *account.Account
|
2019-06-26 01:16:57 -08:00
|
|
|
}
|
|
|
|
|
2019-06-26 20:21:00 -08:00
|
|
|
type requestTest struct {
|
2019-06-27 04:48:18 -08:00
|
|
|
name string
|
|
|
|
method string
|
|
|
|
url string
|
|
|
|
request interface{}
|
|
|
|
token user.Token
|
|
|
|
claims auth.Claims
|
|
|
|
statusCode int
|
|
|
|
error interface{}
|
2019-06-26 20:21:00 -08:00
|
|
|
}
|
|
|
|
|
2019-06-26 01:16:57 -08:00
|
|
|
var roleTests map[string]roleTest
|
|
|
|
|
|
|
|
func init() {
|
|
|
|
roleTests = make(map[string]roleTest)
|
|
|
|
}
|
2019-05-16 10:39:25 -04:00
|
|
|
|
|
|
|
// TestMain is the entry point for testing.
|
|
|
|
func TestMain(m *testing.M) {
|
|
|
|
os.Exit(testMain(m))
|
|
|
|
}
|
|
|
|
|
|
|
|
func testMain(m *testing.M) int {
|
|
|
|
test = tests.New()
|
|
|
|
defer test.TearDown()
|
|
|
|
|
2019-06-26 01:16:57 -08:00
|
|
|
now := time.Date(2018, time.October, 1, 0, 0, 0, 0, time.UTC)
|
2019-05-16 10:39:25 -04:00
|
|
|
|
2019-06-27 04:48:18 -08:00
|
|
|
var err error
|
|
|
|
authenticator, err = auth.NewAuthenticatorMemory(now)
|
2019-05-16 10:39:25 -04:00
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
shutdown := make(chan os.Signal, 1)
|
2019-06-27 04:48:18 -08:00
|
|
|
|
|
|
|
log := test.Log
|
|
|
|
log.SetOutput(ioutil.Discard)
|
|
|
|
a = handlers.API(shutdown, log, test.MasterDB, nil, authenticator)
|
2019-06-26 01:16:57 -08:00
|
|
|
|
|
|
|
// Create a new account directly business logic. This creates an
|
|
|
|
// initial account and user that we will use for admin validated endpoints.
|
2019-06-27 04:48:18 -08:00
|
|
|
signupReq1 := mockSignupRequest()
|
|
|
|
signup1, err := signup.Signup(tests.Context(), auth.Claims{}, test.MasterDB, signupReq1, now)
|
2019-06-26 01:16:57 -08:00
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
2019-05-16 10:39:25 -04:00
|
|
|
}
|
|
|
|
|
2019-06-27 04:48:18 -08:00
|
|
|
expires := time.Now().UTC().Sub(signup1.User.CreatedAt) + time.Hour
|
|
|
|
adminTkn, err := user.Authenticate(tests.Context(), test.MasterDB, authenticator, signupReq1.User.Email, signupReq1.User.Password, expires, now)
|
2019-05-16 10:39:25 -04:00
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
2019-06-26 01:16:57 -08:00
|
|
|
adminClaims, err := authenticator.ParseClaims(adminTkn.AccessToken)
|
2019-05-16 10:39:25 -04:00
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
2019-06-27 04:48:18 -08:00
|
|
|
// Create a second account that the first account user should not have access to.
|
|
|
|
signupReq2 := mockSignupRequest()
|
|
|
|
signup2, err := signup.Signup(tests.Context(), auth.Claims{}, test.MasterDB, signupReq2, now)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// First test will be for role Admin
|
2019-06-26 01:16:57 -08:00
|
|
|
roleTests[auth.RoleAdmin] = roleTest{
|
2019-06-27 04:48:18 -08:00
|
|
|
Role: auth.RoleAdmin,
|
|
|
|
Token: adminTkn,
|
|
|
|
Claims: adminClaims,
|
|
|
|
User: mockUser{signup1.User, signupReq1.User.Password},
|
|
|
|
Account: signup1.Account,
|
|
|
|
ForbiddenUser: mockUser{signup2.User, signupReq2.User.Password},
|
|
|
|
ForbiddenAccount: signup2.Account,
|
2019-06-26 01:16:57 -08:00
|
|
|
}
|
2019-05-16 10:39:25 -04:00
|
|
|
|
|
|
|
// Create a regular user to use when calling regular validated endpoints.
|
2019-06-26 01:16:57 -08:00
|
|
|
userReq := user.UserCreateRequest{
|
|
|
|
Name: "Lucas Brown",
|
|
|
|
Email: uuid.NewRandom().String() + "@geeksinthewoods.com",
|
|
|
|
Password: "akTechFr0n!ier",
|
|
|
|
PasswordConfirm: "akTechFr0n!ier",
|
|
|
|
}
|
|
|
|
usr, err := user.Create(tests.Context(), adminClaims, test.MasterDB, userReq, now)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
2019-05-16 10:39:25 -04:00
|
|
|
}
|
|
|
|
|
2019-06-26 20:21:00 -08:00
|
|
|
_, err = user_account.Create(tests.Context(), adminClaims, test.MasterDB, user_account.UserAccountCreateRequest{
|
2019-06-27 04:48:18 -08:00
|
|
|
UserID: usr.ID,
|
|
|
|
AccountID: signup1.Account.ID,
|
|
|
|
Roles: []user_account.UserAccountRole{user_account.UserAccountRole_User},
|
2019-06-26 20:21:00 -08:00
|
|
|
// Status: use default value
|
|
|
|
}, now)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
2019-06-26 01:16:57 -08:00
|
|
|
userTkn, err := user.Authenticate(tests.Context(), test.MasterDB, authenticator, usr.Email, userReq.Password, expires, now)
|
2019-05-16 10:39:25 -04:00
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
2019-06-26 01:16:57 -08:00
|
|
|
userClaims, err := authenticator.ParseClaims(userTkn.AccessToken)
|
2019-05-16 10:39:25 -04:00
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
2019-06-27 04:48:18 -08:00
|
|
|
// Second test will be for role User
|
2019-06-26 01:16:57 -08:00
|
|
|
roleTests[auth.RoleUser] = roleTest{
|
2019-06-27 04:48:18 -08:00
|
|
|
Role: auth.RoleUser,
|
|
|
|
Token: userTkn,
|
|
|
|
Claims: userClaims,
|
|
|
|
Account: signup1.Account,
|
|
|
|
User: mockUser{usr, userReq.Password},
|
|
|
|
ForbiddenUser: mockUser{signup2.User, signupReq2.User.Password},
|
|
|
|
ForbiddenAccount: signup2.Account,
|
2019-06-26 01:16:57 -08:00
|
|
|
}
|
2019-05-16 10:39:25 -04:00
|
|
|
|
|
|
|
return m.Run()
|
|
|
|
}
|
2019-06-26 20:21:00 -08:00
|
|
|
|
2019-06-27 04:48:18 -08:00
|
|
|
// executeRequestTest provides request execution and basic response validation
|
|
|
|
func executeRequestTest(t *testing.T, tt requestTest, ctx context.Context) (*httptest.ResponseRecorder, bool) {
|
|
|
|
var req []byte
|
|
|
|
var rr io.Reader
|
|
|
|
if tt.request != nil {
|
|
|
|
var ok bool
|
|
|
|
req, ok = tt.request.([]byte)
|
|
|
|
if !ok {
|
|
|
|
var err error
|
|
|
|
req, err = json.Marshal(tt.request)
|
|
|
|
if err != nil {
|
|
|
|
t.Logf("\t\tGot err : %+v", err)
|
|
|
|
t.Logf("\t\tEncode request failed.")
|
|
|
|
return nil, false
|
2019-06-26 20:21:00 -08:00
|
|
|
}
|
2019-06-27 04:48:18 -08:00
|
|
|
}
|
|
|
|
rr = bytes.NewReader(req)
|
|
|
|
}
|
2019-06-26 20:21:00 -08:00
|
|
|
|
2019-06-27 04:48:18 -08:00
|
|
|
r := httptest.NewRequest(tt.method, tt.url, rr).WithContext(ctx)
|
2019-06-26 20:21:00 -08:00
|
|
|
|
2019-06-27 04:48:18 -08:00
|
|
|
w := httptest.NewRecorder()
|
2019-06-26 20:21:00 -08:00
|
|
|
|
2019-06-27 04:48:18 -08:00
|
|
|
r.Header.Set("Content-Type", web.MIMEApplicationJSONCharsetUTF8)
|
|
|
|
if tt.token.AccessToken != "" {
|
|
|
|
r.Header.Set("Authorization", tt.token.AuthorizationHeader())
|
|
|
|
}
|
2019-06-26 20:21:00 -08:00
|
|
|
|
2019-06-27 04:48:18 -08:00
|
|
|
a.ServeHTTP(w, r)
|
2019-06-26 20:21:00 -08:00
|
|
|
|
2019-06-27 04:48:18 -08:00
|
|
|
if w.Code != tt.statusCode {
|
|
|
|
t.Logf("\t\tRequest : %s\n", string(req))
|
|
|
|
t.Logf("\t\tBody : %s\n", w.Body.String())
|
|
|
|
t.Logf("\t\tShould receive a status code of %d for the response : %v", tt.statusCode, w.Code)
|
|
|
|
return w, false
|
|
|
|
}
|
2019-06-26 20:21:00 -08:00
|
|
|
|
2019-06-27 04:48:18 -08:00
|
|
|
if tt.error != nil {
|
|
|
|
var actual web.ErrorResponse
|
|
|
|
if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil {
|
|
|
|
t.Logf("\t\tBody : %s\n", w.Body.String())
|
|
|
|
t.Logf("\t\tGot error : %+v", err)
|
|
|
|
t.Logf("\t\tShould get the expected error.")
|
|
|
|
return w, false
|
|
|
|
}
|
2019-06-26 20:21:00 -08:00
|
|
|
|
2019-06-27 04:48:18 -08:00
|
|
|
if diff := cmp.Diff(actual, tt.error); diff != "" {
|
|
|
|
t.Logf("\t\tDiff : %s\n", diff)
|
|
|
|
t.Logf("\t\tShould get the expected error.")
|
|
|
|
return w, false
|
|
|
|
}
|
|
|
|
}
|
2019-06-26 20:21:00 -08:00
|
|
|
|
2019-06-27 04:48:18 -08:00
|
|
|
return w, true
|
|
|
|
}
|
2019-06-26 20:21:00 -08:00
|
|
|
|
2019-06-27 04:48:18 -08:00
|
|
|
// decodeMapToStruct used to covert map to json struct so don't have a bunch of raw json strings running around test files.
|
|
|
|
func decodeMapToStruct(expectedMap map[string]interface{}, expected interface{}) error {
|
|
|
|
expectedJson, err := json.Marshal(expectedMap)
|
|
|
|
if err != nil {
|
|
|
|
return errors.WithStack(err)
|
|
|
|
}
|
2019-06-26 20:21:00 -08:00
|
|
|
|
2019-06-27 04:48:18 -08:00
|
|
|
if err := json.Unmarshal([]byte(expectedJson), &expected); err != nil {
|
|
|
|
return errors.WithStack(err)
|
2019-06-26 20:21:00 -08:00
|
|
|
}
|
2019-06-27 04:48:18 -08:00
|
|
|
|
|
|
|
return nil
|
2019-06-26 20:21:00 -08:00
|
|
|
}
|
|
|
|
|
2019-06-27 04:48:18 -08:00
|
|
|
// cmpDiff prints out the raw json to help with debugging.
|
|
|
|
func cmpDiff(t *testing.T, actual, expected interface{}) bool {
|
|
|
|
if actual == nil && expected == nil {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
if diff := cmp.Diff(actual, expected); diff != "" {
|
|
|
|
actualJSON, err := json.MarshalIndent(actual, "", " ")
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("\t%s\tGot error : %+v", tests.Failed, err)
|
|
|
|
}
|
|
|
|
t.Logf("\t\tGot : %s\n", actualJSON)
|
|
|
|
|
|
|
|
expectedJSON, err := json.MarshalIndent(expected, "", " ")
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("\t%s\tGot error : %+v", tests.Failed, err)
|
|
|
|
}
|
|
|
|
t.Logf("\t\tExpected : %s\n", expectedJSON)
|
|
|
|
|
|
|
|
t.Logf("\t\tDiff : %s\n", diff)
|
|
|
|
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
2019-06-26 20:21:00 -08:00
|
|
|
|
|
|
|
func printResultMap(ctx context.Context, result []byte) {
|
|
|
|
var m map[string]interface{}
|
|
|
|
if err := json.Unmarshal(result, &m); err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
fmt.Println(`map[string]interface{}{`)
|
|
|
|
printResultMapKeys(ctx, m, 1)
|
|
|
|
fmt.Println(`}`)
|
|
|
|
}
|
|
|
|
|
|
|
|
func printResultMapKeys(ctx context.Context, m map[string]interface{}, depth int) {
|
|
|
|
var isEnum bool
|
|
|
|
if m["value"] != nil && m["title"] != nil && m["options"] != nil {
|
|
|
|
isEnum = true
|
|
|
|
}
|
|
|
|
|
|
|
|
for k, kv := range m {
|
|
|
|
fn := strcase.ToCamel(k)
|
|
|
|
|
|
|
|
switch k {
|
|
|
|
case "created_at", "updated_at", "archived_at":
|
|
|
|
pv := fmt.Sprintf("web.NewTimeResponse(ctx, actual.%s)", fn)
|
|
|
|
fmt.Printf("%s\"%s\": %s,\n", strings.Repeat("\t", depth), k, pv)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
if sm, ok := kv.([]map[string]interface{}); ok {
|
|
|
|
fmt.Printf("%s\"%s\": []map[string]interface{}{\n", strings.Repeat("\t", depth), k)
|
|
|
|
|
|
|
|
for _, smv := range sm {
|
2019-06-27 04:48:18 -08:00
|
|
|
printResultMapKeys(ctx, smv, depth+1)
|
2019-06-26 20:21:00 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
fmt.Printf("%s},\n", strings.Repeat("\t", depth))
|
|
|
|
} else if sm, ok := kv.(map[string]interface{}); ok {
|
|
|
|
fmt.Printf("%s\"%s\": map[string]interface{}{\n", strings.Repeat("\t", depth), k)
|
2019-06-27 04:48:18 -08:00
|
|
|
printResultMapKeys(ctx, sm, depth+1)
|
2019-06-26 20:21:00 -08:00
|
|
|
fmt.Printf("%s},\n", strings.Repeat("\t", depth))
|
|
|
|
} else {
|
|
|
|
var pv string
|
|
|
|
if isEnum {
|
|
|
|
jv, _ := json.Marshal(kv)
|
|
|
|
pv = string(jv)
|
|
|
|
} else {
|
|
|
|
pv = fmt.Sprintf("req.%s", fn)
|
|
|
|
}
|
|
|
|
|
|
|
|
fmt.Printf("%s\"%s\": %s,\n", strings.Repeat("\t", depth), k, pv)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|