//////////////////////////////////////////////////////////////////////////////// // Copyright © 2022 xx foundation // // // // Use of this source code is governed by a license that can be found in the // // LICENSE file. // //////////////////////////////////////////////////////////////////////////////// //go:build js && wasm package main import ( "bytes" "encoding/json" "errors" "fmt" "gitlab.com/elixxir/crypto/database" "os" "strconv" "testing" "time" "github.com/hack-pad/go-indexeddb/idb" jww "github.com/spf13/jwalterweatherman" "github.com/stretchr/testify/require" "gitlab.com/elixxir/client/v4/channels" cft "gitlab.com/elixxir/client/v4/channelsFileTransfer" "gitlab.com/elixxir/client/v4/cmix/rounds" cryptoBroadcast "gitlab.com/elixxir/crypto/broadcast" "gitlab.com/elixxir/crypto/fileTransfer" "gitlab.com/elixxir/crypto/message" "gitlab.com/elixxir/wasm-utils/storage" "gitlab.com/elixxir/xxdk-wasm/indexedDb/impl" "gitlab.com/xx_network/crypto/csprng" "gitlab.com/xx_network/primitives/id" "gitlab.com/xx_network/primitives/netTime" ) func TestMain(m *testing.M) { jww.SetStdoutThreshold(jww.LevelDebug) os.Exit(m.Run()) } type dummyCbs struct{} func (c *dummyCbs) EventUpdate(int64, []byte) {} // Happy path test for receiving, updating, getting, and deleting a File. func TestWasmModel_ReceiveFile(t *testing.T) { testString := "TestWasmModel_ReceiveFile" m, err := newWASMModel(testString, nil, &dummyCbs{}) if err != nil { t.Fatal(err) } testTs := time.Now() testBytes := []byte(testString) testStatus := cft.Downloading // Insert a test row fId := fileTransfer.NewID(testBytes) err = m.ReceiveFile(fId, testBytes, testBytes, testTs, testStatus) if err != nil { t.Fatal(err) } // Attempt to get stored row storedFile, err := m.GetFile(fId) if err != nil { t.Fatal(err) } // Spot check stored attribute if !bytes.Equal(storedFile.Link, testBytes) { t.Fatalf("Got unequal FileLink values") } // Attempt to updated stored row newTs := time.Now() newBytes := []byte("test") newStatus := cft.Complete err = m.UpdateFile(fId, nil, newBytes, &newTs, &newStatus) if err != nil { t.Fatal(err) } // Check that the update took updatedFile, err := m.GetFile(fId) if err != nil { t.Fatal(err) } // Link should not have changed if !bytes.Equal(updatedFile.Link, testBytes) { t.Fatalf("Link should not have changed") } // Other attributes should have changed if !bytes.Equal(updatedFile.Data, newBytes) { t.Fatalf("Data should have updated") } if !updatedFile.Timestamp.Equal(newTs) { t.Fatalf("TS should have updated, expected %s got %s", newTs, updatedFile.Timestamp) } if updatedFile.Status != newStatus { t.Fatalf("Status should have updated") } // Delete the row err = m.DeleteFile(fId) if err != nil { t.Fatal(err) } // Check that the delete operation took and get provides the expected error _, err = m.GetFile(fId) if err == nil || !errors.Is(channels.NoMessageErr, err) { t.Fatal(err) } } // Happy path, insert message and look it up func TestWasmModel_GetMessage(t *testing.T) { cipher, err := database.NewCipher( []byte("testPass"), []byte("testSalt"), 128, csprng.NewSystemRNG()) if err != nil { t.Fatalf("Failed to create cipher") } for _, c := range []database.Cipher{nil, cipher} { cs := "" if c != nil { cs = "_withCipher" } testString := "TestWasmModel_GetMessage" + cs t.Run(testString, func(t *testing.T) { storage.GetLocalStorage().Clear() testMsgId := message.DeriveChannelMessageID(&id.ID{1}, 0, []byte(testString)) eventModel, err := newWASMModel(testString, c, &dummyCbs{}) if err != nil { t.Fatal(err) } testMsg := buildMessage(id.NewIdFromBytes([]byte(testString), t).Marshal(), testMsgId.Bytes(), nil, testString, []byte(testString), []byte{8, 6, 7, 5}, 0, 0, netTime.Now(), time.Second, 0, 0, false, false, channels.Sent) _, err = eventModel.upsertMessage(testMsg) if err != nil { t.Fatal(err) } msg, err := eventModel.GetMessage(testMsgId) if err != nil { t.Fatal(err) } if msg.UUID == 0 { t.Fatalf("Expected to get a UUID!") } }) } } // Happy path, insert message and delete it func TestWasmModel_DeleteMessage(t *testing.T) { storage.GetLocalStorage().Clear() testString := "TestWasmModel_DeleteMessage" testMsgId := message.DeriveChannelMessageID(&id.ID{1}, 0, []byte(testString)) eventModel, err := newWASMModel(testString, nil, &dummyCbs{}) if err != nil { t.Fatal(err) } // Insert a message testMsg := buildMessage([]byte(testString), testMsgId.Bytes(), nil, testString, []byte(testString), []byte{8, 6, 7, 5}, 0, 0, netTime.Now(), time.Second, 0, 0, false, false, channels.Sent) _, err = eventModel.upsertMessage(testMsg) if err != nil { t.Fatal(err) } // Check the resulting status results, err := impl.Dump(eventModel.db, messageStoreName) if err != nil { t.Fatal(err) } if len(results) != 1 { t.Fatalf("Expected 1 message to exist") } // Delete the message err = eventModel.DeleteMessage(testMsgId) if err != nil { t.Fatal(err) } // Check the resulting status results, err = impl.Dump(eventModel.db, messageStoreName) if err != nil { t.Fatal(err) } if len(results) != 0 { t.Fatalf("Expected no messages to exist") } } // Test wasmModel.UpdateSentStatus happy path and ensure fields don't change. func Test_wasmModel_UpdateSentStatus(t *testing.T) { cipher, err := database.NewCipher( []byte("testPass"), []byte("testSalt"), 128, csprng.NewSystemRNG()) if err != nil { t.Fatalf("Failed to create cipher") } for _, c := range []database.Cipher{nil, cipher} { cs := "" if c != nil { cs = "_withCipher" } t.Run("Test_wasmModel_UpdateSentStatus"+cs, func(t *testing.T) { storage.GetLocalStorage().Clear() testString := "Test_wasmModel_UpdateSentStatus" + cs testMsgId := message.DeriveChannelMessageID( &id.ID{1}, 0, []byte(testString)) eventModel, err2 := newWASMModel(testString, c, &dummyCbs{}) if err2 != nil { t.Fatal(err) } cid, err := id.NewRandomID(csprng.NewSystemRNG(), id.DummyUser.GetType()) require.NoError(t, err) // Store a test message testMsg := buildMessage(cid.Bytes(), testMsgId.Bytes(), nil, testString, []byte(testString), []byte{8, 6, 7, 5}, 0, 0, netTime.Now(), time.Second, 0, 0, false, false, channels.Sent) uuid, err2 := eventModel.upsertMessage(testMsg) if err2 != nil { t.Fatal(err2) } // Ensure one message is stored results, err2 := impl.Dump(eventModel.db, messageStoreName) if err2 != nil { t.Fatal(err2) } if len(results) != 1 { t.Fatalf("Expected 1 message to exist") } // Update the sentStatus expectedStatus := channels.Failed eventModel.UpdateFromUUID( uuid, nil, nil, nil, nil, nil, &expectedStatus) // Check the resulting status results, err = impl.Dump(eventModel.db, messageStoreName) if err != nil { t.Fatal(err) } if len(results) != 1 { t.Fatalf("Expected 1 message to exist") } resultMsg := &Message{} err = json.Unmarshal([]byte(results[0]), resultMsg) if err != nil { t.Fatal(err) } if resultMsg.Status != uint8(expectedStatus) { t.Fatalf("Unexpected Status: %v", resultMsg.Status) } // Make sure other fields didn't change if resultMsg.Nickname != testString { t.Fatalf("Unexpected Nickname: %v", resultMsg.Nickname) } }) } } // Smoke test wasmModel.JoinChannel/wasmModel.LeaveChannel happy paths. func Test_wasmModel_JoinChannel_LeaveChannel(t *testing.T) { cipher, err := database.NewCipher( []byte("testPass"), []byte("testSalt"), 128, csprng.NewSystemRNG()) if err != nil { t.Fatalf("Failed to create cipher") } for _, c := range []database.Cipher{nil, cipher} { cs := "" if c != nil { cs = "_withCipher" } t.Run("Test_wasmModel_JoinChannel_LeaveChannel"+cs, func(t *testing.T) { storage.GetLocalStorage().Clear() eventModel, err2 := newWASMModel("test", c, &dummyCbs{}) if err2 != nil { t.Fatal(err2) } testChannel := &cryptoBroadcast.Channel{ ReceptionID: id.NewIdFromString("test", id.Generic, t), Name: "test", Description: "test", Salt: nil, } testChannel2 := &cryptoBroadcast.Channel{ ReceptionID: id.NewIdFromString("test2", id.Generic, t), Name: "test2", Description: "test2", Salt: nil, } eventModel.JoinChannel(testChannel) eventModel.JoinChannel(testChannel2) results, err2 := impl.Dump(eventModel.db, channelStoreName) if err2 != nil { t.Fatal(err2) } if len(results) != 2 { t.Fatalf("Expected 2 channels to exist") } eventModel.LeaveChannel(testChannel.ReceptionID) results, err = impl.Dump(eventModel.db, channelStoreName) if err != nil { t.Fatal(err) } if len(results) != 1 { t.Fatalf("Expected 1 channels to exist") } }) } } // Test UUID gets returned when different messages are added. func Test_wasmModel_UUIDTest(t *testing.T) { cipher, err := database.NewCipher( []byte("testPass"), []byte("testSalt"), 128, csprng.NewSystemRNG()) if err != nil { t.Fatalf("Failed to create cipher") } for _, c := range []database.Cipher{nil, cipher} { cs := "" if c != nil { cs = "_withCipher" } t.Run("Test_wasmModel_UUIDTest"+cs, func(t *testing.T) { storage.GetLocalStorage().Clear() testString := "testHello" + cs eventModel, err2 := newWASMModel(testString, c, &dummyCbs{}) if err2 != nil { t.Fatal(err2) } uuids := make([]uint64, 10) for i := 0; i < 10; i++ { // Store a test message channelID := id.NewIdFromBytes([]byte(testString), t) msgID := message.ID{} copy(msgID[:], testString+fmt.Sprintf("%d", i)) rnd := rounds.Round{ID: id.Round(42)} uuid := eventModel.ReceiveMessage(channelID, msgID, "test", testString+fmt.Sprintf("%d", i), []byte{8, 6, 7, 5}, 0, 0, netTime.Now(), time.Hour, rnd, 0, channels.Sent, false) uuids[i] = uuid } for i := 0; i < 10; i++ { for j := i + 1; j < 10; j++ { if uuids[i] == uuids[j] { t.Fatalf("uuid failed: %d[%d] == %d[%d]", uuids[i], i, uuids[j], j) } } } }) } } // Tests if the same message ID being sent always returns the same UUID. func Test_wasmModel_DuplicateReceives(t *testing.T) { cipher, err := database.NewCipher( []byte("testPass"), []byte("testSalt"), 128, csprng.NewSystemRNG()) if err != nil { t.Fatalf("Failed to create cipher") } for _, c := range []database.Cipher{nil, cipher} { cs := "" if c != nil { cs = "_withCipher" } testString := "Test_wasmModel_DuplicateReceives" + cs t.Run(testString, func(t *testing.T) { storage.GetLocalStorage().Clear() eventModel, err := newWASMModel(testString, c, &dummyCbs{}) if err != nil { t.Fatal(err) } // Store a test message msgID := message.ID{} copy(msgID[:], testString) channelID := id.NewIdFromBytes([]byte(testString), t) rnd := rounds.Round{ID: id.Round(42)} uuid := eventModel.ReceiveMessage(channelID, msgID, "test", testString, []byte{8, 6, 7, 5}, 0, 0, netTime.Now(), time.Hour, rnd, 0, channels.Sent, false) if uuid != 1 { t.Fatalf("Expected UUID to be one for first receive") } // Store duplicate messages with same messageID for i := 0; i < 10; i++ { uuid = eventModel.ReceiveMessage(channelID, msgID, "test", testString+fmt.Sprintf("%d", i), []byte{8, 6, 7, 5}, 0, 0, netTime.Now(), time.Hour, rnd, 0, channels.Sent, false) if uuid != 0 { t.Fatalf("Expected UUID to be zero for duplicate receives") } } }) } } // Happy path: Inserts many messages, deletes some, and checks that the final // result is as expected. func Test_wasmModel_deleteMsgByChannel(t *testing.T) { cipher, err := database.NewCipher( []byte("testPass"), []byte("testSalt"), 128, csprng.NewSystemRNG()) if err != nil { t.Fatalf("Failed to create cipher") } for _, c := range []database.Cipher{nil, cipher} { cs := "" if c != nil { cs = "_withCipher" } testString := "Test_wasmModel_deleteMsgByChannel" + cs t.Run(testString, func(t *testing.T) { storage.GetLocalStorage().Clear() totalMessages := 10 expectedMessages := 5 eventModel, err := newWASMModel(testString, c, &dummyCbs{}) if err != nil { t.Fatal(err) } // Create a test channel id deleteChannel := id.NewIdFromString("deleteMe", id.Generic, t) keepChannel := id.NewIdFromString("dontDeleteMe", id.Generic, t) // Store some test messages for i := 0; i < totalMessages; i++ { testStr := testString + strconv.Itoa(i) // Interleave the channel id to ensure cursor is behaving intelligently thisChannel := deleteChannel if i%2 == 0 { thisChannel = keepChannel } testMsgId := message.DeriveChannelMessageID( &id.ID{byte(i)}, 0, []byte(testStr)) eventModel.ReceiveMessage(thisChannel, testMsgId, testStr, testStr, []byte{8, 6, 7, 5}, 0, 0, netTime.Now(), time.Second, rounds.Round{ID: id.Round(0)}, 0, channels.Sent, false) } // Check pre-results result, err := impl.Dump(eventModel.db, messageStoreName) if err != nil { t.Fatal(err) } if len(result) != totalMessages { t.Fatalf("Expected %d messages, got %d", totalMessages, len(result)) } // Do delete err = eventModel.deleteMsgByChannel(deleteChannel) if err != nil { t.Error(err) } // Check final results result, err = impl.Dump(eventModel.db, messageStoreName) if err != nil { t.Fatal(err) } if len(result) != expectedMessages { t.Fatalf("Expected %d messages, got %d", expectedMessages, len(result)) } }) } } // This test is designed to prove the behavior of unique indexes. // Inserts will not fail, they simply will not happen. func TestWasmModel_receiveHelper_UniqueIndex(t *testing.T) { cipher, err := database.NewCipher( []byte("testPass"), []byte("testSalt"), 128, csprng.NewSystemRNG()) if err != nil { t.Fatalf("Failed to create cipher") } for i, c := range []database.Cipher{nil, cipher} { cs := "" if c != nil { cs = "_withCipher" } t.Run("TestWasmModel_receiveHelper_UniqueIndex"+cs, func(t *testing.T) { storage.GetLocalStorage().Clear() testString := fmt.Sprintf("test_receiveHelper_UniqueIndex_%d", i) eventModel, err := newWASMModel(testString, c, &dummyCbs{}) if err != nil { t.Fatal(err) } // Ensure index is unique txn, err := eventModel.db.Transaction( idb.TransactionReadOnly, messageStoreName) if err != nil { t.Fatal(err) } store, err := txn.ObjectStore(messageStoreName) if err != nil { t.Fatal(err) } idx, err := store.Index(messageStoreMessageIndex) if err != nil { t.Fatal(err) } if isUnique, err3 := idx.Unique(); !isUnique { t.Fatalf("Index is not unique!") } else if err3 != nil { t.Fatal(err3) } testMsgId := message.DeriveChannelMessageID(&id.ID{1}, 0, []byte(testString)) testMsg := buildMessage([]byte(testString), testMsgId.Bytes(), nil, testString, []byte(testString), []byte{8, 6, 7, 5}, 0, 0, netTime.Now(), time.Second, 0, 0, false, false, channels.Sent) testMsgId2 := message.DeriveChannelMessageID(&id.ID{2}, 0, []byte(testString)) testMsg2 := buildMessage([]byte(testString), testMsgId2.Bytes(), nil, testString, []byte(testString), []byte{8, 6, 7, 5}, 0, 0, netTime.Now(), time.Second, 0, 0, false, false, channels.Sent) // First message insert should succeed uuid, err := eventModel.upsertMessage(testMsg) if err != nil { t.Fatal(err) } // The duplicate entry should fail duplicateUuid, err := eventModel.upsertMessage(testMsg) if err == nil { t.Fatal("Expected error to happen") } if duplicateUuid != 0 { t.Fatalf("Expected UUID %d to be 0", duplicateUuid) } // Now insert a message with a different message ID from the first uuid2, err := eventModel.upsertMessage(testMsg2) if err != nil { t.Fatal(err) } if uuid2 == uuid { t.Fatalf("Expected UUID %d to NOT match %d", uuid, uuid2) } // Except this time, we update the second entry to have the same // message ID as the first testMsg2.MessageID = testMsgId.Bytes() duplicateUuid, err = eventModel.upsertMessage(testMsg) if err == nil { t.Fatal("Expected error to happen") } if duplicateUuid != 0 { t.Fatalf("Expected UUID %d to be 0", uuid) } }) } }