Skip to content

Commit b31aa2f

Browse files
committed
ImportTaskService unit tests for entities
1 parent 77f2d0e commit b31aa2f

File tree

7 files changed

+283
-4
lines changed

7 files changed

+283
-4
lines changed

src/Dataverse.ConfigurationMigrationTool/Console.Tests/Extensions/ILoggerTestExtensions.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,13 @@ public static void ShouldHaveLoggedException(this ILogger logger, LogLevel logLe
88
{
99
logger.Received().Log(logLevel, Arg.Any<EventId>(), Arg.Any<object>(), exception, Arg.Any<Func<object, Exception, string>>());
1010
}
11+
public static void ShouldHaveLogged(this ILogger logger, LogLevel logLevel, string message, int count = 1)
12+
{
13+
logger.Received(count).Log(
14+
logLevel,
15+
Arg.Any<EventId>(),
16+
Arg.Is<object>(o => o.ToString() == message),
17+
null,
18+
Arg.Any<Func<object, Exception, string>>());
19+
}
1120
}

src/Dataverse.ConfigurationMigrationTool/Console.Tests/FakeBuilders/FakeEntityMetadataBuilder.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,17 @@ public FakeEntityMetadataBuilder(string entityName, string primaryIdField, strin
1515
PrimaryIdField = primaryIdField;
1616
PrimaryNameField = primaryNameField;
1717
}
18-
public FakeEntityMetadataBuilder AddAttribute<T>(string logicalName)
18+
public FakeEntityMetadataBuilder AddAttribute<T>(string logicalName, Action<T> configureMD = null)
1919
where T : AttributeMetadata, new()
2020
{
2121
var attribute = new T
2222
{
2323
LogicalName = logicalName
2424
};
25+
attribute.IsValidForUpdate = true;
26+
attribute.IsValidForCreate = true;
27+
configureMD?.Invoke(attribute);
28+
2529
Fields.Add(attribute);
2630
return this;
2731
}

src/Dataverse.ConfigurationMigrationTool/Console.Tests/FakeDatasets.cs

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,123 @@ internal static class FakeDatasets
5555
}
5656
}
5757
};
58+
public static readonly EntityImport SelfHiearchyAccountSets = new()
59+
{
60+
Displayname = "Account",
61+
Name = "account",
62+
Records = new()
63+
{
64+
Record = new()
65+
{
66+
new(){
67+
Id = AccountIds[0],
68+
Field = new(){
69+
new()
70+
{
71+
Name = "name",
72+
Value = "Account 1"
73+
},
74+
new()
75+
{
76+
Name = "parentaccountid",
77+
Value = AccountIds[1].ToString(),
78+
Lookupentity = "account",
79+
Lookupentityname = "Account 2"
80+
}
81+
}
82+
},
83+
new(){
84+
Id = AccountIds[1],
85+
Field = new(){
86+
new()
87+
{
88+
Name = "name",
89+
Value = "Account 2"
90+
},
91+
new()
92+
{
93+
Name = "parentaccountid",
94+
Value = AccountIds[2].ToString(),
95+
Lookupentity = "account",
96+
Lookupentityname = "Account 3"
97+
}
98+
}
99+
},
100+
new(){
101+
Id = AccountIds[2],
102+
Field = new(){
103+
new()
104+
{
105+
Name = "name",
106+
Value = "Account 3"
107+
},
108+
new()
109+
{
110+
Name = "parentaccountid",
111+
Value = AccountIds[3].ToString(),
112+
Lookupentity = "account",
113+
Lookupentityname = "Account 4"
114+
}
115+
}
116+
},
117+
new(){
118+
Id = AccountIds[3],
119+
Field = new(){
120+
new()
121+
{
122+
Name = "name",
123+
Value = "Account 4"
124+
}
125+
}
126+
}
127+
}
128+
}
129+
};
130+
public static readonly EntityImport CIrcularSelfHiearchyAccountSets = new()
131+
{
132+
Displayname = "Account",
133+
Name = "account",
134+
Records = new()
135+
{
136+
Record = new()
137+
{
138+
new(){
139+
Id = AccountIds[0],
140+
Field = new(){
141+
new()
142+
{
143+
Name = "name",
144+
Value = "Account 1"
145+
},
146+
new()
147+
{
148+
Name = "parentaccountid",
149+
Value = AccountIds[1].ToString(),
150+
Lookupentity = "account",
151+
Lookupentityname = "Account 2"
152+
}
153+
}
154+
},
155+
new(){
156+
Id = AccountIds[1],
157+
Field = new(){
158+
new()
159+
{
160+
Name = "name",
161+
Value = "Account 2"
162+
},
163+
new()
164+
{
165+
Name = "parentaccountid",
166+
Value = AccountIds[0].ToString(),
167+
Lookupentity = "account",
168+
Lookupentityname = "Account 1"
169+
}
170+
}
171+
}
172+
}
173+
}
174+
};
58175
public static readonly EntityImport OpportunitiesSet = new()
59176
{
60177
Displayname = "Opportunity",

src/Dataverse.ConfigurationMigrationTool/Console.Tests/FakeMetadata.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,13 @@ internal static class FakeMetadata
1010
.AddAttribute<StringAttributeMetadata>("lastname")
1111
.AddRelationship("contact_opportunities", "opportunity")
1212
.Build();
13+
public static EntityMetadata Account =>
14+
new FakeEntityMetadataBuilder("account", "accountid", "name")
15+
.AddAttribute<StringAttributeMetadata>("name")
16+
.AddAttribute<LookupAttributeMetadata>("parentaccountid", (md) =>
17+
{
18+
md.Targets = new[] { "account" };
19+
})
20+
.Build();
1321

1422
}

