You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

340 lines
11 KiB

package main
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/matrix-org/go-neb/clients"
"github.com/matrix-org/go-neb/database"
"maunium.net/go/mautrix/crypto"
"maunium.net/go/mautrix/crypto/olm"
mevt "maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
)
func setupMockServer() (*http.ServeMux, *matrixTripper, *httptest.ResponseRecorder, chan string) {
mux := http.NewServeMux()
mxTripper := newMatrixTripper()
setup(envVars{
BaseURL: "http://go.neb",
DatabaseType: "sqlite3",
DatabaseURL: ":memory:",
}, mux, &http.Client{
Transport: mxTripper,
})
mxTripper.ClearHandlers()
mockWriter := httptest.NewRecorder()
reqChan := make(chan string)
mxTripper.HandlePOSTFilter("@link:hyrule")
mxTripper.Handle("GET", "/_matrix/client/r0/sync",
func(req *http.Request) (*http.Response, error) {
if _, ok := req.URL.Query()["since"]; !ok {
return newResponse(200, `{"next_batch":"11_22_33_44", "rooms": {}}`), nil
}
reqBody := <-reqChan
return newResponse(200, reqBody), nil
},
)
return mux, mxTripper, mockWriter, reqChan
}
func TestConfigureClient(t *testing.T) {
mux, _, mockWriter, _ := setupMockServer()
mockReq, _ := http.NewRequest("POST", "http://go.neb/admin/configureClient", bytes.NewBufferString(`
{
"UserID":"@link:hyrule",
"HomeserverURL":"http://hyrule.loz",
"AccessToken":"dangeroustogoalone",
"Sync":true,
"AutoJoinRooms":true
}`))
mux.ServeHTTP(mockWriter, mockReq)
expectCode := 200
if mockWriter.Code != expectCode {
t.Errorf("TestConfigureClient wanted HTTP status %d, got %d", expectCode, mockWriter.Code)
}
}
func TestRespondToEcho(t *testing.T) {
mux, mxTripper, mockWriter, reqChan := setupMockServer()
mxTripper.Handle("POST", "/_matrix/client/r0/keys/upload", func(req *http.Request) (*http.Response, error) {
return newResponse(200, `{}`), nil
})
var joinedRoom string
var joinedRoomBody []byte
mxTripper.Handle("POST", "/_matrix/client/r0/join/*", func(req *http.Request) (*http.Response, error) {
parts := strings.Split(req.URL.String(), "/")
joinedRoom = parts[len(parts)-1]
joinedRoomBody, _ = ioutil.ReadAll(req.Body)
return newResponse(200, `{}`), nil
})
var roomMsgBody []byte
mxTripper.Handle("PUT", "/_matrix/client/r0/rooms/!greatdekutree:hyrule/send/m.room.message/*", func(req *http.Request) (*http.Response, error) {
roomMsgBody, _ = ioutil.ReadAll(req.Body)
return newResponse(200, `{}`), nil
})
// configure the client
clientConfigReq, _ := http.NewRequest("POST", "http://go.neb/admin/configureClient", bytes.NewBufferString(`
{
"UserID":"@link:hyrule",
"HomeserverURL":"http://hyrule.loz",
"AccessToken":"dangeroustogoalone",
"Sync":true,
"AutoJoinRooms":true
}`))
mux.ServeHTTP(mockWriter, clientConfigReq)
// configure the echo service
serviceConfigReq, _ := http.NewRequest("POST", "http://go.neb/admin/configureService", bytes.NewBufferString(`
{
"Type": "echo",
"Id": "test_echo_service",
"UserID": "@link:hyrule",
"Config": {}
}`))
mux.ServeHTTP(mockWriter, serviceConfigReq)
// send neb an invite to a room
reqChan <- `{
"next_batch":"11_22_33_44",
"rooms": {
"invite": {
"!greatdekutree:hyrule": {"invite_state": {"events": [{
"type": "m.room.member",
"sender": "@navi:hyrule",
"content": {"membership": "invite"},
"state_key": "@link:hyrule",
"origin_server_ts": 10000,
"unsigned": {"age": 100},
"event_id": "evt123"
}]}}}
}
}`
// wait for it to be processed
reqChan <- `{"next_batch":"11_22_33_44", "rooms": {}}`
expectedRoom := "%21greatdekutree:hyrule"
if joinedRoom != expectedRoom {
t.Errorf("Expected join for room %v, got %v", expectedRoom, joinedRoom)
}
if expectedBody := `{"inviter":"@navi:hyrule"}`; string(joinedRoomBody) != expectedBody {
t.Errorf("Expected join message body to be %v, got %v", expectedBody, string(joinedRoomBody))
}
// send neb an !echo message
reqChan <- `{
"next_batch":"11_22_33_44",
"rooms": {
"join": {
"!greatdekutree:hyrule": {"timeline": {"events": [{
"type": "m.room.message",
"sender": "@navi:hyrule",
"content": {"body": "!echo save zelda", "msgtype": "m.text"},
"origin_server_ts": 10000,
"unsigned": {"age": 100},
"event_id": "evt124"
}]}}}
}
}`
// wait for it to be processed
reqChan <- `{"next_batch":"11_22_33_44", "rooms": {}}`
if expectedEchoResp := `{"msgtype":"m.notice","body":"save zelda"}`; string(roomMsgBody) != expectedEchoResp {
t.Errorf("Expected echo response to be `%v`, got `%v`", expectedEchoResp, string(roomMsgBody))
}
}
func TestEncryptedRespondToEcho(t *testing.T) {
mux, mxTripper, mockWriter, reqChan := setupMockServer()
// create the two accounts, inbound and outbound sessions, both the bot and mock ones
accountMock := olm.NewAccount()
accountBot := olm.NewAccount()
signingKeyMock, identityKeyMock := accountMock.IdentityKeys()
signingKeyBot, identityKeyBot := accountBot.IdentityKeys()
// encryptionEvtContent := &mevt.EncryptionEventContent{Algorithm: "m.megolm.v1.aes-sha2"}
ogsBot := crypto.NewOutboundGroupSession("!greatdekutree:hyrule", nil)
ogsBot.Shared = true
igsMock, err := crypto.NewInboundGroupSession(identityKeyBot, signingKeyBot, "!greatdekutree:hyrule", ogsBot.Internal.Key())
if err != nil {
t.Errorf("Error creating mock IGS: %v", err)
}
ogsMock := crypto.NewOutboundGroupSession("!greatdekutree:hyrule", nil)
ogsMock.Shared = true
igsBot, err := crypto.NewInboundGroupSession(identityKeyMock, signingKeyMock, "!greatdekutree:hyrule", ogsMock.Internal.Key())
if err != nil {
t.Errorf("Error creating bot IGS: %v", err)
}
mxTripper.Handle("POST", "/_matrix/client/r0/keys/upload", func(req *http.Request) (*http.Response, error) {
return newResponse(200, `{}`), nil
})
mxTripper.Handle("POST", "/_matrix/client/r0/keys/query", func(req *http.Request) (*http.Response, error) {
return newResponse(200, `{}`), nil
})
var joinedRoom string
var joinedRoomBody []byte
mxTripper.Handle("POST", "/_matrix/client/r0/join/*", func(req *http.Request) (*http.Response, error) {
parts := strings.Split(req.URL.String(), "/")
joinedRoom = parts[len(parts)-1]
joinedRoomBody, _ = ioutil.ReadAll(req.Body)
return newResponse(200, `{}`), nil
})
var decryptedMsg string
mxTripper.Handle("PUT", "/_matrix/client/r0/rooms/!greatdekutree:hyrule/send/m.room.encrypted/*", func(req *http.Request) (*http.Response, error) {
encryptedMsg, _ := ioutil.ReadAll(req.Body)
var encryptedContent mevt.EncryptedEventContent
encryptedContent.UnmarshalJSON(encryptedMsg)
decryptedMsgBytes, _, err := igsMock.Internal.Decrypt(encryptedContent.MegolmCiphertext)
if err != nil {
t.Errorf("Error decrypting message sent by bot: %v", err)
}
decryptedMsg = string(decryptedMsgBytes)
return newResponse(200, `{}`), nil
})
// configure the client
clientConfigReq, _ := http.NewRequest("POST", "http://go.neb/admin/configureClient", bytes.NewBufferString(`
{
"UserID":"@link:hyrule",
"DeviceID":"mastersword",
"HomeserverURL":"http://hyrule.loz",
"AccessToken":"dangeroustogoalone",
"Sync":true,
"AutoJoinRooms":true
}`))
mux.ServeHTTP(mockWriter, clientConfigReq)
// configure the echo service
serviceConfigReq, _ := http.NewRequest("POST", "http://go.neb/admin/configureService", bytes.NewBufferString(`
{
"Type": "echo",
"Id": "test_echo_service",
"UserID": "@link:hyrule",
"Config": {}
}`))
mux.ServeHTTP(mockWriter, serviceConfigReq)
// send neb an invite to a room
reqChan <- `{
"next_batch":"11_22_33_44",
"rooms": {
"invite": {
"!greatdekutree:hyrule": {"invite_state": {"events": [{
"type": "m.room.member",
"sender": "@navi:hyrule",
"content": {"membership": "invite"},
"state_key": "@link:hyrule",
"origin_server_ts": 10000,
"unsigned": {"age": 100},
"event_id": "evt123"
}]}}}
}
}`
// wait for it to be processed
reqChan <- `{"next_batch":"11_22_33_44", "rooms": {}}`
expectedRoom := "%21greatdekutree:hyrule"
if joinedRoom != expectedRoom {
t.Errorf("Expected join for room %v, got %v", expectedRoom, joinedRoom)
}
if expectedBody := `{"inviter":"@navi:hyrule"}`; string(joinedRoomBody) != expectedBody {
t.Errorf("Expected join message body to be %v, got %v", expectedBody, string(joinedRoomBody))
}
// send neb the room state: encrypted with one member
reqChan <- `{
"next_batch":"11_22_33_44",
"rooms": {
"join": {"!greatdekutree:hyrule": {"timeline": {"events": [{
"type": "m.room.encryption",
"state_key": "",
"content": {
"algorithm": "m.megolm.v1.aes-sha2"
}
}, {
"type": "m.room.member",
"sender": "@navi:hyrule",
"content": {"membership": "join", "displayname": "Navi"},
"state_key": "@navi:hyrule",
"origin_server_ts": 100,
"event_id": "evt124"
}]}}}
}
}`
// wait for it to be processed
reqChan <- `{"next_batch":"11_22_33_44", "rooms": {}}`
// DB is initialized, store the megolm sessions from before for the bot to be able to decrypt and encrypt
sqlDB, dialect := database.GetServiceDB().(*database.ServiceDB).GetSQLDb()
cryptoStore := crypto.NewSQLCryptoStore(sqlDB, dialect, "@link:hyrule-mastersword", "mastersword", []byte("masterswordpickle"), clients.CryptoMachineLogger{})
if err := cryptoStore.AddOutboundGroupSession(ogsBot); err != nil {
t.Errorf("Error storing bot OGS: %v", err)
}
if err := cryptoStore.PutGroupSession("!greatdekutree:hyrule", identityKeyMock, igsBot.ID(), igsBot); err != nil {
t.Errorf("Error storing bot IGS: %v", err)
}
cryptoStore.PutDevices("@navi:hyrule", map[id.DeviceID]*crypto.DeviceIdentity{
"NAVI": {
UserID: "@navi:hyrule",
DeviceID: "NAVI",
IdentityKey: identityKeyMock,
SigningKey: signingKeyMock,
},
})
plaintext := `{"room_id":"!greatdekutree:hyrule","type":"m.room.message","content":{"body":"!echo save zelda","msgtype":"m.text"}}`
ciphertext, err := ogsMock.Encrypt([]byte(plaintext))
if err != nil {
t.Errorf("Error encrypting bytes: %v", err)
}
// send neb an !echo message, encrypted with our mock OGS which it has an IGS for
reqChan <- fmt.Sprintf(`{
"next_batch":"11_22_33_44",
"rooms": {
"join": {"!greatdekutree:hyrule": {"timeline": {"events": [{
"type": "m.room.encrypted",
"sender": "@navi:hyrule",
"content": {
"algorithm":"m.megolm.v1.aes-sha2",
"sender_key":"%s",
"ciphertext":"%s",
"session_id":"%s",
"device_id": "NAVI"
},
"origin_server_ts": 10000,
"unsigned": {"age": 100},
"event_id": "evt125"
}]}}}
}
}`, identityKeyMock, string(ciphertext), ogsMock.ID())
// wait for it to be processed
reqChan <- `{"next_batch":"11_22_33_44", "rooms": {}}`
expectedDecryptedMsg := `{"room_id":"!greatdekutree:hyrule","type":"m.room.message","content":{"msgtype":"m.notice","body":"save zelda"}}`
if decryptedMsg != expectedDecryptedMsg {
t.Errorf("Expected decrypted message to be `%v`, got `%v`", expectedDecryptedMsg, decryptedMsg)
}
}