chrly/http/api_test.go
2019-05-06 17:17:44 +03:00

502 lines
16 KiB
Go

package http
import (
"bytes"
"encoding/base64"
"io/ioutil"
"mime/multipart"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"github.com/elyby/chrly/auth"
"github.com/elyby/chrly/db"
"github.com/golang/mock/gomock"
testify "github.com/stretchr/testify/assert"
)
func TestConfig_PostSkin(t *testing.T) {
t.Run("Upload new identity with textures info", func(t *testing.T) {
assert := testify.New(t)
ctrl := gomock.NewController(t)
defer ctrl.Finish()
config, mocks := setupMocks(ctrl)
resultModel := createSkinModel("mock_user", false)
resultModel.SkinId = 5
resultModel.Url = "http://example.com/skin.png"
resultModel.MojangTextures = ""
resultModel.MojangSignature = ""
form := url.Values{
"identityId": {"1"},
"username": {"mock_user"},
"uuid": {"0f657aa8-bfbe-415d-b700-5750090d3af3"},
"skinId": {"5"},
"is1_8": {"0"},
"isSlim": {"0"},
"url": {"http://example.com/skin.png"},
}
req := httptest.NewRequest("POST", "http://chrly/api/skins", bytes.NewBufferString(form.Encode()))
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
w := httptest.NewRecorder()
mocks.Auth.EXPECT().Check(gomock.Any()).Return(nil)
mocks.Skins.EXPECT().FindByUserId(1).Return(nil, &db.SkinNotFoundError{Who: "unknown"})
mocks.Skins.EXPECT().FindByUsername("mock_user").Return(nil, &db.SkinNotFoundError{Who: "mock_user"})
mocks.Skins.EXPECT().Save(resultModel).Return(nil)
mocks.Log.EXPECT().IncCounter("authentication.challenge", int64(1))
mocks.Log.EXPECT().IncCounter("authentication.success", int64(1))
mocks.Log.EXPECT().IncCounter("api.skins.post.request", int64(1))
mocks.Log.EXPECT().IncCounter("api.skins.post.success", int64(1))
config.CreateHandler().ServeHTTP(w, req)
resp := w.Result()
defer resp.Body.Close()
assert.Equal(201, resp.StatusCode)
response, _ := ioutil.ReadAll(resp.Body)
assert.Empty(response)
})
t.Run("Upload new identity with skin file", func(t *testing.T) {
assert := testify.New(t)
ctrl := gomock.NewController(t)
defer ctrl.Finish()
config, mocks := setupMocks(ctrl)
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
part, _ := writer.CreateFormFile("skin", "char.png")
_, _ = part.Write(loadSkinFile())
_ = writer.WriteField("identityId", "1")
_ = writer.WriteField("username", "mock_user")
_ = writer.WriteField("uuid", "0f657aa8-bfbe-415d-b700-5750090d3af3")
_ = writer.WriteField("skinId", "5")
err := writer.Close()
if err != nil {
panic(err)
}
req := httptest.NewRequest("POST", "http://chrly/api/skins", body)
req.Header.Add("Content-Type", writer.FormDataContentType())
w := httptest.NewRecorder()
mocks.Auth.EXPECT().Check(gomock.Any()).Return(nil)
mocks.Log.EXPECT().IncCounter("authentication.challenge", int64(1))
mocks.Log.EXPECT().IncCounter("authentication.success", int64(1))
mocks.Log.EXPECT().IncCounter("api.skins.post.request", int64(1))
mocks.Log.EXPECT().IncCounter("api.skins.post.validation_failed", int64(1))
config.CreateHandler().ServeHTTP(w, req)
resp := w.Result()
defer resp.Body.Close()
assert.Equal(400, resp.StatusCode)
response, _ := ioutil.ReadAll(resp.Body)
assert.JSONEq(`{
"errors": {
"skin": [
"Skin uploading is temporary unavailable"
]
}
}`, string(response))
})
t.Run("Keep the same identityId, uuid and username, but change textures information", func(t *testing.T) {
assert := testify.New(t)
ctrl := gomock.NewController(t)
defer ctrl.Finish()
config, mocks := setupMocks(ctrl)
resultModel := createSkinModel("mock_user", false)
resultModel.SkinId = 5
resultModel.Url = "http://textures-server.com/skin.png"
resultModel.MojangTextures = ""
resultModel.MojangSignature = ""
mocks.Auth.EXPECT().Check(gomock.Any()).Return(nil)
mocks.Skins.EXPECT().FindByUserId(1).Return(createSkinModel("mock_user", false), nil)
mocks.Skins.EXPECT().Save(resultModel).Return(nil)
mocks.Log.EXPECT().IncCounter("authentication.challenge", int64(1))
mocks.Log.EXPECT().IncCounter("authentication.success", int64(1))
mocks.Log.EXPECT().IncCounter("api.skins.post.request", int64(1))
mocks.Log.EXPECT().IncCounter("api.skins.post.success", int64(1))
form := url.Values{
"identityId": {"1"},
"username": {"mock_user"},
"uuid": {"0f657aa8-bfbe-415d-b700-5750090d3af3"},
"skinId": {"5"},
"is1_8": {"0"},
"isSlim": {"0"},
"url": {"http://textures-server.com/skin.png"},
}
req := httptest.NewRequest("POST", "http://chrly/api/skins", bytes.NewBufferString(form.Encode()))
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
w := httptest.NewRecorder()
config.CreateHandler().ServeHTTP(w, req)
resp := w.Result()
defer resp.Body.Close()
assert.Equal(201, resp.StatusCode)
response, _ := ioutil.ReadAll(resp.Body)
assert.Empty(response)
})
t.Run("Keep the same uuid and username, but change identityId", func(t *testing.T) {
assert := testify.New(t)
ctrl := gomock.NewController(t)
defer ctrl.Finish()
config, mocks := setupMocks(ctrl)
resultModel := createSkinModel("mock_user", false)
resultModel.UserId = 2
resultModel.SkinId = 5
resultModel.Url = "http://example.com/skin.png"
resultModel.MojangTextures = ""
resultModel.MojangSignature = ""
form := url.Values{
"identityId": {"2"},
"username": {"mock_user"},
"uuid": {"0f657aa8-bfbe-415d-b700-5750090d3af3"},
"skinId": {"5"},
"is1_8": {"0"},
"isSlim": {"0"},
"url": {"http://example.com/skin.png"},
}
req := httptest.NewRequest("POST", "http://chrly/api/skins", bytes.NewBufferString(form.Encode()))
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
w := httptest.NewRecorder()
mocks.Auth.EXPECT().Check(gomock.Any()).Return(nil)
mocks.Skins.EXPECT().FindByUserId(2).Return(nil, &db.SkinNotFoundError{Who: "unknown"})
mocks.Skins.EXPECT().FindByUsername("mock_user").Return(createSkinModel("mock_user", false), nil)
mocks.Skins.EXPECT().RemoveByUsername("mock_user").Return(nil)
mocks.Skins.EXPECT().Save(resultModel).Return(nil)
mocks.Log.EXPECT().IncCounter("authentication.challenge", int64(1))
mocks.Log.EXPECT().IncCounter("authentication.success", int64(1))
mocks.Log.EXPECT().IncCounter("api.skins.post.request", int64(1))
mocks.Log.EXPECT().IncCounter("api.skins.post.success", int64(1))
config.CreateHandler().ServeHTTP(w, req)
resp := w.Result()
defer resp.Body.Close()
assert.Equal(201, resp.StatusCode)
response, _ := ioutil.ReadAll(resp.Body)
assert.Empty(response)
})
t.Run("Keep the same identityId and uuid, but change username", func(t *testing.T) {
assert := testify.New(t)
ctrl := gomock.NewController(t)
defer ctrl.Finish()
config, mocks := setupMocks(ctrl)
resultModel := createSkinModel("changed_username", false)
resultModel.SkinId = 5
resultModel.Url = "http://example.com/skin.png"
resultModel.MojangTextures = ""
resultModel.MojangSignature = ""
form := url.Values{
"identityId": {"1"},
"username": {"changed_username"},
"uuid": {"0f657aa8-bfbe-415d-b700-5750090d3af3"},
"skinId": {"5"},
"is1_8": {"0"},
"isSlim": {"0"},
"url": {"http://example.com/skin.png"},
}
req := httptest.NewRequest("POST", "http://chrly/api/skins", bytes.NewBufferString(form.Encode()))
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
w := httptest.NewRecorder()
mocks.Auth.EXPECT().Check(gomock.Any()).Return(nil)
mocks.Skins.EXPECT().FindByUserId(1).Return(createSkinModel("mock_user", false), nil)
mocks.Skins.EXPECT().RemoveByUserId(1).Return(nil)
mocks.Skins.EXPECT().Save(resultModel).Return(nil)
mocks.Log.EXPECT().IncCounter("authentication.challenge", int64(1))
mocks.Log.EXPECT().IncCounter("authentication.success", int64(1))
mocks.Log.EXPECT().IncCounter("api.skins.post.request", int64(1))
mocks.Log.EXPECT().IncCounter("api.skins.post.success", int64(1))
config.CreateHandler().ServeHTTP(w, req)
resp := w.Result()
defer resp.Body.Close()
assert.Equal(201, resp.StatusCode)
response, _ := ioutil.ReadAll(resp.Body)
assert.Empty(response)
})
t.Run("Get errors about required fields", func(t *testing.T) {
assert := testify.New(t)
ctrl := gomock.NewController(t)
defer ctrl.Finish()
config, mocks := setupMocks(ctrl)
form := url.Values{
"mojangTextures": {"someBase64EncodedString"},
}
req := httptest.NewRequest("POST", "http://chrly/api/skins", bytes.NewBufferString(form.Encode()))
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
w := httptest.NewRecorder()
mocks.Auth.EXPECT().Check(gomock.Any()).Return(nil)
mocks.Log.EXPECT().IncCounter("authentication.challenge", int64(1))
mocks.Log.EXPECT().IncCounter("authentication.success", int64(1))
mocks.Log.EXPECT().IncCounter("api.skins.post.request", int64(1))
mocks.Log.EXPECT().IncCounter("api.skins.post.validation_failed", int64(1))
config.CreateHandler().ServeHTTP(w, req)
resp := w.Result()
defer resp.Body.Close()
assert.Equal(400, resp.StatusCode)
response, _ := ioutil.ReadAll(resp.Body)
assert.JSONEq(`{
"errors": {
"identityId": [
"The identityId field is required",
"The identityId field must be numeric",
"The identityId field must be minimum 1 char"
],
"skinId": [
"The skinId field is required",
"The skinId field must be numeric",
"The skinId field must be minimum 1 char"
],
"username": [
"The username field is required"
],
"uuid": [
"The uuid field is required",
"The uuid field must contain valid UUID"
],
"url": [
"One of url or skin should be provided, but not both"
],
"skin": [
"One of url or skin should be provided, but not both"
],
"mojangSignature": [
"The mojangSignature field is required"
]
}
}`, string(response))
})
t.Run("Perform request without authorization", func(t *testing.T) {
assert := testify.New(t)
ctrl := gomock.NewController(t)
defer ctrl.Finish()
config, mocks := setupMocks(ctrl)
req := httptest.NewRequest("POST", "http://chrly/api/skins", nil)
req.Header.Add("Authorization", "Bearer invalid.jwt.token")
w := httptest.NewRecorder()
mocks.Auth.EXPECT().Check(gomock.Any()).Return(&auth.Unauthorized{Reason: "Cannot parse passed JWT token"})
mocks.Log.EXPECT().IncCounter("authentication.challenge", int64(1))
mocks.Log.EXPECT().IncCounter("authentication.failed", int64(1))
config.CreateHandler().ServeHTTP(w, req)
resp := w.Result()
defer resp.Body.Close()
assert.Equal(403, resp.StatusCode)
response, _ := ioutil.ReadAll(resp.Body)
assert.JSONEq(`{
"error": "Cannot parse passed JWT token"
}`, string(response))
})
}
func TestConfig_DeleteSkinByUserId(t *testing.T) {
t.Run("Delete skin by its identity id", func(t *testing.T) {
assert := testify.New(t)
ctrl := gomock.NewController(t)
defer ctrl.Finish()
config, mocks := setupMocks(ctrl)
req := httptest.NewRequest("DELETE", "http://chrly/api/skins/id:1", nil)
w := httptest.NewRecorder()
mocks.Auth.EXPECT().Check(gomock.Any()).Return(nil)
mocks.Skins.EXPECT().FindByUserId(1).Return(createSkinModel("mock_user", false), nil)
mocks.Skins.EXPECT().RemoveByUserId(1).Return(nil)
mocks.Log.EXPECT().IncCounter("authentication.challenge", int64(1))
mocks.Log.EXPECT().IncCounter("authentication.success", int64(1))
mocks.Log.EXPECT().IncCounter("api.skins.delete.request", int64(1))
mocks.Log.EXPECT().IncCounter("api.skins.delete.success", int64(1))
config.CreateHandler().ServeHTTP(w, req)
resp := w.Result()
defer resp.Body.Close()
assert.Equal(204, resp.StatusCode)
response, _ := ioutil.ReadAll(resp.Body)
assert.Empty(response)
})
t.Run("Try to remove not exists identity id", func(t *testing.T) {
assert := testify.New(t)
ctrl := gomock.NewController(t)
defer ctrl.Finish()
config, mocks := setupMocks(ctrl)
req := httptest.NewRequest("DELETE", "http://chrly/api/skins/id:2", nil)
w := httptest.NewRecorder()
mocks.Auth.EXPECT().Check(gomock.Any()).Return(nil)
mocks.Skins.EXPECT().FindByUserId(2).Return(nil, &db.SkinNotFoundError{Who: "unknown"})
mocks.Log.EXPECT().IncCounter("authentication.challenge", int64(1))
mocks.Log.EXPECT().IncCounter("authentication.success", int64(1))
mocks.Log.EXPECT().IncCounter("api.skins.delete.request", int64(1))
mocks.Log.EXPECT().IncCounter("api.skins.delete.not_found", int64(1))
config.CreateHandler().ServeHTTP(w, req)
resp := w.Result()
defer resp.Body.Close()
assert.Equal(404, resp.StatusCode)
response, _ := ioutil.ReadAll(resp.Body)
assert.JSONEq(`[
"Cannot find record for requested user id"
]`, string(response))
})
}
func TestConfig_DeleteSkinByUsername(t *testing.T) {
t.Run("Delete skin by its identity username", func(t *testing.T) {
assert := testify.New(t)
ctrl := gomock.NewController(t)
defer ctrl.Finish()
config, mocks := setupMocks(ctrl)
req := httptest.NewRequest("DELETE", "http://chrly/api/skins/mock_user", nil)
w := httptest.NewRecorder()
mocks.Auth.EXPECT().Check(gomock.Any()).Return(nil)
mocks.Skins.EXPECT().FindByUsername("mock_user").Return(createSkinModel("mock_user", false), nil)
mocks.Skins.EXPECT().RemoveByUserId(1).Return(nil)
mocks.Log.EXPECT().IncCounter("authentication.challenge", int64(1))
mocks.Log.EXPECT().IncCounter("authentication.success", int64(1))
mocks.Log.EXPECT().IncCounter("api.skins.delete.request", int64(1))
mocks.Log.EXPECT().IncCounter("api.skins.delete.success", int64(1))
config.CreateHandler().ServeHTTP(w, req)
resp := w.Result()
defer resp.Body.Close()
assert.Equal(204, resp.StatusCode)
response, _ := ioutil.ReadAll(resp.Body)
assert.Empty(response)
})
t.Run("Try to remove not exists identity username", func(t *testing.T) {
assert := testify.New(t)
ctrl := gomock.NewController(t)
defer ctrl.Finish()
config, mocks := setupMocks(ctrl)
req := httptest.NewRequest("DELETE", "http://chrly/api/skins/mock_user_2", nil)
w := httptest.NewRecorder()
mocks.Auth.EXPECT().Check(gomock.Any()).Return(nil)
mocks.Skins.EXPECT().FindByUsername("mock_user_2").Return(nil, &db.SkinNotFoundError{Who: "mock_user_2"})
mocks.Log.EXPECT().IncCounter("authentication.challenge", int64(1))
mocks.Log.EXPECT().IncCounter("authentication.success", int64(1))
mocks.Log.EXPECT().IncCounter("api.skins.delete.request", int64(1))
mocks.Log.EXPECT().IncCounter("api.skins.delete.not_found", int64(1))
config.CreateHandler().ServeHTTP(w, req)
resp := w.Result()
defer resp.Body.Close()
assert.Equal(404, resp.StatusCode)
response, _ := ioutil.ReadAll(resp.Body)
assert.JSONEq(`[
"Cannot find record for requested username"
]`, string(response))
})
}
func TestConfig_Authenticate(t *testing.T) {
t.Run("Test behavior when signing key is not set", func(t *testing.T) {
assert := testify.New(t)
ctrl := gomock.NewController(t)
defer ctrl.Finish()
config, mocks := setupMocks(ctrl)
req := httptest.NewRequest("POST", "http://localhost", nil)
w := httptest.NewRecorder()
mocks.Auth.EXPECT().Check(gomock.Any()).Return(&auth.Unauthorized{Reason: "signing key not available"})
mocks.Log.EXPECT().IncCounter("authentication.challenge", int64(1))
mocks.Log.EXPECT().IncCounter("authentication.failed", int64(1))
res := config.AuthenticationMiddleware(http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {}))
res.ServeHTTP(w, req)
resp := w.Result()
defer resp.Body.Close()
assert.Equal(403, resp.StatusCode)
response, _ := ioutil.ReadAll(resp.Body)
assert.JSONEq(`{
"error": "signing key not available"
}`, string(response))
})
}
// base64 https://github.com/mathiasbynens/small/blob/0ca3c51/png-transparent.png
var OnePxPng = []byte("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==")
func loadSkinFile() []byte {
result := make([]byte, 92)
_, err := base64.StdEncoding.Decode(result, OnePxPng)
if err != nil {
panic(err)
}
return result
}