mirror of
https://git.rwth-aachen.de/acs/public/villas/web-backend-go/
synced 2025-03-30 00:00:12 +01:00

The problem started with an internal error in the database (HTTP code 500, pq: duplicate key value) while trying to POST at /api/user with body {"user":{newUserObject}} to the backend, that already had three users User0 (Admin), UserA and UserB. Every test was succeeding. Eventually, the source of the problem was the initialization of the field `ID` (primary key) of the embedded struct `Model` in model `User`. When the `User` global variables User0, UserA and UserB were created in `common/testdata.go` the `Model.ID` field was set manually to the values 1, 2 and 3 respectively. This ofc was unnecessary but those variables were used for comparison with the relevant responses. When gorm (or postgres, not entirely sure) have a new variable of a model type, it will check if its primary key is zero (which means is uninitialized) and will set it to the last used value +1. That means that the new user that we were trying to insert in the DB got the primary key value 1. This value was already in use by the User0 so the DB refused to add the new user since there was a duplicate primary key value. That's why we were getting an postgre's error and HTTP 500. Hence to fix the bug this commit: - Removes `User.ID` initialization from `common/testdata.go` - Omits the `User.ID` from the json marshaler by setting the struct json tag to `json:"-"`. This is done because the request body is compared to the response *after* is serialized by a call to json.Marshal(). Since the `User.ID` is always uninitialized (value 0) the jsondiff.Compare() will always return a "NoMatch". - Includes check for return of "SupersetMatch" in NewTestEndpoint()'s jsondiff.Compare() call. Since the `User.ID` is not serialized the json representation of the request will not include an "id" key. On the contrary, the response *has* an "id" key so it will always going to be a superset json object of the serialized request. - Modifies the validator's data struct to match the form of the request ({"modelKey":{requestBody}}) by adding an intermediate struct with the proper tag `json:"user"`.
241 lines
6.6 KiB
Go
241 lines
6.6 KiB
Go
package common
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strconv"
|
|
"testing"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/jinzhu/gorm"
|
|
"github.com/nsf/jsondiff"
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
const UserIDCtx = "user_id"
|
|
const UserRoleCtx = "user_role"
|
|
|
|
func ProvideErrorResponse(c *gin.Context, err error) bool {
|
|
if err != nil {
|
|
if err == gorm.ErrRecordNotFound {
|
|
errormsg := "Record not Found in DB: " + err.Error()
|
|
c.JSON(http.StatusNotFound, gin.H{
|
|
"error": errormsg,
|
|
})
|
|
} else {
|
|
errormsg := "Error on DB Query or transaction: " + err.Error()
|
|
c.JSON(http.StatusInternalServerError, gin.H{
|
|
"error": errormsg,
|
|
})
|
|
}
|
|
return true // Error
|
|
}
|
|
return false // No error
|
|
}
|
|
|
|
func LengthOfResponse(router *gin.Engine, token string, url string,
|
|
method string, body []byte) (int, error) {
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
if body != nil {
|
|
req, _ := http.NewRequest(method, url, bytes.NewBuffer(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Add("Authorization", "Bearer "+token)
|
|
router.ServeHTTP(w, req)
|
|
} else {
|
|
req, _ := http.NewRequest(method, url, nil)
|
|
req.Header.Add("Authorization", "Bearer "+token)
|
|
router.ServeHTTP(w, req)
|
|
}
|
|
|
|
// Convert the response in array of bytes
|
|
responseBytes := []byte(w.Body.String())
|
|
|
|
// First we are trying to unmarshal the response into an array of
|
|
// general type variables ([]interface{}). If this fails we will try
|
|
// to unmarshal into a single general type variable (interface{}).
|
|
// If that also fails we will return -1.
|
|
|
|
// Response might be array of objects
|
|
var arrayResponse map[string][]interface{}
|
|
err := json.Unmarshal(responseBytes, &arrayResponse)
|
|
if err == nil {
|
|
|
|
// Get an arbitrary key from tha map. The only key (entry) of
|
|
// course is the model's name. With that trick we do not have to
|
|
// pass the higher level key as argument.
|
|
for arbitrary_key := range arrayResponse {
|
|
return len(arrayResponse[arbitrary_key]), nil
|
|
}
|
|
}
|
|
|
|
// Response might be a single object
|
|
var singleResponse map[string]interface{}
|
|
err = json.Unmarshal(responseBytes, &singleResponse)
|
|
if err == nil {
|
|
return 1, nil
|
|
}
|
|
|
|
// Failed to identify response. It means we got a different HTTP
|
|
// code than 200.
|
|
return 0, fmt.Errorf("Length of response cannot be detected")
|
|
}
|
|
|
|
func NewTestEndpoint(router *gin.Engine, token string, url string,
|
|
method string, responseBody interface{}, expected_code int,
|
|
expectedResponse interface{}) error {
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
// Marshal the HTTP request body
|
|
body, err := json.Marshal(responseBody)
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to marshal reqeust body: %v", err)
|
|
}
|
|
|
|
if body != nil {
|
|
req, err := http.NewRequest(method, url, bytes.NewBuffer(body))
|
|
if err != nil {
|
|
return fmt.Errorf("Faile to create new request: %v", err)
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Add("Authorization", "Bearer "+token)
|
|
router.ServeHTTP(w, req)
|
|
} else {
|
|
req, err := http.NewRequest(method, url, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("Faile to create new request: %v", err)
|
|
}
|
|
req.Header.Add("Authorization", "Bearer "+token)
|
|
router.ServeHTTP(w, req)
|
|
}
|
|
|
|
// Check the return HTTP Code
|
|
if w.Code != expected_code {
|
|
return fmt.Errorf("HTTP Code: Expected \"%v\". Got \"%v\".",
|
|
expected_code, w.Code)
|
|
}
|
|
|
|
// Serialize expected response
|
|
expectedBytes, err := json.Marshal(expectedResponse)
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to marshal epxected response: %v", err)
|
|
}
|
|
|
|
// Check the response
|
|
opts := jsondiff.DefaultConsoleOptions()
|
|
diff, _ := jsondiff.Compare(w.Body.Bytes(), expectedBytes, &opts)
|
|
if diff.String() != "FullMatch" && diff.String() != "SupersetMatch" {
|
|
return fmt.Errorf("Response: Expected \"%v\". Got \"%v\".",
|
|
"(FullMatch OR SupersetMatch)", diff.String())
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func TestEndpoint(t *testing.T, router *gin.Engine, token string, url string, method string, body []byte, expected_code int, expected_response []byte) {
|
|
w := httptest.NewRecorder()
|
|
|
|
if body != nil {
|
|
req, _ := http.NewRequest(method, url, bytes.NewBuffer(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Add("Authorization", "Bearer "+token)
|
|
router.ServeHTTP(w, req)
|
|
} else {
|
|
req, _ := http.NewRequest(method, url, nil)
|
|
req.Header.Add("Authorization", "Bearer "+token)
|
|
router.ServeHTTP(w, req)
|
|
}
|
|
|
|
assert.Equal(t, expected_code, w.Code)
|
|
//fmt.Println("Actual:", w.Body.String())
|
|
//fmt.Println("Expected: ", string(expected_response))
|
|
opts := jsondiff.DefaultConsoleOptions()
|
|
diff, _ := jsondiff.Compare(w.Body.Bytes(), expected_response, &opts)
|
|
assert.Equal(t, "FullMatch", diff.String())
|
|
|
|
}
|
|
|
|
func NewAuthenticateForTest(router *gin.Engine, url string,
|
|
method string, credentials interface{}, expected_code int) (string,
|
|
error) {
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
// Marshal credentials
|
|
body, err := json.Marshal(credentials)
|
|
if err != nil {
|
|
return "", fmt.Errorf("Failed to marshal credentials: %v", err)
|
|
}
|
|
|
|
req, err := http.NewRequest(method, url, bytes.NewBuffer(body))
|
|
if err != nil {
|
|
return "", fmt.Errorf("Faile to create new request: %v", err)
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
router.ServeHTTP(w, req)
|
|
|
|
// Check the return HTTP Code
|
|
if w.Code != expected_code {
|
|
return "", fmt.Errorf("HTTP Code: Expected \"%v\". Got \"%v\".",
|
|
expected_code, w.Code)
|
|
}
|
|
|
|
var body_data map[string]interface{}
|
|
|
|
// Get the response
|
|
err = json.Unmarshal([]byte(w.Body.String()), &body_data)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// Check the response
|
|
success := body_data["success"].(bool)
|
|
if !success {
|
|
fmt.Println("Authentication not successful: ", body_data["message"])
|
|
return "", fmt.Errorf("Authentication unsuccessful!")
|
|
}
|
|
|
|
// Return the token and nil error
|
|
return body_data["token"].(string), nil
|
|
}
|
|
|
|
func AuthenticateForTest(t *testing.T, router *gin.Engine, url string, method string, body []byte, expected_code int) string {
|
|
w := httptest.NewRecorder()
|
|
|
|
req, _ := http.NewRequest(method, url, bytes.NewBuffer(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, expected_code, w.Code)
|
|
|
|
var body_data map[string]interface{}
|
|
|
|
err := json.Unmarshal([]byte(w.Body.String()), &body_data)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
success := body_data["success"].(bool)
|
|
if !success {
|
|
fmt.Println("Authentication not successful: ", body_data["message"])
|
|
panic(-1)
|
|
}
|
|
|
|
fmt.Println(w.Body.String())
|
|
|
|
return body_data["token"].(string)
|
|
}
|
|
|
|
// Read the parameter with name paramName from the gin Context and
|
|
// return it as uint variable
|
|
func UintParamFromCtx(c *gin.Context, paramName string) (uint, error) {
|
|
|
|
param, err := strconv.Atoi(c.Param(paramName))
|
|
|
|
return uint(param), err
|
|
}
|