When upserting a simulation, keep the original/internal/readonly prop… (#261)

This commit is contained in:
Hugh Xiong 2018-09-21 14:56:13 -07:00 коммит произвёл Harleen Thind
Родитель 939333e700
Коммит 2abcd73c33
7 изменённых файлов: 96 добавлений и 77 удалений

Просмотреть файл

@ -250,7 +250,7 @@ namespace Services.Test
// different ETag than the one we're trying to use to upsert // different ETag than the one we're trying to use to upsert
var document = new Document(); var document = new Document();
document.Id = "foo"; document.Id = "foo";
document.SetPropertyValue("ETag", ETAG2); document.SetPropertyValue("_etag", ETAG2);
document.SetPropertyValue("Data", JsonConvert.SerializeObject(updatedSimulation)); document.SetPropertyValue("Data", JsonConvert.SerializeObject(updatedSimulation));
var mockStorageRecord = StorageRecord.FromDocumentDb(document); var mockStorageRecord = StorageRecord.FromDocumentDb(document);
this.mockStorageRecords.Setup(x => x.GetAsync(It.IsAny<string>())).ReturnsAsync(mockStorageRecord); this.mockStorageRecords.Setup(x => x.GetAsync(It.IsAny<string>())).ReturnsAsync(mockStorageRecord);
@ -288,7 +288,7 @@ namespace Services.Test
// same ETag value as the one we're trying to use to upsert with. // same ETag value as the one we're trying to use to upsert with.
var document = new Document(); var document = new Document();
document.Id = "foo"; document.Id = "foo";
document.SetPropertyValue("ETag", ETAG1); document.SetPropertyValue("_etag", ETAG1);
document.SetPropertyValue("Data", JsonConvert.SerializeObject(existingSimulation)); document.SetPropertyValue("Data", JsonConvert.SerializeObject(existingSimulation));
var mockStorageRecord = StorageRecord.FromDocumentDb(document); var mockStorageRecord = StorageRecord.FromDocumentDb(document);
@ -304,7 +304,7 @@ namespace Services.Test
// which will contain an updated ETag // which will contain an updated ETag
var upsertResultDocument = new Document(); var upsertResultDocument = new Document();
upsertResultDocument.Id = "bar"; upsertResultDocument.Id = "bar";
upsertResultDocument.SetPropertyValue("ETag", ETAG2); upsertResultDocument.SetPropertyValue("_etag", ETAG2);
upsertResultDocument.SetPropertyValue("Data", JsonConvert.SerializeObject(initialSimulation)); upsertResultDocument.SetPropertyValue("Data", JsonConvert.SerializeObject(initialSimulation));
var upsertResultStorageRecord = StorageRecord.FromDocumentDb(upsertResultDocument); var upsertResultStorageRecord = StorageRecord.FromDocumentDb(upsertResultDocument);
@ -319,47 +319,6 @@ namespace Services.Test
Assert.Matches(ETAG2, returnedSimulationTask.Result.ETag); Assert.Matches(ETAG2, returnedSimulationTask.Result.ETag);
} }
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
public void ItDoesNotAllowUsersToOverwritePartitioningStatus()
{
// Arrange
var sim = new SimulationModel
{
Id = "1",
Enabled = true,
PartitioningComplete = false
};
var record = new ValueApiModel
{
Key = "1",
Data = JsonConvert.SerializeObject(sim)
};
// Create a DocumentDB Document that will be used to create a StorageRecord object
var document = new Document();
document.Id = "foo";
document.SetPropertyValue("Data", JsonConvert.SerializeObject(sim));
var storageRecord = StorageRecord.FromDocumentDb(document);
this.mockStorageRecords.Setup(x => x.GetAsync(It.IsAny<string>()))
.ReturnsAsync(storageRecord);
this.mockStorageRecords.Setup(x => x.UpsertAsync(It.IsAny<StorageRecord>()))
.ReturnsAsync(storageRecord);
// Act
var update = new SimulationModel
{
Id = sim.Id,
Enabled = true,
PartitioningComplete = true,
ETag = "*"
};
SimulationModel result = this.target.UpsertAsync(update).CompleteOrTimeout().Result;
// Assert
Assert.False(result.PartitioningComplete);
}
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)] [Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
public void ItCreatesNewSimulationsWithPartitioningStateNotComplete() public void ItCreatesNewSimulationsWithPartitioningStateNotComplete()
{ {

Просмотреть файл

@ -91,17 +91,19 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Models
// When unspecified, a simulation is enabled // When unspecified, a simulation is enabled
this.Enabled = true; this.Enabled = true;
this.StartTime = DateTimeOffset.MinValue; // By default, a new simulation requires partitioning
this.EndTime = DateTimeOffset.MaxValue; this.PartitioningComplete = false;
// by default, use environment variable // by default, use environment variable
this.IotHubConnectionStrings = new List<string> { ServicesConfig.USE_DEFAULT_IOTHUB }; this.IotHubConnectionStrings = new List<string> { ServicesConfig.USE_DEFAULT_IOTHUB };
// By default, run forever
this.StartTime = DateTimeOffset.MinValue;
this.EndTime = DateTimeOffset.MaxValue;
this.DeviceModels = new List<DeviceModelRef>(); this.DeviceModels = new List<DeviceModelRef>();
this.PartitioningComplete = false;
this.CustomDevices = new List<CustomDeviceRef>(); this.CustomDevices = new List<CustomDeviceRef>();
this.Statistics = new StatisticsRef(); this.Statistics = new StatisticsRef();
this.PartitioningComplete = false;
} }
public class DeviceModelRef public class DeviceModelRef

Просмотреть файл

@ -226,9 +226,6 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services
} }
simulation.Created = existingSimulation.Created; simulation.Created = existingSimulation.Created;
// This value cannot be set by the user, making sure we persist the existing state
simulation.PartitioningComplete = existingSimulation.PartitioningComplete;
} }
else else
{ {

Просмотреть файл

@ -43,7 +43,7 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Storage
var result = new StorageRecord var result = new StorageRecord
{ {
Id = document.Id, Id = document.Id,
ETag = document.GetPropertyValue<string>("ETag"), ETag = document.ETag,
Data = document.GetPropertyValue<string>("Data") Data = document.GetPropertyValue<string>("Data")
}; };

Просмотреть файл

@ -58,12 +58,41 @@ namespace WebService.Test.v1.Models.SimulationApiModel
var simulationApiModel = this.GetSimulationApiModel(); var simulationApiModel = this.GetSimulationApiModel();
// Act // Act
var result = simulationApiModel.ToServiceModel(simulationApiModel.Id); var result = simulationApiModel.ToServiceModel(null, simulationApiModel.Id);
// Assert // Assert
Assert.IsType<Simulation>(result); Assert.IsType<Simulation>(result);
} }
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
public void ItKeepsInternalReadonlyProperties()
{
// Arrange
var simulationApiModel = this.GetSimulationApiModel();
var existingSimulation1 = new Simulation
{
PartitioningComplete = true,
StoppedTime = DateTimeOffset.UtcNow.AddHours(-10)
};
var existingSimulation2 = new Simulation
{
PartitioningComplete = false,
StoppedTime = DateTimeOffset.UtcNow.AddHours(-20)
};
// Act
var result1 = simulationApiModel.ToServiceModel(existingSimulation1, simulationApiModel.Id);
var result2 = simulationApiModel.ToServiceModel(existingSimulation2, simulationApiModel.Id);
// Assert
Assert.True(result1.PartitioningComplete);
Assert.False(result2.PartitioningComplete);
Assert.Equal(existingSimulation1.StoppedTime, result1.StoppedTime);
Assert.Equal(existingSimulation2.StoppedTime, result2.StoppedTime);
}
[Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)] [Fact, Trait(Constants.TYPE, Constants.UNIT_TEST)]
public void NoExceptionThrownForValidDeviceModelApiModel() public void NoExceptionThrownForValidDeviceModelApiModel()
{ {

Просмотреть файл

@ -5,7 +5,9 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services; using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services;
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Concurrency; using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Concurrency;
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Diagnostics; using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Diagnostics;
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Exceptions;
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.IotHub; using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.IotHub;
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Models;
using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Runtime; using Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Runtime;
using Microsoft.Azure.IoTSolutions.DeviceSimulation.SimulationAgent; using Microsoft.Azure.IoTSolutions.DeviceSimulation.SimulationAgent;
using Microsoft.Azure.IoTSolutions.DeviceSimulation.WebService.v1.Exceptions; using Microsoft.Azure.IoTSolutions.DeviceSimulation.WebService.v1.Exceptions;
@ -13,6 +15,7 @@ using Microsoft.Azure.IoTSolutions.DeviceSimulation.WebService.v1.Filters;
using Microsoft.Azure.IoTSolutions.DeviceSimulation.WebService.v1.Models; using Microsoft.Azure.IoTSolutions.DeviceSimulation.WebService.v1.Models;
using Microsoft.Azure.IoTSolutions.DeviceSimulation.WebService.v1.Models.Devices; using Microsoft.Azure.IoTSolutions.DeviceSimulation.WebService.v1.Models.Devices;
using Microsoft.Azure.IoTSolutions.DeviceSimulation.WebService.v1.Models.SimulationApiModel; using Microsoft.Azure.IoTSolutions.DeviceSimulation.WebService.v1.Models.SimulationApiModel;
using SimulationStatistics = Microsoft.Azure.IoTSolutions.DeviceSimulation.Services.Models.SimulationStatistics;
namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.WebService.v1.Controllers namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.WebService.v1.Controllers
{ {
@ -87,7 +90,7 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.WebService.v1.Controller
simulationApiModel = new SimulationApiModel(); simulationApiModel = new SimulationApiModel();
} }
var simulation = await this.simulationsService.InsertAsync(simulationApiModel.ToServiceModel(), template); var simulation = await this.simulationsService.InsertAsync(simulationApiModel.ToServiceModel(null), template);
return SimulationApiModel.FromServiceModel( return SimulationApiModel.FromServiceModel(
simulation, this.servicesConfig, this.deploymentConfig, this.connectionStringManager, this.simulationRunner, this.rateReporter); simulation, this.servicesConfig, this.deploymentConfig, this.connectionStringManager, this.simulationRunner, this.rateReporter);
} }
@ -105,7 +108,10 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.WebService.v1.Controller
throw new BadRequestException("No data provided, request object is empty."); throw new BadRequestException("No data provided, request object is empty.");
} }
var simulation = await this.simulationsService.UpsertAsync(simulationApiModel.ToServiceModel(id)); // Load the existing resource, so that internal properties can be copied
var existingSimulation = await this.GetExistingSimulationAsync(id);
var simulation = await this.simulationsService.UpsertAsync(simulationApiModel.ToServiceModel(existingSimulation, id));
return SimulationApiModel.FromServiceModel( return SimulationApiModel.FromServiceModel(
simulation, this.servicesConfig, this.deploymentConfig, this.connectionStringManager, this.simulationRunner, this.rateReporter); simulation, this.servicesConfig, this.deploymentConfig, this.connectionStringManager, this.simulationRunner, this.rateReporter);
} }
@ -155,11 +161,10 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.WebService.v1.Controller
throw new BadRequestException("No data or invalid data provided"); throw new BadRequestException("No data or invalid data provided");
} }
var patchServiceModel = patch.ToServiceModel(id); SimulationPatch patchServiceModel = patch.ToServiceModel(id);
if (patchServiceModel.Enabled == false) if (patchServiceModel.Enabled == false)
{ {
patchServiceModel.Statistics = new Services.Models.SimulationStatistics patchServiceModel.Statistics = new SimulationStatistics
{ {
AverageMessagesPerSecond = this.rateReporter.GetThroughputForMessages(), AverageMessagesPerSecond = this.rateReporter.GetThroughputForMessages(),
TotalMessagesSent = this.simulationRunner.TotalMessagesCount TotalMessagesSent = this.simulationRunner.TotalMessagesCount
@ -176,5 +181,17 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.WebService.v1.Controller
{ {
await this.simulationsService.DeleteAsync(id); await this.simulationsService.DeleteAsync(id);
} }
private async Task<Simulation> GetExistingSimulationAsync(string id)
{
try
{
return await this.simulationsService.GetAsync(id);
}
catch (ResourceNotFoundException)
{
return null;
}
}
} }
} }

