Skip to content

Commit b28bc31

Browse files
authored
BREAKING CHANGE - 28 fix domainentity and securedentity must be deterministic and idempotent (#29)
* BREAKING CHANGE - Deterministic and Idempotent required * rowkey to IDomainEntity
1 parent 885539b commit b28bc31

21 files changed

Lines changed: 1055 additions & 1546 deletions
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
using Goodtocode.Domain.Entities;
2+
using Microsoft.VisualStudio.TestTools.UnitTesting;
3+
using System;
4+
5+
namespace Goodtocode.Domain.Tests.Entities;
6+
7+
[TestClass]
8+
public sealed class DomainEntityAuditTests
9+
{
10+
private sealed class TestEntity : DomainEntity<TestEntity>
11+
{
12+
public string Name { get; set; } = string.Empty;
13+
public TestEntity() { }
14+
public TestEntity(Guid id, DateTime createdOn, DateTimeOffset timestamp)
15+
: base(id, createdOn, timestamp) { }
16+
public TestEntity(Guid id, string partitionKey, string rowKey, DateTime createdOn, DateTimeOffset timestamp)
17+
: base(id, partitionKey, rowKey, createdOn, timestamp) { }
18+
public TestEntity(Guid id, string partitionKey, DateTime createdOn, DateTimeOffset timestamp)
19+
: base(id, partitionKey, id.ToString(), createdOn, timestamp) { }
20+
}
21+
22+
[TestMethod]
23+
public void AuditFieldsAreSetCorrectly()
24+
{
25+
var id = Guid.NewGuid();
26+
var now = new DateTime(2024, 1, 4, 12, 0, 0, DateTimeKind.Utc);
27+
var ts = new DateTimeOffset(2024, 1, 4, 12, 0, 0, TimeSpan.Zero);
28+
var entity = new TestEntity(id, "pk", now, ts);
29+
Assert.AreEqual("pk", entity.PartitionKey);
30+
Assert.AreEqual(now, entity.CreatedOn);
31+
Assert.AreEqual(ts, entity.Timestamp);
32+
Assert.AreEqual(entity.Id.ToString(), entity.RowKey);
33+
Assert.IsNull(entity.ModifiedOn);
34+
Assert.IsNull(entity.DeletedOn);
35+
}
36+
37+
[TestMethod]
38+
public void AuditFieldsCanBeAccessedViaProperties()
39+
{
40+
var id = Guid.NewGuid();
41+
var partitionKey = "pk";
42+
var createdOn = new DateTime(2024, 1, 6, 12, 0, 0, DateTimeKind.Utc);
43+
var timestamp = new DateTimeOffset(2024, 1, 6, 12, 0, 0, TimeSpan.Zero);
44+
var entity = new TestEntity(id, partitionKey, createdOn, timestamp);
45+
Assert.AreEqual(createdOn, entity.CreatedOn);
46+
Assert.AreEqual(timestamp, entity.Timestamp);
47+
Assert.AreEqual(partitionKey, entity.PartitionKey);
48+
Assert.AreEqual(id, entity.Id);
49+
}
50+
51+
[TestMethod]
52+
public void MarkModifiedSetsModifiedOn()
53+
{
54+
var id = Guid.NewGuid();
55+
var createdOn = new DateTime(2024, 1, 18, 12, 0, 0, DateTimeKind.Utc);
56+
var timestamp = new DateTimeOffset(2024, 1, 18, 12, 0, 0, TimeSpan.Zero);
57+
var entity = new TestEntity(id, createdOn, timestamp);
58+
var modifiedOn = new DateTime(2024, 1, 18, 13, 0, 0, DateTimeKind.Utc);
59+
entity.MarkModified(modifiedOn);
60+
Assert.AreEqual(modifiedOn, entity.ModifiedOn);
61+
}
62+
63+
[TestMethod]
64+
public void MarkModifiedUpdatesModifiedOnToMoreRecentValue()
65+
{
66+
var id = Guid.NewGuid();
67+
var createdOn = new DateTime(2024, 1, 19, 12, 0, 0, DateTimeKind.Utc);
68+
var timestamp = new DateTimeOffset(2024, 1, 19, 12, 0, 0, TimeSpan.Zero);
69+
var entity = new TestEntity(id, createdOn, timestamp);
70+
var firstModified = new DateTime(2024, 1, 19, 13, 0, 0, DateTimeKind.Utc);
71+
entity.MarkModified(firstModified);
72+
Assert.AreEqual(firstModified, entity.ModifiedOn);
73+
var secondModified = new DateTime(2024, 1, 19, 14, 0, 0, DateTimeKind.Utc);
74+
entity.MarkModified(secondModified);
75+
Assert.AreEqual(secondModified, entity.ModifiedOn);
76+
Assert.IsGreaterThan(firstModified, secondModified, "ModifiedOn should be updated to a more recent value");
77+
}
78+
79+
[TestMethod]
80+
public void MarkDeletedSetsDeletedOn()
81+
{
82+
var id = Guid.NewGuid();
83+
var createdOn = new DateTime(2024, 1, 20, 12, 0, 0, DateTimeKind.Utc);
84+
var timestamp = new DateTimeOffset(2024, 1, 20, 12, 0, 0, TimeSpan.Zero);
85+
var entity = new TestEntity(id, createdOn, timestamp);
86+
var deletedOn = new DateTime(2024, 1, 20, 13, 0, 0, DateTimeKind.Utc);
87+
entity.MarkDeleted(deletedOn);
88+
Assert.AreEqual(deletedOn, entity.DeletedOn);
89+
}
90+
91+
[TestMethod]
92+
public void MarkDeletedDoesNotOverwriteExistingDeletedOn()
93+
{
94+
var id = Guid.NewGuid();
95+
var createdOn = new DateTime(2024, 1, 21, 12, 0, 0, DateTimeKind.Utc);
96+
var timestamp = new DateTimeOffset(2024, 1, 21, 12, 0, 0, TimeSpan.Zero);
97+
var entity = new TestEntity(id, createdOn, timestamp);
98+
var firstDeletedOn = new DateTime(2024, 1, 21, 13, 0, 0, DateTimeKind.Utc);
99+
entity.MarkDeleted(firstDeletedOn);
100+
Assert.AreEqual(firstDeletedOn, entity.DeletedOn);
101+
var secondDeletedOn = new DateTime(2024, 1, 21, 14, 0, 0, DateTimeKind.Utc);
102+
entity.MarkDeleted(secondDeletedOn);
103+
Assert.AreEqual(firstDeletedOn, entity.DeletedOn, "DeletedOn should not be overwritten if already set");
104+
}
105+
106+
[TestMethod]
107+
public void MarkUndeletedClearsDeletedOn()
108+
{
109+
var id = Guid.NewGuid();
110+
var createdOn = new DateTime(2024, 1, 22, 12, 0, 0, DateTimeKind.Utc);
111+
var timestamp = new DateTimeOffset(2024, 1, 22, 12, 0, 0, TimeSpan.Zero);
112+
var entity = new TestEntity(id, createdOn, timestamp);
113+
var deletedOn = new DateTime(2024, 1, 22, 13, 0, 0, DateTimeKind.Utc);
114+
entity.MarkDeleted(deletedOn);
115+
Assert.AreEqual(deletedOn, entity.DeletedOn);
116+
entity.MarkUndeleted();
117+
Assert.IsNull(entity.DeletedOn);
118+
}
119+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
using Goodtocode.Domain.Entities;
2+
using Microsoft.VisualStudio.TestTools.UnitTesting;
3+
using System;
4+
5+
namespace Goodtocode.Domain.Tests.Entities;
6+
7+
[TestClass]
8+
public sealed class DomainEntityConstructorTests
9+
{
10+
private sealed class TestEntity : DomainEntity<TestEntity>
11+
{
12+
public string Name { get; set; } = string.Empty;
13+
public TestEntity() { }
14+
public TestEntity(Guid id, DateTime createdOn, DateTimeOffset timestamp)
15+
: base(id, createdOn, timestamp) { }
16+
public TestEntity(Guid id, string partitionKey, string rowKey, DateTime createdOn, DateTimeOffset timestamp)
17+
: base(id, partitionKey, rowKey, createdOn, timestamp) { }
18+
public TestEntity(Guid id, string partitionKey, DateTime createdOn, DateTimeOffset timestamp)
19+
: base(id, partitionKey, id.ToString(), createdOn, timestamp) { }
20+
}
21+
22+
[TestMethod]
23+
public void ConstructorWithIdSetsId()
24+
{
25+
var id = Guid.NewGuid();
26+
var createdOn = new DateTime(2024, 1, 1, 12, 0, 0, DateTimeKind.Utc);
27+
var timestamp = new DateTimeOffset(2024, 1, 1, 12, 0, 0, TimeSpan.Zero);
28+
var entity = new TestEntity(id, createdOn, timestamp);
29+
Assert.IsNotNull(entity);
30+
Assert.AreEqual(id, entity.Id);
31+
Assert.AreEqual(id.ToString(), entity.PartitionKey);
32+
Assert.AreEqual(id.ToString(), entity.RowKey);
33+
Assert.AreEqual(createdOn, entity.CreatedOn);
34+
Assert.AreEqual(timestamp, entity.Timestamp);
35+
}
36+
37+
[TestMethod]
38+
public void ConstructorWithIdAndPartitionKeySetsProperties()
39+
{
40+
var id = Guid.NewGuid();
41+
var partitionKey = "test-partition";
42+
var createdOn = new DateTime(2024, 1, 2, 12, 0, 0, DateTimeKind.Utc);
43+
var timestamp = new DateTimeOffset(2024, 1, 2, 12, 0, 0, TimeSpan.Zero);
44+
var entity = new TestEntity(id, partitionKey, createdOn, timestamp);
45+
Assert.IsNotNull(entity);
46+
Assert.AreEqual(id, entity.Id);
47+
Assert.AreEqual(partitionKey, entity.PartitionKey);
48+
Assert.AreEqual(id.ToString(), entity.RowKey);
49+
}
50+
51+
[TestMethod]
52+
public void ConstructorWithPartitionKeyOnlyMaintainsPartitionKey()
53+
{
54+
var id = Guid.NewGuid();
55+
var partitionKey = "cosmos-partition-key";
56+
var createdOn = new DateTime(2024, 1, 3, 12, 0, 0, DateTimeKind.Utc);
57+
var timestamp = new DateTimeOffset(2024, 1, 3, 12, 0, 0, TimeSpan.Zero);
58+
var entity = new TestEntity(id, partitionKey, createdOn, timestamp);
59+
Assert.AreEqual(partitionKey, entity.PartitionKey);
60+
Assert.AreEqual(id, entity.Id);
61+
Assert.AreEqual(string.Empty, entity.Name);
62+
Assert.AreEqual(id.ToString(), entity.RowKey);
63+
}
64+
65+
[TestMethod]
66+
public void FullConstructorSetsAllProperties()
67+
{
68+
var id = Guid.NewGuid();
69+
var partitionKey = "full-partition";
70+
var createdOn = new DateTime(2024, 1, 5, 12, 0, 0, DateTimeKind.Utc);
71+
var timestamp = new DateTimeOffset(2024, 1, 5, 12, 0, 0, TimeSpan.Zero);
72+
var entity = new TestEntity(id, partitionKey, createdOn, timestamp);
73+
Assert.AreEqual(id, entity.Id);
74+
Assert.AreEqual(partitionKey, entity.PartitionKey);
75+
Assert.AreEqual(createdOn, entity.CreatedOn);
76+
Assert.AreEqual(timestamp, entity.Timestamp);
77+
Assert.AreEqual(id.ToString(), entity.RowKey);
78+
Assert.IsNull(entity.ModifiedOn);
79+
Assert.IsNull(entity.DeletedOn);
80+
}
81+
82+
[TestMethod]
83+
public void ConstructorWithIdPartitionKeyAndRowKeySetsRowKey()
84+
{
85+
var id = Guid.NewGuid();
86+
var partitionKey = "test-partition";
87+
var rowKey = "custom-row-key";
88+
var createdOn = new DateTime(2024, 1, 23, 12, 0, 0, DateTimeKind.Utc);
89+
var timestamp = new DateTimeOffset(2024, 1, 23, 12, 0, 0, TimeSpan.Zero);
90+
var entity = new TestEntity(id, partitionKey, rowKey, createdOn, timestamp);
91+
Assert.AreEqual(id, entity.Id);
92+
Assert.AreEqual(partitionKey, entity.PartitionKey);
93+
Assert.AreEqual(rowKey, entity.RowKey);
94+
}
95+
}
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
using Goodtocode.Domain.Entities;
2+
using Goodtocode.Domain.Events;
3+
using Microsoft.VisualStudio.TestTools.UnitTesting;
4+
using System;
5+
using System.Collections.Generic;
6+
7+
namespace Goodtocode.Domain.Tests.Entities;
8+
9+
[TestClass]
10+
public sealed class DomainEntityDomainEventTests
11+
{
12+
private sealed class TestEntity : DomainEntity<TestEntity>
13+
{
14+
public string Name { get; set; } = string.Empty;
15+
public TestEntity() { }
16+
public TestEntity(Guid id, DateTime createdOn, DateTimeOffset timestamp)
17+
: base(id, createdOn, timestamp) { }
18+
public TestEntity(Guid id, string partitionKey, string rowKey, DateTime createdOn, DateTimeOffset timestamp)
19+
: base(id, partitionKey, rowKey, createdOn, timestamp) { }
20+
public TestEntity(Guid id, string partitionKey, DateTime createdOn, DateTimeOffset timestamp)
21+
: base(id, partitionKey, id.ToString(), createdOn, timestamp) { }
22+
}
23+
private sealed class TestEvent(TestEntity item) : IDomainEvent<TestEntity>
24+
{
25+
public TestEntity Item { get; set; } = item;
26+
public DateTime OccurredOn { get; set; } = DateTime.UtcNow;
27+
}
28+
29+
[TestMethod]
30+
public void DomainEventsAddAndClearWorks()
31+
{
32+
var id = Guid.NewGuid();
33+
var createdOn = new DateTime(2024, 1, 7, 12, 0, 0, DateTimeKind.Utc);
34+
var timestamp = new DateTimeOffset(2024, 1, 7, 12, 0, 0, TimeSpan.Zero);
35+
var entity = new TestEntity(id, createdOn, timestamp);
36+
var evt = new TestEvent(entity);
37+
entity.AddDomainEvent(evt);
38+
Assert.HasCount(1, entity.DomainEvents);
39+
Assert.AreSame(evt, entity.DomainEvents[0]);
40+
entity.ClearDomainEvents();
41+
Assert.IsEmpty(entity.DomainEvents);
42+
}
43+
44+
[TestMethod]
45+
public void DomainEventsCanAddMultipleEvents()
46+
{
47+
var id = Guid.NewGuid();
48+
var createdOn = new DateTime(2024, 1, 8, 12, 0, 0, DateTimeKind.Utc);
49+
var timestamp = new DateTimeOffset(2024, 1, 8, 12, 0, 0, TimeSpan.Zero);
50+
var entity = new TestEntity(id, createdOn, timestamp);
51+
var evt1 = new TestEvent(entity);
52+
var evt2 = new TestEvent(entity);
53+
entity.AddDomainEvent(evt1);
54+
entity.AddDomainEvent(evt2);
55+
Assert.HasCount(2, entity.DomainEvents);
56+
Assert.AreSame(evt1, entity.DomainEvents[0]);
57+
Assert.AreSame(evt2, entity.DomainEvents[1]);
58+
}
59+
60+
[TestMethod]
61+
public void DomainEventsCollectionIsReadOnly()
62+
{
63+
var id = Guid.NewGuid();
64+
var createdOn = new DateTime(2024, 1, 9, 12, 0, 0, DateTimeKind.Utc);
65+
var timestamp = new DateTimeOffset(2024, 1, 9, 12, 0, 0, TimeSpan.Zero);
66+
var entity = new TestEntity(id, createdOn, timestamp);
67+
Assert.IsInstanceOfType<IReadOnlyList<IDomainEvent<TestEntity>>>(entity.DomainEvents);
68+
}
69+
70+
[TestMethod]
71+
public void DomainEventsCollectionStartsEmpty()
72+
{
73+
var id = Guid.NewGuid();
74+
var createdOn = new DateTime(2024, 1, 10, 12, 0, 0, DateTimeKind.Utc);
75+
var timestamp = new DateTimeOffset(2024, 1, 10, 12, 0, 0, TimeSpan.Zero);
76+
var entity = new TestEntity(id, createdOn, timestamp);
77+
Assert.IsNotNull(entity.DomainEvents);
78+
Assert.IsEmpty(entity.DomainEvents);
79+
}
80+
81+
[TestMethod]
82+
public void DomainEventsCanBeAddedAndClearedMultipleTimes()
83+
{
84+
var id = Guid.NewGuid();
85+
var createdOn = new DateTime(2024, 1, 11, 12, 0, 0, DateTimeKind.Utc);
86+
var timestamp = new DateTimeOffset(2024, 1, 11, 12, 0, 0, TimeSpan.Zero);
87+
var entity = new TestEntity(id, createdOn, timestamp);
88+
var evt1 = new TestEvent(entity);
89+
var evt2 = new TestEvent(entity);
90+
entity.AddDomainEvent(evt1);
91+
Assert.HasCount(1, entity.DomainEvents);
92+
entity.ClearDomainEvents();
93+
Assert.IsEmpty(entity.DomainEvents);
94+
entity.AddDomainEvent(evt2);
95+
Assert.HasCount(1, entity.DomainEvents);
96+
entity.ClearDomainEvents();
97+
Assert.IsEmpty(entity.DomainEvents);
98+
}
99+
100+
[TestMethod]
101+
public void DomainEventsMaintainOrderOfAddition()
102+
{
103+
var id = Guid.NewGuid();
104+
var createdOn = new DateTime(2024, 1, 12, 12, 0, 0, DateTimeKind.Utc);
105+
var timestamp = new DateTimeOffset(2024, 1, 12, 12, 0, 0, TimeSpan.Zero);
106+
var entity = new TestEntity(id, createdOn, timestamp);
107+
var evt1 = new TestEvent(entity) { OccurredOn = new DateTime(2024, 1, 12, 12, 0, 1, DateTimeKind.Utc) };
108+
var evt2 = new TestEvent(entity) { OccurredOn = new DateTime(2024, 1, 12, 12, 0, 2, DateTimeKind.Utc) };
109+
var evt3 = new TestEvent(entity) { OccurredOn = new DateTime(2024, 1, 12, 12, 0, 3, DateTimeKind.Utc) };
110+
entity.AddDomainEvent(evt1);
111+
entity.AddDomainEvent(evt2);
112+
entity.AddDomainEvent(evt3);
113+
Assert.HasCount(3, entity.DomainEvents);
114+
Assert.AreSame(evt1, entity.DomainEvents[0]);
115+
Assert.AreSame(evt2, entity.DomainEvents[1]);
116+
Assert.AreSame(evt3, entity.DomainEvents[2]);
117+
}
118+
119+
[TestMethod]
120+
public void DomainEventsNotAffectedByAuditFieldUpdates()
121+
{
122+
var id = Guid.NewGuid();
123+
var createdOn = new DateTime(2024, 1, 13, 12, 0, 0, DateTimeKind.Utc);
124+
var timestamp = new DateTimeOffset(2024, 1, 13, 12, 0, 0, TimeSpan.Zero);
125+
var entity = new TestEntity(id, createdOn, timestamp);
126+
var evt = new TestEvent(entity);
127+
entity.AddDomainEvent(evt);
128+
var modifiedOn = new DateTime(2024, 1, 13, 13, 0, 0, DateTimeKind.Utc);
129+
var deletedOn = new DateTime(2024, 1, 13, 14, 0, 0, DateTimeKind.Utc);
130+
entity.MarkModified(modifiedOn);
131+
entity.MarkDeleted(deletedOn);
132+
Assert.HasCount(1, entity.DomainEvents);
133+
Assert.AreSame(evt, entity.DomainEvents[0]);
134+
}
135+
136+
[TestMethod]
137+
public void DomainEventPropertiesAreCorrectlySet()
138+
{
139+
var id = Guid.NewGuid();
140+
var createdOn = new DateTime(2024, 1, 17, 12, 0, 0, DateTimeKind.Utc);
141+
var timestamp = new DateTimeOffset(2024, 1, 17, 12, 0, 0, TimeSpan.Zero);
142+
var entity = new TestEntity(id, createdOn, timestamp);
143+
var occurredOn = new DateTime(2024, 1, 17, 13, 0, 0, DateTimeKind.Utc);
144+
var domainEvent = new TestEvent(entity) { OccurredOn = occurredOn };
145+
entity.AddDomainEvent(domainEvent);
146+
Assert.HasCount(1, entity.DomainEvents);
147+
var eventFromEntity = entity.DomainEvents[0];
148+
Assert.AreEqual(entity, eventFromEntity.Item);
149+
Assert.AreEqual(occurredOn, eventFromEntity.OccurredOn);
150+
}
151+
}

0 commit comments

Comments
 (0)