spiderly 19.15.0 → 19.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: backend-testing
3
- description: Unit-test a Spiderly backend service against an EF Core InMemory database — covers the two InMemory pitfalls that silently corrupt assertions (the change tracker masking a missing SaveChanges, and WithTransactionAsync throwing TransactionIgnoredWarning). Use when writing xUnit/NUnit tests for entity services, business logic, or save/delete hooks against an InMemory DbContext, or when an InMemory-backed test passes/throws for reasons that don't match production.
3
+ description: Unit-test a Spiderly backend service against an EF Core InMemory database — covers the two InMemory pitfalls that silently corrupt assertions (the change tracker masking a missing SaveChanges, and WithTransactionAsync throwing TransactionIgnoredWarning). Use when writing xUnit/NUnit tests for entity services, business logic, or save/delete hooks against an InMemory DbContext, or when an InMemory-backed test passes/throws for reasons that don't match production. Also covers setting the clock a test runs at (a fake `TimeProvider`), which Spiderly's audit stamps and token, API-key and outbox expiry read.
4
4
  ---
5
5
 
6
6
  # Backend Testing (EF Core InMemory)
@@ -63,3 +63,41 @@ Pass a stable `dbName` (e.g. one per test, `Guid.NewGuid().ToString()`) so each
63
63
  ## When InMemory isn't enough
64
64
 
65
65
  InMemory ignores relational constraints (unique indexes, FK cascade, check constraints, `[Required]` at the DB level), provider-specific SQL, and real transaction semantics. If the behavior under test depends on any of those, prefer the **SQLite in-memory** provider (`UseSqlite("DataSource=:memory:")`, keep the connection open) or a disposable real database — don't assert relational guarantees against `UseInMemoryDatabase`.
