mautrix-go/crypto/verificationhelper/verificationhelper_test.go

491 lines
24 KiB
Go

package verificationhelper_test
import (
"context"
"database/sql"
"fmt"
"os"
"testing"
"time"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log" // zerolog-allow-global-log
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/crypto"
"maunium.net/go/mautrix/crypto/cryptohelper"
"maunium.net/go/mautrix/crypto/verificationhelper"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
)
var aliceUserID = id.UserID("@alice:example.org")
var bobUserID = id.UserID("@bob:example.org")
var sendingDeviceID = id.DeviceID("sending")
var receivingDeviceID = id.DeviceID("receiving")
func init() {
log.Logger = zerolog.New(zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339}).With().Timestamp().Logger().Level(zerolog.TraceLevel)
zerolog.DefaultContextLogger = &log.Logger
}
func initServerAndLoginTwoAlice(t *testing.T, ctx context.Context) (ts *mockServer, sendingClient, receivingClient *mautrix.Client, sendingCryptoStore, receivingCryptoStore crypto.Store, sendingMachine, receivingMachine *crypto.OlmMachine) {
t.Helper()
ts = createMockServer(t)
sendingClient, sendingCryptoStore = ts.Login(t, ctx, aliceUserID, sendingDeviceID)
sendingMachine = sendingClient.Crypto.(*cryptohelper.CryptoHelper).Machine()
receivingClient, receivingCryptoStore = ts.Login(t, ctx, aliceUserID, receivingDeviceID)
receivingMachine = receivingClient.Crypto.(*cryptohelper.CryptoHelper).Machine()
require.NoError(t, sendingCryptoStore.PutDevice(ctx, aliceUserID, sendingMachine.OwnIdentity()))
require.NoError(t, sendingCryptoStore.PutDevice(ctx, aliceUserID, receivingMachine.OwnIdentity()))
require.NoError(t, receivingCryptoStore.PutDevice(ctx, aliceUserID, sendingMachine.OwnIdentity()))
require.NoError(t, receivingCryptoStore.PutDevice(ctx, aliceUserID, receivingMachine.OwnIdentity()))
return
}
func initServerAndLoginAliceBob(t *testing.T, ctx context.Context) (ts *mockServer, sendingClient, receivingClient *mautrix.Client, sendingCryptoStore, receivingCryptoStore crypto.Store, sendingMachine, receivingMachine *crypto.OlmMachine) {
t.Helper()
ts = createMockServer(t)
sendingClient, sendingCryptoStore = ts.Login(t, ctx, aliceUserID, sendingDeviceID)
sendingMachine = sendingClient.Crypto.(*cryptohelper.CryptoHelper).Machine()
receivingClient, receivingCryptoStore = ts.Login(t, ctx, bobUserID, receivingDeviceID)
receivingMachine = receivingClient.Crypto.(*cryptohelper.CryptoHelper).Machine()
require.NoError(t, sendingCryptoStore.PutDevice(ctx, aliceUserID, sendingMachine.OwnIdentity()))
require.NoError(t, sendingCryptoStore.PutDevice(ctx, bobUserID, receivingMachine.OwnIdentity()))
require.NoError(t, receivingCryptoStore.PutDevice(ctx, aliceUserID, sendingMachine.OwnIdentity()))
require.NoError(t, receivingCryptoStore.PutDevice(ctx, bobUserID, receivingMachine.OwnIdentity()))
return
}
func initDefaultCallbacks(t *testing.T, ctx context.Context, sendingClient, receivingClient *mautrix.Client, sendingMachine, receivingMachine *crypto.OlmMachine) (sendingCallbacks, receivingCallbacks *allVerificationCallbacks, sendingHelper, receivingHelper *verificationhelper.VerificationHelper) {
t.Helper()
sendingCallbacks = newAllVerificationCallbacks()
senderVerificationDB, err := sql.Open("sqlite3", ":memory:")
require.NoError(t, err)
senderVerificationStore, err := NewSQLiteVerificationStore(ctx, senderVerificationDB)
require.NoError(t, err)
sendingHelper = verificationhelper.NewVerificationHelper(sendingClient, sendingMachine, senderVerificationStore, sendingCallbacks, true)
require.NoError(t, sendingHelper.Init(ctx))
receivingCallbacks = newAllVerificationCallbacks()
receiverVerificationDB, err := sql.Open("sqlite3", ":memory:")
require.NoError(t, err)
receiverVerificationStore, err := NewSQLiteVerificationStore(ctx, receiverVerificationDB)
require.NoError(t, err)
receivingHelper = verificationhelper.NewVerificationHelper(receivingClient, receivingMachine, receiverVerificationStore, receivingCallbacks, true)
require.NoError(t, receivingHelper.Init(ctx))
return
}
func TestVerification_Start(t *testing.T) {
ctx := log.Logger.WithContext(context.TODO())
receivingDeviceID2 := id.DeviceID("receiving2")
testCases := []struct {
supportsScan bool
callbacks MockVerificationCallbacks
startVerificationErrMsg string
expectedVerificationMethods []event.VerificationMethod
}{
{false, newBaseVerificationCallbacks(), "no supported verification methods", nil},
{true, newBaseVerificationCallbacks(), "", []event.VerificationMethod{event.VerificationMethodQRCodeScan, event.VerificationMethodReciprocate}},
{false, newSASVerificationCallbacks(), "", []event.VerificationMethod{event.VerificationMethodSAS}},
{true, newSASVerificationCallbacks(), "", []event.VerificationMethod{event.VerificationMethodQRCodeScan, event.VerificationMethodReciprocate, event.VerificationMethodSAS}},
{true, newQRCodeVerificationCallbacks(), "", []event.VerificationMethod{event.VerificationMethodQRCodeScan, event.VerificationMethodQRCodeShow, event.VerificationMethodReciprocate}},
{false, newQRCodeVerificationCallbacks(), "", []event.VerificationMethod{event.VerificationMethodQRCodeShow, event.VerificationMethodReciprocate}},
{false, newAllVerificationCallbacks(), "", []event.VerificationMethod{event.VerificationMethodQRCodeShow, event.VerificationMethodReciprocate, event.VerificationMethodSAS}},
{true, newAllVerificationCallbacks(), "", []event.VerificationMethod{event.VerificationMethodQRCodeScan, event.VerificationMethodQRCodeShow, event.VerificationMethodReciprocate, event.VerificationMethodSAS}},
}
for i, tc := range testCases {
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
ts := createMockServer(t)
defer ts.Close()
client, cryptoStore := ts.Login(t, ctx, aliceUserID, sendingDeviceID)
addDeviceID(ctx, cryptoStore, aliceUserID, sendingDeviceID)
addDeviceID(ctx, cryptoStore, aliceUserID, receivingDeviceID)
addDeviceID(ctx, cryptoStore, aliceUserID, receivingDeviceID2)
senderHelper := verificationhelper.NewVerificationHelper(client, client.Crypto.(*cryptohelper.CryptoHelper).Machine(), nil, tc.callbacks, tc.supportsScan)
err := senderHelper.Init(ctx)
require.NoError(t, err)
txnID, err := senderHelper.StartVerification(ctx, aliceUserID)
if tc.startVerificationErrMsg != "" {
assert.ErrorContains(t, err, tc.startVerificationErrMsg)
return
}
assert.NoError(t, err)
assert.NotEmpty(t, txnID)
toDeviceInbox := ts.DeviceInbox[aliceUserID]
// Ensure that we didn't send a verification request to the
// sending device.
assert.Empty(t, toDeviceInbox[sendingDeviceID])
// Ensure that the verification request was sent to both of
// the other devices.
assert.NotEmpty(t, toDeviceInbox[receivingDeviceID])
assert.NotEmpty(t, toDeviceInbox[receivingDeviceID2])
assert.Equal(t, toDeviceInbox[receivingDeviceID], toDeviceInbox[receivingDeviceID2])
require.Len(t, toDeviceInbox[receivingDeviceID], 1)
// Ensure that the verification request is correct.
verificationRequest := toDeviceInbox[receivingDeviceID][0].Content.AsVerificationRequest()
assert.Equal(t, sendingDeviceID, verificationRequest.FromDevice)
assert.Equal(t, txnID, verificationRequest.TransactionID)
assert.ElementsMatch(t, tc.expectedVerificationMethods, verificationRequest.Methods)
})
}
}
func TestVerification_StartThenCancel(t *testing.T) {
ctx := log.Logger.WithContext(context.TODO())
bystanderDeviceID := id.DeviceID("bystander")
for _, sendingCancels := range []bool{true, false} {
t.Run(fmt.Sprintf("sendingCancels=%t", sendingCancels), func(t *testing.T) {
ts, sendingClient, receivingClient, sendingCryptoStore, receivingCryptoStore, sendingMachine, receivingMachine := initServerAndLoginTwoAlice(t, ctx)
defer ts.Close()
_, _, sendingHelper, receivingHelper := initDefaultCallbacks(t, ctx, sendingClient, receivingClient, sendingMachine, receivingMachine)
bystanderClient, _ := ts.Login(t, ctx, aliceUserID, bystanderDeviceID)
bystanderMachine := bystanderClient.Crypto.(*cryptohelper.CryptoHelper).Machine()
bystanderHelper := verificationhelper.NewVerificationHelper(bystanderClient, bystanderMachine, nil, newAllVerificationCallbacks(), true)
require.NoError(t, bystanderHelper.Init(ctx))
require.NoError(t, sendingCryptoStore.PutDevice(ctx, aliceUserID, bystanderMachine.OwnIdentity()))
require.NoError(t, receivingCryptoStore.PutDevice(ctx, aliceUserID, bystanderMachine.OwnIdentity()))
txnID, err := sendingHelper.StartVerification(ctx, aliceUserID)
require.NoError(t, err)
assert.Empty(t, ts.DeviceInbox[aliceUserID][sendingDeviceID])
// Process the request event on the receiving device.
receivingInbox := ts.DeviceInbox[aliceUserID][receivingDeviceID]
assert.Len(t, receivingInbox, 1)
assert.Equal(t, txnID, receivingInbox[0].Content.AsVerificationRequest().TransactionID)
ts.dispatchToDevice(t, ctx, receivingClient)
// Process the request event on the bystander device.
bystanderInbox := ts.DeviceInbox[aliceUserID][bystanderDeviceID]
assert.Len(t, bystanderInbox, 1)
assert.Equal(t, txnID, bystanderInbox[0].Content.AsVerificationRequest().TransactionID)
ts.dispatchToDevice(t, ctx, bystanderClient)
// Cancel the verification request.
var cancelEvt *event.VerificationCancelEventContent
if sendingCancels {
err = sendingHelper.CancelVerification(ctx, txnID, event.VerificationCancelCodeUser, "Recovery code preferred")
assert.NoError(t, err)
// The sending device should not have a cancellation event.
assert.Empty(t, ts.DeviceInbox[aliceUserID][sendingDeviceID])
// Ensure that the cancellation event was sent to the receiving device.
assert.Len(t, ts.DeviceInbox[aliceUserID][receivingDeviceID], 1)
cancelEvt = ts.DeviceInbox[aliceUserID][receivingDeviceID][0].Content.AsVerificationCancel()
// Ensure that the cancellation event was sent to the bystander device.
assert.Len(t, ts.DeviceInbox[aliceUserID][bystanderDeviceID], 1)
bystanderCancelEvt := ts.DeviceInbox[aliceUserID][bystanderDeviceID][0].Content.AsVerificationCancel()
assert.Equal(t, cancelEvt, bystanderCancelEvt)
} else {
err = receivingHelper.CancelVerification(ctx, txnID, event.VerificationCancelCodeUser, "Recovery code preferred")
assert.NoError(t, err)
// The receiving device should not have a cancellation event.
assert.Empty(t, ts.DeviceInbox[aliceUserID][receivingDeviceID])
// Ensure that the cancellation event was sent to the sending device.
assert.Len(t, ts.DeviceInbox[aliceUserID][sendingDeviceID], 1)
cancelEvt = ts.DeviceInbox[aliceUserID][sendingDeviceID][0].Content.AsVerificationCancel()
// The bystander device should not have a cancellation event.
assert.Empty(t, ts.DeviceInbox[aliceUserID][bystanderDeviceID])
}
assert.Equal(t, txnID, cancelEvt.TransactionID)
assert.Equal(t, event.VerificationCancelCodeUser, cancelEvt.Code)
assert.Equal(t, "Recovery code preferred", cancelEvt.Reason)
if !sendingCancels {
// Process the cancellation event on the sending device.
ts.dispatchToDevice(t, ctx, sendingClient)
// Ensure that the cancellation event was sent to the bystander device.
assert.Len(t, ts.DeviceInbox[aliceUserID][bystanderDeviceID], 1)
bystanderCancelEvt := ts.DeviceInbox[aliceUserID][bystanderDeviceID][0].Content.AsVerificationCancel()
assert.Equal(t, txnID, bystanderCancelEvt.TransactionID)
assert.Equal(t, event.VerificationCancelCodeUser, bystanderCancelEvt.Code)
assert.Equal(t, "The verification was rejected from another device.", bystanderCancelEvt.Reason)
}
})
}
}
func TestVerification_Accept_NoSupportedMethods(t *testing.T) {
ctx := log.Logger.WithContext(context.TODO())
ts := createMockServer(t)
defer ts.Close()
sendingClient, sendingCryptoStore := ts.Login(t, ctx, aliceUserID, sendingDeviceID)
receivingClient, _ := ts.Login(t, ctx, aliceUserID, receivingDeviceID)
addDeviceID(ctx, sendingCryptoStore, aliceUserID, sendingDeviceID)
addDeviceID(ctx, sendingCryptoStore, aliceUserID, receivingDeviceID)
sendingMachine := sendingClient.Crypto.(*cryptohelper.CryptoHelper).Machine()
recoveryKey, cache, err := sendingMachine.GenerateAndUploadCrossSigningKeys(ctx, nil, "")
assert.NoError(t, err)
assert.NotEmpty(t, recoveryKey)
assert.NotNil(t, cache)
sendingHelper := verificationhelper.NewVerificationHelper(sendingClient, sendingMachine, nil, newAllVerificationCallbacks(), true)
err = sendingHelper.Init(ctx)
require.NoError(t, err)
receivingCallbacks := newBaseVerificationCallbacks()
receivingHelper := verificationhelper.NewVerificationHelper(receivingClient, receivingClient.Crypto.(*cryptohelper.CryptoHelper).Machine(), nil, receivingCallbacks, false)
err = receivingHelper.Init(ctx)
require.NoError(t, err)
txnID, err := sendingHelper.StartVerification(ctx, aliceUserID)
require.NoError(t, err)
require.NotEmpty(t, txnID)
ts.dispatchToDevice(t, ctx, receivingClient)
// Ensure that the receiver ignored the request because it
// doesn't support any of the verification methods in the
// request.
assert.Empty(t, receivingCallbacks.GetRequestedVerifications())
}
func TestVerification_Accept_CorrectMethodsPresented(t *testing.T) {
ctx := log.Logger.WithContext(context.TODO())
testCases := []struct {
sendingSupportsScan bool
receivingSupportsScan bool
sendingCallbacks MockVerificationCallbacks
receivingCallbacks MockVerificationCallbacks
expectedVerificationMethods []event.VerificationMethod
}{
{false, false, newSASVerificationCallbacks(), newSASVerificationCallbacks(), []event.VerificationMethod{event.VerificationMethodSAS}},
{true, false, newQRCodeVerificationCallbacks(), newQRCodeVerificationCallbacks(), []event.VerificationMethod{event.VerificationMethodQRCodeShow, event.VerificationMethodReciprocate}},
{false, true, newQRCodeVerificationCallbacks(), newQRCodeVerificationCallbacks(), []event.VerificationMethod{event.VerificationMethodQRCodeScan, event.VerificationMethodReciprocate}},
{true, false, newAllVerificationCallbacks(), newAllVerificationCallbacks(), []event.VerificationMethod{event.VerificationMethodQRCodeShow, event.VerificationMethodReciprocate, event.VerificationMethodSAS}},
{true, true, newAllVerificationCallbacks(), newAllVerificationCallbacks(), []event.VerificationMethod{event.VerificationMethodQRCodeShow, event.VerificationMethodQRCodeScan, event.VerificationMethodReciprocate, event.VerificationMethodSAS}},
}
for i, tc := range testCases {
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
ts, sendingClient, receivingClient, _, _, sendingMachine, receivingMachine := initServerAndLoginTwoAlice(t, ctx)
defer ts.Close()
recoveryKey, sendingCrossSigningKeysCache, err := sendingMachine.GenerateAndUploadCrossSigningKeys(ctx, nil, "")
assert.NoError(t, err)
assert.NotEmpty(t, recoveryKey)
assert.NotNil(t, sendingCrossSigningKeysCache)
sendingHelper := verificationhelper.NewVerificationHelper(sendingClient, sendingMachine, nil, tc.sendingCallbacks, tc.sendingSupportsScan)
err = sendingHelper.Init(ctx)
require.NoError(t, err)
receivingHelper := verificationhelper.NewVerificationHelper(receivingClient, receivingMachine, nil, tc.receivingCallbacks, tc.receivingSupportsScan)
err = receivingHelper.Init(ctx)
require.NoError(t, err)
txnID, err := sendingHelper.StartVerification(ctx, aliceUserID)
require.NoError(t, err)
// Process the verification request on the receiving device.
ts.dispatchToDevice(t, ctx, receivingClient)
// Ensure that the receiving device received a verification
// request with the correct transaction ID.
assert.ElementsMatch(t, []id.VerificationTransactionID{txnID}, tc.receivingCallbacks.GetRequestedVerifications()[aliceUserID])
// Have the receiving device accept the verification request.
err = receivingHelper.AcceptVerification(ctx, txnID)
require.NoError(t, err)
_, sendingIsQRCallbacks := tc.sendingCallbacks.(*qrCodeVerificationCallbacks)
_, sendingIsAllCallbacks := tc.sendingCallbacks.(*allVerificationCallbacks)
sendingCanShowQR := sendingIsQRCallbacks || sendingIsAllCallbacks
_, receivingIsQRCallbacks := tc.receivingCallbacks.(*qrCodeVerificationCallbacks)
_, receivingIsAllCallbacks := tc.receivingCallbacks.(*allVerificationCallbacks)
receivingCanShowQR := receivingIsQRCallbacks || receivingIsAllCallbacks
// Ensure that if the receiving device should show a QR code that
// it has the correct content.
if tc.sendingSupportsScan && receivingCanShowQR {
receivingShownQRCode := tc.receivingCallbacks.GetQRCodeShown(txnID)
require.NotNil(t, receivingShownQRCode)
assert.Equal(t, txnID, receivingShownQRCode.TransactionID)
assert.NotEmpty(t, receivingShownQRCode.SharedSecret)
}
// Check for whether the receiving device should be scanning a QR
// code.
if tc.receivingSupportsScan && sendingCanShowQR {
assert.Contains(t, tc.receivingCallbacks.GetScanQRCodeTransactions(), txnID)
}
// Check that the m.key.verification.ready event has the correct
// content.
sendingInbox := ts.DeviceInbox[aliceUserID][sendingDeviceID]
assert.Len(t, sendingInbox, 1)
readyEvt := sendingInbox[0].Content.AsVerificationReady()
assert.Equal(t, txnID, readyEvt.TransactionID)
assert.Equal(t, receivingDeviceID, readyEvt.FromDevice)
assert.ElementsMatch(t, tc.expectedVerificationMethods, readyEvt.Methods)
// Receive the m.key.verification.ready event on the sending
// device.
ts.dispatchToDevice(t, ctx, sendingClient)
// Ensure that if the sending device should show a QR code that it
// has the correct content.
if tc.receivingSupportsScan && sendingCanShowQR {
sendingShownQRCode := tc.sendingCallbacks.GetQRCodeShown(txnID)
require.NotNil(t, sendingShownQRCode)
assert.Equal(t, txnID, sendingShownQRCode.TransactionID)
assert.NotEmpty(t, sendingShownQRCode.SharedSecret)
}
// Check for whether the sending device should be scanning a QR
// code.
if tc.sendingSupportsScan && receivingCanShowQR {
assert.Contains(t, tc.sendingCallbacks.GetScanQRCodeTransactions(), txnID)
}
})
}
}
// TestAcceptSelfVerificationCancelOnNonParticipatingDevices ensures that we do
// not regress https://github.com/mautrix/go/pull/230.
func TestVerification_Accept_CancelOnNonParticipatingDevices(t *testing.T) {
ctx := log.Logger.WithContext(context.TODO())
ts, sendingClient, receivingClient, sendingCryptoStore, receivingCryptoStore, sendingMachine, receivingMachine := initServerAndLoginTwoAlice(t, ctx)
defer ts.Close()
_, _, sendingHelper, receivingHelper := initDefaultCallbacks(t, ctx, sendingClient, receivingClient, sendingMachine, receivingMachine)
nonParticipatingDeviceID1 := id.DeviceID("non-participating1")
nonParticipatingDeviceID2 := id.DeviceID("non-participating2")
addDeviceID(ctx, sendingCryptoStore, aliceUserID, nonParticipatingDeviceID1)
addDeviceID(ctx, sendingCryptoStore, aliceUserID, nonParticipatingDeviceID2)
addDeviceID(ctx, receivingCryptoStore, aliceUserID, nonParticipatingDeviceID1)
addDeviceID(ctx, receivingCryptoStore, aliceUserID, nonParticipatingDeviceID2)
_, _, err := sendingMachine.GenerateAndUploadCrossSigningKeys(ctx, nil, "")
assert.NoError(t, err)
// Send the verification request from the sender device and accept it on
// the receiving device.
txnID, err := sendingHelper.StartVerification(ctx, aliceUserID)
require.NoError(t, err)
ts.dispatchToDevice(t, ctx, receivingClient)
err = receivingHelper.AcceptVerification(ctx, txnID)
require.NoError(t, err)
// Receive the m.key.verification.ready event on the sending device.
ts.dispatchToDevice(t, ctx, sendingClient)
// The sending and receiving devices should not have any cancellation
// events in their inboxes.
assert.Empty(t, ts.DeviceInbox[aliceUserID][sendingDeviceID])
assert.Empty(t, ts.DeviceInbox[aliceUserID][receivingDeviceID])
// There should now be cancellation events in the non-participating devices
// inboxes (in addition to the request event).
assert.Len(t, ts.DeviceInbox[aliceUserID][nonParticipatingDeviceID1], 2)
assert.Len(t, ts.DeviceInbox[aliceUserID][nonParticipatingDeviceID2], 2)
assert.Equal(t, ts.DeviceInbox[aliceUserID][nonParticipatingDeviceID1][1], ts.DeviceInbox[aliceUserID][nonParticipatingDeviceID2][1])
cancellationEvent := ts.DeviceInbox[aliceUserID][nonParticipatingDeviceID1][1].Content.AsVerificationCancel()
assert.Equal(t, txnID, cancellationEvent.TransactionID)
assert.Equal(t, event.VerificationCancelCodeAccepted, cancellationEvent.Code)
}
func TestVerification_ErrorOnDoubleAccept(t *testing.T) {
ctx := log.Logger.WithContext(context.TODO())
ts, sendingClient, receivingClient, _, _, sendingMachine, receivingMachine := initServerAndLoginTwoAlice(t, ctx)
defer ts.Close()
_, _, sendingHelper, receivingHelper := initDefaultCallbacks(t, ctx, sendingClient, receivingClient, sendingMachine, receivingMachine)
_, _, err := sendingMachine.GenerateAndUploadCrossSigningKeys(ctx, nil, "")
require.NoError(t, err)
txnID, err := sendingHelper.StartVerification(ctx, aliceUserID)
require.NoError(t, err)
ts.dispatchToDevice(t, ctx, receivingClient)
err = receivingHelper.AcceptVerification(ctx, txnID)
require.NoError(t, err)
err = receivingHelper.AcceptVerification(ctx, txnID)
assert.ErrorContains(t, err, "transaction is not in the requested state")
}
// TestVerification_CancelOnDoubleStart ensures that the receiving device
// cancels both transactions if the sending device starts two verifications.
//
// This test ensures that the following bullet point from [Section 10.12.2.2.1
// of the Spec] is followed:
//
// - When the same device attempts to initiate multiple verification attempts,
// the recipient should cancel all attempts with that device.
//
// [Section 10.12.2.2.1 of the Spec]: https://spec.matrix.org/v1.10/client-server-api/#error-and-exception-handling
func TestVerification_CancelOnDoubleStart(t *testing.T) {
ctx := log.Logger.WithContext(context.TODO())
ts, sendingClient, receivingClient, _, _, sendingMachine, receivingMachine := initServerAndLoginTwoAlice(t, ctx)
defer ts.Close()
sendingCallbacks, receivingCallbacks, sendingHelper, receivingHelper := initDefaultCallbacks(t, ctx, sendingClient, receivingClient, sendingMachine, receivingMachine)
_, _, err := sendingMachine.GenerateAndUploadCrossSigningKeys(ctx, nil, "")
require.NoError(t, err)
// Send and accept the first verification request.
txnID1, err := sendingHelper.StartVerification(ctx, aliceUserID)
require.NoError(t, err)
ts.dispatchToDevice(t, ctx, receivingClient)
err = receivingHelper.AcceptVerification(ctx, txnID1)
require.NoError(t, err)
ts.dispatchToDevice(t, ctx, sendingClient) // Process the m.key.verification.ready event
// Send a second verification request
txnID2, err := sendingHelper.StartVerification(ctx, aliceUserID)
require.NoError(t, err)
ts.dispatchToDevice(t, ctx, receivingClient)
// Ensure that the sending device received a cancellation event for both of
// the ongoing transactions.
sendingInbox := ts.DeviceInbox[aliceUserID][sendingDeviceID]
require.Len(t, sendingInbox, 2)
cancelEvt1 := sendingInbox[0].Content.AsVerificationCancel()
cancelEvt2 := sendingInbox[1].Content.AsVerificationCancel()
cancelledTxnIDs := []id.VerificationTransactionID{cancelEvt1.TransactionID, cancelEvt2.TransactionID}
assert.Contains(t, cancelledTxnIDs, txnID1)
assert.Contains(t, cancelledTxnIDs, txnID2)
assert.Equal(t, event.VerificationCancelCodeUnexpectedMessage, cancelEvt1.Code)
assert.Equal(t, event.VerificationCancelCodeUnexpectedMessage, cancelEvt2.Code)
assert.Equal(t, "received multiple verification requests from the same device", cancelEvt1.Reason)
assert.Equal(t, "received multiple verification requests from the same device", cancelEvt2.Reason)
assert.NotNil(t, receivingCallbacks.GetVerificationCancellation(txnID1))
assert.NotNil(t, receivingCallbacks.GetVerificationCancellation(txnID2))
ts.dispatchToDevice(t, ctx, sendingClient) // Process the m.key.verification.cancel events
assert.NotNil(t, sendingCallbacks.GetVerificationCancellation(txnID1))
assert.NotNil(t, sendingCallbacks.GetVerificationCancellation(txnID2))
}