When upserting a simulation, keep the original/internal/readonly prop… (#261)
This commit is contained in:
Родитель
939333e700
Коммит
2abcd73c33
|
@ -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)
|
||||||
|
|
Загрузка…
Ссылка в новой задаче