diff --git a/src/Blazored.LocalStorage/Blazored.LocalStorage.csproj b/src/Blazored.LocalStorage/Blazored.LocalStorage.csproj index c033dc7..35d3094 100644 --- a/src/Blazored.LocalStorage/Blazored.LocalStorage.csproj +++ b/src/Blazored.LocalStorage/Blazored.LocalStorage.csproj @@ -6,7 +6,7 @@ Blazored.LocalStorage Blazored.LocalStorage - 2.1.4 + 2.1.5 Chris Sainty A library to provide access to local storage in Blazor applications Copyright 2018 (c) Chris Sainty. All rights reserved. diff --git a/src/Blazored.LocalStorage/LocalStorageService.cs b/src/Blazored.LocalStorage/LocalStorageService.cs index dde290f..928993f 100644 --- a/src/Blazored.LocalStorage/LocalStorageService.cs +++ b/src/Blazored.LocalStorage/LocalStorageService.cs @@ -121,7 +121,8 @@ public T GetItem(string key) return default(T); if (serialisedData.StartsWith("{") && serialisedData.EndsWith("}") - || serialisedData.StartsWith("\"") && serialisedData.EndsWith("\"")) + || serialisedData.StartsWith("\"") && serialisedData.EndsWith("\"") + || typeof(T) != typeof(string)) { return JsonSerializer.Deserialize(serialisedData, _jsonOptions); } @@ -180,7 +181,7 @@ private async Task RaiseOnChangingAsync(string key, object da var e = new ChangingEventArgs { Key = key, - OldValue = await GetItemAsync(key), + OldValue = await GetItemInternalAsync(key), NewValue = data }; @@ -189,12 +190,33 @@ private async Task RaiseOnChangingAsync(string key, object da return e; } + private async Task GetItemInternalAsync(string key) + { + if (string.IsNullOrEmpty(key)) + throw new ArgumentNullException(nameof(key)); + + var serialisedData = await _jSRuntime.InvokeAsync("localStorage.getItem", key); + + if (string.IsNullOrWhiteSpace(serialisedData)) + return default(T); + + if (serialisedData.StartsWith("{") && serialisedData.EndsWith("}") + || serialisedData.StartsWith("\"") && serialisedData.EndsWith("\"")) + { + return JsonSerializer.Deserialize(serialisedData, _jsonOptions); + } + else + { + return (T)(object)serialisedData; + } + } + private ChangingEventArgs RaiseOnChangingSync(string key, object data) { var e = new ChangingEventArgs { Key = key, - OldValue = ((ISyncLocalStorageService)this).GetItem(key), + OldValue = GetItemInternal(key), NewValue = data }; @@ -203,6 +225,30 @@ private ChangingEventArgs RaiseOnChangingSync(string key, object data) return e; } + public T GetItemInternal(string key) + { + if (string.IsNullOrEmpty(key)) + throw new ArgumentNullException(nameof(key)); + + if (_jSInProcessRuntime == null) + throw new InvalidOperationException("IJSInProcessRuntime not available"); + + var serialisedData = _jSInProcessRuntime.Invoke("localStorage.getItem", key); + + if (string.IsNullOrWhiteSpace(serialisedData)) + return default(T); + + if (serialisedData.StartsWith("{") && serialisedData.EndsWith("}") + || serialisedData.StartsWith("\"") && serialisedData.EndsWith("\"")) + { + return JsonSerializer.Deserialize(serialisedData, _jsonOptions); + } + else + { + return (T)(object)serialisedData; + } + } + public event EventHandler Changed; private void RaiseOnChanged(string key, object oldValue, object data) { diff --git a/tests/Blazored.LocalStorage.Tests/LocalStorageServiceTests/GetItem.cs b/tests/Blazored.LocalStorage.Tests/LocalStorageServiceTests/GetItem.cs new file mode 100644 index 0000000..432085f --- /dev/null +++ b/tests/Blazored.LocalStorage.Tests/LocalStorageServiceTests/GetItem.cs @@ -0,0 +1,103 @@ +using Blazored.LocalStorage.JsonConverters; +using Blazored.LocalStorage.Tests.Mocks; +using Blazored.LocalStorage.Tests.TestAssets; +using FluentAssertions; +using Moq; +using System.Text.Json; +using System.Threading.Tasks; +using Xunit; + +namespace Blazored.LocalStorage.Tests.LocalStorageServiceTests +{ + public class GetItem + { + private JsonSerializerOptions _jsonOptions; + private Mock _mockJSRuntime; + private LocalStorageService _sut; + + private static string _key = "testKey"; + + public GetItem() + { + _mockJSRuntime = new Mock(); + _jsonOptions = new JsonSerializerOptions(); + _jsonOptions.Converters.Add(new TimespanJsonConverter()); + _sut = new LocalStorageService(_mockJSRuntime.Object); + } + + [Theory] + [InlineData("stringTest")] + [InlineData(11)] + [InlineData(11.11)] + public void Should_DeserialiseToCorrectType(T value) + { + // Arrange + var serialisedData = ""; + if (typeof(T) == typeof(string)) + serialisedData = value.ToString(); + else + serialisedData = JsonSerializer.Serialize(value, _jsonOptions); + + _mockJSRuntime.Setup(x => x.Invoke("localStorage.getItem", new[] { _key })) + .Returns(() => serialisedData); + + // Act + var result = _sut.GetItem(_key); + + // Assert + Assert.Equal(value, result); + _mockJSRuntime.Verify(); + } + + [Fact] + public void Should_DeserialiseValueToNullableInt() + { + // Arrange + int? value = 6; + var serialisedData = JsonSerializer.Serialize(value, _jsonOptions); + + _mockJSRuntime.Setup(x => x.Invoke("localStorage.getItem", new[] { _key })) + .Returns(() => serialisedData); + + // Act + var result = _sut.GetItem(_key); + + // Assert + Assert.Equal(value, result); + } + + [Fact] + public void Should_DeserialiseValueToDecimal() + { + // Arrange + decimal value = 6.00m; + var serialisedData = JsonSerializer.Serialize(value, _jsonOptions); + + _mockJSRuntime.Setup(x => x.Invoke("localStorage.getItem", new[] { _key })) + .Returns(() => serialisedData); + + // Act + var result = _sut.GetItem(_key); + + // Assert + Assert.Equal(value, result); + } + + [Fact] + public void Should_DeserialiseValueToComplexType() + { + // Arrange + TestObject value = new TestObject { Id = 1, Name = "John Smith" }; + var serialisedData = JsonSerializer.Serialize(value, _jsonOptions); + + _mockJSRuntime.Setup(x => x.Invoke("localStorage.getItem", new[] { _key })) + .Returns(() => serialisedData); + + // Act + var result = _sut.GetItem(_key); + + // Assert + result.Should().BeEquivalentTo(value); + } + } +} diff --git a/tests/Blazored.LocalStorage.Tests/LocalStorageServiceTests/GetItemAsync.cs b/tests/Blazored.LocalStorage.Tests/LocalStorageServiceTests/GetItemAsync.cs index 03c4575..7935b99 100644 --- a/tests/Blazored.LocalStorage.Tests/LocalStorageServiceTests/GetItemAsync.cs +++ b/tests/Blazored.LocalStorage.Tests/LocalStorageServiceTests/GetItemAsync.cs @@ -1,5 +1,6 @@ using Blazored.LocalStorage.JsonConverters; using Blazored.LocalStorage.Tests.Mocks; +using Blazored.LocalStorage.Tests.TestAssets; using FluentAssertions; using Moq; using System.Text.Json; @@ -11,14 +12,14 @@ namespace Blazored.LocalStorage.Tests.LocalStorageServiceTests public class GetItemAsync { private JsonSerializerOptions _jsonOptions; - private Mock _mockJSRuntime; + private Mock _mockJSRuntime; private LocalStorageService _sut; private static string _key = "testKey"; public GetItemAsync() { - _mockJSRuntime = new Mock(); + _mockJSRuntime = new Mock(); _jsonOptions = new JsonSerializerOptions(); _jsonOptions.Converters.Add(new TimespanJsonConverter()); _sut = new LocalStorageService(_mockJSRuntime.Object); @@ -99,10 +100,4 @@ public async Task Should_DeserialiseValueToComplexType() result.Should().BeEquivalentTo(value); } } - - public class TestObject - { - public int Id { get; set; } - public string Name { get; set; } - } } diff --git a/tests/Blazored.LocalStorage.Tests/LocalStorageServiceTests/SetItem.cs b/tests/Blazored.LocalStorage.Tests/LocalStorageServiceTests/SetItem.cs new file mode 100644 index 0000000..777c8bb --- /dev/null +++ b/tests/Blazored.LocalStorage.Tests/LocalStorageServiceTests/SetItem.cs @@ -0,0 +1,44 @@ +using Blazored.LocalStorage.JsonConverters; +using Blazored.LocalStorage.Tests.Mocks; +using FluentAssertions; +using Moq; +using System.Text.Json; +using Xunit; + +namespace Blazored.LocalStorage.Tests.LocalStorageServiceTests +{ + public class SetItem + { + private JsonSerializerOptions _jsonOptions; + private Mock _mockJSRuntime; + private LocalStorageService _sut; + + private static string _key = "testKey"; + + public SetItem() + { + _mockJSRuntime = new Mock(); + _jsonOptions = new JsonSerializerOptions(); + _jsonOptions.Converters.Add(new TimespanJsonConverter()); + _sut = new LocalStorageService(_mockJSRuntime.Object); + } + + [Fact] + public void Should_OverwriteExistingValue() + { + // Arrange + string existingValue = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiYWRtaW4iLCJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dzLzIwMDgvMDYvaWRlbnRpdHkvY2xhaW1zL3JvbGUiOiJBZG1pbmlzdHJhdG9yIiwiZXhwIjoxNTg1NjYwNzEyLCJpc3MiOiJDb2RlUmVkQm9va2luZy5TZXJ2ZXIiLCJhdWQiOiJDb2RlUmVkQm9va2luZy5DbGllbnRzIn0.JhK1M1H7NLCFexujJYCDjTn9La0HloGYADMHXGCFksU"; + string newValue = "6QLE0LL7iw7tHPAwold31qUENt3lVTUZxDGqeXQFx38="; + + _mockJSRuntime.Setup(x => x.Invoke("localStorage.getItem", new[] { _key })) + .Returns(() => existingValue); + _mockJSRuntime.Setup(x => x.InvokeVoid("localStorage.setItem", new[] { _key, newValue })); + + // Act + _sut.SetItem(_key, newValue); + + // Assert + _mockJSRuntime.Verify(); + } + } +} diff --git a/tests/Blazored.LocalStorage.Tests/LocalStorageServiceTests/SetItemAsync.cs b/tests/Blazored.LocalStorage.Tests/LocalStorageServiceTests/SetItemAsync.cs new file mode 100644 index 0000000..cb80787 --- /dev/null +++ b/tests/Blazored.LocalStorage.Tests/LocalStorageServiceTests/SetItemAsync.cs @@ -0,0 +1,45 @@ +using Blazored.LocalStorage.JsonConverters; +using Blazored.LocalStorage.Tests.Mocks; +using FluentAssertions; +using Moq; +using System.Text.Json; +using System.Threading.Tasks; +using Xunit; + +namespace Blazored.LocalStorage.Tests.LocalStorageServiceTests +{ + public class SetItemAsync + { + private JsonSerializerOptions _jsonOptions; + private Mock _mockJSRuntime; + private LocalStorageService _sut; + + private static string _key = "testKey"; + + public SetItemAsync() + { + _mockJSRuntime = new Mock(); + _jsonOptions = new JsonSerializerOptions(); + _jsonOptions.Converters.Add(new TimespanJsonConverter()); + _sut = new LocalStorageService(_mockJSRuntime.Object); + } + + [Fact] + public async Task Should_OverwriteExistingValue() + { + // Arrange + string existingValue = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiYWRtaW4iLCJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dzLzIwMDgvMDYvaWRlbnRpdHkvY2xhaW1zL3JvbGUiOiJBZG1pbmlzdHJhdG9yIiwiZXhwIjoxNTg1NjYwNzEyLCJpc3MiOiJDb2RlUmVkQm9va2luZy5TZXJ2ZXIiLCJhdWQiOiJDb2RlUmVkQm9va2luZy5DbGllbnRzIn0.JhK1M1H7NLCFexujJYCDjTn9La0HloGYADMHXGCFksU"; + string newValue = "6QLE0LL7iw7tHPAwold31qUENt3lVTUZxDGqeXQFx38="; + + _mockJSRuntime.Setup(x => x.InvokeAsync("localStorage.getItem", new[] { _key })) + .Returns(() => new ValueTask(existingValue)); + _mockJSRuntime.Setup(x => x.InvokeVoidAsync("localStorage.setItem", new[] { _key, newValue })); + + // Act + await _sut.SetItemAsync(_key, newValue); + + // Assert + _mockJSRuntime.Verify(); + } + } +} diff --git a/tests/Blazored.LocalStorage.Tests/Mocks/JSRuntimeWrapper.cs b/tests/Blazored.LocalStorage.Tests/Mocks/JSRuntimeWrapper.cs deleted file mode 100644 index 1d1c257..0000000 --- a/tests/Blazored.LocalStorage.Tests/Mocks/JSRuntimeWrapper.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Microsoft.JSInterop; -using Microsoft.JSInterop.Infrastructure; -using System; -using System.Collections.Generic; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -namespace Blazored.LocalStorage.Tests.Mocks -{ - public class JSRuntimeWrapper : IJSRuntime - { - public virtual ValueTask InvokeAsync(string identifier, object[] args) - { - throw new NotImplementedException(); - } - - public virtual ValueTask InvokeAsync(string identifier, CancellationToken cancellationToken, object[] args) - { - throw new NotImplementedException(); - } - } -} diff --git a/tests/Blazored.LocalStorage.Tests/Mocks/JSRuntimeWrapperAsync.cs b/tests/Blazored.LocalStorage.Tests/Mocks/JSRuntimeWrapperAsync.cs new file mode 100644 index 0000000..4b04506 --- /dev/null +++ b/tests/Blazored.LocalStorage.Tests/Mocks/JSRuntimeWrapperAsync.cs @@ -0,0 +1,54 @@ +using Microsoft.JSInterop; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Blazored.LocalStorage.Tests.Mocks +{ + /// + /// This class is just for mocking purposes + /// + public class JSRuntimeWrapperAsync : IJSRuntime + { + public virtual ValueTask InvokeAsync(string identifier, object[] args) + { + throw new NotImplementedException(); + } + + public virtual ValueTask InvokeAsync(string identifier, CancellationToken cancellationToken, object[] args) + { + throw new NotImplementedException(); + } + + public virtual ValueTask InvokeVoidAsync(string identifier, object[] args) + { + throw new NotImplementedException(); + } + } + + /// + /// This class is just for mocking purposes + /// + public class JSRuntimeWrapper : IJSInProcessRuntime + { + public virtual T Invoke(string identifier, params object[] args) + { + throw new NotImplementedException(); + } + + public ValueTask InvokeAsync(string identifier, object[] args) + { + throw new NotImplementedException(); + } + + public ValueTask InvokeAsync(string identifier, CancellationToken cancellationToken, object[] args) + { + throw new NotImplementedException(); + } + + public virtual void InvokeVoid(string identifier, object[] args) + { + throw new NotImplementedException(); + } + } +} diff --git a/tests/Blazored.LocalStorage.Tests/TestAssets/TestObject.cs b/tests/Blazored.LocalStorage.Tests/TestAssets/TestObject.cs new file mode 100644 index 0000000..cf2dd33 --- /dev/null +++ b/tests/Blazored.LocalStorage.Tests/TestAssets/TestObject.cs @@ -0,0 +1,8 @@ +namespace Blazored.LocalStorage.Tests.TestAssets +{ + public class TestObject + { + public int Id { get; set; } + public string Name { get; set; } + } +}