status-im/status-go

View on GitHub
server/handlers_test.go

Summary

Maintainability
A
0 mins
Test Coverage
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)
}