diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e1a8ef567bb5b16fa78d4bf6b5537c5103d45dc9..b145fc985b6ea48f9ef9ba9d06c0ba082b0f8955 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -25,6 +25,8 @@ stages: build: stage: build + except: + - tags script: - go mod vendor -v - mkdir -p release @@ -36,6 +38,8 @@ build: wasm-test: stage: test + except: + - tags script: - export PATH=/root/go/bin:$PATH - echo > utils/utils_js.s @@ -44,23 +48,29 @@ wasm-test: go-test: stage: test + except: + - tags script: - go mod vendor -v - go test ./... -v version_check: stage: version_check + except: + - tags only: - master - release image: $DOCKER_IMAGE script: - GITTAG=$(git describe --tags) - - CODEVERS=$(cat utils/version.go | grep "const SEMVER =" | cut -d ' ' -f4 | tr -d '"') + - CODEVERS=$(cat storage/version.go | grep "const SEMVER =" | cut -d ' ' -f4 | tr -d '"') - if [[ $GITTAG != $CODEVERS ]]; then echo "VERSION NUMBER BAD $GITTAG != $CODEVER"; exit -1; fi tag: stage: build + except: + - tags image: $DOCKER_IMAGE script: - git remote add origin_tags git@$GITLAB_SERVER:elixxir/xxdk-wasm.git || true @@ -76,6 +86,8 @@ tag: # master/release, this will fail to pull the latest client, and the docs will not update. doc-update: stage: doc-update + except: + - tags image: $DOCKER_IMAGE script: # We use GOPRIVATE blank because not want to directly pull client, we want to use the public cache. diff --git a/indexedDb/implementation.go b/indexedDb/channels/implementation.go similarity index 69% rename from indexedDb/implementation.go rename to indexedDb/channels/implementation.go index d819fde24d9d9404e3f23fb20d7376594eb07278..875682329d393031c8b89401d376374c6659e538 100644 --- a/indexedDb/implementation.go +++ b/indexedDb/channels/implementation.go @@ -7,13 +7,13 @@ //go:build js && wasm -package indexedDb +package channels import ( - "context" "crypto/ed25519" "encoding/base64" "encoding/json" + "gitlab.com/elixxir/xxdk-wasm/indexedDb" "sync" "syscall/js" "time" @@ -30,10 +30,6 @@ import ( "gitlab.com/xx_network/primitives/id" ) -// dbTimeout is the global timeout for operations with the storage -// [context.Context]. -const dbTimeout = time.Second - // wasmModel implements [channels.EventModel] interface, which uses the channels // system passed an object that adheres to in order to get events on the // channel. @@ -44,11 +40,6 @@ type wasmModel struct { updateMux sync.Mutex } -// newContext builds a context for database operations. -func newContext() (context.Context, context.CancelFunc) { - return context.WithTimeout(context.Background(), dbTimeout) -} - // JoinChannel is called whenever a channel is joined locally. func (w *wasmModel) JoinChannel(channel *cryptoBroadcast.Channel) { parentErr := errors.New("failed to JoinChannel") @@ -74,38 +65,11 @@ func (w *wasmModel) JoinChannel(channel *cryptoBroadcast.Channel) { return } - // Prepare the Transaction - txn, err := w.db.Transaction(idb.TransactionReadWrite, channelsStoreName) - if err != nil { - jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, - "Unable to create Transaction: %+v", err)) - return - } - store, err := txn.ObjectStore(channelsStoreName) - if err != nil { - jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, - "Unable to get ObjectStore: %+v", err)) - return - } - - // Perform the operation - _, err = store.Put(channelObj) - if err != nil { - jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, - "Unable to Add Channel: %+v", err)) - return - } - - // Wait for the operation to return - ctx, cancel := newContext() - err = txn.Await(ctx) - cancel() + _, err = indexedDb.Put(w.db, channelsStoreName, channelObj) if err != nil { jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, - "Adding Channel failed: %+v", err)) - return + "Unable to put Channel: %+v", err)) } - jww.DEBUG.Printf("Successfully added channel: %s", channel.ReceptionID) } // LeaveChannel is called whenever a channel is left locally. @@ -135,7 +99,7 @@ func (w *wasmModel) LeaveChannel(channelID *id.ID) { } // Wait for the operation to return - ctx, cancel := newContext() + ctx, cancel := indexedDb.NewContext() err = txn.Await(ctx) cancel() if err != nil { @@ -183,7 +147,7 @@ func (w *wasmModel) deleteMsgByChannel(channelID *id.ID) error { if err != nil { return errors.WithMessagef(parentErr, "Unable to open Cursor: %+v", err) } - ctx, cancel := newContext() + ctx, cancel := indexedDb.NewContext() err = cursorRequest.Iter(ctx, func(cursor *idb.CursorWithValue) error { _, err := cursor.Delete() @@ -322,17 +286,22 @@ func (w *wasmModel) UpdateSentStatus(uuid uint64, key := js.ValueOf(uuid) // Use the key to get the existing Message - currentMsg, err := w.get(messageStoreName, key) + currentMsg, err := indexedDb.Get(w.db, messageStoreName, key) if err != nil { + jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, + "Unable to get message: %+v", err)) return } // Extract the existing Message and update the Status newMessage := &Message{} - err = json.Unmarshal([]byte(currentMsg), newMessage) + err = json.Unmarshal([]byte(utils.JsToJson(currentMsg)), newMessage) if err != nil { + jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, + "Could not JSON unmarshal message: %+v", err)) return } + newMessage.Status = uint8(status) if !messageID.Equals(cryptoChannel.MessageID{}) { newMessage.MessageID = messageID.Bytes() @@ -405,31 +374,15 @@ func (w *wasmModel) receiveHelper(newMessage *Message, isUpdate bool) (uint64, messageObj.Delete("id") } - // Prepare the Transaction - txn, err := w.db.Transaction(idb.TransactionReadWrite, messageStoreName) - if err != nil { - return 0, errors.Errorf("Unable to create Transaction: %+v", - err) - } - store, err := txn.ObjectStore(messageStoreName) - if err != nil { - return 0, errors.Errorf("Unable to get ObjectStore: %+v", err) - } - - // Perform the upsert (put) operation - addReq, err := store.Put(messageObj) + // Store message to database + addReq, err := indexedDb.Put(w.db, messageStoreName, messageObj) if err != nil { - return 0, errors.Errorf("Unable to upsert Message: %+v", err) + return 0, errors.Errorf("Unable to put Message: %+v", err) } - - // Wait for the operation to return - ctx, cancel := newContext() - err = txn.Await(ctx) - cancel() + res, err := addReq.Result() if err != nil { - return 0, errors.Errorf("Upserting Message failed: %+v", err) + return 0, errors.Errorf("Unable to get Message result: %+v", err) } - res, err := addReq.Result() // NOTE: Sometimes the insert fails to return an error but hits a duplicate // insert, so this fallthrough returns the UUID entry in that case. @@ -448,135 +401,19 @@ func (w *wasmModel) receiveHelper(newMessage *Message, isUpdate bool) (uint64, return uuid, nil } -// get is a generic private helper for getting values from the given -// [idb.ObjectStore]. -func (w *wasmModel) get(objectStoreName string, key js.Value) (string, error) { - parentErr := errors.Errorf("failed to get %s/%s", objectStoreName, key) - - // Prepare the Transaction - txn, err := w.db.Transaction(idb.TransactionReadOnly, objectStoreName) - if err != nil { - return "", errors.WithMessagef(parentErr, - "Unable to create Transaction: %+v", err) - } - store, err := txn.ObjectStore(objectStoreName) - if err != nil { - return "", errors.WithMessagef(parentErr, - "Unable to get ObjectStore: %+v", err) - } - - // Perform the operation - getRequest, err := store.Get(key) - if err != nil { - return "", errors.WithMessagef(parentErr, - "Unable to Get from ObjectStore: %+v", err) - } - - // Wait for the operation to return - ctx, cancel := newContext() - resultObj, err := getRequest.Await(ctx) - cancel() - if err != nil { - return "", errors.WithMessagef(parentErr, - "Unable to get from ObjectStore: %+v", err) - } - - // Process result into string - resultStr := utils.JsToJson(resultObj) - jww.DEBUG.Printf("Got from %s/%s: %s", objectStoreName, key, resultStr) - return resultStr, nil -} - +// msgIDLookup gets the UUID of the Message with the given messageID. func (w *wasmModel) msgIDLookup(messageID cryptoChannel.MessageID) (uint64, error) { - parentErr := errors.Errorf("failed to get %s/%s", messageStoreName, - messageID) - - // Prepare the Transaction - txn, err := w.db.Transaction(idb.TransactionReadOnly, messageStoreName) - if err != nil { - return 0, errors.WithMessagef(parentErr, - "Unable to create Transaction: %+v", err) - } - store, err := txn.ObjectStore(messageStoreName) - if err != nil { - return 0, errors.WithMessagef(parentErr, - "Unable to get ObjectStore: %+v", err) - } - idx, err := store.Index(messageStoreMessageIndex) - if err != nil { - return 0, errors.WithMessagef(parentErr, - "Unable to get index: %+v", err) - } - - msgIDStr := base64.StdEncoding.EncodeToString(messageID.Bytes()) - - keyReq, err := idx.Get(js.ValueOf(msgIDStr)) + msgIDStr := js.ValueOf(base64.StdEncoding.EncodeToString(messageID.Bytes())) + resultObj, err := indexedDb.GetIndex(w.db, messageStoreName, + messageStoreMessageIndex, msgIDStr) if err != nil { - return 0, errors.WithMessagef(parentErr, - "Unable to get keyReq: %+v", err) + return 0, err } - // Wait for the operation to return - ctx, cancel := newContext() - keyObj, err := keyReq.Await(ctx) - cancel() - if err != nil { - return 0, errors.WithMessagef(parentErr, - "Unable to get from ObjectStore: %+v", err) - } - - // Process result into string - resultStr := utils.JsToJson(keyObj) - jww.DEBUG.Printf("Index lookup of %s/%s/%s: %s", messageStoreName, - messageStoreMessageIndex, msgIDStr, resultStr) uuid := uint64(0) - if !keyObj.IsUndefined() { - uuid = uint64(keyObj.Get("id").Int()) + if !resultObj.IsUndefined() { + uuid = uint64(resultObj.Get("id").Int()) } return uuid, nil } - -// dump returns the given [idb.ObjectStore] contents to string slice for -// debugging purposes. -func (w *wasmModel) dump(objectStoreName string) ([]string, error) { - parentErr := errors.Errorf("failed to dump %s", objectStoreName) - - txn, err := w.db.Transaction(idb.TransactionReadOnly, objectStoreName) - if err != nil { - return nil, errors.WithMessagef(parentErr, - "Unable to create Transaction: %+v", err) - } - store, err := txn.ObjectStore(objectStoreName) - if err != nil { - return nil, errors.WithMessagef(parentErr, - "Unable to get ObjectStore: %+v", err) - } - cursorRequest, err := store.OpenCursor(idb.CursorNext) - if err != nil { - return nil, errors.WithMessagef(parentErr, - "Unable to open Cursor: %+v", err) - } - - // Run the query - jww.DEBUG.Printf("%s values:", objectStoreName) - results := make([]string, 0) - ctx, cancel := newContext() - err = cursorRequest.Iter(ctx, - func(cursor *idb.CursorWithValue) error { - value, err := cursor.Value() - if err != nil { - return err - } - valueStr := utils.JsToJson(value) - results = append(results, valueStr) - jww.DEBUG.Printf("- %v", valueStr) - return nil - }) - cancel() - if err != nil { - return nil, errors.WithMessagef(parentErr, - "Unable to dump ObjectStore: %+v", err) - } - return results, nil -} diff --git a/indexedDb/channels/implementation_test.go b/indexedDb/channels/implementation_test.go new file mode 100644 index 0000000000000000000000000000000000000000..27f33649eac4d02514a09b6e2117fb148c6b764e --- /dev/null +++ b/indexedDb/channels/implementation_test.go @@ -0,0 +1,452 @@ +//////////////////////////////////////////////////////////////////////////////// +// 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 channels + +import ( + "encoding/json" + "fmt" + "github.com/hack-pad/go-indexeddb/idb" + "gitlab.com/elixxir/xxdk-wasm/indexedDb" + "gitlab.com/elixxir/xxdk-wasm/storage" + "gitlab.com/xx_network/crypto/csprng" + "gitlab.com/xx_network/primitives/netTime" + "os" + "strconv" + "testing" + "time" + + jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/client/v4/channels" + "gitlab.com/elixxir/client/v4/cmix/rounds" + cryptoBroadcast "gitlab.com/elixxir/crypto/broadcast" + "gitlab.com/elixxir/crypto/channel" + cryptoChannel "gitlab.com/elixxir/crypto/channel" + "gitlab.com/xx_network/primitives/id" +) + +func TestMain(m *testing.M) { + jww.SetStdoutThreshold(jww.LevelDebug) + os.Exit(m.Run()) +} + +func dummyCallback(uint64, *id.ID, bool) {} + +// Happy path, insert message and look it up +func TestWasmModel_msgIDLookup(t *testing.T) { + cipher, err := cryptoChannel.NewCipher([]byte("testpass"), []byte("testsalt"), 128, csprng.NewSystemRNG()) + if err != nil { + t.Fatalf("Failed to create cipher") + } + for _, c := range []cryptoChannel.Cipher{nil, cipher} { + cs := "" + if cipher != nil { + cs = "_withCipher" + } + t.Run(fmt.Sprintf("TestWasmModel_msgIDLookup%s", cs), func(t *testing.T) { + + storage.GetLocalStorage().Clear() + testString := "test" + testMsgId := channel.MakeMessageID([]byte(testString), &id.ID{1}) + eventModel, err := newWASMModel(testString, c, dummyCallback) + if err != nil { + t.Fatalf("%+v", err) + } + + testMsg := buildMessage([]byte(testString), testMsgId.Bytes(), nil, + testString, []byte(testString), []byte{8, 6, 7, 5}, 0, netTime.Now(), + time.Second, 0, 0, channels.Sent) + _, err = eventModel.receiveHelper(testMsg, false) + if err != nil { + t.Fatalf("%+v", err) + } + + uuid, err := eventModel.msgIDLookup(testMsgId) + if err != nil { + t.Fatalf("%+v", err) + } + if uuid == 0 { + t.Fatalf("Expected to get a UUID!") + } + }) + } +} + +// Test wasmModel.UpdateSentStatus happy path and ensure fields don't change. +func Test_wasmModel_UpdateSentStatus(t *testing.T) { + cipher, err := cryptoChannel.NewCipher([]byte("testpass"), []byte("testsalt"), 128, csprng.NewSystemRNG()) + if err != nil { + t.Fatalf("Failed to create cipher") + } + for _, c := range []cryptoChannel.Cipher{nil, cipher} { + cs := "" + if cipher != nil { + cs = "_withCipher" + } + t.Run(fmt.Sprintf("Test_wasmModel_UpdateSentStatus%s", cs), func(t *testing.T) { + storage.GetLocalStorage().Clear() + testString := "test" + testMsgId := channel.MakeMessageID([]byte(testString), &id.ID{1}) + eventModel, err := newWASMModel(testString, c, dummyCallback) + if err != nil { + t.Fatalf("%+v", err) + } + + // Store a test message + testMsg := buildMessage([]byte(testString), testMsgId.Bytes(), nil, + testString, []byte(testString), []byte{8, 6, 7, 5}, 0, netTime.Now(), + time.Second, 0, 0, channels.Sent) + uuid, err := eventModel.receiveHelper(testMsg, false) + if err != nil { + t.Fatalf("%+v", err) + } + + // Ensure one message is stored + results, err := indexedDb.Dump(eventModel.db, messageStoreName) + if err != nil { + t.Fatalf("%+v", err) + } + if len(results) != 1 { + t.Fatalf("Expected 1 message to exist") + } + + // Update the sentStatus + expectedStatus := channels.Failed + eventModel.UpdateSentStatus(uuid, testMsgId, netTime.Now(), + rounds.Round{ID: 8675309}, expectedStatus) + + // Check the resulting status + results, err = indexedDb.Dump(eventModel.db, messageStoreName) + if err != nil { + t.Fatalf("%+v", 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.Fatalf("%+v", 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 := cryptoChannel.NewCipher([]byte("testpass"), []byte("testsalt"), 128, csprng.NewSystemRNG()) + if err != nil { + t.Fatalf("Failed to create cipher") + } + for _, c := range []cryptoChannel.Cipher{nil, cipher} { + cs := "" + if cipher != nil { + cs = "_withCipher" + } + t.Run(fmt.Sprintf("Test_wasmModel_JoinChannel_LeaveChannel%s", cs), func(t *testing.T) { + storage.GetLocalStorage().Clear() + eventModel, err := newWASMModel("test", c, dummyCallback) + if err != nil { + t.Fatalf("%+v", err) + } + + 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, err := indexedDb.Dump(eventModel.db, channelsStoreName) + if err != nil { + t.Fatalf("%+v", err) + } + if len(results) != 2 { + t.Fatalf("Expected 2 channels to exist") + } + eventModel.LeaveChannel(testChannel.ReceptionID) + results, err = indexedDb.Dump(eventModel.db, channelsStoreName) + if err != nil { + t.Fatalf("%+v", 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 := cryptoChannel.NewCipher([]byte("testpass"), []byte("testsalt"), 128, csprng.NewSystemRNG()) + if err != nil { + t.Fatalf("Failed to create cipher") + } + for _, c := range []cryptoChannel.Cipher{nil, cipher} { + cs := "" + if cipher != nil { + cs = "_withCipher" + } + t.Run(fmt.Sprintf("Test_wasmModel_UUIDTest%s", cs), func(t *testing.T) { + storage.GetLocalStorage().Clear() + testString := "testHello" + eventModel, err := newWASMModel(testString, c, dummyCallback) + if err != nil { + t.Fatalf("%+v", err) + } + + uuids := make([]uint64, 10) + + for i := 0; i < 10; i++ { + // Store a test message + channelID := id.NewIdFromBytes([]byte(testString), t) + msgID := channel.MessageID{} + 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, + netTime.Now(), time.Hour, rnd, 0, channels.Sent) + 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 := cryptoChannel.NewCipher([]byte("testpass"), []byte("testsalt"), 128, csprng.NewSystemRNG()) + if err != nil { + t.Fatalf("Failed to create cipher") + } + for _, c := range []cryptoChannel.Cipher{nil, cipher} { + cs := "" + if cipher != nil { + cs = "_withCipher" + } + t.Run(fmt.Sprintf("Test_wasmModel_DuplicateReceives%s", cs), func(t *testing.T) { + storage.GetLocalStorage().Clear() + testString := "testHello" + eventModel, err := newWASMModel(testString, c, dummyCallback) + if err != nil { + t.Fatalf("%+v", err) + } + + uuids := make([]uint64, 10) + + msgID := channel.MessageID{} + copy(msgID[:], testString) + for i := 0; i < 10; i++ { + // Store a test message + channelID := id.NewIdFromBytes([]byte(testString), t) + rnd := rounds.Round{ID: id.Round(42)} + uuid := eventModel.ReceiveMessage(channelID, msgID, "test", + testString+fmt.Sprintf("%d", i), []byte{8, 6, 7, 5}, 0, + netTime.Now(), time.Hour, rnd, 0, channels.Sent) + 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) + } + } + } + }) + } + +} + +// 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 := cryptoChannel.NewCipher([]byte("testpass"), []byte("testsalt"), 128, csprng.NewSystemRNG()) + if err != nil { + t.Fatalf("Failed to create cipher") + } + for _, c := range []cryptoChannel.Cipher{nil, cipher} { + cs := "" + if cipher != nil { + cs = "_withCipher" + } + t.Run(fmt.Sprintf("Test_wasmModel_deleteMsgByChannel%s", cs), func(t *testing.T) { + storage.GetLocalStorage().Clear() + testString := "test_deleteMsgByChannel" + totalMessages := 10 + expectedMessages := 5 + eventModel, err := newWASMModel(testString, c, dummyCallback) + if err != nil { + t.Fatalf("%+v", 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 := channel.MakeMessageID([]byte(testStr), &id.ID{1}) + eventModel.ReceiveMessage(thisChannel, testMsgId, testStr, testStr, + []byte{8, 6, 7, 5}, 0, netTime.Now(), time.Second, + rounds.Round{ID: id.Round(0)}, 0, channels.Sent) + } + + // Check pre-results + result, err := indexedDb.Dump(eventModel.db, messageStoreName) + if err != nil { + t.Fatalf("%+v", err) + } + if len(result) != totalMessages { + t.Errorf("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 = indexedDb.Dump(eventModel.db, messageStoreName) + if err != nil { + t.Fatalf("%+v", err) + } + if len(result) != expectedMessages { + t.Errorf("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 := cryptoChannel.NewCipher([]byte("testpass"), []byte("testsalt"), 128, csprng.NewSystemRNG()) + if err != nil { + t.Fatalf("Failed to create cipher") + } + for i, c := range []cryptoChannel.Cipher{nil, cipher} { + cs := "" + if cipher != nil { + cs = "_withCipher" + } + t.Run(fmt.Sprintf("TestWasmModel_receiveHelper_UniqueIndex%s", cs), func(t *testing.T) { + storage.GetLocalStorage().Clear() + testString := fmt.Sprintf("test_receiveHelper_UniqueIndex_%d", i) + eventModel, err := newWASMModel(testString, c, dummyCallback) + 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, err2 := idx.Unique(); !isUnique { + t.Fatalf("Index is not unique!") + } else if err2 != nil { + t.Fatal(err2) + } + + // First message insert should succeed + testMsgId := channel.MakeMessageID([]byte(testString), &id.ID{1}) + testMsg := buildMessage([]byte(testString), testMsgId.Bytes(), nil, + testString, []byte(testString), []byte{8, 6, 7, 5}, 0, netTime.Now(), + time.Second, 0, 0, channels.Sent) + _, err = eventModel.receiveHelper(testMsg, false) + if err != nil { + t.Fatal(err) + } + + // The duplicate entry won't fail, but it just silently shouldn't happen + _, err = eventModel.receiveHelper(testMsg, false) + if err != nil { + t.Fatalf("%+v", err) + } + results, err := indexedDb.Dump(eventModel.db, messageStoreName) + if err != nil { + t.Fatalf("%+v", err) + } + if len(results) != 1 { + t.Fatalf("Expected only a single message, got %d", len(results)) + } + + // Now insert a message with a different message ID from the first + testMsgId2 := channel.MakeMessageID([]byte(testString), &id.ID{2}) + testMsg = buildMessage([]byte(testString), testMsgId2.Bytes(), nil, + testString, []byte(testString), []byte{8, 6, 7, 5}, 0, netTime.Now(), + time.Second, 0, 0, channels.Sent) + primaryKey, err := eventModel.receiveHelper(testMsg, false) + if err != nil { + t.Fatal(err) + } + + // Except this time, we update the second entry to have the same + // message ID as the first + testMsg.ID = primaryKey + testMsg.MessageID = testMsgId.Bytes() + _, err = eventModel.receiveHelper(testMsg, true) + if err != nil { + t.Fatal(err) + } + + // The update to duplicate message ID won't fail, + // but it just silently shouldn't happen + results, err = indexedDb.Dump(eventModel.db, messageStoreName) + if err != nil { + t.Fatalf("%+v", err) + } + // TODO: Convert JSON to Message, ensure Message ID fields differ + + }) + } +} diff --git a/indexedDb/init.go b/indexedDb/channels/init.go similarity index 96% rename from indexedDb/init.go rename to indexedDb/channels/init.go index f3f4503dec9284896ce6f66d2db44209640a46ad..12d6bb8840f3d75c8778b871ac883a84ad008ada 100644 --- a/indexedDb/init.go +++ b/indexedDb/channels/init.go @@ -7,7 +7,7 @@ //go:build js && wasm -package indexedDb +package channels import ( "github.com/hack-pad/go-indexeddb/idb" @@ -15,6 +15,7 @@ import ( jww "github.com/spf13/jwalterweatherman" "gitlab.com/elixxir/client/v4/channels" cryptoChannel "gitlab.com/elixxir/crypto/channel" + "gitlab.com/elixxir/xxdk-wasm/indexedDb" "gitlab.com/elixxir/xxdk-wasm/storage" "gitlab.com/xx_network/primitives/id" "syscall/js" @@ -58,7 +59,7 @@ func NewWASMEventModel(path string, encryption cryptoChannel.Cipher, func newWASMModel(databaseName string, encryption cryptoChannel.Cipher, cb MessageReceivedCallback) (*wasmModel, error) { // Attempt to open database object - ctx, cancel := newContext() + ctx, cancel := indexedDb.NewContext() defer cancel() openRequest, err := idb.Global().Open(ctx, databaseName, currentVersion, func(db *idb.Database, oldVersion, newVersion uint) error { @@ -109,9 +110,6 @@ func newWASMModel(databaseName string, encryption cryptoChannel.Cipher, } // Attempt to ensure the database has been properly initialized - if err != nil { - return nil, err - } openRequest, err = idb.Global().Open(ctx, databaseName, currentVersion, func(db *idb.Database, oldVersion, newVersion uint) error { return nil @@ -209,11 +207,11 @@ func (w *wasmModel) hackTestDb() error { if helper != nil { return helper } - result, err := w.get(messageStoreName, js.ValueOf(msgId)) + result, err := indexedDb.Get(w.db, messageStoreName, js.ValueOf(msgId)) if err != nil { return err } - if len(result) == 0 { + if result.IsUndefined() { return errors.Errorf("Failed to test db, record not present") } return nil diff --git a/indexedDb/model.go b/indexedDb/channels/model.go similarity index 95% rename from indexedDb/model.go rename to indexedDb/channels/model.go index 839ed29c3c7b6aafe33ab9a0001d7b919964dd18..629c9f43353ade792f024f88aba769c3c89f22c0 100644 --- a/indexedDb/model.go +++ b/indexedDb/channels/model.go @@ -7,7 +7,7 @@ //go:build js && wasm -package indexedDb +package channels import ( "time" @@ -60,8 +60,8 @@ type Message struct { Round uint64 `json:"round"` // User cryptographic Identity struct -- could be pulled out - Pubkey []byte `json:"pubkey"` // Index - DmToken []byte `json:"dm_token"` // Index + Pubkey []byte `json:"pubkey"` + DmToken []byte `json:"dm_token"` CodesetVersion uint8 `json:"codeset_version"` } diff --git a/indexedDb/implementation_test.go b/indexedDb/implementation_test.go index 511a022b6c6257f6e8c5877c7be914addedd725d..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644 --- a/indexedDb/implementation_test.go +++ b/indexedDb/implementation_test.go @@ -1,335 +0,0 @@ -//////////////////////////////////////////////////////////////////////////////// -// 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 indexedDb - -import ( - "encoding/json" - "fmt" - "github.com/hack-pad/go-indexeddb/idb" - "gitlab.com/elixxir/xxdk-wasm/storage" - "gitlab.com/xx_network/primitives/netTime" - "os" - "strconv" - "testing" - "time" - - jww "github.com/spf13/jwalterweatherman" - "gitlab.com/elixxir/client/v4/channels" - "gitlab.com/elixxir/client/v4/cmix/rounds" - cryptoBroadcast "gitlab.com/elixxir/crypto/broadcast" - "gitlab.com/elixxir/crypto/channel" - "gitlab.com/xx_network/primitives/id" -) - -func TestMain(m *testing.M) { - jww.SetStdoutThreshold(jww.LevelDebug) - os.Exit(m.Run()) -} - -func dummyCallback(uint64, *id.ID, bool) {} - -// Test wasmModel.UpdateSentStatus happy path and ensure fields don't change. -func Test_wasmModel_UpdateSentStatus(t *testing.T) { - testString := "test" - testMsgId := channel.MakeMessageID([]byte(testString), &id.ID{1}) - eventModel, err := newWASMModel(testString, nil, dummyCallback) - if err != nil { - t.Fatalf("%+v", err) - } - - // Store a test message - testMsg := buildMessage([]byte(testString), testMsgId.Bytes(), nil, - testString, []byte(testString), []byte{8, 6, 7, 5}, nil, 0, netTime.Now(), - time.Second, 0, 0, channels.Sent) - uuid, err := eventModel.receiveHelper(testMsg, false) - if err != nil { - t.Fatalf("%+v", err) - } - - // Ensure one message is stored - results, err := eventModel.dump(messageStoreName) - if err != nil { - t.Fatalf("%+v", err) - } - if len(results) != 1 { - t.Fatalf("Expected 1 message to exist") - } - - // Update the sentStatus - expectedStatus := channels.Failed - eventModel.UpdateSentStatus(uuid, testMsgId, netTime.Now(), - rounds.Round{ID: 8675309}, expectedStatus) - - // Check the resulting status - results, err = eventModel.dump(messageStoreName) - if err != nil { - t.Fatalf("%+v", 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.Fatalf("%+v", 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) { - storage.GetLocalStorage().Clear() - eventModel, err := newWASMModel("test", nil, dummyCallback) - if err != nil { - t.Fatalf("%+v", err) - } - - 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, err := eventModel.dump(channelsStoreName) - if err != nil { - t.Fatalf("%+v", err) - } - if len(results) != 2 { - t.Fatalf("Expected 2 channels to exist") - } - eventModel.LeaveChannel(testChannel.ReceptionID) - results, err = eventModel.dump(channelsStoreName) - if err != nil { - t.Fatalf("%+v", 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) { - testString := "testHello" - eventModel, err := newWASMModel(testString, nil, dummyCallback) - if err != nil { - t.Fatalf("%+v", err) - } - - uuids := make([]uint64, 10) - - for i := 0; i < 10; i++ { - // Store a test message - channelID := id.NewIdFromBytes([]byte(testString), t) - msgID := channel.MessageID{} - 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}, nil, 0, - netTime.Now(), time.Hour, rnd, 0, channels.Sent) - uuids[i] = uuid - } - - _, _ = eventModel.dump(messageStoreName) - - 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) { - storage.GetLocalStorage().Clear() - testString := "testHello" - eventModel, err := newWASMModel(testString, nil, dummyCallback) - if err != nil { - t.Fatalf("%+v", err) - } - - uuids := make([]uint64, 10) - - msgID := channel.MessageID{} - copy(msgID[:], testString) - for i := 0; i < 10; i++ { - // Store a test message - channelID := id.NewIdFromBytes([]byte(testString), t) - rnd := rounds.Round{ID: id.Round(42)} - uuid := eventModel.ReceiveMessage(channelID, msgID, "test", - testString+fmt.Sprintf("%d", i), []byte{8, 6, 7, 5}, nil, 0, - netTime.Now(), time.Hour, rnd, 0, channels.Sent) - uuids[i] = uuid - } - - _, _ = eventModel.dump(messageStoreName) - - 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) - } - } - } -} - -// Happy path: Inserts many messages, deletes some, and checks that the final -// result is as expected. -func Test_wasmModel_deleteMsgByChannel(t *testing.T) { - testString := "test_deleteMsgByChannel" - totalMessages := 10 - expectedMessages := 5 - eventModel, err := newWASMModel(testString, nil, dummyCallback) - if err != nil { - t.Fatalf("%+v", 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 := channel.MakeMessageID([]byte(testStr), &id.ID{1}) - eventModel.ReceiveMessage(thisChannel, testMsgId, testStr, testStr, - []byte{8, 6, 7, 5}, nil, 0, netTime.Now(), time.Second, - rounds.Round{ID: id.Round(0)}, 0, channels.Sent) - } - - // Check pre-results - result, err := eventModel.dump(messageStoreName) - if err != nil { - t.Fatalf("%+v", err) - } - if len(result) != totalMessages { - t.Errorf("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 = eventModel.dump(messageStoreName) - if err != nil { - t.Fatalf("%+v", err) - } - if len(result) != expectedMessages { - t.Errorf("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) { - testString := "test_receiveHelper_UniqueIndex" - eventModel, err := newWASMModel(testString, nil, dummyCallback) - 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, err2 := idx.Unique(); !isUnique { - t.Fatalf("Index is not unique!") - } else if err2 != nil { - t.Fatal(err2) - } - - // First message insert should succeed - testMsgId := channel.MakeMessageID([]byte(testString), &id.ID{1}) - testMsg := buildMessage([]byte(testString), testMsgId.Bytes(), nil, - testString, []byte(testString), []byte{8, 6, 7, 5}, nil, 0, netTime.Now(), - time.Second, 0, 0, channels.Sent) - _, err = eventModel.receiveHelper(testMsg, false) - if err != nil { - t.Fatal(err) - } - - // The duplicate entry won't fail, but it just silently shouldn't happen - _, err = eventModel.receiveHelper(testMsg, false) - if err != nil { - t.Fatalf("%+v", err) - } - results, err := eventModel.dump(messageStoreName) - if err != nil { - t.Fatalf("%+v", err) - } - if len(results) != 1 { - t.Fatalf("Expected only a single message, got %d", len(results)) - } - - // Now insert a message with a different message ID from the first - testMsgId2 := channel.MakeMessageID([]byte(testString), &id.ID{2}) - testMsg = buildMessage([]byte(testString), testMsgId2.Bytes(), nil, - testString, []byte(testString), []byte{8, 6, 7, 5}, nil, 0, netTime.Now(), - time.Second, 0, 0, channels.Sent) - primaryKey, err := eventModel.receiveHelper(testMsg, false) - if err != nil { - t.Fatal(err) - } - - // Except this time, we update the second entry to have the same - // message ID as the first - testMsg.ID = primaryKey - testMsg.MessageID = testMsgId.Bytes() - _, err = eventModel.receiveHelper(testMsg, true) - if err != nil { - t.Fatal(err) - } - - // The update to duplicate message ID won't fail, - // but it just silently shouldn't happen - results, err = eventModel.dump(messageStoreName) - if err != nil { - t.Fatalf("%+v", err) - } - // TODO: Convert JSON to Message, ensure Message ID fields differ -} diff --git a/indexedDb/utils.go b/indexedDb/utils.go new file mode 100644 index 0000000000000000000000000000000000000000..1aa6ff45805c1c65b9ad27e51a8b0590e7b8201d --- /dev/null +++ b/indexedDb/utils.go @@ -0,0 +1,231 @@ +//////////////////////////////////////////////////////////////////////////////// +// 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 + +// This file contains several generic IndexedDB helper functions that +// may be useful for any IndexedDB implementations. + +package indexedDb + +import ( + "context" + "github.com/hack-pad/go-indexeddb/idb" + "github.com/pkg/errors" + jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/xxdk-wasm/utils" + "syscall/js" + "time" +) + +// dbTimeout is the global timeout for operations with the storage +// [context.Context]. +const dbTimeout = time.Second + +// NewContext builds a context for indexedDb operations. +func NewContext() (context.Context, context.CancelFunc) { + return context.WithTimeout(context.Background(), dbTimeout) +} + +// Get is a generic helper for getting values from the given [idb.ObjectStore]. +func Get(db *idb.Database, objectStoreName string, key js.Value) (js.Value, error) { + parentErr := errors.Errorf("failed to Get %s/%s", objectStoreName, key) + + // Prepare the Transaction + txn, err := db.Transaction(idb.TransactionReadOnly, objectStoreName) + if err != nil { + return js.Undefined(), errors.WithMessagef(parentErr, + "Unable to create Transaction: %+v", err) + } + store, err := txn.ObjectStore(objectStoreName) + if err != nil { + return js.Undefined(), errors.WithMessagef(parentErr, + "Unable to get ObjectStore: %+v", err) + } + + // Perform the operation + getRequest, err := store.Get(key) + if err != nil { + return js.Undefined(), errors.WithMessagef(parentErr, + "Unable to Get from ObjectStore: %+v", err) + } + + // Wait for the operation to return + ctx, cancel := NewContext() + resultObj, err := getRequest.Await(ctx) + cancel() + if err != nil { + return js.Undefined(), errors.WithMessagef(parentErr, + "Unable to get from ObjectStore: %+v", err) + } else if resultObj.IsUndefined() { + return js.Undefined(), errors.WithMessage(parentErr, + "Unable to get from ObjectStore: result is undefined") + } + + // Process result into string + jww.DEBUG.Printf("Got from %s/%s: %s", + objectStoreName, key, utils.JsToJson(resultObj)) + return resultObj, nil +} + +// GetIndex is a generic helper for getting values from the given +// [idb.ObjectStore] using the given [idb.Index]. +func GetIndex(db *idb.Database, objectStoreName string, + indexName string, key js.Value) (js.Value, error) { + parentErr := errors.Errorf("failed to GetIndex %s/%s/%s", + objectStoreName, indexName, key) + + // Prepare the Transaction + txn, err := db.Transaction(idb.TransactionReadOnly, objectStoreName) + if err != nil { + return js.Undefined(), errors.WithMessagef(parentErr, + "Unable to create Transaction: %+v", err) + } + store, err := txn.ObjectStore(objectStoreName) + if err != nil { + return js.Undefined(), errors.WithMessagef(parentErr, + "Unable to get ObjectStore: %+v", err) + } + idx, err := store.Index(indexName) + if err != nil { + return js.Undefined(), errors.WithMessagef(parentErr, + "Unable to get Index: %+v", err) + } + + // Perform the operation + getRequest, err := idx.Get(key) + if err != nil { + return js.Undefined(), errors.WithMessagef(parentErr, + "Unable to Get from ObjectStore: %+v", err) + } + + // Wait for the operation to return + ctx, cancel := NewContext() + resultObj, err := getRequest.Await(ctx) + cancel() + if err != nil { + return js.Undefined(), errors.WithMessagef(parentErr, + "Unable to get from ObjectStore: %+v", err) + } else if resultObj.IsUndefined() { + return js.Undefined(), errors.WithMessage(parentErr, + "Unable to get from ObjectStore: result is undefined") + } + + // Process result into string + jww.DEBUG.Printf("Got from %s/%s/%s: %s", + objectStoreName, indexName, key, utils.JsToJson(resultObj)) + return resultObj, nil +} + +// Put is a generic helper for putting values into the given [idb.ObjectStore]. +// Equivalent to insert if not exists else update. +func Put(db *idb.Database, objectStoreName string, value js.Value) (*idb.Request, error) { + // Prepare the Transaction + txn, err := db.Transaction(idb.TransactionReadWrite, objectStoreName) + if err != nil { + return nil, errors.Errorf("Unable to create Transaction: %+v", err) + } + store, err := txn.ObjectStore(objectStoreName) + if err != nil { + return nil, errors.Errorf("Unable to get ObjectStore: %+v", err) + } + + // Perform the operation + request, err := store.Put(value) + if err != nil { + return nil, errors.Errorf("Unable to Put: %+v", err) + } + + // Wait for the operation to return + ctx, cancel := NewContext() + err = txn.Await(ctx) + cancel() + if err != nil { + return nil, errors.Errorf("Putting value failed: %+v", err) + } + jww.DEBUG.Printf("Successfully put value in %s: %v", + objectStoreName, utils.JsToJson(value)) + return request, nil +} + +// Delete is a generic helper for removing values from the given [idb.ObjectStore]. +func Delete(db *idb.Database, objectStoreName string, key js.Value) error { + parentErr := errors.Errorf("failed to Delete %s/%s", objectStoreName, key) + + // Prepare the Transaction + txn, err := db.Transaction(idb.TransactionReadOnly, objectStoreName) + if err != nil { + return errors.WithMessagef(parentErr, + "Unable to create Transaction: %+v", err) + } + store, err := txn.ObjectStore(objectStoreName) + if err != nil { + return errors.WithMessagef(parentErr, + "Unable to get ObjectStore: %+v", err) + } + + // Perform the operation + deleteRequest, err := store.Delete(key) + if err != nil { + return errors.WithMessagef(parentErr, + "Unable to Get from ObjectStore: %+v", err) + } + + // Wait for the operation to return + ctx, cancel := NewContext() + err = deleteRequest.Await(ctx) + cancel() + if err != nil { + return errors.WithMessagef(parentErr, + "Unable to delete from ObjectStore: %+v", err) + } + return nil +} + +// Dump returns the given [idb.ObjectStore] contents to string slice for +// testing and debugging purposes. +func Dump(db *idb.Database, objectStoreName string) ([]string, error) { + parentErr := errors.Errorf("failed to Dump %s", objectStoreName) + + txn, err := db.Transaction(idb.TransactionReadOnly, objectStoreName) + if err != nil { + return nil, errors.WithMessagef(parentErr, + "Unable to create Transaction: %+v", err) + } + store, err := txn.ObjectStore(objectStoreName) + if err != nil { + return nil, errors.WithMessagef(parentErr, + "Unable to get ObjectStore: %+v", err) + } + cursorRequest, err := store.OpenCursor(idb.CursorNext) + if err != nil { + return nil, errors.WithMessagef(parentErr, + "Unable to open Cursor: %+v", err) + } + + // Run the query + jww.DEBUG.Printf("%s values:", objectStoreName) + results := make([]string, 0) + ctx, cancel := NewContext() + err = cursorRequest.Iter(ctx, + func(cursor *idb.CursorWithValue) error { + value, err := cursor.Value() + if err != nil { + return err + } + valueStr := utils.JsToJson(value) + results = append(results, valueStr) + jww.DEBUG.Printf("- %v", valueStr) + return nil + }) + cancel() + if err != nil { + return nil, errors.WithMessagef(parentErr, + "Unable to dump ObjectStore: %+v", err) + } + return results, nil +} diff --git a/indexedDb/utils_test.go b/indexedDb/utils_test.go new file mode 100644 index 0000000000000000000000000000000000000000..1c657ebd5dbb4607c977323dc8883042989f05f9 --- /dev/null +++ b/indexedDb/utils_test.go @@ -0,0 +1,80 @@ +//////////////////////////////////////////////////////////////////////////////// +// 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 indexedDb + +import ( + "github.com/hack-pad/go-indexeddb/idb" + "strings" + "syscall/js" + "testing" +) + +// Error path: Tests that Get returns an error when trying to get a message that +// does not exist. +func TestGet_NoMessageError(t *testing.T) { + db := newTestDB("messages", "index", t) + + _, err := Get(db, "messages", js.ValueOf(5)) + if err == nil || !strings.Contains(err.Error(), "undefined") { + t.Errorf("Did not get expected error when getting a message that "+ + "does not exist: %+v", err) + } +} + +// Error path: Tests that GetIndex returns an error when trying to get a message +// that does not exist. +func TestGetIndex_NoMessageError(t *testing.T) { + db := newTestDB("messages", "index", t) + + _, err := GetIndex(db, "messages", "index", js.ValueOf(5)) + if err == nil || !strings.Contains(err.Error(), "undefined") { + t.Errorf("Did not get expected error when getting a message that "+ + "does not exist: %+v", err) + } +} + +// newTestDB creates a new idb.Database for testing. +func newTestDB(name, index string, t *testing.T) *idb.Database { + // Attempt to open database object + ctx, cancel := NewContext() + defer cancel() + openRequest, err := idb.Global().Open(ctx, "databaseName", 0, + func(db *idb.Database, _ uint, _ uint) error { + storeOpts := idb.ObjectStoreOptions{ + KeyPath: js.ValueOf("id"), + AutoIncrement: true, + } + + // Build Message ObjectStore and Indexes + messageStore, err := db.CreateObjectStore(name, storeOpts) + if err != nil { + return err + } + + _, err = messageStore.CreateIndex( + index, js.ValueOf("id"), idb.IndexOptions{}) + if err != nil { + return err + } + + return nil + }) + if err != nil { + t.Fatal(err) + } + + // Wait for database open to finish + db, err := openRequest.Await(ctx) + if err != nil { + t.Fatal(err) + } + + return db +} diff --git a/storage/version.go b/storage/version.go index 61bc23937a227f407b059b08362938c62222d558..9530aca55472b737e1905fe233f70a78689a52b5 100644 --- a/storage/version.go +++ b/storage/version.go @@ -18,7 +18,7 @@ import ( ) // SEMVER is the current semantic version of xxDK WASM. -const SEMVER = "0.1.8" +const SEMVER = "0.1.13" // Storage keys. const ( diff --git a/wasm/channels.go b/wasm/channels.go index c906ae53f50081b8bdc6c26c55a56992c0ba1bd4..447bea74ea42a6674c3859f10a12ef5ab2bf6fce 100644 --- a/wasm/channels.go +++ b/wasm/channels.go @@ -11,12 +11,12 @@ package wasm import ( "encoding/base64" + "gitlab.com/elixxir/xxdk-wasm/indexedDb/channels" "gitlab.com/xx_network/primitives/id" "sync" "syscall/js" "gitlab.com/elixxir/client/v4/bindings" - "gitlab.com/elixxir/xxdk-wasm/indexedDb" "gitlab.com/elixxir/xxdk-wasm/utils" ) @@ -394,7 +394,7 @@ func newChannelsManagerWithIndexedDb(cmixID int, privateIdentity []byte, cb.Invoke(uuid, utils.CopyBytesToJS(channelID.Marshal()), update) } - model := indexedDb.NewWASMEventModelBuilder(cipher, messageReceivedCB) + model := channels.NewWASMEventModelBuilder(cipher, messageReceivedCB) promiseFn := func(resolve, reject func(args ...any) js.Value) { cm, err := bindings.NewChannelsManagerGoEventModel( @@ -493,7 +493,7 @@ func loadChannelsManagerWithIndexedDb(cmixID int, storageTag string, cb.Invoke(uuid, utils.CopyBytesToJS(channelID.Marshal()), updated) } - model := indexedDb.NewWASMEventModelBuilder(cipher, messageReceivedCB) + model := channels.NewWASMEventModelBuilder(cipher, messageReceivedCB) promiseFn := func(resolve, reject func(args ...any) js.Value) { cm, err := bindings.LoadChannelsManagerGoEventModel(