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)
{ {