status-go/server/handlers_test.go

664 lines
20 KiB
Go

package server
import (
"database/sql"
"encoding/json"
"image/color"
"net/http"
"net/http/httptest"
"net/url"
"path/filepath"
"testing"
"github.com/status-im/status-go/images"
"github.com/status-im/status-go/eth-node/crypto"
"github.com/status-im/status-go/common/dbsetup"
"github.com/status-im/status-go/multiaccounts"
mc "github.com/status-im/status-go/multiaccounts/common"
"github.com/golang/protobuf/proto"
"github.com/stretchr/testify/suite"
"go.uber.org/zap"
"github.com/status-im/status-go/appdatabase"
"github.com/status-im/status-go/protocol/common"
"github.com/status-im/status-go/protocol/protobuf"
"github.com/status-im/status-go/protocol/sqlite"
"github.com/status-im/status-go/protocol/tt"
"github.com/status-im/status-go/t/helpers"
)
func TestHandlersSuite(t *testing.T) {
suite.Run(t, new(HandlersSuite))
}
type HandlersSuite struct {
suite.Suite
db *sql.DB
logger *zap.Logger
}
func (s *HandlersSuite) SetupTest() {
db, err := helpers.SetupTestMemorySQLDB(appdatabase.DbInitializer{})
s.Require().NoError(err)
err = sqlite.Migrate(db)
s.Require().NoError(err)
s.logger = tt.MustCreateTestLogger()
s.db = db
}
func (s *HandlersSuite) saveUserMessage(msg *common.Message) {
whisperTimestamp := 0
source := ""
text := ""
contentType := 0
timestamp := 0
chatID := "1"
localChatID := "1"
responseTo := ""
clockValue := 0
stmt, err := s.db.Prepare(`
INSERT INTO user_messages (
id,
whisper_timestamp,
source,
text,
content_type,
timestamp,
chat_id,
local_chat_id,
response_to,
clock_value,
unfurled_links,
unfurled_status_links
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)
`)
s.Require().NoError(err)
links := []byte{}
statusLinks := []byte{}
if msg.UnfurledLinks != nil {
links, err = json.Marshal(msg.UnfurledLinks)
s.Require().NoError(err)
}
if msg.UnfurledStatusLinks != nil {
statusLinks, err = proto.Marshal(msg.UnfurledStatusLinks)
s.Require().NoError(err)
}
_, err = stmt.Exec(
msg.ID,
whisperTimestamp,
source,
text,
contentType,
timestamp,
chatID,
localChatID,
responseTo,
clockValue,
links,
statusLinks,
)
s.Require().NoError(err)
}
func (s *HandlersSuite) httpGetReqRecorder(handler http.HandlerFunc, reqURL string) *httptest.ResponseRecorder {
req, err := http.NewRequest("GET", reqURL, nil)
s.Require().NoError(err)
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
return rr
}
func (s *HandlersSuite) verifyHTTPResponseThumbnail(rr *httptest.ResponseRecorder, expectedPayload []byte) {
s.Require().Equal(expectedPayload, rr.Body.Bytes())
s.Require().Equal("image/jpeg", rr.HeaderMap.Get("Content-Type"))
s.Require().Equal("no-store", rr.HeaderMap.Get("Cache-Control"))
}
func (s *HandlersSuite) TestHandleLinkPreviewThumbnail() {
previewURL := "https://github.com"
defaultPayload := []byte{0xff, 0xd8, 0xff, 0xdb, 0x0, 0x84, 0x0, 0x50, 0x37, 0x3c, 0x46, 0x3c, 0x32, 0x50}
msg := common.Message{
ID: "1",
ChatMessage: &protobuf.ChatMessage{
UnfurledLinks: []*protobuf.UnfurledLink{
{
Type: protobuf.UnfurledLink_LINK,
Url: previewURL,
ThumbnailWidth: 100,
ThumbnailHeight: 200,
},
},
},
}
s.saveUserMessage(&msg)
testCases := []struct {
Name string
ExpectedHTTPStatusCode int
ThumbnailPayload []byte
Parameters url.Values
CheckFunc func(s *HandlersSuite, rr *httptest.ResponseRecorder)
}{
{
Name: "Test happy path",
ExpectedHTTPStatusCode: http.StatusOK,
ThumbnailPayload: defaultPayload,
Parameters: url.Values{
"message-id": {msg.ID},
"url": {previewURL},
},
CheckFunc: func(s *HandlersSuite, rr *httptest.ResponseRecorder) {
s.verifyHTTPResponseThumbnail(rr, msg.UnfurledLinks[0].ThumbnailPayload)
},
},
{
Name: "Test request with missing 'url' parameter",
ThumbnailPayload: defaultPayload,
ExpectedHTTPStatusCode: http.StatusBadRequest,
Parameters: url.Values{
"message-id": {msg.ID},
},
CheckFunc: func(s *HandlersSuite, rr *httptest.ResponseRecorder) {
s.Require().Equal("missing query parameter 'url'\n", rr.Body.String())
},
},
{
Name: "Test request with missing 'message-id' parameter",
ThumbnailPayload: defaultPayload,
ExpectedHTTPStatusCode: http.StatusBadRequest,
Parameters: url.Values{
"url": {previewURL},
},
CheckFunc: func(s *HandlersSuite, rr *httptest.ResponseRecorder) {
s.Require().Equal("missing query parameter 'message-id'\n", rr.Body.String())
},
},
{
Name: "Test mime type not supported",
ThumbnailPayload: []byte("unsupported image"),
ExpectedHTTPStatusCode: http.StatusNotImplemented,
Parameters: url.Values{
"message-id": {msg.ID},
"url": {previewURL},
},
},
}
handler := handleLinkPreviewThumbnail(s.db, s.logger)
for _, tc := range testCases {
s.Run(tc.Name, func() {
msg.UnfurledLinks[0].ThumbnailPayload = tc.ThumbnailPayload
s.saveUserMessage(&msg)
requestURL := "/dummy?" + tc.Parameters.Encode()
rr := s.httpGetReqRecorder(handler, requestURL)
s.Require().Equal(tc.ExpectedHTTPStatusCode, rr.Code)
if tc.CheckFunc != nil {
tc.CheckFunc(s, rr)
}
})
}
}
func (s *HandlersSuite) TestHandleStatusLinkPreviewThumbnail() {
contact := &protobuf.UnfurledStatusContactLink{
PublicKey: []byte("PublicKey_1"),
Icon: &protobuf.UnfurledLinkThumbnail{
Width: 10,
Height: 20,
Payload: []byte{0xff, 0xd8, 0xff, 0xdb, 0x0, 0x84, 0x0, 0x50, 0x37, 0x3c, 0x46, 0x3c, 0x32, 0x50},
},
}
contactWithUnsupportedImage := &protobuf.UnfurledStatusContactLink{
PublicKey: []byte("PublicKey_2"),
Icon: &protobuf.UnfurledLinkThumbnail{
Width: 10,
Height: 20,
Payload: []byte("unsupported image"),
},
}
community := &protobuf.UnfurledStatusCommunityLink{
CommunityId: []byte("CommunityId_1"),
Icon: &protobuf.UnfurledLinkThumbnail{
Width: 30,
Height: 40,
Payload: []byte{0xff, 0xd8, 0xff, 0xdb, 0x0, 0x84, 0x0, 0x50, 0x37, 0x3c, 0x46, 0x3c, 0x32, 0x51},
},
Banner: &protobuf.UnfurledLinkThumbnail{
Width: 50,
Height: 60,
Payload: []byte{0xff, 0xd8, 0xff, 0xdb, 0x0, 0x84, 0x0, 0x50, 0x37, 0x3c, 0x46, 0x3c, 0x32, 0x52},
},
}
channel := &protobuf.UnfurledStatusChannelLink{
ChannelUuid: "ChannelUuid_1",
Community: &protobuf.UnfurledStatusCommunityLink{
CommunityId: []byte("CommunityId_2"),
Icon: &protobuf.UnfurledLinkThumbnail{
Width: 70,
Height: 80,
Payload: []byte{0xff, 0xd8, 0xff, 0xdb, 0x0, 0x84, 0x0, 0x50, 0x37, 0x3c, 0x46, 0x3c, 0x32, 0x53},
},
Banner: &protobuf.UnfurledLinkThumbnail{
Width: 90,
Height: 100,
Payload: []byte{0xff, 0xd8, 0xff, 0xdb, 0x0, 0x84, 0x0, 0x50, 0x37, 0x3c, 0x46, 0x3c, 0x32, 0x54},
},
},
}
unfurledContact := &protobuf.UnfurledStatusLink{
Url: "https://status.app/u/",
Payload: &protobuf.UnfurledStatusLink_Contact{
Contact: contact,
},
}
unfurledContactWithUnsupportedImage := &protobuf.UnfurledStatusLink{
Url: "https://status.app/u/",
Payload: &protobuf.UnfurledStatusLink_Contact{
Contact: contactWithUnsupportedImage,
},
}
unfurledCommunity := &protobuf.UnfurledStatusLink{
Url: "https://status.app/c/",
Payload: &protobuf.UnfurledStatusLink_Community{
Community: community,
},
}
unfurledChannel := &protobuf.UnfurledStatusLink{
Url: "https://status.app/cc/",
Payload: &protobuf.UnfurledStatusLink_Channel{
Channel: channel,
},
}
const (
messageIDContactOnly = "1"
messageIDCommunityOnly = "2"
messageIDChannelOnly = "3"
messageIDAllLinks = "4"
messageIDUnsupportedImage = "5"
)
s.saveUserMessage(&common.Message{
ID: messageIDContactOnly,
ChatMessage: &protobuf.ChatMessage{
UnfurledStatusLinks: &protobuf.UnfurledStatusLinks{
UnfurledStatusLinks: []*protobuf.UnfurledStatusLink{
unfurledContact,
},
},
},
})
s.saveUserMessage(&common.Message{
ID: messageIDCommunityOnly,
ChatMessage: &protobuf.ChatMessage{
UnfurledStatusLinks: &protobuf.UnfurledStatusLinks{
UnfurledStatusLinks: []*protobuf.UnfurledStatusLink{
unfurledCommunity,
},
},
},
})
s.saveUserMessage(&common.Message{
ID: messageIDChannelOnly,
ChatMessage: &protobuf.ChatMessage{
UnfurledStatusLinks: &protobuf.UnfurledStatusLinks{
UnfurledStatusLinks: []*protobuf.UnfurledStatusLink{
unfurledChannel,
},
},
},
})
s.saveUserMessage(&common.Message{
ID: messageIDAllLinks,
ChatMessage: &protobuf.ChatMessage{
UnfurledStatusLinks: &protobuf.UnfurledStatusLinks{
UnfurledStatusLinks: []*protobuf.UnfurledStatusLink{
unfurledContact,
unfurledCommunity,
unfurledChannel,
},
},
},
})
s.saveUserMessage(&common.Message{
ID: messageIDUnsupportedImage,
ChatMessage: &protobuf.ChatMessage{
UnfurledStatusLinks: &protobuf.UnfurledStatusLinks{
UnfurledStatusLinks: []*protobuf.UnfurledStatusLink{
unfurledContactWithUnsupportedImage,
},
},
},
})
testCases := []struct {
Name string
ExpectedHTTPStatusCode int
Parameters url.Values
CheckFunc func(s *HandlersSuite, rr *httptest.ResponseRecorder)
}{
{
Name: "Test valid contact icon link",
Parameters: url.Values{
"message-id": {messageIDContactOnly},
"url": {unfurledContact.Url},
"image-id": {string(common.MediaServerContactIcon)},
},
ExpectedHTTPStatusCode: http.StatusOK,
CheckFunc: func(s *HandlersSuite, rr *httptest.ResponseRecorder) {
s.verifyHTTPResponseThumbnail(rr, unfurledContact.GetContact().Icon.Payload)
},
},
{
Name: "Test invalid request for community icon in a contact link",
Parameters: url.Values{
"message-id": {messageIDContactOnly},
"url": {unfurledContact.Url},
"image-id": {string(common.MediaServerCommunityIcon)},
},
ExpectedHTTPStatusCode: http.StatusBadRequest,
CheckFunc: func(s *HandlersSuite, rr *httptest.ResponseRecorder) {
s.Require().Equal("invalid query parameter 'image-id' value: this is not a community link\n", rr.Body.String())
},
},
{
Name: "Test invalid request for cahnnel community banner in a contact link",
Parameters: url.Values{
"message-id": {messageIDContactOnly},
"url": {unfurledContact.Url},
"image-id": {string(common.MediaServerChannelCommunityBanner)},
},
ExpectedHTTPStatusCode: http.StatusBadRequest,
CheckFunc: func(s *HandlersSuite, rr *httptest.ResponseRecorder) {
s.Require().Equal("invalid query parameter 'image-id' value: this is not a community channel link\n", rr.Body.String())
},
},
{
Name: "Test invalid request for channel community banner in a contact link",
Parameters: url.Values{
"message-id": {messageIDContactOnly},
"url": {unfurledContact.Url},
"image-id": {"contact-banner"},
},
ExpectedHTTPStatusCode: http.StatusBadRequest,
CheckFunc: func(s *HandlersSuite, rr *httptest.ResponseRecorder) {
s.Require().Equal("invalid query parameter 'image-id' value: value not supported\n", rr.Body.String())
},
},
{
Name: "Test valid community icon link",
Parameters: url.Values{
"message-id": {messageIDCommunityOnly},
"url": {unfurledCommunity.Url},
"image-id": {string(common.MediaServerCommunityIcon)},
},
ExpectedHTTPStatusCode: http.StatusOK,
CheckFunc: func(s *HandlersSuite, rr *httptest.ResponseRecorder) {
s.verifyHTTPResponseThumbnail(rr, unfurledCommunity.GetCommunity().Icon.Payload)
},
},
{
Name: "Test valid community banner link",
Parameters: url.Values{
"message-id": {messageIDCommunityOnly},
"url": {unfurledCommunity.Url},
"image-id": {string(common.MediaServerCommunityBanner)},
},
ExpectedHTTPStatusCode: http.StatusOK,
CheckFunc: func(s *HandlersSuite, rr *httptest.ResponseRecorder) {
s.verifyHTTPResponseThumbnail(rr, unfurledCommunity.GetCommunity().Banner.Payload)
},
},
{
Name: "Test valid channel community icon link",
Parameters: url.Values{
"message-id": {messageIDChannelOnly},
"url": {unfurledChannel.Url},
"image-id": {string(common.MediaServerChannelCommunityIcon)},
},
ExpectedHTTPStatusCode: http.StatusOK,
CheckFunc: func(s *HandlersSuite, rr *httptest.ResponseRecorder) {
s.verifyHTTPResponseThumbnail(rr, unfurledChannel.GetChannel().GetCommunity().Icon.Payload)
},
},
{
Name: "Test valid channel community banner link",
Parameters: url.Values{
"message-id": {messageIDChannelOnly},
"url": {unfurledChannel.Url},
"image-id": {string(common.MediaServerChannelCommunityBanner)},
},
ExpectedHTTPStatusCode: http.StatusOK,
CheckFunc: func(s *HandlersSuite, rr *httptest.ResponseRecorder) {
s.verifyHTTPResponseThumbnail(rr, unfurledChannel.GetChannel().GetCommunity().Banner.Payload)
},
},
{
Name: "Test valid contact icon link in a diverse message",
Parameters: url.Values{
"message-id": {messageIDAllLinks},
"url": {unfurledContact.Url},
"image-id": {string(common.MediaServerContactIcon)},
},
ExpectedHTTPStatusCode: http.StatusOK,
CheckFunc: func(s *HandlersSuite, rr *httptest.ResponseRecorder) {
s.verifyHTTPResponseThumbnail(rr, unfurledContact.GetContact().Icon.Payload)
},
},
{
Name: "Test valid community icon link in a diverse message",
Parameters: url.Values{
"message-id": {messageIDAllLinks},
"url": {unfurledCommunity.Url},
"image-id": {string(common.MediaServerCommunityIcon)},
},
ExpectedHTTPStatusCode: http.StatusOK,
CheckFunc: func(s *HandlersSuite, rr *httptest.ResponseRecorder) {
s.verifyHTTPResponseThumbnail(rr, unfurledCommunity.GetCommunity().Icon.Payload)
},
},
{
Name: "Test valid channel community icon link in a diverse message",
Parameters: url.Values{
"message-id": {messageIDAllLinks},
"url": {unfurledChannel.Url},
"image-id": {string(common.MediaServerChannelCommunityIcon)},
},
ExpectedHTTPStatusCode: http.StatusOK,
CheckFunc: func(s *HandlersSuite, rr *httptest.ResponseRecorder) {
s.verifyHTTPResponseThumbnail(rr, unfurledChannel.GetChannel().GetCommunity().Icon.Payload)
},
},
{
Name: "Test mime type not supported",
Parameters: url.Values{
"message-id": {messageIDUnsupportedImage},
"url": {unfurledContactWithUnsupportedImage.Url},
"image-id": {string(common.MediaServerContactIcon)},
},
ExpectedHTTPStatusCode: http.StatusNotImplemented,
},
{
Name: "Test request with missing 'message-id' parameter",
Parameters: url.Values{
"url": {unfurledCommunity.Url},
"image-id": {string(common.MediaServerCommunityIcon)},
},
ExpectedHTTPStatusCode: http.StatusBadRequest,
CheckFunc: func(s *HandlersSuite, rr *httptest.ResponseRecorder) {
s.Require().Equal("missing query parameter 'message-id'\n", rr.Body.String())
},
},
{
Name: "Test request with missing 'url' parameter",
Parameters: url.Values{
"message-id": {messageIDCommunityOnly},
"image-id": {string(common.MediaServerCommunityIcon)},
},
ExpectedHTTPStatusCode: http.StatusBadRequest,
CheckFunc: func(s *HandlersSuite, rr *httptest.ResponseRecorder) {
s.Require().Equal("missing query parameter 'url'\n", rr.Body.String())
},
},
{
Name: "Test request with missing 'image-id' parameter",
Parameters: url.Values{
"message-id": {messageIDCommunityOnly},
"url": {unfurledCommunity.Url},
},
ExpectedHTTPStatusCode: http.StatusBadRequest,
CheckFunc: func(s *HandlersSuite, rr *httptest.ResponseRecorder) {
s.Require().Equal("missing query parameter 'image-id'\n", rr.Body.String())
},
},
}
handler := handleStatusLinkPreviewThumbnail(s.db, s.logger)
for _, tc := range testCases {
s.Run(tc.Name, func() {
requestURL := "/dummy?" + tc.Parameters.Encode()
rr := s.httpGetReqRecorder(handler, requestURL)
s.Require().Equal(tc.ExpectedHTTPStatusCode, rr.Code)
if tc.CheckFunc != nil {
tc.CheckFunc(s, rr)
}
})
}
}
func (s *HandlersSuite) validateResponse(w *httptest.ResponseRecorder) {
s.Require().Equal(http.StatusOK, w.Code)
s.Require().Equal("image/png", w.Header().Get("Content-Type"))
n, err := w.Result().Body.Read(make([]byte, 100))
s.Require().NoError(err)
s.Require().Greater(n, 0)
}
// TestHandleAccountInitialsImpl tests the handleAccountInitialsImpl function
func (s *HandlersSuite) TestHandleAccountInitialsImpl() {
// given an account without public key, and request to generate ring with keyUID of the account,
// it should still response with a valid image without ring rather than response with empty image
dbFile := filepath.Join(s.T().TempDir(), "accounts-tests-")
db, err := multiaccounts.InitializeDB(dbFile)
s.Require().NoError(err)
defer db.Close()
keyUID := "0x1"
name := "Lopsided Goodnatured Bedbug"
expected := multiaccounts.Account{Name: name, KeyUID: keyUID, CustomizationColor: mc.CustomizationColorBlue, ColorHash: nil, ColorID: 10, KDFIterations: dbsetup.ReducedKDFIterationsNumber, Timestamp: 1712856359}
s.Require().NoError(db.SaveAccount(expected))
accounts, err := db.GetAccounts()
s.Require().NoError(err)
s.Require().Len(accounts, 1)
s.Require().Equal(expected, accounts[0])
w := httptest.NewRecorder()
f, err := filepath.Abs("../_assets/tests/UbuntuMono-Regular.ttf")
s.Require().NoError(err)
p := ImageParams{
Ring: true,
RingWidth: 1,
KeyUID: keyUID,
InitialsLength: 2,
BgColor: color.Transparent,
Color: color.Transparent,
FontFile: f,
BgSize: 1,
FontSize: 1,
UppercaseRatio: 1.0,
}
handleAccountInitialsImpl(db, s.logger, w, p)
s.validateResponse(w)
// pass a public key to generate ring
k, err := crypto.GenerateKey()
s.Require().NoError(err)
p.PublicKey = common.PubkeyToHex(&k.PublicKey)
w = httptest.NewRecorder()
handleAccountInitialsImpl(db, s.logger, w, p)
s.Require().Equal(http.StatusOK, w.Code)
}
// TestHandleAccountImagesImpl tests the handleAccountImagesImpl function
func (s *HandlersSuite) TestHandleAccountImagesImpl() {
// given an account with identity images and without public key, and request to generate ring with keyUID of the account,
// it should still response with a valid image without ring rather than response with empty image
dbFile := filepath.Join(s.T().TempDir(), "accounts-tests-")
db, err := multiaccounts.InitializeDB(dbFile)
s.Require().NoError(err)
defer db.Close()
keyUID := "0x1"
name := "Lopsided Goodnatured Bedbug"
expected := multiaccounts.Account{
Name: name,
KeyUID: keyUID,
CustomizationColor: mc.CustomizationColorBlue,
ColorHash: nil,
ColorID: 10,
KDFIterations: dbsetup.ReducedKDFIterationsNumber,
Timestamp: 1712856359,
Images: images.SampleIdentityImageForQRCode(),
}
s.Require().NoError(db.SaveAccount(expected))
accounts, err := db.GetAccounts()
s.Require().NoError(err)
s.Require().Len(accounts, 1)
s.Require().Equal(expected, accounts[0])
w := httptest.NewRecorder()
f, err := filepath.Abs("../_assets/tests/UbuntuMono-Regular.ttf")
s.Require().NoError(err)
p := ImageParams{
Ring: true,
RingWidth: 1,
KeyUID: keyUID,
InitialsLength: 2,
BgColor: color.Transparent,
Color: color.Transparent,
FontFile: f,
BgSize: 1,
FontSize: 1,
UppercaseRatio: 1.0,
ImageName: images.LargeDimName,
}
handleAccountImagesImpl(db, s.logger, w, p)
s.validateResponse(w)
// pass a public key to generate ring
k, err := crypto.GenerateKey()
s.Require().NoError(err)
p.PublicKey = common.PubkeyToHex(&k.PublicKey)
w = httptest.NewRecorder()
handleAccountImagesImpl(db, s.logger, w, p)
s.Require().Equal(http.StatusOK, w.Code)
}