1
0
mirror of https://github.com/raseels-repos/golang-saas-starter-kit.git synced 2025-12-21 23:57:41 +02:00

checkpoint

This commit is contained in:
Lee Brown
2019-06-26 20:21:00 -08:00
parent b68bcf2c2c
commit 48ae19bd6a
23 changed files with 1952 additions and 859 deletions

View File

@@ -0,0 +1,339 @@
package tests
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strconv"
"testing"
"time"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/mid"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/account"
"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"
"github.com/google/go-cmp/cmp"
"github.com/pborman/uuid"
)
func mockAccount() *account.Account {
req := account.AccountCreateRequest{
Name: uuid.NewRandom().String(),
Address1: "103 East Main St",
Address2: "Unit 546",
City: "Valdez",
Region: "AK",
Country: "USA",
Zipcode: "99686",
}
a, err := account.Create(tests.Context(), auth.Claims{}, test.MasterDB, req, time.Now().UTC().AddDate(-1, -1, -1))
if err != nil {
panic(err)
}
return a
}
// TestAccount is the entry point for the account endpoints.
func TestAccount(t *testing.T) {
defer tests.Recover(t)
t.Run("getAccount", getAccount)
t.Run("patchAccount", patchAccount)
}
// getAccount validates get account by ID endpoint.
func getAccount(t *testing.T) {
var rtests []requestTest
forbiddenAccount := mockAccount()
// Both roles should be able to read the account.
for rn, tr := range roleTests {
acc := tr.SignupResult.Account
// Test 200.
rtests = append(rtests, requestTest{
fmt.Sprintf("Role %s 200", rn),
http.MethodGet,
fmt.Sprintf("/v1/accounts/%s", acc.ID),
nil,
tr.Token,
tr.Claims,
http.StatusOK,
nil,
func(treq requestTest, body []byte) bool {
var actual account.AccountResponse
if err := json.Unmarshal(body, &actual); err != nil {
t.Logf("\t\tGot error : %+v", err)
return false
}
// Add claims to the context so they can be retrieved later.
ctx := context.WithValue(tests.Context(), auth.Key, tr.Claims)
expectedMap := map[string]interface{}{
"updated_at": web.NewTimeResponse(ctx, acc.UpdatedAt),
"id": acc.ID,
"address2": acc.Address2,
"region": acc.Region,
"zipcode": acc.Zipcode,
"timezone": acc.Timezone,
"created_at": web.NewTimeResponse(ctx, acc.CreatedAt),
"country": acc.Country,
"billing_user_id": &acc.BillingUserID,
"name": acc.Name,
"address1": acc.Address1,
"city": acc.City,
"status": map[string]interface{}{
"value": "active",
"title": "Active",
"options": []map[string]interface{}{{"selected": false, "title": "[Active Pending Disabled]", "value": "[active pending disabled]"}},
},
"signup_user_id": &acc.SignupUserID,
}
expectedJson, err := json.Marshal(expectedMap)
if err != nil {
t.Logf("\t\tGot error : %+v", err)
return false
}
var expected account.AccountResponse
if err := json.Unmarshal([]byte(expectedJson), &expected); err != nil {
t.Logf("\t\tGot error : %+v", err)
printResultMap(ctx, body)
return false
}
if diff := cmp.Diff(actual, expected); diff != "" {
actualJSON, err := json.MarshalIndent(actual, "", " ")
if err != nil {
t.Logf("\t\tGot error : %+v", err)
return false
}
t.Logf("\t\tGot : %s\n", actualJSON)
expectedJSON, err := json.MarshalIndent(expected, "", " ")
if err != nil {
t.Logf("\t\tGot error : %+v", err)
return false
}
t.Logf("\t\tExpected : %s\n", expectedJSON)
t.Logf("\t\tDiff : %s\n", diff)
if len(expectedMap) == 0 {
printResultMap(ctx, body)
}
return false
}
return true
},
})
// Test 404.
invalidID := uuid.NewRandom().String()
rtests = append(rtests, requestTest{
fmt.Sprintf("Role %s 404 w/invalid ID", rn),
http.MethodGet,
fmt.Sprintf("/v1/accounts/%s", invalidID),
nil,
tr.Token,
tr.Claims,
http.StatusNotFound,
web.ErrorResponse{
Error: fmt.Sprintf("account %s not found: Entity not found", invalidID),
},
func(treq requestTest, body []byte) bool {
return true
},
})
// Test 404 - Account exists but not allowed.
rtests = append(rtests, requestTest{
fmt.Sprintf("Role %s 404 w/random account ID", rn),
http.MethodGet,
fmt.Sprintf("/v1/accounts/%s", forbiddenAccount.ID),
nil,
tr.Token,
tr.Claims,
http.StatusNotFound,
web.ErrorResponse{
Error: fmt.Sprintf("account %s not found: Entity not found", forbiddenAccount.ID),
},
func(treq requestTest, body []byte) bool {
return true
},
})
}
runRequestTests(t, rtests)
}
// patchAccount validates update account by ID endpoint.
func patchAccount(t *testing.T) {
var rtests []requestTest
// Test update an account
// Admin role: 204
// User role 403
for rn, tr := range roleTests {
var expectedStatus int
var expectedErr interface{}
// Test 204.
if rn == auth.RoleAdmin {
expectedStatus = http.StatusNoContent
} else {
expectedStatus = http.StatusForbidden
expectedErr = web.ErrorResponse{
Error: mid.ErrForbidden.Error(),
}
}
newName := rn + uuid.NewRandom().String() + strconv.Itoa(len(rtests))
rtests = append(rtests, requestTest{
fmt.Sprintf("Role %s %d", rn, expectedStatus),
http.MethodPatch,
"/v1/accounts",
account.AccountUpdateRequest{
ID: tr.SignupResult.Account.ID,
Name: &newName,
},
tr.Token,
tr.Claims,
expectedStatus,
expectedErr,
func(treq requestTest, body []byte) bool {
return true
},
})
}
// Test update an account with invalid data.
// Admin role: 400
// User role 400
for rn, tr := range roleTests {
var expectedStatus int
var expectedErr interface{}
if rn == auth.RoleAdmin {
expectedStatus = http.StatusBadRequest
expectedErr = web.ErrorResponse{
Error: "field validation error",
Fields: []web.FieldError{
{Field: "status", Error: "Key: 'AccountUpdateRequest.status' Error:Field validation for 'status' failed on the 'oneof' tag"},
},
}
} else {
expectedStatus = http.StatusForbidden
expectedErr = web.ErrorResponse{
Error: mid.ErrForbidden.Error(),
}
}
invalidStatus := account.AccountStatus("invalid status")
rtests = append(rtests, requestTest{
fmt.Sprintf("Role %s %d w/invalid data", rn, expectedStatus),
http.MethodPatch,
"/v1/accounts",
account.AccountUpdateRequest{
ID: tr.SignupResult.User.ID,
Status: &invalidStatus,
},
tr.Token,
tr.Claims,
expectedStatus,
expectedErr,
func(treq requestTest, body []byte) bool {
return true
},
})
}
// Test update an account for with an invalid ID.
// Admin role: 403
// User role 403
for rn, tr := range roleTests {
var expectedStatus int
var expectedErr interface{}
// Test 403.
if rn == auth.RoleAdmin {
expectedStatus = http.StatusForbidden
expectedErr = web.ErrorResponse{
Error: account.ErrForbidden.Error(),
}
} else {
expectedStatus = http.StatusForbidden
expectedErr = web.ErrorResponse{
Error: mid.ErrForbidden.Error(),
}
}
newName := rn + uuid.NewRandom().String() + strconv.Itoa(len(rtests))
invalidID := uuid.NewRandom().String()
rtests = append(rtests, requestTest{
fmt.Sprintf("Role %s %d w/invalid ID", rn, expectedStatus),
http.MethodPatch,
"/v1/accounts",
account.AccountUpdateRequest{
ID: invalidID,
Name: &newName,
},
tr.Token,
tr.Claims,
expectedStatus,
expectedErr,
func(treq requestTest, body []byte) bool {
return true
},
})
}
// Test update an account for with random account ID.
// Admin role: 403
// User role 403
forbiddenAccount := mockAccount()
for rn, tr := range roleTests {
var expectedStatus int
var expectedErr interface{}
// Test 403.
if rn == auth.RoleAdmin {
expectedStatus = http.StatusForbidden
expectedErr = web.ErrorResponse{
Error: account.ErrForbidden.Error(),
}
} else {
expectedStatus = http.StatusForbidden
expectedErr = web.ErrorResponse{
Error: mid.ErrForbidden.Error(),
}
}
newName := rn+uuid.NewRandom().String()+strconv.Itoa(len(rtests))
rtests = append(rtests, requestTest{
fmt.Sprintf("Role %s %d w/random account ID", rn, expectedStatus),
http.MethodPatch,
"/v1/accounts",
account.AccountUpdateRequest{
ID: forbiddenAccount.ID,
Name: &newName,
},
tr.Token,
tr.Claims,
expectedStatus,
expectedErr,
func(treq requestTest, body []byte) bool {
return true
},
})
}
runRequestTests(t, rtests)
}

