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