Просмотреть файл

@ -38,6 +38,7 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.WebService.v1.Models.Sim
[JsonProperty(PropertyName = "Enabled")] [JsonProperty(PropertyName = "Enabled")]
public bool? Enabled { get; set; } public bool? Enabled { get; set; }
// Note: read-only property, used only to report the simulation status
[JsonProperty(PropertyName = "Running")] [JsonProperty(PropertyName = "Running")]
public bool? Running { get; set; } public bool? Running { get; set; }
@ -50,15 +51,18 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.WebService.v1.Models.Sim
[JsonProperty(PropertyName = "EndTime")] [JsonProperty(PropertyName = "EndTime")]
public string EndTime { get; set; } public string EndTime { get; set; }
// Note: read-only property, used only to report the simulation status
[JsonProperty(PropertyName = "StoppedTime")] [JsonProperty(PropertyName = "StoppedTime")]
public string StoppedTime { get; set; } public string StoppedTime { get; set; }
[JsonProperty(PropertyName = "DeviceModels")] [JsonProperty(PropertyName = "DeviceModels")]
public IList<SimulationDeviceModelRef> DeviceModels { get; set; } public IList<SimulationDeviceModelRef> DeviceModels { get; set; }
// Note: read-only property, used only to report the simulation status
[JsonProperty(PropertyName = "Statistics")] [JsonProperty(PropertyName = "Statistics")]
public SimulationStatistics Statistics{ get; set; } public SimulationStatistics Statistics { get; set; }
// Note: read-only metadata
[JsonProperty(PropertyName = "$metadata", Order = 1000)] [JsonProperty(PropertyName = "$metadata", Order = 1000)]
public IDictionary<string, string> Metadata => new Dictionary<string, string> public IDictionary<string, string> Metadata => new Dictionary<string, string>
{ {
@ -86,25 +90,36 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.WebService.v1.Models.Sim
this.Statistics = new SimulationStatistics(); this.Statistics = new SimulationStatistics();
} }
// Map API model to service model // Map API model to service model, keeping the original fields when needed
public Simulation ToServiceModel(string id = "") public Simulation ToServiceModel(Simulation existingSimulation, string id = "")
{ {
this.Id = id;
var now = DateTimeOffset.UtcNow; var now = DateTimeOffset.UtcNow;
var result = new Simulation // ID can be empty, e.g. with POST requests
this.Id = id;
// Use the existing simulation fields if available, so that read-only values are not lost
// e.g. the state of partitioning, device creation, etc.
var result = new Simulation();
if (existingSimulation != null)
{ {
ETag = this.ETag, result = existingSimulation;
Id = this.Id, }
Name = this.Name,
Description = this.Description, result.ETag = this.ETag;
// When unspecified, a simulation is enabled result.Id = this.Id;
Enabled = this.Enabled ?? true, result.Name = this.Name;
StartTime = DateHelper.ParseDateExpression(this.StartTime, now), result.Description = this.Description;
EndTime = DateHelper.ParseDateExpression(this.EndTime, now), result.StartTime = DateHelper.ParseDateExpression(this.StartTime, now);
DeviceModels = this.DeviceModels?.Select(x => x.ToServiceModel()).ToList() result.EndTime = DateHelper.ParseDateExpression(this.EndTime, now);
}; result.DeviceModels = this.DeviceModels?.Select(x => x.ToServiceModel()).ToList();
// Overwrite the value only if the request included the field, i.e. don't
// enable/disable the simulation if the user didn't explicitly ask to.
if (this.Enabled.HasValue)
{
result.Enabled = this.Enabled.Value;
}
foreach (var hub in this.IotHubs) foreach (var hub in this.IotHubs)
{ {
@ -228,8 +243,8 @@ namespace Microsoft.Azure.IoTSolutions.DeviceSimulation.WebService.v1.Models.Sim
// Append additional Hub properties and Statistics // Append additional Hub properties and Statistics
private void AppendHubPropertiesAndStatistics( private void AppendHubPropertiesAndStatistics(
IServicesConfig servicesConfig, IServicesConfig servicesConfig,
IDeploymentConfig deploymentConfig, IDeploymentConfig deploymentConfig,
IIotHubConnectionStringManager connectionStringManager, IIotHubConnectionStringManager connectionStringManager,
ISimulationRunner simulationRunner, ISimulationRunner simulationRunner,
IRateLimiting rateReporter) IRateLimiting rateReporter)