View File

@@ -0,0 +1,185 @@
package tests
import (
"encoding/json"
"net/http"
"testing"
"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"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/signup"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/user"
"github.com/google/go-cmp/cmp"
"github.com/pborman/uuid"
)
func mockSignupRequest() signup.SignupRequest {
return signup.SignupRequest{
Account: signup.SignupAccount{
Name: uuid.NewRandom().String(),
Address1: "103 East Main St",
Address2: "Unit 546",
City: "Valdez",
Region: "AK",
Country: "USA",
Zipcode: "99686",
},
User: signup.SignupUser{
Name: "Lee Brown",
Email: uuid.NewRandom().String() + "@geeksinthewoods.com",
Password: "akTechFr0n!ier",
PasswordConfirm: "akTechFr0n!ier",
},
}
}
// TestSignup is the entry point for the signup
func TestSignup(t *testing.T) {
defer tests.Recover(t)
t.Run("postSigup", postSigup)
}
// postSigup validates the signup endpoint.
func postSigup(t *testing.T) {
var rtests []requestTest
// Test 201.
// Signup does not require auth, so empty token and claims should result in success.
req1 := mockSignupRequest()
rtests = append(rtests, requestTest{
"No Authorization Valid",
http.MethodPost,
"/v1/signup",
req1,
user.Token{},
auth.Claims{},
http.StatusCreated,
nil,
func(treq requestTest, body []byte) bool {
var actual signup.SignupResponse
if err := json.Unmarshal(body, &actual); err != nil {
t.Logf("\t\tGot error : %+v", err)
return false
}
ctx := tests.Context()
req := treq.request.(signup.SignupRequest )
expectedMap := map[string]interface{}{
"user": map[string]interface{}{
"id": actual.User.ID,
"name": req.User.Name,
"email": req.User.Email,
"timezone": actual.User.Timezone,
"created_at": web.NewTimeResponse(ctx, actual.User.CreatedAt.Value),
"updated_at": web.NewTimeResponse(ctx, actual.User.UpdatedAt.Value),
},
"account": map[string]interface{}{
"updated_at": web.NewTimeResponse(ctx, actual.Account.UpdatedAt.Value),
"id": actual.Account.ID,
"address2": req.Account.Address2,
"region": req.Account.Region,
"zipcode": req.Account.Zipcode,
"timezone": actual.Account.Timezone,
"created_at": web.NewTimeResponse(ctx, actual.Account.CreatedAt.Value),
"country": req.Account.Country,
"billing_user_id": &actual.Account.BillingUserID,
"name": req.Account.Name,
"address1": req.Account.Address1,
"city": req.Account.City,
"status": map[string]interface{}{
"value": "active",
"title": "Active",
"options": []map[string]interface{}{{"selected":false,"title":"[Active Pending Disabled]","value":"[active pending disabled]"}},
},
"signup_user_id": &actual.Account.SignupUserID,
},
}
expectedJson, err := json.Marshal(expectedMap)
if err != nil {
t.Logf("\t\tGot error : %+v", err)
return false
}
var expected signup.SignupResponse
if err := json.Unmarshal([]byte(expectedJson), &expected); err != nil {
t.Logf("\t\tGot error : %+v", err)
printResultMap(ctx, body)
return false
}
if diff := cmp.Diff(actual, expected); diff != "" {
actualJSON, err := json.MarshalIndent(actual, "", " ")
if err != nil {
t.Logf("\t\tGot error : %+v", err)
return false
}
t.Logf("\t\tGot : %s\n", actualJSON)
expectedJSON, err := json.MarshalIndent(expected, "", " ")
if err != nil {
t.Logf("\t\tGot error : %+v", err)
return false
}
t.Logf("\t\tExpected : %s\n", expectedJSON)
t.Logf("\t\tDiff : %s\n", diff)
if len(expectedMap) == 0 {
printResultMap(ctx, body)
}
return false
}
return true
},
})
// Test 404 w/empty request.
rtests = append(rtests, requestTest{
"Empty request",
http.MethodPost,
"/v1/signup",
nil,
user.Token{},
auth.Claims{},
http.StatusBadRequest,
web.ErrorResponse{
Error: "decode request body failed: EOF",
},
func(req requestTest, body []byte) bool {
return true
},
})
// Test 404 w/validation errors.
invalidReq := mockSignupRequest()
invalidReq.User.Email = ""
invalidReq.Account.Name = ""
rtests = append(rtests, requestTest{
"Invalid request",
http.MethodPost,
"/v1/signup",
invalidReq,
user.Token{},
auth.Claims{},
http.StatusBadRequest,
web.ErrorResponse{
Error: "field validation error",
Fields: []web.FieldError{
{Field: "name", Error: "Key: 'SignupRequest.account.name' Error:Field validation for 'name' failed on the 'required' tag"},
{Field: "email", Error: "Key: 'SignupRequest.user.email' Error:Field validation for 'email' failed on the 'required' tag"},
},
},
func(req requestTest, body []byte) bool {
return true
},
})
runRequestTests(t, rtests)
}

