diff --git a/Binner/Binner.Testing/Binner.Testing.csproj b/Binner/Binner.Testing/Binner.Testing.csproj
new file mode 100644
index 00000000..a82c5c2c
--- /dev/null
+++ b/Binner/Binner.Testing/Binner.Testing.csproj
@@ -0,0 +1,24 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Binner/Binner.Testing/GlobalUsings.cs b/Binner/Binner.Testing/GlobalUsings.cs
new file mode 100644
index 00000000..cefced49
--- /dev/null
+++ b/Binner/Binner.Testing/GlobalUsings.cs
@@ -0,0 +1 @@
+global using NUnit.Framework;
\ No newline at end of file
diff --git a/Binner/Binner.Testing/InMemoryStorageProvider.cs b/Binner/Binner.Testing/InMemoryStorageProvider.cs
new file mode 100644
index 00000000..9fbd788a
--- /dev/null
+++ b/Binner/Binner.Testing/InMemoryStorageProvider.cs
@@ -0,0 +1,434 @@
+using Binner.Global.Common;
+using Binner.Model;
+using Binner.Model.Responses;
+using Binner.Model.Swarm;
+using System.Linq.Expressions;
+
+namespace Binner.Testing
+{
+ public class InMemoryStorageProvider : IStorageProvider
+ {
+ private readonly Dictionary _parts = new();
+ private readonly Dictionary _projects = new();
+ private readonly Dictionary _projectPartAssignments = new();
+ private readonly Dictionary _projectPcbAssignments = new();
+ private readonly Dictionary _partTypes = new();
+ private readonly Dictionary _pcbs = new();
+ private readonly Dictionary _storedFiles = new();
+ private readonly Dictionary _pcbStoredFileAssignments = new();
+ private readonly Dictionary _partSuppliers = new();
+ private readonly Dictionary _users = new();
+
+ public InMemoryStorageProvider(bool createEmpty = false)
+ {
+ if (!createEmpty)
+ {
+ _parts.Add(1, new Part { PartNumber = "LM358", PartId = 1 });
+ _projects.Add(1, new Project { Name = "Test Project", ProjectId = 1 });
+ }
+ _partTypes.Add(1, new PartType { Name = "IC", PartTypeId = 1 });
+ _partTypes.Add(2, new PartType { Name = "Resistor", PartTypeId = 2 });
+ _partTypes.Add(3, new PartType { Name = "Capacitor", PartTypeId = 3 });
+ _partTypes.Add(4, new PartType { Name = "Inductor", PartTypeId = 4 });
+ }
+
+ public async Task AddPartAsync(Part part, IUserContext? userContext)
+ {
+ part.UserId = userContext?.UserId;
+ var id = _parts.OrderByDescending(x => x.Key).Select(x => x.Key).FirstOrDefault() + 1;
+ part.PartId = id;
+ _parts.Add(id, part);
+ return part;
+ }
+
+ public async Task AddPartSupplierAsync(PartSupplier partSupplier, IUserContext? userContext)
+ {
+ partSupplier.UserId = userContext?.UserId;
+ var id = _partSuppliers.OrderByDescending(x => x.Key).Select(x => x.Key).FirstOrDefault() + 1;
+ partSupplier.PartSupplierId = id;
+ _partSuppliers.Add(id, partSupplier);
+ return partSupplier;
+ }
+
+ public async Task AddPcbAsync(Pcb pcb, IUserContext? userContext)
+ {
+ pcb.UserId = userContext?.UserId;
+ var id = _pcbs.OrderByDescending(x => x.Key).Select(x => x.Key).FirstOrDefault() + 1;
+ pcb.PcbId = id;
+ _pcbs.Add(id, pcb);
+ return pcb;
+ }
+
+ public async Task AddPcbStoredFileAssignmentAsync(PcbStoredFileAssignment assignment, IUserContext? userContext)
+ {
+ assignment.UserId = userContext?.UserId ?? 0;
+ var id = _pcbStoredFileAssignments.OrderByDescending(x => x.Key).Select(x => x.Key).FirstOrDefault() + 1;
+ assignment.PcbStoredFileAssignmentId = id;
+ _pcbStoredFileAssignments.Add(id, assignment);
+ return assignment;
+ }
+
+ public async Task AddProjectAsync(Project project, IUserContext? userContext)
+ {
+ project.UserId = userContext?.UserId;
+ var id = _projects.OrderByDescending(x => x.Key).Select(x => x.Key).FirstOrDefault() + 1;
+ project.ProjectId = id;
+ _projects.Add(id, project);
+ return project;
+ }
+
+ public async Task AddProjectPartAssignmentAsync(ProjectPartAssignment assignment, IUserContext? userContext)
+ {
+ assignment.UserId = userContext?.UserId ?? 0;
+ var id = _projectPartAssignments.OrderByDescending(x => x.Key).Select(x => x.Key).FirstOrDefault() + 1;
+ assignment.ProjectPartAssignmentId = id;
+ _projectPartAssignments.Add(id, assignment);
+ return assignment;
+ }
+
+ public async Task AddProjectPcbAssignmentAsync(ProjectPcbAssignment assignment, IUserContext? userContext)
+ {
+ assignment.UserId = userContext?.UserId ?? 0;
+ var id = _projectPcbAssignments.OrderByDescending(x => x.Key).Select(x => x.Key).FirstOrDefault() + 1;
+ assignment.ProjectPcbAssignmentId = id;
+ _projectPcbAssignments.Add(id, assignment);
+ return assignment;
+ }
+
+ public async Task AddStoredFileAsync(StoredFile storedFile, IUserContext? userContext)
+ {
+ storedFile.UserId = userContext?.UserId ?? 0;
+ var id = _storedFiles.OrderByDescending(x => x.Key).Select(x => x.Key).FirstOrDefault() + 1;
+ storedFile.StoredFileId = id;
+ _storedFiles.Add(id, storedFile);
+ return storedFile;
+ }
+
+ public Task CreateOAuthRequestAsync(OAuthAuthorization authRequest, IUserContext? userContext)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task DeletePartAsync(Part part, IUserContext? userContext)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task DeletePartSupplierAsync(PartSupplier partSupplier, IUserContext? userContext)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task DeletePartTypeAsync(PartType partType, IUserContext? userContext)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task DeletePcbAsync(Pcb pcb, IUserContext? userContext)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task DeleteProjectAsync(Project project, IUserContext? userContext)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task DeleteStoredFileAsync(StoredFile storedFile, IUserContext? userContext)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task>> FindPartsAsync(string keywords, IUserContext? userContext)
+ {
+ throw new NotImplementedException();
+ }
+
+ public async Task GetDatabaseAsync(IUserContext? userContext)
+ {
+ return new LegacyBinnerDb
+ {
+ Parts = _parts.Values,
+ Pcbs = _pcbs.Values,
+ PartTypes = _partTypes.Values,
+ ProjectPartAssignments = _projectPartAssignments.Values,
+ PartSuppliers = _partSuppliers.Values,
+ PcbStoredFileAssignments = _pcbStoredFileAssignments.Values,
+ ProjectPcbAssignments = _projectPcbAssignments.Values,
+ Projects = _projects.Values,
+ StoredFiles = _storedFiles.Values,
+ Count = _parts.Count,
+ };
+ }
+
+ public Task> GetLowStockAsync(PaginatedRequest request, IUserContext? userContext)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task GetOAuthCredentialAsync(string providerName, IUserContext? userContext)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task GetOAuthRequestAsync(Guid requestId, IUserContext? userContext)
+ {
+ throw new NotImplementedException();
+ }
+
+ public async Task GetOrCreatePartTypeAsync(PartType partType, IUserContext? userContext)
+ {
+ if (_partTypes.Where(x => x.Value.Name == partType.Name).Any())
+ return _partTypes.Where(x => x.Value.Name == partType.Name).Select(x => x.Value).First();
+ var id = _partTypes.OrderByDescending(x => x.Key).Select(x => x.Key).FirstOrDefault() + 1;
+ partType.PartTypeId = id;
+ _partTypes.Add(id, partType);
+ return partType;
+ }
+
+ public Task> GetPartAssignmentsAsync(long partId, IUserContext? userContext)
+ {
+ throw new NotImplementedException();
+ }
+
+ public async Task GetPartAsync(long partId, IUserContext? userContext)
+ {
+ return _parts.Where(x => x.Key == partId).Select(x => x.Value).FirstOrDefault();
+ }
+
+ public async Task GetPartAsync(string partNumber, IUserContext? userContext)
+ {
+ return _parts.Where(x => x.Value.PartNumber == partNumber).Select(x => x.Value).FirstOrDefault();
+ }
+
+ public async Task> GetPartsAsync(PaginatedRequest request, IUserContext? userContext)
+ {
+ //return _parts.Select(x => x.Value).ToList();
+ throw new NotImplementedException();
+ }
+
+ public async Task> GetPartsAsync(Expression> predicate, IUserContext? userContext)
+ {
+ return _parts.Select(x => x.Value).ToList();
+ }
+
+ public async Task GetPartsCountAsync(IUserContext? userContext)
+ {
+ return _parts.Count();
+ }
+
+ public Task GetPartSupplierAsync(long partSupplierId, IUserContext? userContext)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task> GetPartSuppliersAsync(long partId, IUserContext? userContext)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task GetPartsValueAsync(IUserContext? userContext)
+ {
+ throw new NotImplementedException();
+ }
+
+ public async Task GetPartTypeAsync(long partTypeId, IUserContext? userContext)
+ {
+ return _partTypes.Where(x => x.Key == partTypeId).Select(x => x.Value).FirstOrDefault();
+ }
+
+ public async Task> GetPartTypesAsync(IUserContext? userContext)
+ {
+ return _partTypes.Select(x => x.Value).ToList();
+ }
+
+ public async Task GetPcbAsync(long pcbId, IUserContext? userContext)
+ {
+ return _pcbs.Where(x => x.Key == pcbId).Select(x => x.Value).FirstOrDefault();
+ }
+
+ public async Task> GetPcbsAsync(long projectId, IUserContext? userContext)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task GetPcbStoredFileAssignmentAsync(long pcbStoredFileAssignmentId, IUserContext? userContext)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task> GetPcbStoredFileAssignmentsAsync(long pcbId, IUserContext? userContext)
+ {
+ throw new NotImplementedException();
+ }
+
+ public async Task GetProjectAsync(long projectId, IUserContext? userContext)
+ {
+ return _projects.Where(x => x.Key == projectId).Select(x => x.Value).FirstOrDefault();
+ }
+
+ public async Task GetProjectAsync(string projectName, IUserContext? userContext)
+ {
+ return _projects.Where(x => x.Value.Name == projectName).Select(x => x.Value).FirstOrDefault();
+ }
+
+ public Task GetProjectPartAssignmentAsync(long projectPartAssignmentId, IUserContext? userContext)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task GetProjectPartAssignmentAsync(long projectId, long partId, IUserContext? userContext)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task GetProjectPartAssignmentAsync(long projectId, string partName, IUserContext? userContext)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task> GetProjectPartAssignmentsAsync(long projectId, IUserContext? userContext)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task> GetProjectPartAssignmentsAsync(long projectId, PaginatedRequest request, IUserContext? userContext)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task GetProjectPcbAssignmentAsync(long projectPcbAssignmentId, IUserContext? userContext)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task> GetProjectPcbAssignmentsAsync(long projectId, IUserContext? userContext)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task> GetProjectsAsync(PaginatedRequest request, IUserContext? userContext)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task GetStoredFileAsync(long storedFileId, IUserContext? userContext)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task GetStoredFileAsync(string filename, IUserContext? userContext)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task> GetStoredFilesAsync(long partId, StoredFileType? fileType, IUserContext? userContext)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task> GetStoredFilesAsync(PaginatedRequest request, IUserContext? userContext)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task GetUniquePartsCountAsync(IUserContext? userContext)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task RemoveOAuthCredentialAsync(string providerName, IUserContext? userContext)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task RemovePcbStoredFileAssignmentAsync(PcbStoredFileAssignment assignment, IUserContext? userContext)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task RemoveProjectPartAssignmentAsync(ProjectPartAssignment assignment, IUserContext? userContext)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task RemoveProjectPcbAssignmentAsync(ProjectPcbAssignment assignment, IUserContext? userContext)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task SaveOAuthCredentialAsync(OAuthCredential credential, IUserContext? userContext)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task TestConnectionAsync()
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task UpdateOAuthRequestAsync(OAuthAuthorization authRequest, IUserContext? userContext)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task UpdatePartAsync(Part part, IUserContext? userContext)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task UpdatePartSupplierAsync(PartSupplier partSupplier, IUserContext? userContext)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task UpdatePartTypeAsync(PartType partType, IUserContext? userContext)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task UpdatePcbAsync(Pcb pcb, IUserContext? userContext)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task UpdatePcbStoredFileAssignmentAsync(PcbStoredFileAssignment assignment, IUserContext? userContext)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task UpdateProjectAsync(Project project, IUserContext? userContext)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task UpdateProjectPartAssignmentAsync(ProjectPartAssignment assignment, IUserContext? userContext)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task UpdateProjectPcbAssignmentAsync(ProjectPcbAssignment assignment, IUserContext? userContext)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task UpdateStoredFileAsync(StoredFile storedFile, IUserContext? userContext)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void Dispose()
+ {
+ _parts.Clear();
+ _projects.Clear();
+ _projectPartAssignments.Clear();
+ _projectPcbAssignments.Clear();
+ _partTypes.Clear();
+ _pcbs.Clear();
+ _storedFiles.Clear();
+ _pcbStoredFileAssignments.Clear();
+ _partSuppliers.Clear();
+ _users.Clear();
+ }
+ }
+}
diff --git a/Binner/Binner.sln b/Binner/Binner.sln
index 481147d9..6cefc7a5 100644
--- a/Binner/Binner.sln
+++ b/Binner/Binner.sln
@@ -49,6 +49,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Binner.Global.Common", "Lib
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Barcoder.Renderer.Image", "External\barcoder\Barcoder.Renderer.Image\Barcoder.Renderer.Image.csproj", "{0037D5E6-5FE2-4989-8668-266DB89DBAF2}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Binner.Testing", "Binner.Testing\Binner.Testing.csproj", "{958AE9B3-B6AD-4FA7-A463-0AD41D8B3665}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -127,6 +129,10 @@ Global
{0037D5E6-5FE2-4989-8668-266DB89DBAF2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0037D5E6-5FE2-4989-8668-266DB89DBAF2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0037D5E6-5FE2-4989-8668-266DB89DBAF2}.Release|Any CPU.Build.0 = Release|Any CPU
+ {958AE9B3-B6AD-4FA7-A463-0AD41D8B3665}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {958AE9B3-B6AD-4FA7-A463-0AD41D8B3665}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {958AE9B3-B6AD-4FA7-A463-0AD41D8B3665}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {958AE9B3-B6AD-4FA7-A463-0AD41D8B3665}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -150,6 +156,7 @@ Global
{DEEF8BAC-16E2-4FD3-ACDF-8E464189A3B1} = {2D9C4743-1B04-41B1-A12F-4CCF6F5BAB3E}
{12E4F3B4-00DB-4FB8-B326-EE81A2FFE64B} = {2D9C4743-1B04-41B1-A12F-4CCF6F5BAB3E}
{0037D5E6-5FE2-4989-8668-266DB89DBAF2} = {5E8D1F9D-68C0-444A-B04F-5AAC822D684E}
+ {958AE9B3-B6AD-4FA7-A463-0AD41D8B3665} = {3FF195C3-1F60-48E5-9EAB-1C4AAC791ABA}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {413F8D19-37B6-4A57-858F-DD8AFF2DAE96}
diff --git a/Binner/Library/Binner.Common/Extensions/IPAddressExtensions.cs b/Binner/Library/Binner.Common/Extensions/IPAddressExtensions.cs
index 4e46d5f4..525e3350 100644
--- a/Binner/Library/Binner.Common/Extensions/IPAddressExtensions.cs
+++ b/Binner/Library/Binner.Common/Extensions/IPAddressExtensions.cs
@@ -5,6 +5,40 @@ namespace Binner.Common.Extensions
{
public static class IpAddressExtensions
{
+ ///
+ /// Get an IPAddress as 32 bit integer
+ ///
+ ///
+ ///
+ public static int ToInt(this IPAddress ipAddress)
+ {
+ try
+ {
+ return IPAddress.NetworkToHostOrder(BitConverter.ToInt32(ipAddress.GetAddressBytes(), 0));
+ }
+ catch (Exception)
+ {
+ return 0;
+ }
+ }
+
+ ///
+ /// Get an IPAddress as 32 bit integer
+ ///
+ ///
+ ///
+ public static uint ToUInt(this IPAddress ipAddress)
+ {
+ try
+ {
+ return (uint)ToInt(ipAddress);
+ }
+ catch (Exception)
+ {
+ return 0;
+ }
+ }
+
///
/// Get an IPAddress as 64 bit integer
///
@@ -14,8 +48,24 @@ public static long ToLong(this IPAddress ipAddress)
{
try
{
- var ipLong = BitConverter.ToInt32(ipAddress.GetAddressBytes(), 0);
- return IPAddress.NetworkToHostOrder(ipLong);
+ return IPAddress.NetworkToHostOrder(BitConverter.ToInt64(ipAddress.GetAddressBytes(), 0));
+ }
+ catch (Exception)
+ {
+ return 0;
+ }
+ }
+
+ ///
+ /// Get an IPAddress as 64 bit integer
+ ///
+ ///
+ ///
+ public static ulong ToULong(this IPAddress ipAddress)
+ {
+ try
+ {
+ return (ulong)ToLong(ipAddress);
}
catch (Exception)
{
@@ -32,7 +82,58 @@ public static IPAddress FromLong(long ipAddress)
=> ipAddress.ToIpAddress();
///
- /// Get an IPAddress from a 64 bit integer
+ /// Get an IPAddress from a 32 bit integer
+ ///
+ ///
+ ///
+ public static IPAddress FromInt(int ipAddress)
+ {
+ try
+ {
+ return new IPAddress(IPAddress.NetworkToHostOrder(ipAddress));
+ }
+ catch (Exception)
+ {
+ }
+ return IPAddress.None;
+ }
+
+ ///
+ /// Get an IPAddress from a 32 bit integer
+ ///
+ ///
+ ///
+ public static IPAddress ToIpAddress(this int ipAddress)
+ {
+ try
+ {
+ return new IPAddress(IPAddress.NetworkToHostOrder(ipAddress));
+ }
+ catch (Exception)
+ {
+ }
+ return IPAddress.None;
+ }
+
+ ///
+ /// Get an IPAddress from a 32 bit integer
+ ///
+ ///
+ ///
+ public static IPAddress ToIpAddress(this uint ipAddress)
+ {
+ try
+ {
+ return new IPAddress(IPAddress.NetworkToHostOrder((int)ipAddress));
+ }
+ catch (Exception)
+ {
+ }
+ return IPAddress.None;
+ }
+
+ ///
+ /// Get an IPAddress from a 32 bit integer
///
///
///
@@ -40,9 +141,24 @@ public static IPAddress ToIpAddress(this long ipAddress)
{
try
{
- var ipAddressStr = ipAddress.ToString();
- if (IPAddress.TryParse(ipAddressStr, out var ip))
- return ip;
+ return new IPAddress(IPAddress.NetworkToHostOrder(ipAddress));
+ }
+ catch (Exception)
+ {
+ }
+ return IPAddress.None;
+ }
+
+ ///
+ /// Get an IPAddress from a 64 bit integer
+ ///
+ ///
+ ///
+ public static IPAddress ToIpAddress(this ulong ipAddress)
+ {
+ try
+ {
+ return new IPAddress(IPAddress.NetworkToHostOrder((long)ipAddress));
}
catch (Exception)
{
diff --git a/Binner/Library/Binner.Common/IO/CsvDataImporter.cs b/Binner/Library/Binner.Common/IO/CsvDataImporter.cs
index e9bb5167..bd78d579 100644
--- a/Binner/Library/Binner.Common/IO/CsvDataImporter.cs
+++ b/Binner/Library/Binner.Common/IO/CsvDataImporter.cs
@@ -62,7 +62,7 @@ public async Task ImportAsync(IEnumerable files, IUser
private string? GetValueFromHeader(string[] rowData, Header header, string name)
{
- var headerIndex = header.GetHeaderIndex("Description");
+ var headerIndex = header.GetHeaderIndex(name);
if (headerIndex >= 0)
return rowData[headerIndex];
return null;
diff --git a/Binner/Library/Binner.Model/IBinnerDb.cs b/Binner/Library/Binner.Model/IBinnerDb.cs
index 38d17ee7..fd675969 100644
--- a/Binner/Library/Binner.Model/IBinnerDb.cs
+++ b/Binner/Library/Binner.Model/IBinnerDb.cs
@@ -1,6 +1,4 @@
-using System.Collections.Generic;
-
-namespace Binner.Model
+namespace Binner.Model
{
public interface IBinnerDb
{
diff --git a/Binner/Library/Binner.Model/StoredFile.cs b/Binner/Library/Binner.Model/StoredFile.cs
index e000cd85..9a95e159 100644
--- a/Binner/Library/Binner.Model/StoredFile.cs
+++ b/Binner/Library/Binner.Model/StoredFile.cs
@@ -37,6 +37,11 @@ public class StoredFile
///
public int Crc32 { get; set; }
+ ///
+ /// Optional user id to associate
+ ///
+ public int UserId { get; set; }
+
///
/// Creation date
///
diff --git a/Binner/Tests/Binner.Common.Tests/Binner.Common.Tests.csproj b/Binner/Tests/Binner.Common.Tests/Binner.Common.Tests.csproj
index 94b281db..7d30424f 100644
--- a/Binner/Tests/Binner.Common.Tests/Binner.Common.Tests.csproj
+++ b/Binner/Tests/Binner.Common.Tests/Binner.Common.Tests.csproj
@@ -1,4 +1,4 @@
-
+
net8.0
@@ -9,14 +9,37 @@
false
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+
-
-
-
+
+
+
+
diff --git a/Binner/Tests/Binner.Common.Tests/IO/BinnerParts.xlsx b/Binner/Tests/Binner.Common.Tests/IO/BinnerParts.xlsx
new file mode 100644
index 00000000..2b2c1359
Binary files /dev/null and b/Binner/Tests/Binner.Common.Tests/IO/BinnerParts.xlsx differ
diff --git a/Binner/Tests/Binner.Common.Tests/IO/CsvDataImporterTests.cs b/Binner/Tests/Binner.Common.Tests/IO/CsvDataImporterTests.cs
new file mode 100644
index 00000000..6827183c
--- /dev/null
+++ b/Binner/Tests/Binner.Common.Tests/IO/CsvDataImporterTests.cs
@@ -0,0 +1,218 @@
+using Binner.Common.IO;
+using Binner.Global.Common;
+using Binner.Testing;
+using NUnit.Framework;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Binner.Common.Tests.IO
+{
+
+ [TestFixture]
+ public class CsvDataImporterTests
+ {
+ [Test]
+ public async Task ShouldImportCsvAsync()
+ {
+ using var storageProvider = new InMemoryStorageProvider(true);
+ var importer = new CsvDataImporter(storageProvider);
+ var userContext = new UserContext { UserId = 99 };
+
+ var files = new List();
+ using var stream = new MemoryStream();
+ using var writer = new StreamWriter(stream);
+ writer.WriteLine(@$"#ProjectId, Name, Description, Location, Color, DateCreatedUtc, DateModifiedUtc");
+ writer.WriteLine($@"1, 'Test Project 1', 'test description', 'location', 1, '2022-01-01 00:00:00', '2022-01-01 00:00:00'");
+ writer.Flush();
+ stream.Position = 0;
+ files.Add(new UploadFile("Projects.csv", stream));
+
+ var result = await importer.ImportAsync(files, userContext);
+
+ var db = await storageProvider.GetDatabaseAsync(userContext);
+ Assert.That(result.Success, Is.True);
+ Assert.That(result.TotalRowsImported, Is.EqualTo(1));
+ Assert.That(result.RowsImportedByTable["Projects"], Is.EqualTo(1));
+ Assert.That(db.Projects.Count, Is.EqualTo(1));
+ Assert.That(db.Projects.First().UserId, Is.EqualTo(99));
+ }
+
+ [Test]
+ public async Task ShouldImportQuotedDelimiterCsvAsync()
+ {
+ using var storageProvider = new InMemoryStorageProvider(true);
+ var importer = new CsvDataImporter(storageProvider);
+ var userContext = new UserContext { UserId = 99 };
+
+ var files = new List();
+ using var stream = new MemoryStream();
+ using var writer = new StreamWriter(stream);
+ writer.WriteLine(@$"#ProjectId, Name, Description, Location, Color, DateCreatedUtc, DateModifiedUtc");
+ writer.WriteLine($@"1, 'Test Project 1', 'test, description', 'location', 1, '2022-01-01 00:00:00', '2022-01-01 00:00:00'");
+ writer.Flush();
+ stream.Position = 0;
+ files.Add(new UploadFile("Projects.csv", stream));
+
+ var result = await importer.ImportAsync(files, userContext);
+
+ var db = await storageProvider.GetDatabaseAsync(userContext);
+ Assert.That(result.Success, Is.True);
+ Assert.That(result.TotalRowsImported, Is.EqualTo(1));
+ Assert.That(result.RowsImportedByTable["Projects"], Is.EqualTo(1));
+ Assert.That(db.Projects.Count, Is.EqualTo(1));
+ Assert.That(db.Projects.First().UserId, Is.EqualTo(99));
+ }
+
+ [Test]
+ public async Task ShouldIgnoreUserIdAsync()
+ {
+ using var storageProvider = new InMemoryStorageProvider(true);
+ var importer = new CsvDataImporter(storageProvider);
+ var userContext = new UserContext { UserId = 99 };
+
+ using var stream = new MemoryStream();
+ using var writer = new StreamWriter(stream);
+ writer.WriteLine(@$"#ProjectId, Name, Description, Location, Color, DateCreatedUtc, DateModifiedUtc, UserId");
+ writer.WriteLine($@"1, 'Test Project 1', 'test description', 'location', 1, '2022-01-01 00:00:00', '2022-01-01 00:00:00',1");
+ writer.Flush();
+ stream.Position = 0;
+
+ var result = await importer.ImportAsync("Projects.csv", stream, userContext);
+
+ var db = await storageProvider.GetDatabaseAsync(userContext);
+ Assert.That(result.Success, Is.True);
+ Assert.That(result.TotalRowsImported, Is.EqualTo(1));
+ Assert.That(result.RowsImportedByTable["Projects"], Is.EqualTo(1));
+ Assert.That(db.Projects.Count, Is.EqualTo(1));
+ Assert.That(db.Projects.First().UserId, Is.EqualTo(99));
+ }
+
+ [Test]
+ public async Task ShouldImportMultipleRowsAsync()
+ {
+ using var storageProvider = new InMemoryStorageProvider(true);
+ var importer = new CsvDataImporter(storageProvider);
+ var userContext = new UserContext { UserId = 99 };
+
+ using var stream = new MemoryStream();
+ using var writer = new StreamWriter(stream);
+ writer.WriteLine(@$"#ProjectId, Name, Description, Location, Color, DateCreatedUtc, DateModifiedUtc");
+ writer.WriteLine($@"1, 'Test Project 1', 'test description', 'location', 1, '2022-01-01 00:00:00', '2022-01-01 00:00:00'");
+ writer.WriteLine($@"2, 'Test Project 2', 'test description', 'location', 1, '2022-01-01 00:00:00', '2022-01-01 00:00:00'");
+ writer.WriteLine($@"3, 'Test Project 3', 'test description', 'location', 1, '2022-01-01 00:00:00', '2022-01-01 00:00:00'");
+ writer.WriteLine($@"4, 'Test Project 4', 'test description', 'location', 1, '2022-01-01 00:00:00', '2022-01-01 00:00:00'");
+ // try insert with quoted line-break content
+ writer.WriteLine($@"5, 'Test Project 5', 'test description\nsome extra data', 'location', 1, '2022-01-01 00:00:00', '2022-01-01 00:00:00'");
+ writer.Flush();
+ stream.Position = 0;
+
+ var result = await importer.ImportAsync("Projects.csv", stream, userContext);
+
+ var db = await storageProvider.GetDatabaseAsync(userContext);
+ Assert.That(result.Success, Is.True);
+ Assert.That(result.TotalRowsImported, Is.EqualTo(5));
+ Assert.That(result.RowsImportedByTable["Projects"], Is.EqualTo(5));
+ Assert.That(db.Projects.Count, Is.EqualTo(5));
+ }
+
+ [Test]
+ public async Task ShouldNotImportWithUnsupportedTableAsync()
+ {
+ using var storageProvider = new InMemoryStorageProvider(true);
+ var importer = new CsvDataImporter(storageProvider);
+ var userContext = new UserContext { UserId = 99 };
+
+ using var stream = new MemoryStream();
+ using var writer = new StreamWriter(stream);
+ writer.WriteLine(@$"#ProjectId, Name, Description, Location, Color, DateCreatedUtc, DateModifiedUtc");
+ writer.WriteLine($@"1, 'Test Project 1', 'test description', 'location', 1, '2022-01-01 00:00:00', '2022-01-01 00:00:00'");
+ writer.Flush();
+ stream.Position = 0;
+
+ var result = await importer.ImportAsync("SomeTable.csv", stream, userContext);
+
+ var db = await storageProvider.GetDatabaseAsync(userContext);
+ Assert.That(result.Success, Is.False);
+ Assert.That(result.TotalRowsImported, Is.EqualTo(0));
+ Assert.That(result.Errors.Count, Is.EqualTo(1));
+ }
+
+ [Test]
+ public async Task ShouldSkipInvalidRowAsync()
+ {
+ using var storageProvider = new InMemoryStorageProvider(true);
+ var importer = new CsvDataImporter(storageProvider);
+ var userContext = new UserContext { UserId = 99 };
+
+ using var stream = new MemoryStream();
+ using var writer = new StreamWriter(stream);
+ writer.WriteLine(@$"#ProjectId, Name, Description, Location, Color, DateCreatedUtc, DateModifiedUtc");
+ writer.WriteLine($@"1, 'Test Project 1', 'test description', 'location', 1, '2022-01-01 00:00:00', '2022-01-01 00:00:00'");
+ writer.WriteLine($@"2, 'Invalid Test', 'test description', 'location', 1");
+ writer.WriteLine($@"3, 'Test Project 2', 'test description', 'location', 1, '2022-01-01 00:00:00', '2022-01-01 00:00:00'");
+ writer.Flush();
+ stream.Position = 0;
+
+ var result = await importer.ImportAsync("Projects.csv", stream, userContext);
+
+ var db = await storageProvider.GetDatabaseAsync(userContext);
+ Assert.That(result.Success, Is.True);
+ Assert.That(result.TotalRowsImported, Is.EqualTo(2));
+ Assert.That(result.RowsImportedByTable["Projects"], Is.EqualTo(2));
+ Assert.That(result.Warnings.Count, Is.EqualTo(1));
+ Assert.That(db.Projects.Count, Is.EqualTo(2));
+ }
+
+ [Test]
+ public async Task ShouldImportUnquotedAsync()
+ {
+ using var storageProvider = new InMemoryStorageProvider(true);
+ var importer = new CsvDataImporter(storageProvider);
+ var userContext = new UserContext { UserId = 99 };
+
+ using var stream = new MemoryStream();
+ using var writer = new StreamWriter(stream);
+ writer.WriteLine(@$"#ProjectId, Name, Description, Location, Color, DateCreatedUtc, DateModifiedUtc");
+ writer.WriteLine($@"1, 'Test Project 1',test description, location, 1, '2022-01-01 00:00:00', '2022-01-01 00:00:00'");
+ writer.WriteLine($@"2, 'Test Project 2', 'test description', 'location', 1, '2022-01-01 00:00:00', '2022-01-01 00:00:00'");
+ writer.Flush();
+ stream.Position = 0;
+
+ var result = await importer.ImportAsync("Projects.csv", stream, userContext);
+
+ var db = await storageProvider.GetDatabaseAsync(userContext);
+ Assert.That(result.Success, Is.True);
+ Assert.That(result.TotalRowsImported, Is.EqualTo(2));
+ Assert.That(result.RowsImportedByTable["Projects"], Is.EqualTo(2));
+ Assert.That(db.Projects.Count, Is.EqualTo(2));
+ }
+
+ [Test]
+ public async Task ShouldImportEncodedLineBreaksAsync()
+ {
+ using var storageProvider = new InMemoryStorageProvider(true);
+ var importer = new CsvDataImporter(storageProvider);
+ var userContext = new UserContext { UserId = 99 };
+
+ using var stream = new MemoryStream();
+ using var writer = new StreamWriter(stream);
+ writer.WriteLine(@$"#ProjectId, Name, Description, Location, Color, DateCreatedUtc, DateModifiedUtc");
+ writer.WriteLine($@"1, 'Test Project 1', 'test description
+This is a test
+another test', location, 1, '2022-01-01 00:00:00', '2022-01-01 00:00:00'");
+ writer.WriteLine($@"2, 'Test Project 2', test description\r\nunquoted strings result in decoded line breaks, 'location', 1, '2022-01-01 00:00:00', '2022-01-01 00:00:00'");
+ writer.Flush();
+ stream.Position = 0;
+
+ var result = await importer.ImportAsync("Projects.csv", stream, userContext);
+
+ var db = await storageProvider.GetDatabaseAsync(userContext);
+ Assert.That(result.Success, Is.True);
+ Assert.That(result.TotalRowsImported, Is.EqualTo(2));
+ Assert.That(result.RowsImportedByTable["Projects"], Is.EqualTo(2));
+ Assert.That(db.Projects.Count, Is.EqualTo(2));
+ }
+ }
+}
diff --git a/Binner/Tests/Binner.Common.Tests/IO/ExcelDataImporterTests.cs b/Binner/Tests/Binner.Common.Tests/IO/ExcelDataImporterTests.cs
new file mode 100644
index 00000000..994007c2
--- /dev/null
+++ b/Binner/Tests/Binner.Common.Tests/IO/ExcelDataImporterTests.cs
@@ -0,0 +1,37 @@
+using Binner.Common.IO;
+using Binner.Global.Common;
+using Binner.Testing;
+using NUnit.Framework;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Binner.Common.Tests.IO
+{
+ [TestFixture]
+ public class ExcelDataImporterTests
+ {
+ [Test]
+ public async Task ShouldImportExcelAsync()
+ {
+ using var storageProvider = new InMemoryStorageProvider(true);
+ var importer = new ExcelDataImporter(storageProvider);
+ var userContext = new UserContext { UserId = 99 };
+
+ using var stream = new FileStream(".\\IO\\BinnerParts.xlsx", FileMode.Open);
+ var result = await importer.ImportAsync("BinnerParts.xlsx", stream, userContext);
+
+ var db = await storageProvider.GetDatabaseAsync(userContext);
+ Assert.That(result.Success, Is.True);
+ // data based on test set in BinnerParts.xlsx
+ Assert.That(result.TotalRowsImported, Is.EqualTo(204));
+ Assert.That(result.RowsImportedByTable["Projects"], Is.EqualTo(2));
+ Assert.That(result.RowsImportedByTable["Parts"], Is.EqualTo(1));
+ Assert.That(result.RowsImportedByTable["PartTypes"], Is.EqualTo(201));
+ Assert.That(db.Projects.Count, Is.EqualTo(2));
+ Assert.That(db.Parts.Count, Is.EqualTo(1));
+ Assert.That(db.PartTypes.Count, Is.EqualTo(205));
+ Assert.That(db.Projects.First().UserId, Is.EqualTo(99));
+ }
+ }
+}
diff --git a/Binner/Tests/Binner.Common.Tests/IO/IPAddressExtensionsTests.cs b/Binner/Tests/Binner.Common.Tests/IO/IPAddressExtensionsTests.cs
new file mode 100644
index 00000000..cfb171c2
--- /dev/null
+++ b/Binner/Tests/Binner.Common.Tests/IO/IPAddressExtensionsTests.cs
@@ -0,0 +1,39 @@
+using Binner.Common.Extensions;
+using NUnit.Framework;
+using System.Net;
+
+namespace Binner.Common.Tests.IO
+{
+ [TestFixture]
+ public class IPAddressExtensionsTests
+ {
+ [TestCase("0.0.0.0", ExpectedResult = 0U, Description = "0.0.0.0")]
+ [TestCase("127.0.0.1", ExpectedResult = 2130706433U, Description = "127.0.0.1")]
+ [TestCase("192.168.1.55", ExpectedResult = 3232235831U, Description = "192.168.1.55")]
+ [TestCase("54.22.161.99", ExpectedResult = 907452771U, Description = "54.22.161.99")]
+ [TestCase("12.24.36.48", ExpectedResult = 202908720U, Description = "12.24.36.48")]
+ [TestCase("1.1.1.1", ExpectedResult = 16843009U, Description = "1.1.1.1")]
+ [TestCase("255.255.255.255", ExpectedResult = 4294967295U, Description = "255.255.255.255")]
+ [Test]
+ public uint ShouldIpToInt(string ipAddressStr)
+ {
+ var ipAddress = IPAddress.Parse(ipAddressStr);
+ var ip = ipAddress.ToUInt();
+ return ip;
+ }
+
+ [TestCase(0U, ExpectedResult = "0.0.0.0")]
+ [TestCase(2130706433U, ExpectedResult = "127.0.0.1")]
+ [TestCase(3232235831U, ExpectedResult = "192.168.1.55")]
+ [TestCase(907452771U, ExpectedResult = "54.22.161.99")]
+ [TestCase(202908720U, ExpectedResult = "12.24.36.48")]
+ [TestCase(16843009U, ExpectedResult = "1.1.1.1")]
+ [TestCase(4294967295U, ExpectedResult = "255.255.255.255")]
+ [Test]
+ public string ShouldIntToIp(uint ip)
+ {
+ var ipAddress = ip.ToIpAddress();
+ return ipAddress.ToString();
+ }
+ }
+}
diff --git a/Binner/Tests/Binner.Common.Tests/IO/PartTypes.csv b/Binner/Tests/Binner.Common.Tests/IO/PartTypes.csv
new file mode 100644
index 00000000..c2b2edef
--- /dev/null
+++ b/Binner/Tests/Binner.Common.Tests/IO/PartTypes.csv
@@ -0,0 +1,205 @@
+#PartTypeId,ParentPartTypeId,Name,DateCreatedUtc,UserId
+12,0,"Cable",2022-04-10 02:26:28,0
+2,0,"Capacitor",2022-04-10 02:26:28,0
+13,0,"Connector",2022-04-10 02:26:28,0
+9,0,"Crystal",2022-04-10 02:26:28,0
+4,0,"Diode",2022-04-10 02:26:28,0
+16,0,"Evaluation",2022-04-10 02:26:28,0
+17,0,"Hardware",2022-04-10 02:26:28,0
+14,0,"IC",2022-04-10 02:26:28,0
+3,0,"Inductor",2022-04-10 02:26:28,0
+201,0,"Kit",2022-04-10 02:26:28,0
+5,0,"LED",2022-04-10 02:26:28,0
+15,0,"Module",2022-04-10 02:26:28,0
+18,0,"Other",2022-04-10 02:26:28,0
+7,0,"Relay",2022-04-10 02:26:28,0
+1,0,"Resistor",2022-04-10 02:26:28,0
+10,0,"Sensor",2022-04-10 02:26:28,0
+11,0,"Switch",2022-04-10 02:26:28,0
+8,0,"Transformer",2022-04-10 02:26:28,0
+6,0,"Transistor",2022-04-10 02:26:28,0
+202,2,"CapacitorKit",2022-04-10 02:26:28,0
+39,2,"CeramicCapacitor",2022-04-10 02:26:28,0
+40,2,"ElectrolyticCapacitor",2022-04-10 02:26:28,0
+41,2,"FilmCapacitor",2022-04-10 02:26:28,0
+42,2,"MicaCapacitor",2022-04-10 02:26:28,0
+43,2,"NonPolarizedCapacitor",2022-04-10 02:26:28,0
+45,2,"PaperCapacitor",2022-04-10 02:26:28,0
+199,2,"SafetyCapacitor",2022-04-10 02:26:28,0
+44,2,"SupercapacitorCapacitor",2022-04-10 02:26:28,0
+200,2,"TantalumCapacitor",2022-04-10 02:26:28,0
+46,2,"VariableCapacitor",2022-04-10 02:26:28,0
+74,4,"CrystalDiode",2022-04-10 02:26:28,0
+203,4,"DiodeKit",2022-04-10 02:26:28,0
+69,4,"GunnDiode",2022-04-10 02:26:28,0
+66,4,"LargeSignalDiode",2022-04-10 02:26:28,0
+68,4,"PeltierDiode",2022-04-10 02:26:28,0
+64,4,"Schottky",2022-04-10 02:26:28,0
+67,4,"Shockley",2022-04-10 02:26:28,0
+65,4,"SmallSignalDiode",2022-04-10 02:26:28,0
+71,4,"StepRecoveryDiode",2022-04-10 02:26:28,0
+73,4,"TransientVoltageSuppressionDiode",2022-04-10 02:26:28,0
+70,4,"TunnelDiode",2022-04-10 02:26:28,0
+72,4,"VaractorDiode",2022-04-10 02:26:28,0
+63,4,"Zener",2022-04-10 02:26:28,0
+124,16,"Alchitry",2022-04-10 02:26:28,0
+125,16,"Amica",2022-04-10 02:26:28,0
+119,16,"Arduino",2022-04-10 02:26:28,0
+141,16,"AsusTinker",2022-04-10 02:26:28,0
+132,16,"BasicEvaluation",2022-04-10 02:26:28,0
+120,16,"BeagleBoard",2022-04-10 02:26:28,0
+135,16,"LattePanda",2022-04-10 02:26:28,0
+123,16,"Launchpad",2022-04-10 02:26:28,0
+131,16,"MikroElektronika",2022-04-10 02:26:28,0
+121,16,"NVidiaJetson",2022-04-10 02:26:28,0
+134,16,"Odriod",2022-04-10 02:26:28,0
+142,16,"OtherEvaluation",2022-04-10 02:26:28,0
+126,16,"Particle",2022-04-10 02:26:28,0
+129,16,"Pic",2022-04-10 02:26:28,0
+133,16,"Pine",2022-04-10 02:26:28,0
+140,16,"PocketBeagle",2022-04-10 02:26:28,0
+128,16,"Qwiic",2022-04-10 02:26:28,0
+118,16,"RaspberryPi",2022-04-10 02:26:28,0
+138,16,"RockPi",2022-04-10 02:26:28,0
+136,16,"Seeeduino",2022-04-10 02:26:28,0
+137,16,"SiliconLabs",2022-04-10 02:26:28,0
+127,16,"Sparkfun",2022-04-10 02:26:28,0
+130,16,"STM32",2022-04-10 02:26:28,0
+122,16,"Teensy",2022-04-10 02:26:28,0
+139,16,"Udoo",2022-04-10 02:26:28,0
+143,17,"Adapter",2022-04-10 02:26:28,0
+150,17,"BallBearing",2022-04-10 02:26:28,0
+158,17,"Belt",2022-04-10 02:26:28,0
+151,17,"Bracket",2022-04-10 02:26:28,0
+149,17,"Coupler",2022-04-10 02:26:28,0
+192,17,"Enclosure",2022-04-10 02:26:28,0
+160,17,"Fan",2022-04-10 02:26:28,0
+148,17,"Gear",2022-04-10 02:26:28,0
+177,17,"Grommet",2022-04-10 02:26:28,0
+159,17,"Hub",2022-04-10 02:26:28,0
+157,17,"Mount",2022-04-10 02:26:28,0
+146,17,"Nut",2022-04-10 02:26:28,0
+155,17,"Plate",2022-04-10 02:26:28,0
+156,17,"RawMaterial",2022-04-10 02:26:28,0
+162,17,"Robotics",2022-04-10 02:26:28,0
+144,17,"Screw",2022-04-10 02:26:28,0
+152,17,"Shaft",2022-04-10 02:26:28,0
+153,17,"Spacer",2022-04-10 02:26:28,0
+176,17,"Spring",2022-04-10 02:26:28,0
+147,17,"Standoff",2022-04-10 02:26:28,0
+154,17,"Tube",2022-04-10 02:26:28,0
+145,17,"Washer",2022-04-10 02:26:28,0
+161,17,"Wheel",2022-04-10 02:26:28,0
+26,14,"ADC",2022-04-10 02:26:28,0
+20,14,"Amplifier",2022-04-10 02:26:28,0
+30,14,"AudioIc",2022-04-10 02:26:28,0
+25,14,"ClockIc",2022-04-10 02:26:28,0
+31,14,"ComparatorIc",2022-04-10 02:26:28,0
+32,14,"CounterIc",2022-04-10 02:26:28,0
+36,14,"DataAcquisitionIc",2022-04-10 02:26:28,0
+33,14,"DividerIc",2022-04-10 02:26:28,0
+37,14,"EmbeddedIc",2022-04-10 02:26:28,0
+28,14,"EnergyMeteringIc",2022-04-10 02:26:28,0
+163,14,"FlipFlopIc",2022-04-10 02:26:28,0
+35,14,"FPGA",2022-04-10 02:26:28,0
+23,14,"InterfaceIc",2022-04-10 02:26:28,0
+29,14,"LedDriverIc",2022-04-10 02:26:28,0
+22,14,"LogicIc",2022-04-10 02:26:28,0
+21,14,"MemoryIc",2022-04-10 02:26:28,0
+24,14,"Microcontroller",2022-04-10 02:26:28,0
+19,14,"OpAmp",2022-04-10 02:26:28,0
+34,14,"PMIC",2022-04-10 02:26:28,0
+38,14,"SpecializedIc",2022-04-10 02:26:28,0
+27,14,"VoltageRegulatorIc",2022-04-10 02:26:28,0
+164,3,"AdjustableInductor",2022-04-10 02:26:28,0
+55,3,"AirCoreInductor",2022-04-10 02:26:28,0
+60,3,"BobbinInductor",2022-04-10 02:26:28,0
+57,3,"FerriteCoreInductor",2022-04-10 02:26:28,0
+204,3,"InductorKit",2022-04-10 02:26:28,0
+56,3,"IronCoreInductor",2022-04-10 02:26:28,0
+58,3,"IronPowderInductor",2022-04-10 02:26:28,0
+59,3,"LaminatedCoreInductor",2022-04-10 02:26:28,0
+62,3,"MultiLayerCeramicInductor",2022-04-10 02:26:28,0
+61,3,"ToroidalInductor",2022-04-10 02:26:28,0
+115,15,"ArduinoShield",2022-04-10 02:26:28,0
+112,15,"CurrentVoltageModule",2022-04-10 02:26:28,0
+116,15,"EvaluationModule",2022-04-10 02:26:28,0
+113,15,"ExperimentModule",2022-04-10 02:26:28,0
+117,15,"OtherModule",2022-04-10 02:26:28,0
+114,15,"RaspberryPiShield",2022-04-10 02:26:28,0
+111,15,"WirelessModule",2022-04-10 02:26:28,0
+81,7,"ElectromagneticRelay",2022-04-10 02:26:28,0
+89,7,"HighVoltageRelay",2022-04-10 02:26:28,0
+82,7,"LatchingRelay",2022-04-10 02:26:28,0
+84,7,"ReedRelay",2022-04-10 02:26:28,0
+88,7,"RotaryRelay",2022-04-10 02:26:28,0
+87,7,"SequenceRelay",2022-04-10 02:26:28,0
+83,7,"SolidStateRelay",2022-04-10 02:26:28,0
+86,7,"ThermalRelay",2022-04-10 02:26:28,0
+85,7,"TimeRelay",2022-04-10 02:26:28,0
+47,1,"CarbonFilmResistor",2022-04-10 02:26:28,0
+193,1,"CeramicResistor",2022-04-10 02:26:28,0
+194,1,"CurrentSenseResistor",2022-04-10 02:26:28,0
+195,1,"HighFrequencyResistor",2022-04-10 02:26:28,0
+48,1,"MetalFilmResistor",2022-04-10 02:26:28,0
+196,1,"MetalFoilResistor",2022-04-10 02:26:28,0
+50,1,"MetalOxideResistor",2022-04-10 02:26:28,0
+51,1,"MetalStripResistor",2022-04-10 02:26:28,0
+198,1,"Potentiometer",2022-04-10 02:26:28,0
+52,1,"PowerResistor",2022-04-10 02:26:28,0
+53,1,"ResistorArray",2022-04-10 02:26:28,0
+197,1,"ResistorKit",2022-04-10 02:26:28,0
+54,1,"VariableResistor",2022-04-10 02:26:28,0
+49,1,"WirewoundResistor",2022-04-10 02:26:28,0
+185,10,"AccelerationSensor",2022-04-10 02:26:28,0
+184,10,"AirQualitySensor",2022-04-10 02:26:28,0
+179,10,"AudioSensor",2022-04-10 02:26:28,0
+107,10,"BiometricSensor",2022-04-10 02:26:28,0
+106,10,"CapacitiveSensor",2022-04-10 02:26:28,0
+167,10,"ColorSensor",2022-04-10 02:26:28,0
+99,10,"CurrentSensor",2022-04-10 02:26:28,0
+102,10,"DistanceSensor",2022-04-10 02:26:28,0
+108,10,"EnvironmentSensor",2022-04-10 02:26:28,0
+170,10,"FlowSensor",2022-04-10 02:26:28,0
+103,10,"ForceSensor",2022-04-10 02:26:28,0
+169,10,"GasSensor",2022-04-10 02:26:28,0
+187,10,"GyroscopeSensor",2022-04-10 02:26:28,0
+182,10,"HallEffectSensor",2022-04-10 02:26:28,0
+168,10,"HumiditySensor",2022-04-10 02:26:28,0
+98,10,"ImagingSensor",2022-04-10 02:26:28,0
+188,10,"InclineSensor",2022-04-10 02:26:28,0
+191,10,"InfraredSensor",2022-04-10 02:26:28,0
+97,10,"LightSensor",2022-04-10 02:26:28,0
+180,10,"LiquidSensor",2022-04-10 02:26:28,0
+101,10,"LoadSensor",2022-04-10 02:26:28,0
+181,10,"MagneticSensor",2022-04-10 02:26:28,0
+105,10,"MotionSensor",2022-04-10 02:26:28,0
+110,10,"OtherSensor",2022-04-10 02:26:28,0
+166,10,"Photodiodes",2022-04-10 02:26:28,0
+186,10,"PositionSensor",2022-04-10 02:26:28,0
+165,10,"PressureSensor",2022-04-10 02:26:28,0
+172,10,"ProximitySensor",2022-04-10 02:26:28,0
+109,10,"RadiationSensor",2022-04-10 02:26:28,0
+104,10,"RfSensor",2022-04-10 02:26:28,0
+96,10,"SensorAssembly",2022-04-10 02:26:28,0
+183,10,"SmokeSensor",2022-04-10 02:26:28,0
+189,10,"SpeedSensor",2022-04-10 02:26:28,0
+173,10,"TemperatureSensor",2022-04-10 02:26:28,0
+171,10,"TiltSensor",2022-04-10 02:26:28,0
+174,10,"TouchSensor",2022-04-10 02:26:28,0
+175,10,"UltrasonicSensor",2022-04-10 02:26:28,0
+190,10,"VibrationSensor",2022-04-10 02:26:28,0
+100,10,"VoltageSensor",2022-04-10 02:26:28,0
+95,8,"AudioTransformer",2022-04-10 02:26:28,0
+92,8,"IsolationTransformer",2022-04-10 02:26:28,0
+94,8,"RfTransformer",2022-04-10 02:26:28,0
+93,8,"SolidStateTransformer",2022-04-10 02:26:28,0
+90,8,"StepDownTransformer",2022-04-10 02:26:28,0
+91,8,"StepUpTransformer",2022-04-10 02:26:28,0
+178,6,"BJT",2022-04-10 02:26:28,0
+79,6,"DIAC",2022-04-10 02:26:28,0
+76,6,"IGBT",2022-04-10 02:26:28,0
+77,6,"JFET",2022-04-10 02:26:28,0
+75,6,"MOSFET",2022-04-10 02:26:28,0
+78,6,"SCR",2022-04-10 02:26:28,0
+80,6,"TRIAC",2022-04-10 02:26:28,0
diff --git a/Binner/Tests/Binner.Common.Tests/IO/Parts.csv b/Binner/Tests/Binner.Common.Tests/IO/Parts.csv
new file mode 100644
index 00000000..a613dbe0
--- /dev/null
+++ b/Binner/Tests/Binner.Common.Tests/IO/Parts.csv
@@ -0,0 +1,2 @@
+#PartId,Quantity,LowStockThreshold,Cost,PartNumber,DigiKeyPartNumber,MouserPartNumber,Description,PartTypeId,MountingTypeId,PackageType,ProductUrl,ImageUrl,LowestCostSupplier,LowestCostSupplierUrl,ProjectId,Keywords,DatasheetUrl,Location,BinNumber,BinNumber2,Manufacturer,ManufacturerPartNumber,SwarmPartNumberManufacturerId,UserId,DateCreatedUtc
+1,6,10,0.0875,"LM358","2156-LM358SNG-ON-ND","","General Purpose Amplifier 2 Circuit Differential 8-PDIP",14,1,"8-DIP","https://www.digikey.ca/en/products/detail/onsemi/LM358SNG/5404322","https://d2e86la87jxppk.cloudfront.net/50/b8/09/c8/e6/dd/40/21/ac89b5a73720_1.png","DigiKey","https://www.digikey.ca/en/products/detail/onsemi/LM358DG/1476852",0,"amplifier,lm358sng,general,purpose,2,circuit,0","https://d2e86la87jxppk.cloudfront.net/f3/da/be/59/e1/d0/42/87/0aaadaff6084.pdf","HOME","1","2","Onsemi","LM358SNG",0,1,2022-04-14 03:12:22
diff --git a/Binner/Tests/Binner.Common.Tests/IO/Projects.csv b/Binner/Tests/Binner.Common.Tests/IO/Projects.csv
new file mode 100644
index 00000000..2a583f87
--- /dev/null
+++ b/Binner/Tests/Binner.Common.Tests/IO/Projects.csv
@@ -0,0 +1,2 @@
+#ProjectId,Name,Description,Location,Color,DateCreatedUtc,DateModifiedUtc,UserId
+3,"Test Project 1","This is a test project","Vancouver",1,2022-06-02 00:59:50,2022-06-02 00:59:57,1
diff --git a/Binner/Tests/Binner.Common.Tests/IO/SqlDataImporterTests.cs b/Binner/Tests/Binner.Common.Tests/IO/SqlDataImporterTests.cs
new file mode 100644
index 00000000..a3df1ff5
--- /dev/null
+++ b/Binner/Tests/Binner.Common.Tests/IO/SqlDataImporterTests.cs
@@ -0,0 +1,251 @@
+using Binner.Common.IO;
+using Binner.Global.Common;
+using Binner.Testing;
+using NUnit.Framework;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Binner.Common.Tests.IO
+{
+ [TestFixture]
+ public class SqlDataImporterTests
+ {
+ [Test]
+ public async Task ShouldImportSqlAsync()
+ {
+ using var storageProvider = new InMemoryStorageProvider(true);
+ var importer = new SqlDataImporter(storageProvider);
+ var userContext = new UserContext { UserId = 99 };
+
+ using var stream = new MemoryStream();
+ using var writer = new StreamWriter(stream);
+ writer.Write(@$"INSERT INTO Projects (ProjectId, Name, Description, Location, Color, DateCreatedUtc, DateModifiedUtc) VALUES (1, 'Test Project 1', 'test description', 'location', 1, '2022-01-01 00:00:00', '2022-01-01 00:00:00');");
+ writer.Flush();
+ stream.Position = 0;
+
+ var result = await importer.ImportAsync("testfile.sql", stream, userContext);
+
+ var db = await storageProvider.GetDatabaseAsync(userContext);
+ Assert.That(result.Success, Is.True);
+ Assert.That(result.TotalRowsImported, Is.EqualTo(1));
+ Assert.That(result.RowsImportedByTable["Projects"], Is.EqualTo(1));
+ Assert.That(db.Projects.Count, Is.EqualTo(1));
+ Assert.That(db.Projects.First().UserId, Is.EqualTo(99));
+ }
+
+ [Test]
+ public async Task ShouldImportQuotedDelimiterSqlAsync()
+ {
+ using var storageProvider = new InMemoryStorageProvider(true);
+ var importer = new SqlDataImporter(storageProvider);
+ var userContext = new UserContext { UserId = 99 };
+
+ using var stream = new MemoryStream();
+ using var writer = new StreamWriter(stream);
+ writer.Write(@$"INSERT INTO Projects (ProjectId, Name, Description, Location, Color, DateCreatedUtc, DateModifiedUtc) VALUES (1, 'Test Project 1', 'test, description', 'location', 1, '2022-01-01 00:00:00', '2022-01-01 00:00:00');");
+ writer.Flush();
+ stream.Position = 0;
+
+ var result = await importer.ImportAsync("testfile.sql", stream, userContext);
+
+ var db = await storageProvider.GetDatabaseAsync(userContext);
+ Assert.That(result.Success, Is.True);
+ Assert.That(result.TotalRowsImported, Is.EqualTo(1));
+ Assert.That(result.RowsImportedByTable["Projects"], Is.EqualTo(1));
+ Assert.That(db.Projects.Count, Is.EqualTo(1));
+ Assert.That(db.Projects.First().UserId, Is.EqualTo(99));
+ }
+
+ [Test]
+ public async Task ShouldIgnoreUserIdAsync()
+ {
+ using var storageProvider = new InMemoryStorageProvider(true);
+ var importer = new SqlDataImporter(storageProvider);
+ var userContext = new UserContext { UserId = 99 };
+
+ using var stream = new MemoryStream();
+ using var writer = new StreamWriter(stream);
+ writer.Write(@$"INSERT INTO Projects (ProjectId, UserId, Name, Description, Location, Color, DateCreatedUtc, DateModifiedUtc) VALUES (1, 1, 'Test Project 1', 'test description', 'location', 1, '2022-01-01 00:00:00', '2022-01-01 00:00:00');");
+ writer.Flush();
+ stream.Position = 0;
+
+ var result = await importer.ImportAsync("testfile.sql", stream, userContext);
+
+ var db = await storageProvider.GetDatabaseAsync(userContext);
+ Assert.That(result.Success, Is.True);
+ Assert.That(result.TotalRowsImported, Is.EqualTo(1));
+ Assert.That(result.RowsImportedByTable["Projects"], Is.EqualTo(1));
+ Assert.That(db.Projects.Count, Is.EqualTo(1));
+ Assert.That(db.Projects.First().UserId, Is.EqualTo(99));
+ }
+
+ [Test]
+ public async Task ShouldImportSqlQuotedAsync()
+ {
+ using var storageProvider = new InMemoryStorageProvider(true);
+ var importer = new SqlDataImporter(storageProvider);
+ var userContext = new UserContext { UserId = 99 };
+
+ using var stream = new MemoryStream();
+ using var writer = new StreamWriter(stream);
+ writer.Write(@$"INSERT INTO ""Projects"" (""ProjectId"", Name, Description, Location, Color, DateCreatedUtc, DateModifiedUtc) VALUES (1, 'Test Project 1', 'test description', 'location', 1, '2022-01-01 00:00:00', '2022-01-01 00:00:00');");
+ writer.Flush();
+ stream.Position = 0;
+
+ var result = await importer.ImportAsync("testfile.sql", stream, userContext);
+
+ var db = await storageProvider.GetDatabaseAsync(userContext);
+ Assert.That(result.Success, Is.True);
+ Assert.That(result.TotalRowsImported, Is.EqualTo(1));
+ Assert.That(result.RowsImportedByTable["Projects"], Is.EqualTo(1));
+ Assert.That(db.Projects.Count, Is.EqualTo(1));
+ Assert.That(db.Projects.First().UserId, Is.EqualTo(99));
+ }
+
+ [Test]
+ public async Task ShouldImportMultipleRowsAsync()
+ {
+ using var storageProvider = new InMemoryStorageProvider(true);
+ var importer = new SqlDataImporter(storageProvider);
+ var userContext = new UserContext { UserId = 99 };
+
+ using var stream = new MemoryStream();
+ using var writer = new StreamWriter(stream);
+ // try insert's with no line-break
+ writer.Write(@$"INSERT INTO PartTypes (ParentPartTypeId, Name, DateCreatedUtc) VALUES (1, 'Custom Type 1', '2022-01-01 00:00:00');");
+ writer.Write(@$"INSERT INTO PartTypes (ParentPartTypeId, Name, DateCreatedUtc) VALUES (null, 'Custom Type 2', '2022-01-01 00:00:00');");
+ // insert with line-breaks
+ writer.WriteLine(@$"INSERT INTO Projects (Name, Description, Location, Color, DateCreatedUtc, DateModifiedUtc) VALUES ('Test Project 1', 'test description', 'location', 1, '2022-01-01 00:00:00', '2022-01-01 00:00:00');");
+ writer.WriteLine(@$"INSERT INTO Projects (Name, Description, Location, Color, DateCreatedUtc, DateModifiedUtc) VALUES ('Test Project 2', 'test description', 'location', 1, '2022-01-01 00:00:00', '2022-01-01 00:00:00');");
+ writer.WriteLine(@$"INSERT INTO Projects (Name, Description, Location, Color, DateCreatedUtc, DateModifiedUtc) VALUES ('Test Project 3', 'test description', 'location', 1, '2022-01-01 00:00:00', '2022-01-01 00:00:00');");
+ writer.WriteLine(@$"INSERT INTO Projects (Name, Description, Location, Color, DateCreatedUtc, DateModifiedUtc) VALUES ('Test Project 4', 'test description', 'location', 1, '2022-01-01 00:00:00', '2022-01-01 00:00:00');");
+ // try insert with quoted line-break content
+ writer.WriteLine(@$"INSERT INTO Projects (Name, Description, Location, Color, DateCreatedUtc, DateModifiedUtc) VALUES ('Test Project 5', 'test description
+more description
+more text', 'location', 1, '2022-01-01 00:00:00', '2022-01-01 00:00:00');");
+ writer.Flush();
+ stream.Position = 0;
+
+ var result = await importer.ImportAsync("testfile.sql", stream, userContext);
+
+ var db = await storageProvider.GetDatabaseAsync(userContext);
+ Assert.That(result.Success, Is.True);
+ Assert.That(result.TotalRowsImported, Is.EqualTo(7));
+ Assert.That(result.RowsImportedByTable["Projects"], Is.EqualTo(5));
+ Assert.That(result.RowsImportedByTable["PartTypes"], Is.EqualTo(2));
+ Assert.That(db.Projects.Count, Is.EqualTo(5));
+ Assert.That(db.PartTypes.Count, Is.EqualTo(6));
+ }
+
+ [Test]
+ public async Task ShouldImportSqlWithSchemaAsync()
+ {
+ using var storageProvider = new InMemoryStorageProvider(true);
+ var importer = new SqlDataImporter(storageProvider);
+ var userContext = new UserContext { UserId = 99 };
+
+ using var stream = new MemoryStream();
+ using var writer = new StreamWriter(stream);
+ writer.Write(@$"INSERT INTO dbo.Projects (ProjectId, Name, Description, Location, Color, DateCreatedUtc, DateModifiedUtc) VALUES (1, 'Test Project 1', 'test description', 'location', 1, '2022-01-01 00:00:00', '2022-01-01 00:00:00');");
+ writer.Flush();
+ stream.Position = 0;
+
+ var result = await importer.ImportAsync("testfile.sql", stream, userContext);
+
+ var db = await storageProvider.GetDatabaseAsync(userContext);
+ Assert.That(result.Success, Is.True);
+ Assert.That(result.TotalRowsImported, Is.EqualTo(1));
+ Assert.That(result.RowsImportedByTable["Projects"], Is.EqualTo(1));
+ Assert.That(db.Projects.Count, Is.EqualTo(1));
+ }
+
+ [Test]
+ public async Task ShouldNotImportSqlWithUnsupportedTableAsync()
+ {
+ using var storageProvider = new InMemoryStorageProvider(true);
+ var importer = new SqlDataImporter(storageProvider);
+ var userContext = new UserContext { UserId = 99 };
+
+ using var stream = new MemoryStream();
+ using var writer = new StreamWriter(stream);
+ writer.Write(@$"INSERT INTO SomeOtherTable (ProjectId, Name, Description, Location, Color, DateCreatedUtc, DateModifiedUtc) VALUES (1, 'Test Project 1', 'test description', 'location', 1, '2022-01-01 00:00:00', '2022-01-01 00:00:00');");
+ writer.Flush();
+ stream.Position = 0;
+
+ var result = await importer.ImportAsync("testfile.sql", stream, userContext);
+
+ var db = await storageProvider.GetDatabaseAsync(userContext);
+ Assert.That(result.Success, Is.False);
+ Assert.That(result.TotalRowsImported, Is.EqualTo(0));
+ }
+
+ [Test]
+ public async Task ShouldNotImportSqlWithUnsupportedSchemaAsync()
+ {
+ using var storageProvider = new InMemoryStorageProvider(true);
+ var importer = new SqlDataImporter(storageProvider);
+ var userContext = new UserContext { UserId = 99 };
+
+ using var stream = new MemoryStream();
+ using var writer = new StreamWriter(stream);
+ writer.Write(@$"INSERT INTO test.Projects (ProjectId, Name, Description, Location, Color, DateCreatedUtc, DateModifiedUtc) VALUES (1, 'Test Project 1', 'test description', 'location', 1, '2022-01-01 00:00:00', '2022-01-01 00:00:00');");
+ writer.Flush();
+ stream.Position = 0;
+
+ var result = await importer.ImportAsync("testfile.sql", stream, userContext);
+
+ var db = await storageProvider.GetDatabaseAsync(userContext);
+ Assert.That(result.Success, Is.False);
+ Assert.That(result.TotalRowsImported, Is.EqualTo(0));
+ Assert.That(result.Errors.Count, Is.EqualTo(1));
+ }
+
+ [Test]
+ public async Task ShouldNotImportSqlWithInvalidContentAsync()
+ {
+ using var storageProvider = new InMemoryStorageProvider(true);
+ var importer = new SqlDataImporter(storageProvider);
+ var userContext = new UserContext { UserId = 99 };
+
+ using var stream = new MemoryStream();
+ using var writer = new StreamWriter(stream);
+ writer.WriteLine(@$"#Name, Description, Location, Color, DateCreatedUtc, DateModifiedUtc");
+ writer.WriteLine($@"'Test Project 1', 'test description', 'location', 1, '2022-01-01 00:00:00', '2022-01-01 00:00:00'");
+ writer.Flush();
+ stream.Position = 0;
+
+ var result = await importer.ImportAsync("SomeTable.sql", stream, userContext);
+
+ var db = await storageProvider.GetDatabaseAsync(userContext);
+ Assert.That(result.Success, Is.False);
+ Assert.That(result.TotalRowsImported, Is.EqualTo(0));
+ Assert.That(result.Errors.Count, Is.EqualTo(1));
+ }
+
+ [Test]
+ public async Task ShouldImportSqlWithUnescapedLineBreaks()
+ {
+ using var storageProvider = new InMemoryStorageProvider(true);
+ var importer = new SqlDataImporter(storageProvider);
+ var userContext = new UserContext { UserId = 99 };
+
+ using var stream = new MemoryStream();
+ using var writer = new StreamWriter(stream);
+ writer.Write(@$"INSERT INTO Projects (ProjectId, Name, Description, Location, Color, DateCreatedUtc, DateModifiedUtc) VALUES (1, 'Test Project 1', 'test description
+
+more text
+something else', 'location', 1, '2022-01-01 00:00:00', '2022-01-01 00:00:00');");
+ writer.Flush();
+ stream.Position = 0;
+
+ var result = await importer.ImportAsync("testfile.sql", stream, userContext);
+
+ var db = await storageProvider.GetDatabaseAsync(userContext);
+ Assert.That(result.Success, Is.True);
+ Assert.That(result.TotalRowsImported, Is.EqualTo(1));
+ Assert.That(result.RowsImportedByTable["Projects"], Is.EqualTo(1));
+ Assert.That(db.Projects.Count, Is.EqualTo(1));
+ }
+ }
+}
diff --git a/Binner/Tests/Binner.Web.Tests/Binner.Web.Tests.csproj b/Binner/Tests/Binner.Web.Tests/Binner.Web.Tests.csproj
index efec562b..4cddef71 100644
--- a/Binner/Tests/Binner.Web.Tests/Binner.Web.Tests.csproj
+++ b/Binner/Tests/Binner.Web.Tests/Binner.Web.Tests.csproj
@@ -10,16 +10,16 @@
+
-
-
-
- all
- runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
+
+
+
+
diff --git a/Binner/Tests/Binner.Web.Tests/Middleware/VersionHeaderMiddlewareTests.cs b/Binner/Tests/Binner.Web.Tests/Middleware/VersionHeaderMiddlewareTests.cs
new file mode 100644
index 00000000..78dd35d7
--- /dev/null
+++ b/Binner/Tests/Binner.Web.Tests/Middleware/VersionHeaderMiddlewareTests.cs
@@ -0,0 +1,38 @@
+using Binner.Web.Middleware;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.Hosting;
+using NUnit.Framework;
+using System.Threading.Tasks;
+
+namespace Binner.Web.Tests.Middleware
+{
+ [TestFixture]
+ public class VersionHeaderMiddlewareTests
+ {
+ [Test]
+ public async Task VersionMiddleware_ShouldMatch()
+ {
+ using var host = await new HostBuilder()
+ .ConfigureWebHost(webBuilder =>
+ {
+ webBuilder.Configure(app =>
+ {
+ app.UseMiddleware();
+ });
+ webBuilder.UseTestServer();
+ }).StartAsync();
+
+ var server = host.GetTestServer();
+ var context = await server.SendAsync(context =>
+ {
+ context.Request.Method = HttpMethods.Get;
+ context.Request.Path = "/fake";
+ });
+
+ Assert.That(context.Response.Headers.ContainsKey("X-Version"), Is.True);
+ }
+ }
+}
diff --git a/Binner/Tests/Binner.Web.Tests/UnitTest1.cs b/Binner/Tests/Binner.Web.Tests/UnitTest1.cs
deleted file mode 100644
index 453c4e13..00000000
--- a/Binner/Tests/Binner.Web.Tests/UnitTest1.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-using NUnit.Framework;
-
-namespace Binner.Web.Tests
-{
- public class Tests
- {
- [SetUp]
- public void Setup()
- {
- }
-
- [Test]
- public void Test1()
- {
- Assert.Pass();
- }
- }
-}
\ No newline at end of file