Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save salaros/fe7b9fb7e383de1428e9c744f90f23dd to your computer and use it in GitHub Desktop.
Save salaros/fe7b9fb7e383de1428e9c744f90f23dd to your computer and use it in GitHub Desktop.
Patch for cross-solar-dotnet project
From 5e27d3e4b8896e7ad49afe19cd5426dc0c9fe106 Mon Sep 17 00:00:00 2001
From: salaros <[email protected]>
Date: Sun, 1 Jul 2018 18:50:14 +0300
Subject: [PATCH] My submission
XOV:v2
---
.../Controller/AnalyticsControllerTests.cs | 150 +++++++++++++++++++++
.../Controller/PanelControllerTests.cs | 38 +++++-
CrossSolar.Tests/CrossSolar.Tests.csproj | 3 +-
CrossSolar.Tests/Extensions.cs | 26 ++++
CrossSolar/Controllers/AnalyticsController.cs | 59 +++++---
CrossSolar/Controllers/PanelController.cs | 2 +-
CrossSolar/Domain/CrossSolarDbContext.cs | 5 +-
CrossSolar/Domain/OneHourElectricity.cs | 8 +-
CrossSolar/Domain/Panel.cs | 3 +-
CrossSolar/Models/OneDayElectricityModel.cs | 2 +-
CrossSolar/Models/OneHourElectricityModel.cs | 6 +-
CrossSolar/Models/PanelModel.cs | 14 +-
CrossSolar/Properties/launchSettings.json | 4 +-
CrossSolar/Repository/DayAnalyticsRepository.cs | 1 +
CrossSolar/Repository/GenericRepository.cs | 3 +-
CrossSolar/Repository/IDayAnalyticsRepository.cs | 2 +-
16 files changed, 282 insertions(+), 44 deletions(-)
create mode 100644 CrossSolar.Tests/Controller/AnalyticsControllerTests.cs
create mode 100644 CrossSolar.Tests/Extensions.cs
diff --git a/CrossSolar.Tests/Controller/AnalyticsControllerTests.cs b/CrossSolar.Tests/Controller/AnalyticsControllerTests.cs
new file mode 100644
index 0000000..1ad3e02
--- /dev/null
+++ b/CrossSolar.Tests/Controller/AnalyticsControllerTests.cs
@@ -0,0 +1,150 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using CrossSolar.Controllers;
+using CrossSolar.Domain;
+using CrossSolar.Models;
+using CrossSolar.Repository;
+using Microsoft.AspNetCore.Mvc;
+using MockQueryable.Moq;
+using Moq;
+using Xunit;
+
+namespace CrossSolar.Tests.Controller
+{
+ public class AnalyticsControllerTests
+ {
+ private readonly AnalyticsController _analyticsController;
+ private const string PanelTestSerial = "AAAA1111BBBB2222";
+
+ public AnalyticsControllerTests()
+ {
+ var analyticsData = new List<OneHourElectricity>()
+ {
+ new OneHourElectricity
+ {
+ PanelId = PanelTestSerial,
+ DateTime = DateTime.UtcNow,
+ KiloWatt = 1
+ },
+ new OneHourElectricity
+ {
+ PanelId = PanelTestSerial,
+ DateTime = DateTime.UtcNow,
+ KiloWatt = 105
+ },
+ new OneHourElectricity
+ {
+ PanelId = PanelTestSerial,
+ DateTime = DateTime.UtcNow,
+ KiloWatt = 12
+ }
+ }.AsQueryable().BuildMock();
+
+ var analyticsRepositoryMock = new Mock<IAnalyticsRepository>();
+ analyticsRepositoryMock.Setup(x => x.Query()).Returns(analyticsData.Object);
+
+ var panelsData = new List<Panel>()
+ {
+ new Panel
+ {
+ Brand = "Areva",
+ Latitude = 45.312678,
+ Longitude = 76.5543982,
+ Serial = PanelTestSerial
+ }
+ }.AsQueryable().BuildMock();
+ var panelRepositoryMock = new Mock<IPanelRepository>();
+ panelRepositoryMock.Setup(x => x.Query()).Returns(panelsData.Object);
+
+ _analyticsController =
+ new AnalyticsController(analyticsRepositoryMock.Object, panelRepositoryMock.Object);
+ }
+
+ [Fact]
+ public async Task Get_ReturnsAnalytics()
+ {
+ // Arrange
+ // Act
+ var actionResult = await _analyticsController.Get(PanelTestSerial);
+
+ // Assert
+ var okObjectResult = actionResult as OkObjectResult;
+ Assert.NotNull(okObjectResult);
+
+ var model = okObjectResult.Value as OneHourElectricityListModel;
+ Assert.NotNull(model);
+ Assert.NotNull(model.OneHourElectricitys);
+ Assert.Equal(3, model.OneHourElectricitys.Count());
+ }
+
+ [Fact]
+ public async Task DayResults_ReturnsCorrectStats()
+ {
+ // Arrange
+ // Act
+ var actionResult = await _analyticsController.DayResults(PanelTestSerial);
+
+ // Assert
+ var okObjectResult = actionResult as OkObjectResult;
+ Assert.NotNull(okObjectResult);
+
+ var model = (okObjectResult.Value as IEnumerable<OneDayElectricityModel>)?.FirstOrDefault();
+ Assert.NotNull(model);
+
+ Assert.Equal(DateTime.UtcNow.Date, model.DateTime);
+ Assert.Equal(1, model.Minimum);
+ Assert.Equal(105, model.Maximum);
+ Assert.Equal(39.333333333333336, model.Average);
+ Assert.Equal(118, model.Sum);
+ }
+
+ [Fact]
+ public async Task Post_ShouldInsertAnalytics()
+ {
+ // Arrange
+ var analyticsItem = new OneHourElectricityModel
+ {
+ KiloWatt = 123
+ };
+
+ // Act
+ var result = await _analyticsController.Post(PanelTestSerial, analyticsItem);
+
+ // Assert
+ Assert.NotNull(result);
+
+ var createdResult = result as CreatedResult;
+ Assert.NotNull(createdResult);
+ Assert.Equal(201, createdResult.StatusCode);
+ }
+
+ [Fact]
+ public async Task Post_WontInsertAnalytics()
+ {
+ // Arrange
+ var analyticsItem = new OneHourElectricityModel
+ {
+ KiloWatt = -123
+ };
+ _analyticsController.SetModelStateFromModel(analyticsItem);
+
+ // Act
+ var result = await _analyticsController.Post(PanelTestSerial, analyticsItem);
+ var modelState = _analyticsController.ModelState;
+
+ // Assert
+ Assert.NotNull(result);
+
+ var badCreatedResult = result as BadRequestObjectResult;
+ Assert.NotNull(badCreatedResult);
+ Assert.Equal(400, badCreatedResult.StatusCode);
+ Assert.False(modelState.IsValid);
+ Assert.False(modelState.Keys.Except(new[]
+ {
+ nameof(OneHourElectricityModel.KiloWatt), // KiloWatt is wrong (it's out of range 0 -> 0x7FFFFFFFFFFFFFFF)
+ }).Any());
+ }
+ }
+}
diff --git a/CrossSolar.Tests/Controller/PanelControllerTests.cs b/CrossSolar.Tests/Controller/PanelControllerTests.cs
index faa277e..ab56822 100644
--- a/CrossSolar.Tests/Controller/PanelControllerTests.cs
+++ b/CrossSolar.Tests/Controller/PanelControllerTests.cs
@@ -1,3 +1,6 @@
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
using System.Threading.Tasks;
using CrossSolar.Controllers;
using CrossSolar.Models;
@@ -22,6 +25,7 @@ namespace CrossSolar.Tests.Controller
[Fact]
public async Task Register_ShouldInsertPanel()
{
+ // Arrange
var panel = new PanelModel
{
Brand = "Areva",
@@ -30,8 +34,6 @@ namespace CrossSolar.Tests.Controller
Serial = "AAAA1111BBBB2222"
};
- // Arrange
-
// Act
var result = await _panelController.Register(panel);
@@ -42,5 +44,37 @@ namespace CrossSolar.Tests.Controller
Assert.NotNull(createdResult);
Assert.Equal(201, createdResult.StatusCode);
}
+
+ [Fact]
+ public async Task Register_WontInsertAnalytics()
+ {
+ // Arrange
+ var panel = new PanelModel
+ {
+ Brand = "Areva",
+ Latitude = 90,
+ Longitude = -198.7655432,
+ Serial = "123456789"
+ };
+ _panelController.SetModelStateFromModel(panel);
+
+ // Act
+ var result = await _panelController.Register(panel);
+ var modelState = _panelController.ModelState;
+
+ // Assert
+ Assert.NotNull(result);
+
+ var badCreatedResult = result as BadRequestObjectResult;
+ Assert.NotNull(badCreatedResult);
+ Assert.Equal(400, badCreatedResult.StatusCode);
+ Assert.False(modelState.IsValid);
+ Assert.False(modelState.Keys.Except(new[]
+ {
+ nameof(PanelModel.Latitude), // Latitude is wrong (no digits after decimal separator)
+ nameof(PanelModel.Longitude), // Longitude is out of range
+ nameof(PanelModel.Serial) // Serial is too short
+ }).Any());
+ }
}
}
\ No newline at end of file
diff --git a/CrossSolar.Tests/CrossSolar.Tests.csproj b/CrossSolar.Tests/CrossSolar.Tests.csproj
index 3801997..6ff24c3 100644
--- a/CrossSolar.Tests/CrossSolar.Tests.csproj
+++ b/CrossSolar.Tests/CrossSolar.Tests.csproj
@@ -1,4 +1,4 @@
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
@@ -9,6 +9,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.6.0" />
<DotNetCliToolReference Include="MiniCover" Version="2.0.0-ci-20180304114938" />
+ <PackageReference Include="MockQueryable.Moq" Version="1.0.2" />
<PackageReference Include="moq" Version="4.8.2" />
<PackageReference Include="xunit" Version="2.3.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" />
diff --git a/CrossSolar.Tests/Extensions.cs b/CrossSolar.Tests/Extensions.cs
new file mode 100644
index 0000000..ff2102b
--- /dev/null
+++ b/CrossSolar.Tests/Extensions.cs
@@ -0,0 +1,26 @@
+
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+
+namespace CrossSolar.Tests
+{
+ public static class ControllerExtensions
+ {
+ public static void SetModelStateFromModel(this Microsoft.AspNetCore.Mvc.Controller controller, object model)
+ {
+ var context = new ValidationContext(model, null, null);
+ var validationResults = new List<ValidationResult>();
+ var isValid = Validator.TryValidateObject(model, context, validationResults, true);
+ if (isValid)
+ return;
+
+ foreach (var validationResult in validationResults)
+ {
+ foreach (var memberName in validationResult.MemberNames)
+ {
+ controller.ModelState.AddModelError(memberName, validationResult.ErrorMessage);
+ }
+ }
+ }
+ }
+}
diff --git a/CrossSolar/Controllers/AnalyticsController.cs b/CrossSolar/Controllers/AnalyticsController.cs
index d2217d7..f71849d 100644
--- a/CrossSolar/Controllers/AnalyticsController.cs
+++ b/CrossSolar/Controllers/AnalyticsController.cs
@@ -1,5 +1,4 @@
using System;
-using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using CrossSolar.Domain;
@@ -24,25 +23,25 @@ namespace CrossSolar.Controllers
}
// GET panel/XXXX1111YYYY2222/analytics
- [HttpGet("{banelId}/[controller]")]
+ [HttpGet("{panelId}/[controller]")]
public async Task<IActionResult> Get([FromRoute] string panelId)
{
var panel = await _panelRepository.Query()
.FirstOrDefaultAsync(x => x.Serial.Equals(panelId, StringComparison.CurrentCultureIgnoreCase));
- if (panel == null) return NotFound();
-
- var analytics = await _analyticsRepository.Query()
- .Where(x => x.PanelId.Equals(panelId, StringComparison.CurrentCultureIgnoreCase)).ToListAsync();
+ if (panel == null) return NotFound(); // One shall not see data of non-longer existent panels (there is no rule for cascade delete)
var result = new OneHourElectricityListModel
{
- OneHourElectricitys = analytics.Select(c => new OneHourElectricityModel
- {
- Id = c.Id,
- KiloWatt = c.KiloWatt,
- DateTime = c.DateTime
- })
+ OneHourElectricitys = _analyticsRepository
+ .Query()
+ .Where(a => a.PanelId.Equals(panelId, StringComparison.CurrentCultureIgnoreCase))
+ .Select(c => new OneHourElectricityModel
+ {
+ Id = c.Id,
+ KiloWatt = c.KiloWatt,
+ DateTime = c.DateTime
+ })
};
return Ok(result);
@@ -52,7 +51,24 @@ namespace CrossSolar.Controllers
[HttpGet("{panelId}/[controller]/day")]
public async Task<IActionResult> DayResults([FromRoute] string panelId)
{
- var result = new List<OneDayElectricityModel>();
+ var panel = await _panelRepository.Query()
+ .FirstOrDefaultAsync(x => x.Serial.Equals(panelId, StringComparison.CurrentCultureIgnoreCase));
+
+ if (panel == null) return NotFound(); // One shall not see data of non-longer existent panels (there is no rule for cascade delete)
+
+ var result = _analyticsRepository
+ .Query()
+ .Where(a => a.PanelId.Equals(panelId, StringComparison.CurrentCultureIgnoreCase))
+ .GroupBy(a => a.DateTime.Date)
+ .AsParallel()
+ .Select(g => new OneDayElectricityModel
+ {
+ DateTime = g.Key,
+ Sum = g.Sum(a => a.KiloWatt),
+ Minimum = g.Min(a => a.KiloWatt),
+ Maximum = g.Max(a => a.KiloWatt),
+ Average = g.Average(a => a.KiloWatt)
+ });
return Ok(result);
}
@@ -63,21 +79,20 @@ namespace CrossSolar.Controllers
{
if (!ModelState.IsValid) return BadRequest(ModelState);
- var oneHourElectricityContent = new OneHourElectricity
+ var panel = await _panelRepository
+ .Query()
+ .FirstOrDefaultAsync(x => x.Serial.Equals(panelId, StringComparison.CurrentCultureIgnoreCase));
+
+ if (panel == null) return NotFound();
+
+ var result = new OneHourElectricity
{
PanelId = panelId,
KiloWatt = value.KiloWatt,
DateTime = DateTime.UtcNow
};
- await _analyticsRepository.InsertAsync(oneHourElectricityContent);
-
- var result = new OneHourElectricityModel
- {
- Id = oneHourElectricityContent.Id,
- KiloWatt = oneHourElectricityContent.KiloWatt,
- DateTime = oneHourElectricityContent.DateTime
- };
+ await _analyticsRepository.InsertAsync(result);
return Created($"panel/{panelId}/analytics/{result.Id}", result);
}
diff --git a/CrossSolar/Controllers/PanelController.cs b/CrossSolar/Controllers/PanelController.cs
index 7b72543..01e81a3 100644
--- a/CrossSolar/Controllers/PanelController.cs
+++ b/CrossSolar/Controllers/PanelController.cs
@@ -16,7 +16,7 @@ namespace CrossSolar.Controllers
_panelRepository = panelRepository;
}
- // POST api/panel
+ // POST panel
[HttpPost]
public async Task<IActionResult> Register([FromBody] PanelModel value)
{
diff --git a/CrossSolar/Domain/CrossSolarDbContext.cs b/CrossSolar/Domain/CrossSolarDbContext.cs
index 98fbb4b..7dcc240 100644
--- a/CrossSolar/Domain/CrossSolarDbContext.cs
+++ b/CrossSolar/Domain/CrossSolarDbContext.cs
@@ -12,14 +12,13 @@ namespace CrossSolar.Domain
{
}
- public DbSet<Panel> Panels { get; set; }
+ public virtual DbSet<Panel> Panels { get; set; }
- public DbSet<OneHourElectricity> OneHourElectricitys { get; set; }
+ public virtual DbSet<OneHourElectricity> OneHourElectricitys { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
}
-
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
}
diff --git a/CrossSolar/Domain/OneHourElectricity.cs b/CrossSolar/Domain/OneHourElectricity.cs
index 8bcdf82..0d70656 100644
--- a/CrossSolar/Domain/OneHourElectricity.cs
+++ b/CrossSolar/Domain/OneHourElectricity.cs
@@ -1,15 +1,17 @@
using System;
+using System.ComponentModel.DataAnnotations;
namespace CrossSolar.Domain
{
public class OneHourElectricity
{
+ [Key]
public int Id { get; set; }
- public string PanelId { get; set; }
+ [Required] public string PanelId { get; set; }
- public long KiloWatt { get; set; }
+ [Required] public long KiloWatt { get; set; }
- public DateTime DateTime { get; set; }
+ [Required] public DateTime DateTime { get; set; }
}
}
\ No newline at end of file
diff --git a/CrossSolar/Domain/Panel.cs b/CrossSolar/Domain/Panel.cs
index 1deec97..48c2659 100644
--- a/CrossSolar/Domain/Panel.cs
+++ b/CrossSolar/Domain/Panel.cs
@@ -4,11 +4,12 @@ namespace CrossSolar.Domain
{
public class Panel
{
+ [Key]
public int Id { get; set; }
[Required] public double Latitude { get; set; }
- public double Longitude { get; set; }
+ [Required] public double Longitude { get; set; }
[Required] public string Serial { get; set; }
diff --git a/CrossSolar/Models/OneDayElectricityModel.cs b/CrossSolar/Models/OneDayElectricityModel.cs
index e0d422b..7621e9a 100644
--- a/CrossSolar/Models/OneDayElectricityModel.cs
+++ b/CrossSolar/Models/OneDayElectricityModel.cs
@@ -1,6 +1,6 @@
using System;
-namespace CrossSolar.Domain
+namespace CrossSolar.Models
{
public class OneDayElectricityModel
{
diff --git a/CrossSolar/Models/OneHourElectricityModel.cs b/CrossSolar/Models/OneHourElectricityModel.cs
index a278d9d..4e08317 100644
--- a/CrossSolar/Models/OneHourElectricityModel.cs
+++ b/CrossSolar/Models/OneHourElectricityModel.cs
@@ -1,13 +1,17 @@
using System;
+using System.ComponentModel.DataAnnotations;
namespace CrossSolar.Models
{
public class OneHourElectricityModel
{
- public int Id { get; set; }
+ public int Id { get; set; } // Should be long
+ [Range(0, long.MaxValue)]
public long KiloWatt { get; set; }
+ [Required]
+ [DataType(DataType.DateTime)]
public DateTime DateTime { get; set; }
}
}
\ No newline at end of file
diff --git a/CrossSolar/Models/PanelModel.cs b/CrossSolar/Models/PanelModel.cs
index 5e31cb5..cad2191 100644
--- a/CrossSolar/Models/PanelModel.cs
+++ b/CrossSolar/Models/PanelModel.cs
@@ -4,16 +4,22 @@ namespace CrossSolar.Models
{
public class PanelModel
{
- public int Id { get; set; }
+ public int Id { get; set; } // Should be long
[Required]
[Range(-90, 90)]
- [RegularExpression(@"^\d+(\.\d{6})$")]
+ [RegularExpression(@"^-?\d{1,2}[\.,]\d{6}$")]
public double Latitude { get; set; }
- [Range(-180, 180)] public double Longitude { get; set; }
+ [Required]
+ [Range(-180, 180)]
+ [RegularExpression(@"^-?\d{1,3}[\.,]\d{6}$")]
+ public double Longitude { get; set; }
- [Required] public string Serial { get; set; }
+ [Required]
+ [MinLength(16)]
+ [MaxLength(16)]
+ public string Serial { get; set; }
public string Brand { get; set; }
}
diff --git a/CrossSolar/Properties/launchSettings.json b/CrossSolar/Properties/launchSettings.json
index 671e816..9e710d3 100644
--- a/CrossSolar/Properties/launchSettings.json
+++ b/CrossSolar/Properties/launchSettings.json
@@ -10,7 +10,7 @@
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
- "launchBrowser": true,
+ "launchBrowser": false,
"launchUrl": "api/values",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
@@ -18,7 +18,7 @@
},
"CrossSolar": {
"commandName": "Project",
- "launchBrowser": true,
+ "launchBrowser": false,
"launchUrl": "api/values",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
diff --git a/CrossSolar/Repository/DayAnalyticsRepository.cs b/CrossSolar/Repository/DayAnalyticsRepository.cs
index 65cac6f..5e957d2 100644
--- a/CrossSolar/Repository/DayAnalyticsRepository.cs
+++ b/CrossSolar/Repository/DayAnalyticsRepository.cs
@@ -1,4 +1,5 @@
using CrossSolar.Domain;
+using CrossSolar.Models;
namespace CrossSolar.Repository
{
diff --git a/CrossSolar/Repository/GenericRepository.cs b/CrossSolar/Repository/GenericRepository.cs
index 0cba46f..f60e7df 100644
--- a/CrossSolar/Repository/GenericRepository.cs
+++ b/CrossSolar/Repository/GenericRepository.cs
@@ -14,8 +14,7 @@ namespace CrossSolar.Repository
{
return await _dbContext.FindAsync<T>(id);
}
-
- public IQueryable<T> Query()
+ public virtual IQueryable<T> Query()
{
return _dbContext.Set<T>().AsQueryable();
}
diff --git a/CrossSolar/Repository/IDayAnalyticsRepository.cs b/CrossSolar/Repository/IDayAnalyticsRepository.cs
index dcf7168..8ea2866 100644
--- a/CrossSolar/Repository/IDayAnalyticsRepository.cs
+++ b/CrossSolar/Repository/IDayAnalyticsRepository.cs
@@ -1,4 +1,4 @@
-using CrossSolar.Domain;
+using CrossSolar.Models;
namespace CrossSolar.Repository
{
--
2.7.4
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment