Skip to content

Commit 3eca19e

Browse files
test(audience-sdk): introduce WireFixture and migrate mock-store fixtures
Names a small builder for wire-format envelope JSON so tests stop hand-rolling escape-laden strings, and a rename on MessageFields lights up runtime emit and test fixtures together. - WireFixture.cs: new internal helper with Track / Identify / Alias overloads that build envelopes from MessageFields and MessageTypes constants. - DiskStoreTests.cs, GzipTests.cs, HttpTransportTests.cs, EventQueueTests.cs: hand-rolled "{type:track,...}" mock-store fixtures read from WireFixture; envelope key access uses MessageFields.
1 parent 6da0e17 commit 3eca19e

5 files changed

Lines changed: 78 additions & 43 deletions

File tree

src/Packages/Audience/Tests/Runtime/Transport/DiskStoreTests.cs

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ public void ReadBatch_ExcludesAndDeletesStaleFiles()
9696

9797
// Manually plant a stale file (ticks from 31 days ago)
9898
var staleTime = DateTime.UtcNow.AddDays(-(Constants.StaleEventDays + 1));
99-
var staleName = $"{staleTime.Ticks}_{Guid.NewGuid():N}.json";
99+
var staleName = $"{staleTime.Ticks}_{Guid.NewGuid():N}{AudiencePaths.QueueFileExtension}";
100100
var queueDir = AudiencePaths.QueueDir(_testDir);
101101
File.WriteAllText(Path.Combine(queueDir, staleName), "{\"stale\":true}");
102102

@@ -151,7 +151,7 @@ public void CrashRecovery_PicksUpFilesFromPreviousRun()
151151
{
152152
// Simulate a previous run by writing a file directly
153153
var queueDir = AudiencePaths.QueueDir(_testDir);
154-
var survivingName = $"{DateTime.UtcNow.Ticks}_{Guid.NewGuid():N}.json";
154+
var survivingName = $"{DateTime.UtcNow.Ticks}_{Guid.NewGuid():N}{AudiencePaths.QueueFileExtension}";
155155
File.WriteAllText(Path.Combine(queueDir, survivingName), "{\"survived\":true}");
156156

157157
// Create a new DiskStore instance pointing at the same path (simulates restart)
@@ -164,10 +164,19 @@ public void CrashRecovery_PicksUpFilesFromPreviousRun()
164164
[Test]
165165
public void ApplyAnonymousDowngrade_DeletesIdentifyAndAlias_StripsUserIdFromTrack()
166166
{
167-
_store.Write("{\"type\":\"identify\",\"anonymousId\":\"a\",\"userId\":\"u\"}");
168-
_store.Write("{\"type\":\"alias\",\"fromId\":\"a\",\"toId\":\"u\"}");
169-
_store.Write("{\"type\":\"track\",\"eventName\":\"x\",\"anonymousId\":\"a\",\"userId\":\"u\"}");
170-
_store.Write("{\"type\":\"track\",\"eventName\":\"y\",\"anonymousId\":\"a\"}");
167+
_store.Write(WireFixture.Identify(
168+
(MessageFields.AnonymousId, "a"),
169+
(MessageFields.UserId, "u")));
170+
_store.Write(WireFixture.Alias(
171+
(MessageFields.FromId, "a"),
172+
(MessageFields.ToId, "u")));
173+
_store.Write(WireFixture.Track(
174+
(MessageFields.EventName, "x"),
175+
(MessageFields.AnonymousId, "a"),
176+
(MessageFields.UserId, "u")));
177+
_store.Write(WireFixture.Track(
178+
(MessageFields.EventName, "y"),
179+
(MessageFields.AnonymousId, "a")));
171180

172181
_store.ApplyAnonymousDowngrade();
173182

@@ -178,8 +187,8 @@ public void ApplyAnonymousDowngrade_DeletesIdentifyAndAlias_StripsUserIdFromTrac
178187
{
179188
var json = File.ReadAllText(path);
180189
var msg = JsonReader.DeserializeObject(json);
181-
Assert.AreEqual("track", msg["type"]);
182-
Assert.IsFalse(msg.ContainsKey("userId"), "userId must be stripped from queued track messages");
190+
Assert.AreEqual(MessageTypes.Track, msg[MessageFields.Type]);
191+
Assert.IsFalse(msg.ContainsKey(MessageFields.UserId), "userId must be stripped from queued track messages");
183192
}
184193
}
185194

@@ -218,7 +227,7 @@ public void ApplyAnonymousDowngrade_DeletesMalformedFiles()
218227
// Seed the queue directory with a file that is not valid JSON so the
219228
// downgrade cannot leave it to potentially leak identified data.
220229
var queueDir = AudiencePaths.QueueDir(_testDir);
221-
var badName = $"{DateTime.UtcNow.Ticks}_{Guid.NewGuid():N}.json";
230+
var badName = $"{DateTime.UtcNow.Ticks}_{Guid.NewGuid():N}{AudiencePaths.QueueFileExtension}";
222231
File.WriteAllText(Path.Combine(queueDir, badName), "{not valid json");
223232

224233
_store.ApplyAnonymousDowngrade();

src/Packages/Audience/Tests/Runtime/Transport/EventQueueTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,9 +156,9 @@ public void PurgeAll_ClearsMemoryAndDisk()
156156
{
157157
using var queue = new EventQueue(_store, flushIntervalSeconds: 60, flushSize: 100);
158158

159-
queue.Enqueue(new Dictionary<string, object> { [MessageFields.Type] = "track", [MessageFields.EventName] = "a" });
159+
queue.Enqueue(new Dictionary<string, object> { [MessageFields.Type] = MessageTypes.Track, [MessageFields.EventName] = "a" });
160160
queue.FlushSync();
161-
queue.Enqueue(new Dictionary<string, object> { [MessageFields.Type] = "track", [MessageFields.EventName] = "b" });
161+
queue.Enqueue(new Dictionary<string, object> { [MessageFields.Type] = MessageTypes.Track, [MessageFields.EventName] = "b" });
162162

163163
queue.PurgeAll();
164164

src/Packages/Audience/Tests/Runtime/Transport/HttpTransportTests.cs

Lines changed: 30 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@ public void TearDown()
4949
[Test]
5050
public async Task SendBatchAsync_200_DeletesFilesFromDisk()
5151
{
52-
_store.Write("{\"type\":\"track\",\"eventName\":\"a\"}");
53-
_store.Write("{\"type\":\"track\",\"eventName\":\"b\"}");
52+
_store.Write(WireFixture.Track((MessageFields.EventName, "a")));
53+
_store.Write(WireFixture.Track((MessageFields.EventName, "b")));
5454

5555
var handler = new MockHandler(HttpStatusCode.OK, $"{{\"accepted\":2,\"{ResponseFields.Rejected}\":0}}");
5656
using var transport = new HttpTransport(_store, TestDefaults.PublishableKey, handler: handler);
@@ -65,7 +65,7 @@ public async Task SendBatchAsync_200_DeletesFilesFromDisk()
6565
[Test]
6666
public async Task SendBatchAsync_200_SendsGzippedPayloadWithCorrectHeaders()
6767
{
68-
_store.Write("{\"type\":\"track\",\"eventName\":\"test\"}");
68+
_store.Write(WireFixture.Track((MessageFields.EventName, "test")));
6969

7070
byte[]? capturedBody = null;
7171
string? capturedKey = null;
@@ -75,7 +75,7 @@ public async Task SendBatchAsync_200_SendsGzippedPayloadWithCorrectHeaders()
7575
var handler = new MockHandler(HttpStatusCode.OK, $"{{\"accepted\":1,\"{ResponseFields.Rejected}\":0}}",
7676
onRequest: req =>
7777
{
78-
capturedKey = string.Join("", req.Headers.GetValues("x-immutable-publishable-key"));
78+
capturedKey = string.Join("", req.Headers.GetValues(Constants.PublishableKeyHeader));
7979
capturedContentType = req.Content!.Headers.ContentType!.MediaType;
8080
capturedContentEncoding = string.Join("", req.Content.Headers.ContentEncoding);
8181
capturedBody = req.Content.ReadAsByteArrayAsync().Result;
@@ -91,13 +91,13 @@ public async Task SendBatchAsync_200_SendsGzippedPayloadWithCorrectHeaders()
9191
var decompressed = DecompressGzip(capturedBody!);
9292
StringAssert.StartsWith($"{{\"{ResponseFields.MessagesEnvelope}\":[", decompressed);
9393
StringAssert.EndsWith("]}", decompressed);
94-
StringAssert.Contains("\"eventName\":\"test\"", decompressed);
94+
StringAssert.Contains($"\"{MessageFields.EventName}\":\"test\"", decompressed);
9595
}
9696
#else
9797
[Test]
9898
public async Task SendBatchAsync_200_SendsPlainJsonPayloadWithoutContentEncoding()
9999
{
100-
_store.Write("{\"type\":\"track\",\"eventName\":\"test\"}");
100+
_store.Write(WireFixture.Track((MessageFields.EventName, "test")));
101101

102102
string? capturedKey = null;
103103
string? capturedContentType = null;
@@ -106,7 +106,7 @@ public async Task SendBatchAsync_200_SendsPlainJsonPayloadWithoutContentEncoding
106106
var handler = new MockHandler(HttpStatusCode.OK, $"{{\"accepted\":1,\"{ResponseFields.Rejected}\":0}}",
107107
onRequest: req =>
108108
{
109-
capturedKey = string.Join("", req.Headers.GetValues("x-immutable-publishable-key"));
109+
capturedKey = string.Join("", req.Headers.GetValues(Constants.PublishableKeyHeader));
110110
capturedContentType = req.Content!.Headers.ContentType!.MediaType;
111111
capturedContentEncodingCount = req.Content.Headers.ContentEncoding.Count;
112112
capturedBody = req.Content.ReadAsStringAsync().Result;
@@ -120,14 +120,14 @@ public async Task SendBatchAsync_200_SendsPlainJsonPayloadWithoutContentEncoding
120120
Assert.AreEqual(0, capturedContentEncodingCount, "no Content-Encoding header is permitted in v1");
121121
StringAssert.StartsWith($"{{\"{ResponseFields.MessagesEnvelope}\":[", capturedBody);
122122
StringAssert.EndsWith("]}", capturedBody);
123-
StringAssert.Contains("\"eventName\":\"test\"", capturedBody);
123+
StringAssert.Contains($"\"{MessageFields.EventName}\":\"test\"", capturedBody);
124124
}
125125
#endif
126126

127127
[Test]
128128
public async Task SendBatchAsync_200_UsesCorrectUrlForTestKey()
129129
{
130-
_store.Write("{\"type\":\"track\"}");
130+
_store.Write(WireFixture.Track());
131131

132132
HttpRequestMessage? captured = null;
133133
var handler = new MockHandler(HttpStatusCode.OK, $"{{\"accepted\":1,\"{ResponseFields.Rejected}\":0}}",
@@ -142,7 +142,7 @@ public async Task SendBatchAsync_200_UsesCorrectUrlForTestKey()
142142
[Test]
143143
public async Task SendBatchAsync_200_UsesCorrectUrlForProdKey()
144144
{
145-
_store.Write("{\"type\":\"track\"}");
145+
_store.Write(WireFixture.Track());
146146

147147
HttpRequestMessage? captured = null;
148148
var handler = new MockHandler(HttpStatusCode.OK, $"{{\"accepted\":1,\"{ResponseFields.Rejected}\":0}}",
@@ -157,7 +157,7 @@ public async Task SendBatchAsync_200_UsesCorrectUrlForProdKey()
157157
[Test]
158158
public async Task SendBatchAsync_BaseUrlOverride_WinsOverKeyPrefix()
159159
{
160-
_store.Write("{\"type\":\"track\"}");
160+
_store.Write(WireFixture.Track());
161161

162162
HttpRequestMessage? captured = null;
163163
var handler = new MockHandler(HttpStatusCode.OK, $"{{\"accepted\":1,\"{ResponseFields.Rejected}\":0}}",
@@ -188,7 +188,7 @@ public async Task SendBatchAsync_EmptyQueue_ReturnsFalse()
188188
[Test]
189189
public async Task SendBatchAsync_4xx_DeletesFilesAndResetsBackoff()
190190
{
191-
_store.Write("{\"type\":\"track\"}");
191+
_store.Write(WireFixture.Track());
192192

193193
var handler = new MockHandler(HttpStatusCode.BadRequest, "");
194194
AudienceError? reportedError = null;
@@ -206,7 +206,7 @@ public async Task SendBatchAsync_4xx_DeletesFilesAndResetsBackoff()
206206
[Test]
207207
public async Task SendBatchAsync_429_NoRetryAfter_KeepsFilesAndUsesExpoBackoff_NoError()
208208
{
209-
_store.Write("{\"type\":\"track\"}");
209+
_store.Write(WireFixture.Track());
210210

211211
var handler = new MockHandler((HttpStatusCode)429, "");
212212
AudienceError? reportedError = null;
@@ -224,7 +224,7 @@ public async Task SendBatchAsync_429_NoRetryAfter_KeepsFilesAndUsesExpoBackoff_N
224224
[Test]
225225
public async Task SendBatchAsync_429_RetryAfterDeltaSeconds_OverridesExpoBackoff()
226226
{
227-
_store.Write("{\"type\":\"track\"}");
227+
_store.Write(WireFixture.Track());
228228

229229
var handler = new MockHandler(() =>
230230
{
@@ -247,7 +247,7 @@ public async Task SendBatchAsync_429_RetryAfterHttpDate_OverridesExpoBackoff()
247247
// ParseRetryAfter computes the delta against DateTimeOffset.UtcNow,
248248
// which we can't pin from outside; assert only that a future date
249249
// engages the window. The seconds-form test above pins exact math.
250-
_store.Write("{\"type\":\"track\"}");
250+
_store.Write(WireFixture.Track());
251251

252252
var handler = new MockHandler(() =>
253253
{
@@ -269,7 +269,7 @@ public async Task SendBatchAsync_429_PastRetryAfterDate_FallsBackToExpoBackoff()
269269
{
270270
// Past Retry-After (clock skew or server bug) must not let
271271
// IsInBackoffWindow flip false and trigger instant retry.
272-
_store.Write("{\"type\":\"track\"}");
272+
_store.Write(WireFixture.Track());
273273

274274
var handler = new MockHandler(() =>
275275
{
@@ -289,7 +289,7 @@ public async Task SendBatchAsync_429_PastRetryAfterDate_FallsBackToExpoBackoff()
289289
[Test]
290290
public async Task SendBatchAsync_429ThenSuccess_DeliversBatchAndClearsBackoff()
291291
{
292-
_store.Write("{\"type\":\"track\"}");
292+
_store.Write(WireFixture.Track());
293293

294294
var callCount = 0;
295295
var handler = new MockHandler(() =>
@@ -322,8 +322,8 @@ public async Task SendBatchAsync_200_WithRejected_DeletesFilesAndSurfacesValidat
322322
// per-message validation errors. The batch is deleted (retries
323323
// would not help) and the count is surfaced via onError so
324324
// studios can observe silently dropped events.
325-
_store.Write("{\"type\":\"track\",\"eventName\":\"a\"}");
326-
_store.Write("{\"type\":\"track\",\"eventName\":\"b\"}");
325+
_store.Write(WireFixture.Track((MessageFields.EventName, "a")));
326+
_store.Write(WireFixture.Track((MessageFields.EventName, "b")));
327327

328328
var handler = new MockHandler(HttpStatusCode.OK, $"{{\"accepted\":1,\"{ResponseFields.Rejected}\":1}}");
329329
AudienceError? reportedError = null;
@@ -341,7 +341,7 @@ public async Task SendBatchAsync_200_WithRejected_DeletesFilesAndSurfacesValidat
341341
[Test]
342342
public async Task SendBatchAsync_200_ZeroRejected_DoesNotFireOnError()
343343
{
344-
_store.Write("{\"type\":\"track\",\"eventName\":\"a\"}");
344+
_store.Write(WireFixture.Track((MessageFields.EventName, "a")));
345345

346346
var handler = new MockHandler(HttpStatusCode.OK, $"{{\"accepted\":1,\"{ResponseFields.Rejected}\":0}}");
347347
AudienceError? reportedError = null;
@@ -357,7 +357,7 @@ public async Task SendBatchAsync_200_ZeroRejected_DoesNotFireOnError()
357357
public async Task SendBatchAsync_200_MalformedBody_TreatsAsZeroRejected()
358358
{
359359
// Malformed diagnostic body must not block the success path.
360-
_store.Write("{\"type\":\"track\",\"eventName\":\"a\"}");
360+
_store.Write(WireFixture.Track((MessageFields.EventName, "a")));
361361

362362
var handler = new MockHandler(HttpStatusCode.OK, "not-json");
363363
AudienceError? reportedError = null;
@@ -373,7 +373,7 @@ public async Task SendBatchAsync_200_MalformedBody_TreatsAsZeroRejected()
373373
[Test]
374374
public async Task SendBatchAsync_5xx_KeepsFilesAndIncreasesBackoff()
375375
{
376-
_store.Write("{\"type\":\"track\"}");
376+
_store.Write(WireFixture.Track());
377377

378378
var handler = new MockHandler(HttpStatusCode.InternalServerError, "");
379379
AudienceError? reportedError = null;
@@ -392,7 +392,7 @@ public async Task SendBatchAsync_5xx_KeepsFilesAndIncreasesBackoff()
392392
[Test]
393393
public async Task BackoffMs_EscalatesOnlyAfterWindowElapsed()
394394
{
395-
_store.Write("{\"type\":\"track\"}");
395+
_store.Write(WireFixture.Track());
396396
var handler = new MockHandler(HttpStatusCode.InternalServerError, "");
397397
using var transport = new HttpTransport(_store, TestDefaults.PublishableKey,
398398
handler: handler, getUtcNow: _getUtcNow);
@@ -426,7 +426,7 @@ public async Task BackoffMs_EscalatesOnlyAfterWindowElapsed()
426426
[Test]
427427
public async Task BackoffMs_DoesNotEscalateWhileInsidePreviousWindow()
428428
{
429-
_store.Write("{\"type\":\"track\"}");
429+
_store.Write(WireFixture.Track());
430430
var handler = new MockHandler(HttpStatusCode.InternalServerError, "");
431431
using var transport = new HttpTransport(_store, TestDefaults.PublishableKey,
432432
handler: handler, getUtcNow: _getUtcNow);
@@ -458,7 +458,7 @@ public async Task BackoffMs_DoesNotEscalateWhileInsidePreviousWindow()
458458
[Test]
459459
public async Task BackoffMs_ResetsAfterSuccess()
460460
{
461-
_store.Write("{\"type\":\"track\"}");
461+
_store.Write(WireFixture.Track());
462462

463463
var callCount = 0;
464464
var handler = new MockHandler(() =>
@@ -489,7 +489,7 @@ public async Task BackoffMs_ResetsAfterSuccess()
489489
[Test]
490490
public async Task SendBatchAsync_NetworkError_KeepsFilesAndBacksOff()
491491
{
492-
_store.Write("{\"type\":\"track\"}");
492+
_store.Write(WireFixture.Track());
493493

494494
var handler = new MockHandler(() => throw new HttpRequestException("connection refused"));
495495
AudienceError? reportedError = null;
@@ -512,7 +512,7 @@ public async Task SendBatchAsync_HttpClientTimeout_TreatedAsNetworkError()
512512
// guard, timeouts would be silently swallowed as "shutdown": no backoff, no error
513513
// callback, next cycle hot-loops. This test ensures timeouts flow through the
514514
// NetworkError path.
515-
_store.Write("{\"type\":\"track\"}");
515+
_store.Write(WireFixture.Track());
516516

517517
var handler = new MockHandler(() => throw new TaskCanceledException("Request timed out"));
518518
AudienceError? reportedError = null;
@@ -538,7 +538,7 @@ public async Task SendBatchAsync_CallerCancelled_Throws_DoesNotDeleteOrRecordFai
538538
// with the batch still on disk, and a FlushAsync loop watching
539539
// that return value would re-enter on the same cancelled token
540540
// forever: nothing ever drains, nothing ever throws.
541-
_store.Write("{\"type\":\"track\"}");
541+
_store.Write(WireFixture.Track());
542542

543543
var handler = new MockHandler(() => throw new OperationCanceledException("simulated"));
544544
AudienceError? reportedError = null;
@@ -570,7 +570,7 @@ public async Task SendBatchAsync_CallerCancelled_Throws_DoesNotDeleteOrRecordFai
570570
[Test]
571571
public async Task IsInBackoffWindow_ClearsAfterNextAttemptAtElapses()
572572
{
573-
_store.Write("{\"type\":\"track\"}");
573+
_store.Write(WireFixture.Track());
574574

575575
var now = new DateTime(2026, 4, 17, 12, 0, 0, DateTimeKind.Utc);
576576
var handler = new MockHandler(HttpStatusCode.InternalServerError, "");
@@ -594,7 +594,7 @@ public async Task IsInBackoffWindow_ClearsAfterNextAttemptAtElapses()
594594
[Test]
595595
public async Task SendBatchAsync_ErrorCallbackThrows_DoesNotCrash()
596596
{
597-
_store.Write("{\"type\":\"track\"}");
597+
_store.Write(WireFixture.Track());
598598

599599
var handler = new MockHandler(HttpStatusCode.BadRequest, "");
600600
using var transport = new HttpTransport(_store, TestDefaults.PublishableKey,

src/Packages/Audience/Tests/Runtime/Utility/GzipTests.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ internal class GzipTests
1212
[Test]
1313
public void Compress_ProducesValidGzip_ThatDecompressesToOriginal()
1414
{
15-
const string original = "{\"type\":\"track\",\"eventName\":\"test\"}";
15+
var original = WireFixture.Track((MessageFields.EventName, "test"));
1616

1717
var compressed = Gzip.Compress(original);
1818

@@ -33,7 +33,9 @@ public void Compress_OutputIsSmallerThanInput_ForRealisticPayload()
3333
for (var i = 0; i < 20; i++)
3434
{
3535
if (i > 0) sb.Append(',');
36-
sb.Append($"{{\"type\":\"track\",\"eventName\":\"level_complete\",\"anonymousId\":\"anon-{i}\"}}");
36+
sb.Append(WireFixture.Track(
37+
(MessageFields.EventName, "level_complete"),
38+
(MessageFields.AnonymousId, $"anon-{i}")));
3739
}
3840

3941
sb.Append("]}");
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
using System.Collections.Generic;
2+
3+
namespace Immutable.Audience.Tests
4+
{
5+
// Builds JSON message envelopes from MessageFields / MessageTypes for tests.
6+
internal static class WireFixture
7+
{
8+
internal static string Track(params (string key, object value)[] extra) =>
9+
Build(MessageTypes.Track, extra);
10+
11+
internal static string Identify(params (string key, object value)[] extra) =>
12+
Build(MessageTypes.Identify, extra);
13+
14+
internal static string Alias(params (string key, object value)[] extra) =>
15+
Build(MessageTypes.Alias, extra);
16+
17+
private static string Build(string type, (string key, object value)[] extra)
18+
{
19+
var dict = new Dictionary<string, object> { [MessageFields.Type] = type };
20+
foreach (var (key, value) in extra) dict[key] = value;
21+
return Json.Serialize(dict);
22+
}
23+
}
24+
}

0 commit comments

Comments
 (0)