src/Dataverse.ConfigurationMigrationTool/Console.Tests/FakeSchemas.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,33 @@ internal static class FakeSchemas
3030
}
3131
},
3232
};
33+
public static readonly EntitySchema SelfHiearchyAccount = new EntitySchema()
34+
{
35+
Name = "account",
36+
Primaryidfield = "accountid",
37+
Primarynamefield = "name",
38+
Fields = new FieldsSchema
39+
{
40+
Field = new List<FieldSchema>
41+
{
42+
new FieldSchema
43+
{
44+
Name = "name",
45+
Type = "string"
46+
47+
},
48+
new FieldSchema
49+
{
50+
Name = "parentaccountid",
51+
Type = "entityreference",
52+
Customfield = true,
53+
Displayname = "Parent Account",
54+
LookupType = "account",
55+
PrimaryKey = false
56+
}
57+
}
58+
},
59+
};
3360
public static readonly EntitySchema Opportunity = new EntitySchema()
3461
{
3562
Name = "opportunity",

src/Dataverse.ConfigurationMigrationTool/Console.Tests/Features/Import/ImportTaskProcessorServiceTests.cs

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using Dataverse.ConfigurationMigrationTool.Console.Features.Import.ValueConverters;
44
using Dataverse.ConfigurationMigrationTool.Console.Features.Shared;
55
using Dataverse.ConfigurationMigrationTool.Console.Services.Dataverse;
6+
using Dataverse.ConfigurationMigrationTool.Console.Tests.Extensions;
67
using Dataverse.ConfigurationMigrationTool.Console.Tests.FakeBuilders;
78
using Microsoft.Extensions.Logging;
89
using Microsoft.Xrm.Sdk;
@@ -175,4 +176,116 @@ public async Task GivenARelationshipImportTaskWhereRelationshipNotFound_WhenExec
175176
// Assert
176177
result.ShouldBe(TaskResult.Failed);
177178
}
179+
[Fact]
180+
public async Task GivenASelfHiearchyEntityTaskImport_WhenExecuted_ThenItShouldProcessInCorrectOrderAndReturnCompleted()
181+
{
182+
// Arrange
183+
var task = new ImportDataTask
184+
{
185+
EntitySchema = FakeSchemas.SelfHiearchyAccount,
186+
};
187+
var dataImport = new Entities
188+
{
189+
Entity = new List<EntityImport>
190+
{
191+
FakeDatasets.SelfHiearchyAccountSets
192+
}
193+
};
194+
_dataverseValueConverter.Convert(
195+
Arg.Is<LookupAttributeMetadata>(md => md.LogicalName == "parentaccountid"),
196+
Arg.Is<Field>(f => f.Name == "parentaccountid"))
197+
.Returns(x => new EntityReference { LogicalName = "account", Id = Guid.Parse(x.Arg<Field>().Value) });
198+
_dataverseValueConverter.Convert(
199+
Arg.Is<StringAttributeMetadata>(md => md.LogicalName == "name"),
200+
Arg.Is<Field>(f => f.Name == "name")).Returns(x => x.Arg<Field>().Value);
201+
metadataService.GetEntity(FakeSchemas.SelfHiearchyAccount.Name).Returns(FakeMetadata.Account);
202+
bulkOrganizationService.Upsert(Arg.Is<UpsertRequest>(r => r.Target.GetAttributeValue<EntityReference>("parentaccountid") != null))
203+
.Returns(x => new(new UpsertResponse() { ["Target"] = x.Arg<UpsertRequest>().Target.ToEntityReference() }));
204+
// Act
205+
var result = await importService.Execute(task, dataImport);
206+
// Assert
207+
Received.InOrder(() =>
208+
{
209+
bulkOrganizationService.UpsertBulk(Arg.Is<IEnumerable<UpsertRequest>>(r => r.Count() == 1));
210+
bulkOrganizationService.Upsert(Arg.Is<UpsertRequest>(r => r.Target.Id == FakeDatasets.AccountIds[2]));
211+
bulkOrganizationService.Upsert(Arg.Is<UpsertRequest>(r => r.Target.Id == FakeDatasets.AccountIds[1]));
212+
bulkOrganizationService.Upsert(Arg.Is<UpsertRequest>(r => r.Target.Id == FakeDatasets.AccountIds[0]));
213+
});
214+
result.ShouldBe(TaskResult.Completed);
215+
}
216+
[Fact]
217+
public async Task GivenASelfHiearchyEntityTaskImportWothIssues_WhenExecuted_ThenItShouldProcessInCorrectOrderAndReturnFailed()
218+
{
219+
// Arrange
220+
var task = new ImportDataTask
221+
{
222+
EntitySchema = FakeSchemas.SelfHiearchyAccount,
223+
};
224+
var dataImport = new Entities
225+
{
226+
Entity = new List<EntityImport>
227+
{
228+
FakeDatasets.SelfHiearchyAccountSets
229+
}
230+
};
231+
var fault = new OrganizationServiceFault { Message = "Fault message" };
232+
_dataverseValueConverter.Convert(
233+
Arg.Is<LookupAttributeMetadata>(md => md.LogicalName == "parentaccountid"),
234+
Arg.Is<Field>(f => f.Name == "parentaccountid"))
235+
.Returns(x => new EntityReference { LogicalName = "account", Id = Guid.Parse(x.Arg<Field>().Value) });
236+
_dataverseValueConverter.Convert(
237+
Arg.Is<StringAttributeMetadata>(md => md.LogicalName == "name"),
238+
Arg.Is<Field>(f => f.Name == "name")).Returns(x => x.Arg<Field>().Value);
239+
metadataService.GetEntity(FakeSchemas.SelfHiearchyAccount.Name).Returns(FakeMetadata.Account);
240+
bulkOrganizationService.Upsert(Arg.Is<UpsertRequest>(r => r.Target.GetAttributeValue<EntityReference>("parentaccountid") != null))
241+
.Returns(x => new(new OrganizationResponseFaultedResult() { Fault = fault, OriginalRequest = x.Arg<UpsertRequest>() }));
242+
243+
bulkOrganizationService.UpsertBulk(Arg.Is<IEnumerable<UpsertRequest>>(r => r.Count() == 1))
244+
.Returns(x => [new() { Fault = fault, OriginalRequest = x.Arg<IEnumerable<UpsertRequest>>().First() }]);
245+
// Act
246+
var result = await importService.Execute(task, dataImport);
247+
// Assert
248+
Received.InOrder(() =>
249+
{
250+
bulkOrganizationService.UpsertBulk(Arg.Is<IEnumerable<UpsertRequest>>(r => r.Count() == 1));
251+
bulkOrganizationService.Upsert(Arg.Is<UpsertRequest>(r => r.Target.Id == FakeDatasets.AccountIds[2]));
252+
bulkOrganizationService.Upsert(Arg.Is<UpsertRequest>(r => r.Target.Id == FakeDatasets.AccountIds[1]));
253+
bulkOrganizationService.Upsert(Arg.Is<UpsertRequest>(r => r.Target.Id == FakeDatasets.AccountIds[0]));
254+
});
255+
result.ShouldBe(TaskResult.Failed);
256+
}
257+
[Fact]
258+
public async Task GivenACircularSelfHiearchyEntityTaskImport_WhenExecuted_ThenItShouldSkipThoseAndReturnCompleted()
259+
{
260+
// Arrange
261+
var task = new ImportDataTask
262+
{
263+
EntitySchema = FakeSchemas.SelfHiearchyAccount,
264+
};
265+
var dataImport = new Entities
266+
{
267+
Entity = new List<EntityImport>
268+
{
269+
FakeDatasets.CIrcularSelfHiearchyAccountSets
270+
}
271+
};
272+
_dataverseValueConverter.Convert(
273+
Arg.Is<LookupAttributeMetadata>(md => md.LogicalName == "parentaccountid"),
274+
Arg.Is<Field>(f => f.Name == "parentaccountid"))
275+
.Returns(x => new EntityReference { LogicalName = "account", Id = Guid.Parse(x.Arg<Field>().Value) });
276+
_dataverseValueConverter.Convert(
277+
Arg.Is<StringAttributeMetadata>(md => md.LogicalName == "name"),
278+
Arg.Is<Field>(f => f.Name == "name")).Returns(x => x.Arg<Field>().Value);
279+
metadataService.GetEntity(FakeSchemas.SelfHiearchyAccount.Name).Returns(FakeMetadata.Account);
280+
bulkOrganizationService.Upsert(Arg.Is<UpsertRequest>(r => r.Target.GetAttributeValue<EntityReference>("parentaccountid") != null))
281+
.Returns(x => new(new UpsertResponse() { ["Target"] = x.Arg<UpsertRequest>().Target.ToEntityReference() }));
282+
// Act
283+
var result = await importService.Execute(task, dataImport);
284+
// Assert
285+
await bulkOrganizationService.Received().UpsertBulk(Arg.Is<IEnumerable<UpsertRequest>>(r => r.Count() == 0));
286+
await bulkOrganizationService.DidNotReceive().Upsert(Arg.Any<UpsertRequest>());
287+
logger.ShouldHaveLogged(LogLevel.Warning, $"account({FakeDatasets.AccountIds[0]}) was skipped because his parent was not proccessed.", count: 1);
288+
logger.ShouldHaveLogged(LogLevel.Warning, $"account({FakeDatasets.AccountIds[1]}) was skipped because his parent was not proccessed.", count: 1);
289+
result.ShouldBe(TaskResult.Completed);
290+
}
178291
}