66
+
67
+ ## Controlling the clock
68
+
69
+ Spiderly reads the time from the `TimeProvider` registered in the app's container. `AddSpiderly` registers `TimeProvider.System` unless the app registers its own, the same way ASP.NET Core's `AddAuthentication` does. That one clock:
70
+
71
+ - stamps `CreatedAt` / `ModifiedAt` in `SaveChanges`;
72
+ - sets and checks the expiry of access, refresh and login-verification tokens and the external-login nonce, checks an API key's `ExpiresAt`, and judges a bearer token's lifetime on each request;
73
+ - decides when an outbox row is due for retry, which handled rows retention purges, and how old the health check says the backlog is.
74
+
75
+ ASP.NET Core's own authentication handlers read the same registration. A test sets the time by registering a fake clock, e.g. `FakeTimeProvider` from the `Microsoft.Extensions.TimeProvider.Testing` package:
76
+
77
+ ```csharp
78
+ using Microsoft.Extensions.Time.Testing;
79
+
80
+ FakeTimeProvider clock = new(new DateTimeOffset(2030, 1, 1, 12, 0, 0, TimeSpan.Zero));
81
+ services.AddSingleton<TimeProvider>(clock); // before or after AddSpiderly, which adds the system clock only when none is registered
82
+
83
+ // ... save, issue a token, create an API key ...
84
+
85
+ clock.Advance(TimeSpan.FromHours(2)); // the fake stands still until the test moves it
86
+ ```
87
+
88
+ In a `WebApplicationFactory`, make that registration in `ConfigureTestServices`. Your own code that needs the current time should take `TimeProvider` in its constructor and call `GetUtcNow()`, so it reads the same clock as the stamps and expiries it compares against. Reading `DateTime.UtcNow` beside it gives the code two clocks, and a test with a fake clock can only control one of them.
89
+
90
+ A context built with `new`, as in the InMemory examples above, is outside the container and stamps the system clock. Give it the test's clock through its options:
91
+
92
+ ```csharp
93
+ ServiceProvider clockOnly = new ServiceCollection()
94
+ .AddSingleton<TimeProvider>(clock)
95
+ .BuildServiceProvider();
96
+
97
+ MyAppDbContext context = new(new DbContextOptionsBuilder<MyAppDbContext>()
98
+ .UseInMemoryDatabase(dbName)
99
+ .UseApplicationServiceProvider(clockOnly)
100
+ .Options);
101
+ ```
102
+
103
+ The clock does not move a cookie's `Expires`: the client judges that by its own clock, so `CookieManager` keeps setting it from the machine's. Libraries that read the machine's clock themselves, such as Hangfire's schedules, are not moved by it either.
@@ -0,0 +1,87 @@
1
+ ---
2
+ name: background-jobs
3
+ description: Write and schedule Hangfire background jobs in a Spiderly app so the container builds every job. Use when adding a job class, enqueueing a job, adding a recurring job, opting in with spiderly.AddJobs, or seeing "is not registered in the container, so it is not built".
4
+ ---
5
+
6
+ # Background Jobs
7
+
8
+ Spiderly runs background work on Hangfire. Opt in to the job seam and every job is a registered service: a host that
9
+ validates its service graph at boot judges each job's constructor, and Hangfire builds jobs only from the container.
10
+
11
+ ## Opt in
12
+
13
+ ```csharp
14
+ services.AddSpiderly<MyDbContext>(Configuration, spiderly =>
15
+ {
16
+ // ...
17
+ spiderly.AddJobs(typeof(Startup).Assembly, typeof(InvoiceExportJob).Assembly);
18
+ });
19
+ ```
20
+
21
+ `AddJobs` registers every non-abstract class implementing `ISpiderlyJob` in the given assemblies (scoped: one instance
22
+ per job run) and replaces Hangfire.NetCore's job activator with `SpiderlyJobActivator`. It works with
23
+ `services.AddHangfire(...)` before or after it. A second call adds assemblies.
24
+
25
+ ## Write a job
26
+
27
+ ```csharp
28
+ using Spiderly.Shared.Jobs;
29
+
30
+ public class InvoiceExportJob : ISpiderlyJob
31
+ {
32
+ private readonly IApplicationDbContext _context;
33
+
34
+ public InvoiceExportJob(IApplicationDbContext context) => _context = context;
35
+
36
+ public async Task ExportAsync(long invoiceId) { /* ... */ }
37
+ }
38
+ ```
39
+
40
+ - Implement `ISpiderlyJob`, take dependencies in the constructor, keep method arguments simple and serializable.
41
+ - No `services.AddScoped<InvoiceExportJob>()` needed: the scan registers it. A registration of your own (another
42
+ lifetime, a factory) wins over the scan's.
43
+
44
+ ## Schedule it
45
+
46
+ Use the typed methods, which accept only an `ISpiderlyJob`:
47
+
48
+ ```csharp
49
+ // Injected IBackgroundJobClient
50
+ backgroundJobClient.EnqueueJob<InvoiceExportJob>(job => job.ExportAsync(invoiceId));
51
+
52
+ // IRecurringJobManager, in the Configure phase
53
+ IRecurringJobManager jobs = app.ApplicationServices.GetRequiredService<IRecurringJobManager>();
54
+ jobs.AddOrUpdateJob<InvoiceExportJob>("invoice-export-nightly", job => job.ExportAllAsync(), "0 2 * * *",
55
+ new RecurringJobOptions { TimeZone = TimeZoneInfo.Utc });
56
+ ```
57
+
58
+ Do not schedule with Hangfire's `Enqueue<T>` / `AddOrUpdate<T>`: they accept any type, so an unmarked job compiles,
59
+ and the activator refuses it when it runs. An app can ban them with a banned-symbols analyzer to make that a compile
60
+ error.
61
+
62
+ ## What fails where
63
+
64
+ | Mistake | Where it fails |
65
+ | --- | --- |
66
+ | Job class without `ISpiderlyJob` scheduled through the typed methods | Compile error |
67
+ | Job scheduled through Hangfire's own API, never registered | When it runs: `The Hangfire job '...' is not registered in the container, so it is not built.` |
68
+ | Job's constructor needs a service nobody registered | At boot when the host validates its graph (`ValidateOnBuild`), else when it runs |
69
+
70
+ ## Spiderly's own jobs
71
+
72
+ `AddOutbox<TOutbox>()` registers the dispatcher, retention and health jobs that `app.SpiderlyUseOutboxRecurringJob<TOutbox>()`
73
+ schedules; `AddNotifications(...)` registers `NotificationDeliveryJob`. They are registered whether or not you call
74
+ `AddJobs`. `SpiderlyUseOutboxRecurringJob` schedules through the container's `IRecurringJobManager`, so it needs
75
+ `services.AddHangfire(...)`.
76
+
77
+ ## Generic jobs
78
+
79
+ A marked open generic class (`ImportJob<TSource> : ISpiderlyJob`) is registered as an open generic. The container
80
+ cannot validate an open generic registration at boot, so also register each closed form you schedule
81
+ (`services.AddScoped<ImportJob<CsvSource>>()`) to have it judged there.
82
+
83
+ ## Testing
84
+
85
+ A job is a plain class: construct it with its dependencies and call its method. To test what code schedules, pass a fake
86
+ `IBackgroundJobClient` / `IRecurringJobManager` that records the `Job` it is handed (`job.Type`, `job.Method`,
87
+ `job.Args`).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spiderly",
3
- "version": "19.15.0",
3
+ "version": "19.16.1",
4
4
  "author": "Filip Trivan",
5
5
  "license": "MIT",
6
6
  "description": "Spiderly Angular Library to use in combination with Spiderly.",