Skip to content

Commit d12ab40

Browse files
committed
Update Sqlite plugin with unit tests and refactoring
- Bumped Visual Studio version and added unit test project. - Added `InternalsVisibleTo` for unit test access. - Introduced `DefaultReflectionGuard` and `GuidProvider` classes. - Refactored `SqlitePlugin` to use dependency injection. - Improved method readability with new exception handling methods. - Updated plugin version to 1.1.1 and changed category to Database. - Added unit tests for initialization and exception handling. #3
1 parent f757a26 commit d12ab40

9 files changed

+202
-14
lines changed

Plugins.Sqlite.sln

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FlowSynx.Plugins.Sqlite", "
77
EndProject
88
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{A3F3B36D-19C8-459A-A856-45829CFCD9B0}"
99
EndProject
10+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
11+
EndProject
12+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FlowSynx.Plugins.Sqlite.UnitTests", "tests\FlowSynx.Plugins.Sqlite.UnitTests.csproj", "{AD5FAB7C-540D-4130-95F9-5CF6748988CB}"
13+
EndProject
1014
Global
1115
GlobalSection(SolutionConfigurationPlatforms) = preSolution
1216
Debug|Any CPU = Debug|Any CPU
@@ -17,11 +21,16 @@ Global
1721
{466CF7F6-511B-4383-B9A9-A5A21E6C0E8C}.Debug|Any CPU.Build.0 = Debug|Any CPU
1822
{466CF7F6-511B-4383-B9A9-A5A21E6C0E8C}.Release|Any CPU.ActiveCfg = Release|Any CPU
1923
{466CF7F6-511B-4383-B9A9-A5A21E6C0E8C}.Release|Any CPU.Build.0 = Release|Any CPU
24+
{AD5FAB7C-540D-4130-95F9-5CF6748988CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
25+
{AD5FAB7C-540D-4130-95F9-5CF6748988CB}.Debug|Any CPU.Build.0 = Debug|Any CPU
26+
{AD5FAB7C-540D-4130-95F9-5CF6748988CB}.Release|Any CPU.ActiveCfg = Release|Any CPU
27+
{AD5FAB7C-540D-4130-95F9-5CF6748988CB}.Release|Any CPU.Build.0 = Release|Any CPU
2028
EndGlobalSection
2129
GlobalSection(SolutionProperties) = preSolution
2230
HideSolutionNode = FALSE
2331
EndGlobalSection
2432
GlobalSection(NestedProjects) = preSolution
2533
{466CF7F6-511B-4383-B9A9-A5A21E6C0E8C} = {A3F3B36D-19C8-459A-A856-45829CFCD9B0}
34+
{AD5FAB7C-540D-4130-95F9-5CF6748988CB} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
2635
EndGlobalSection
2736
EndGlobal

src/FlowSynx.Plugins.Sqlite.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@
1616
<PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.7" />
1717
</ItemGroup>
1818

19+
<ItemGroup>
20+
<InternalsVisibleTo Include="FlowSynx.Plugins.Sqlite.UnitTests" />
21+
</ItemGroup>
22+
1923
<ItemGroup>
2024
<Compile Update="Resources.Designer.cs">
2125
<DesignTime>True</DesignTime>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
using FlowSynx.PluginCore.Helpers;
2+
3+
namespace FlowSynx.Plugins.Sqlite.Services;
4+
5+
internal class DefaultReflectionGuard : IReflectionGuard
6+
{
7+
public bool IsCalledViaReflection() => ReflectionHelper.IsCalledViaReflection();
8+
}

src/Services/GuidProvider.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
namespace FlowSynx.Plugins.Sqlite.Services;
2+
3+
internal class GuidProvider : IGuidProvider
4+
{
5+
public Guid NewGuid() => Guid.NewGuid();
6+
}

src/Services/IGuidProvider.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
namespace FlowSynx.Plugins.Sqlite.Services;
2+
3+
public interface IGuidProvider
4+
{
5+
Guid NewGuid();
6+
}

src/Services/IReflectionGuard.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
namespace FlowSynx.Plugins.Sqlite.Services;
2+
3+
public interface IReflectionGuard
4+
{
5+
bool IsCalledViaReflection();
6+
}

src/SqlitePlugin.cs

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,35 @@
1-
using FlowSynx.PluginCore.Helpers;
2-
using FlowSynx.PluginCore;
1+
using FlowSynx.PluginCore;
32
using FlowSynx.PluginCore.Extensions;
43
using FlowSynx.Plugins.Sqlite.Models;
4+
using FlowSynx.Plugins.Sqlite.Services;
55
using Microsoft.Data.Sqlite;
66

77
namespace FlowSynx.Plugins.Sqlite;
88

99
public class SqlitePlugin: IPlugin
1010
{
1111
private IPluginLogger? _logger;
12+
private readonly IGuidProvider _guidProvider;
13+
private readonly IReflectionGuard _reflectionGuard;
1214
private SqlitePluginSpecifications _sqliteSpecifications = null!;
1315
private bool _isInitialized;
1416

17+
public SqlitePlugin() : this(new GuidProvider(), new DefaultReflectionGuard()) { }
18+
19+
internal SqlitePlugin(IGuidProvider guidProvider, IReflectionGuard reflectionGuard)
20+
{
21+
_guidProvider = guidProvider ?? throw new ArgumentNullException(nameof(guidProvider));
22+
_reflectionGuard = reflectionGuard ?? throw new ArgumentNullException(nameof(reflectionGuard));
23+
}
24+
1525
public PluginMetadata Metadata => new PluginMetadata
1626
{
1727
Id = Guid.Parse("6457ab5d-0487-4c06-a313-1ebf789f2b52"),
1828
Name = "Sqlite",
1929
CompanyName = "FlowSynx",
2030
Description = Resources.PluginDescription,
21-
Version = new Version(1, 1, 0),
22-
Category = PluginCategory.Data,
31+
Version = new Version(1, 1, 1),
32+
Category = PluginCategory.Database,
2333
Authors = new List<string> { "FlowSynx" },
2434
Copyright = "© FlowSynx. All rights reserved.",
2535
Icon = "flowsynx.png",
@@ -44,9 +54,7 @@ public class SqlitePlugin: IPlugin
4454

4555
public Task Initialize(IPluginLogger logger)
4656
{
47-
if (ReflectionHelper.IsCalledViaReflection())
48-
throw new InvalidOperationException(Resources.ReflectionBasedAccessIsNotAllowed);
49-
57+
ThrowIfReflection();
5058
ArgumentNullException.ThrowIfNull(logger);
5159
_sqliteSpecifications = Specifications.ToObject<SqlitePluginSpecifications>();
5260
_logger = logger;
@@ -57,12 +65,8 @@ public Task Initialize(IPluginLogger logger)
5765
public async Task<object?> ExecuteAsync(PluginParameters parameters, CancellationToken cancellationToken)
5866
{
5967
cancellationToken.ThrowIfCancellationRequested();
60-
61-
if (ReflectionHelper.IsCalledViaReflection())
62-
throw new InvalidOperationException(Resources.ReflectionBasedAccessIsNotAllowed);
63-
64-
if (!_isInitialized)
65-
throw new InvalidOperationException($"Plugin '{Metadata.Name}' v{Metadata.Version} is not initialized.");
68+
ThrowIfReflection();
69+
ThrowIfNotInitialized();
6670

6771
var inputParameter = parameters.ToObject<InputParameter>();
6872
var operation = inputParameter.Operation;
@@ -75,6 +79,18 @@ public Task Initialize(IPluginLogger logger)
7579
throw new NotSupportedException($"Sqlite plugin: Operation '{operation}' is not supported.");
7680
}
7781

82+
private void ThrowIfReflection()
83+
{
84+
if (_reflectionGuard.IsCalledViaReflection())
85+
throw new InvalidOperationException(Resources.ReflectionBasedAccessIsNotAllowed);
86+
}
87+
88+
private void ThrowIfNotInitialized()
89+
{
90+
if (!_isInitialized)
91+
throw new InvalidOperationException($"Plugin '{Metadata.Name}' v{Metadata.Version} is not initialized.");
92+
}
93+
7894
private async Task ExecuteNonQueryAsync(InputParameter parameters, CancellationToken cancellationToken)
7995
{
8096
var (sql, sqlParams) = GetSqlAndParameters(parameters);
@@ -127,7 +143,7 @@ private async Task<PluginContext> ExecuteQueryAsync(InputParameter parameters, C
127143
}
128144

129145
_logger?.LogInfo($"Query executed successfully. Rows returned: {result.Count}.");
130-
string key = $"{Guid.NewGuid().ToString()}";
146+
string key = $"{_guidProvider.NewGuid()}";
131147
return new PluginContext(key, "Data")
132148
{
133149
Format = "Database",
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net9.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
<IsPackable>false</IsPackable>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<PackageReference Include="coverlet.collector" Version="6.0.2" />
12+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
13+
<PackageReference Include="Moq" Version="4.20.72" />
14+
<PackageReference Include="xunit" Version="2.9.2" />
15+
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
16+
</ItemGroup>
17+
18+
<ItemGroup>
19+
<ProjectReference Include="..\src\FlowSynx.Plugins.Sqlite.csproj" />
20+
</ItemGroup>
21+
22+
<ItemGroup>
23+
<Using Include="Xunit" />
24+
</ItemGroup>
25+
26+
</Project>

tests/SqlitePluginTests.cs

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
using FlowSynx.Plugins.Sqlite;
2+
using FlowSynx.Plugins.Sqlite.Models;
3+
using FlowSynx.PluginCore;
4+
using FlowSynx.Plugins.Sqlite.Services;
5+
using Microsoft.Data.Sqlite;
6+
using Moq;
7+
using System.Reflection;
8+
9+
public class SqlitePluginTests
10+
{
11+
private SqlitePlugin CreatePlugin(string? connectionString = null, Mock<IPluginLogger>? loggerMock = null)
12+
{
13+
var guidProvider = new Mock<IGuidProvider>();
14+
guidProvider.Setup(g => g.NewGuid()).Returns(Guid.NewGuid());
15+
16+
var reflectionGuard = new Mock<IReflectionGuard>();
17+
reflectionGuard.Setup(r => r.IsCalledViaReflection()).Returns(false);
18+
19+
var plugin = new SqlitePlugin(guidProvider.Object, reflectionGuard.Object)
20+
{
21+
Specifications = new SqlitePluginSpecifications
22+
{
23+
ConnectionString = connectionString ?? "Data Source=:memory:"
24+
}
25+
};
26+
27+
if (loggerMock != null)
28+
plugin.Initialize(loggerMock.Object).Wait();
29+
else
30+
plugin.Initialize(Mock.Of<IPluginLogger>()).Wait();
31+
32+
return plugin;
33+
}
34+
35+
[Fact]
36+
public async Task Initialize_SetsIsInitialized()
37+
{
38+
var plugin = CreatePlugin();
39+
Assert.True(typeof(SqlitePlugin).GetField("_isInitialized", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)!.GetValue(plugin) as bool?);
40+
}
41+
42+
[Fact]
43+
public async Task ExecuteAsync_ThrowsOnUnsupportedOperation()
44+
{
45+
var plugin = CreatePlugin();
46+
var parameters = new PluginParameters
47+
{
48+
{ "Operation", "invalid" },
49+
{ "Sql", "SELECT 1" }
50+
};
51+
52+
await Assert.ThrowsAsync<NotSupportedException>(() => plugin.ExecuteAsync(parameters, CancellationToken.None));
53+
}
54+
55+
[Fact]
56+
public void GetSqlAndParameters_ThrowsOnMissingSql()
57+
{
58+
var plugin = CreatePlugin();
59+
var input = new InputParameter { Sql = null! };
60+
var method = typeof(SqlitePlugin).GetMethod("GetSqlAndParameters", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)!;
61+
var ex = Assert.Throws<TargetInvocationException>(() => method.Invoke(plugin, new object[] { input }));
62+
Assert.IsType<ArgumentException>(ex.InnerException);
63+
Assert.Contains("Missing 'sql' parameter", ex.InnerException!.Message);
64+
}
65+
66+
[Fact]
67+
public void AddParameters_AddsVariousTypes()
68+
{
69+
var plugin = CreatePlugin();
70+
var cmd = new SqliteCommand();
71+
var parameters = new Dictionary<string, object>
72+
{
73+
{ "int", 1 },
74+
{ "str", "hello" },
75+
{ "guid", Guid.NewGuid() },
76+
{ "bool", true },
77+
{ "date", DateTime.Now },
78+
{ "bytes", new byte[] { 1, 2, 3 } }
79+
};
80+
var method = typeof(SqlitePlugin).GetMethod("AddParameters", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)!;
81+
method.Invoke(plugin, new object[] { cmd, parameters });
82+
Assert.Equal(6, cmd.Parameters.Count);
83+
}
84+
85+
[Fact]
86+
public void ThrowIfReflection_ThrowsIfCalledViaReflection()
87+
{
88+
var guidProvider = new Mock<IGuidProvider>();
89+
var reflectionGuard = new Mock<IReflectionGuard>();
90+
reflectionGuard.Setup(r => r.IsCalledViaReflection()).Returns(true);
91+
92+
var plugin = new SqlitePlugin(guidProvider.Object, reflectionGuard.Object);
93+
var method = typeof(SqlitePlugin).GetMethod("ThrowIfReflection", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)!;
94+
var ex = Assert.Throws<TargetInvocationException>(() => method.Invoke(plugin, Array.Empty<object>()));
95+
Assert.IsType<InvalidOperationException>(ex.InnerException);
96+
}
97+
98+
[Fact]
99+
public void ThrowIfNotInitialized_ThrowsIfNotInitialized()
100+
{
101+
var plugin = CreatePlugin();
102+
typeof(SqlitePlugin).GetField("_isInitialized", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)!.SetValue(plugin, false);
103+
var method = typeof(SqlitePlugin).GetMethod("ThrowIfNotInitialized", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)!;
104+
var ex = Assert.Throws<TargetInvocationException>(() => method.Invoke(plugin, Array.Empty<object>()));
105+
Assert.IsType<InvalidOperationException>(ex.InnerException);
106+
}
107+
}

0 commit comments

Comments
 (0)