src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Features/Import/ImportTaskProcessorService.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public class ImportTaskProcessorService : IImportTaskProcessorService
2525
private readonly ILogger<ImportTaskProcessorService> logger;
2626
private readonly IDataverseValueConverter _dataverseValueConverter;
2727
private readonly IBulkOrganizationService bulkOrganizationService;
28-
28+
const int MAX_RETRIES = 3;
2929
public ImportTaskProcessorService(IMetadataService metadataService,
3030
ILogger<ImportTaskProcessorService> logger,
3131
IDataverseValueConverter dataverseValueConverter,
@@ -147,10 +147,11 @@ private async Task<IEnumerable<OrganizationResponseFaultedResult>> ProcessDepend
147147
{
148148
var record = queue.Dequeue();
149149

150-
if (record.Field.Any(f => f.Lookupentity == entityImport.Name && queue.Any(r => r.Id.ToString() == f.Value)))
150+
if (record.Field.Any(f => f.Lookupentity == entityImport.Name && (queue.Any(r => r.Id.ToString() == f.Value) ||
151+
retries.Any(kv => kv.Key.ToString() == f.Value && kv.Value >= MAX_RETRIES))))
151152
{
152153

153-
if (retries.ContainsKey(record.Id) && retries[record.Id] >= 3)
154+
if (retries.ContainsKey(record.Id) && retries[record.Id] >= MAX_RETRIES)
154155
{
155156
logger.LogWarning("{entityType}({id}) was skipped because his parent was not proccessed.", entityImport.Name, record.Id);
156157
continue;

0 commit comments

Comments
 (0)