View File

@@ -1,14 +1,25 @@
package tests
import (
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/account"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/signup"
"github.com/pborman/uuid"
"bytes"
"context"
"encoding/json"
"fmt"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web"
"github.com/google/go-cmp/cmp"
"io"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
"time"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/account"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/signup"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/user_account"
"github.com/iancoleman/strcase"
"github.com/pborman/uuid"
"geeks-accelerator/oss/saas-starter-kit/example-project/cmd/web-api/handlers"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/tests"
@@ -23,11 +34,23 @@ type roleTest struct {
Token user.Token
Claims auth.Claims
SignupRequest *signup.SignupRequest
SignupResponse *signup.SignupResponse
SignupResult *signup.SignupResult
User *user.User
Account *account.Account
}
type requestTest struct {
name string
method string
url string
request interface{}
token user.Token
claims auth.Claims
statusCode int
error interface{}
expected func(req requestTest, result []byte) bool
}
var roleTests map[string]roleTest
func init() {
@@ -92,7 +115,7 @@ func testMain(m *testing.M) int {
Token: adminTkn,
Claims: adminClaims,
SignupRequest: &signupReq,
SignupResponse: signup,
SignupResult: signup,
User: signup.User,
Account: signup.Account,
}
@@ -109,6 +132,16 @@ func testMain(m *testing.M) int {
panic(err)
}
_, err = user_account.Create(tests.Context(), adminClaims, test.MasterDB, user_account.UserAccountCreateRequest{
UserID: usr.ID,
AccountID: signup.Account.ID,
Roles: []user_account.UserAccountRole{user_account.UserAccountRole_User},
// Status: use default value
}, now)
if err != nil {
panic(err)
}
userTkn, err := user.Authenticate(tests.Context(), test.MasterDB, authenticator, usr.Email, userReq.Password, expires, now)
if err != nil {
panic(err)
@@ -123,10 +156,130 @@ func testMain(m *testing.M) int {
Token: userTkn,
Claims: userClaims,
SignupRequest: &signupReq,
SignupResponse: signup,
SignupResult: signup,
Account: signup.Account,
User: usr,
}
return m.Run()
}
// runRequestTests helper function for testing endpoints.
func runRequestTests(t *testing.T, rtests []requestTest ) {
for i, tt := range rtests {
t.Logf("\tTest: %d\tWhen running test: %s", i, tt.name)
{
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.Fatalf("\t%s\tEncode request failed.", tests.Failed)
}
}
rr = bytes.NewReader(req)
}
r := httptest.NewRequest(tt.method, tt.url , rr)
w := httptest.NewRecorder()
r.Header.Set("Content-Type", web.MIMEApplicationJSONCharsetUTF8)
if tt.token.AccessToken != "" {
r.Header.Set("Authorization", tt.token.AuthorizationHeader())
}
a.ServeHTTP(w, r)
if w.Code != tt.statusCode {
t.Logf("\t\tRequest : %s\n", string(req))
t.Logf("\t\tBody : %s\n", w.Body.String())
t.Fatalf("\t%s\tShould receive a status code of %d for the response : %v", tests.Failed, tt.statusCode, w.Code)
}
t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code)
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.Fatalf("\t%s\tShould get the expected error.", tests.Failed)
}
if diff := cmp.Diff(actual, tt.error); diff != "" {
t.Logf("\t\tDiff : %s\n", diff)
t.Fatalf("\t%s\tShould get the expected error.", tests.Failed)
}
}
if ok := tt.expected(tt, w.Body.Bytes()); !ok {
t.Fatalf("\t%s\tShould get the expected result.", tests.Failed)
}
t.Logf("\t%s\tReceived expected result.", tests.Success)
}
}
}
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 {
printResultMapKeys(ctx, smv, depth +1)
}
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)
printResultMapKeys(ctx, sm, depth +1)
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)
}
}
}

File diff suppressed because it is too large Load Diff