diff --git a/testFrontend/SafeNSound.FakeUser/MonitorService.cs b/testFrontend/SafeNSound.FakeUser/MonitorService.cs
new file mode 100644
index 0000000..57d90a5
--- /dev/null
+++ b/testFrontend/SafeNSound.FakeUser/MonitorService.cs
@@ -0,0 +1,52 @@
+using ArcaneLibs.Extensions;
+
+namespace SafeNSound.FakeUser;
+
+public class MonitorService(ILogger<MonitorService> logger, UserStore userStore): IHostedService {
+ private Task? _getAllAlarmsTask, _assignBudgetTask;
+ private readonly CancellationTokenSource _cts = new();
+
+ public async Task StartAsync(CancellationToken cancellationToken) {
+ _getAllAlarmsTask = GetAllAlarms(_cts.Token);
+ _assignBudgetTask = AssignBudget(_cts.Token);
+ }
+
+ private async Task GetAllAlarms(CancellationToken cancellationToken) {
+ while (!cancellationToken.IsCancellationRequested) {
+ try {
+ var user = userStore.GetRandomMonitor();
+ var alarms = await user.Client!.GetAllAlarms();
+ if(alarms.Count > 0)
+ logger.LogInformation("Monitor {UserId} has outstanding alarms: {Alarm}", user.Auth.Username, alarms.ToJson(indent: false));
+ // else
+ // logger.LogInformation("Monitor {UserId} found no alarms to query", user.Auth.Username);
+ await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
+ } catch (Exception ex) {
+ logger.LogError(ex, "Error querying alarm");
+ }
+ }
+ }
+
+ private async Task AssignBudget(CancellationToken cancellationToken) {
+ while (!cancellationToken.IsCancellationRequested) {
+ try {
+ var user = userStore.GetRandomMonitor();
+ // var alarms = await user.Client!.GetAllAlarms();
+ // if(alarms.Count > 0)
+ // logger.LogInformation("Monitor {UserId} has outstanding alarms: {Alarm}", user.Auth.Username, alarms.ToJson(indent: false));
+ // else
+ // logger.LogInformation("Monitor {UserId} found no alarms to query", user.Auth.Username);
+ await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
+
+ } catch (Exception ex) {
+ logger.LogError(ex, "Error querying alarm");
+ }
+
+ }
+ }
+
+ public async Task StopAsync(CancellationToken cancellationToken) {
+ await _cts.CancelAsync();
+ await _getAllAlarmsTask!;
+ }
+}
\ No newline at end of file
diff --git a/testFrontend/SafeNSound.FakeUser/Program.cs b/testFrontend/SafeNSound.FakeUser/Program.cs
index 3751555..7c3eaff 100644
--- a/testFrontend/SafeNSound.FakeUser/Program.cs
+++ b/testFrontend/SafeNSound.FakeUser/Program.cs
@@ -1,2 +1,24 @@
// See https://aka.ms/new-console-template for more information
+
+using SafeNSound.FakeUser;
+using SafeNSound.Sdk;
+
Console.WriteLine("Hello, World!");
+
+var builder = Host.CreateApplicationBuilder(args);
+// longer shutdown timeout
+builder.Services.Configure<HostOptions>(options => {
+ options.ShutdownTimeout = TimeSpan.FromSeconds(120);
+});
+
+builder.Services.AddSingleton<SafeNSoundConfiguration>();
+builder.Services.AddSingleton<SafeNSoundAuthentication>();
+builder.Services.AddSingleton<UserStore>();
+builder.Services.AddHostedService<UserStore>(sp => sp.GetRequiredService<UserStore>());
+builder.Services.AddHostedService<RandomAlarmService>();
+builder.Services.AddHostedService<MonitorService>();
+
+// WrappedHttpClient.LogRequests = false;
+
+var host = builder.Build();
+host.Run();
\ No newline at end of file
diff --git a/testFrontend/SafeNSound.FakeUser/RandomAlarmService.cs b/testFrontend/SafeNSound.FakeUser/RandomAlarmService.cs
new file mode 100644
index 0000000..7835f89
--- /dev/null
+++ b/testFrontend/SafeNSound.FakeUser/RandomAlarmService.cs
@@ -0,0 +1,35 @@
+namespace SafeNSound.FakeUser;
+
+public class RandomAlarmService(UserStore userStore): IHostedService {
+ private Task? _listenerTask;
+ private readonly CancellationTokenSource _cts = new();
+
+ public async Task StartAsync(CancellationToken cancellationToken) {
+ _listenerTask = Run(_cts.Token);
+ }
+
+ private async Task Run(CancellationToken cancellationToken) {
+ while (!cancellationToken.IsCancellationRequested) {
+ try {
+ var user = userStore.GetRandomUser();
+ if (Random.Shared.Next(100) > 90) {
+ await user.Client!.SetAlarm(new Sdk.AlarmDto {
+ Reason = "fall"
+ });
+ }
+ else {
+ await user.Client!.DeleteAlarm();
+ }
+ }
+ catch (Exception ex) {
+ Console.WriteLine($"Error setting/deleting alarm: {ex.Message}");
+ }
+
+ await Task.Delay(TimeSpan.FromMilliseconds(100), cancellationToken);
+ }
+ }
+
+ public async Task StopAsync(CancellationToken cancellationToken) {
+ await _cts.CancelAsync();
+ }
+}
\ No newline at end of file
diff --git a/testFrontend/SafeNSound.FakeUser/SafeNSound.FakeUser.csproj b/testFrontend/SafeNSound.FakeUser/SafeNSound.FakeUser.csproj
index fd4bd08..830faaa 100644
--- a/testFrontend/SafeNSound.FakeUser/SafeNSound.FakeUser.csproj
+++ b/testFrontend/SafeNSound.FakeUser/SafeNSound.FakeUser.csproj
@@ -1,4 +1,4 @@
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk.Worker">
<PropertyGroup>
<OutputType>Exe</OutputType>
@@ -7,4 +7,12 @@
<Nullable>enable</Nullable>
</PropertyGroup>
+ <ItemGroup>
+ <PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.2"/>
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\SafeNSound.Sdk\SafeNSound.Sdk.csproj" />
+ </ItemGroup>
+
</Project>
diff --git a/testFrontend/SafeNSound.FakeUser/UserStore.cs b/testFrontend/SafeNSound.FakeUser/UserStore.cs
new file mode 100644
index 0000000..9b04efb
--- /dev/null
+++ b/testFrontend/SafeNSound.FakeUser/UserStore.cs
@@ -0,0 +1,97 @@
+using SafeNSound.Sdk;
+
+namespace SafeNSound.FakeUser;
+
+public class UserStore(SafeNSoundAuthentication authService, SafeNSoundConfiguration config) : IHostedService {
+ public List<ClientContainer> Admins { get; } = Enumerable.Range(0, 1).Select(_ => new ClientContainer()).ToList();
+ public List<ClientContainer> Monitors { get; } = Enumerable.Range(0, 5).Select(_ => new ClientContainer()).ToList();
+ public List<ClientContainer> Users { get; } = Enumerable.Range(0, 150000).Select(_ => new ClientContainer()).ToList();
+ public List<ClientContainer> AllUsers => [.. Users, .. Monitors, .. Admins];
+
+ public ClientContainer GetRandomUser() {
+ ClientContainer user;
+ do {
+ user = Users[new Random().Next(Users.Count)];
+ } while (user.Client == null);
+
+ return user;
+ }
+
+ public ClientContainer GetRandomMonitor() {
+ ClientContainer user;
+ do {
+ user = Monitors[new Random().Next(Monitors.Count)];
+ } while (user.Client == null);
+
+ return user;
+ }
+
+ public ClientContainer GetRandomAdmin() {
+ ClientContainer user;
+ do {
+ user = Admins[new Random().Next(Admins.Count)];
+ } while (user.Client == null);
+
+ return user;
+ }
+
+ public ClientContainer GetRandomUserOfAnyType() {
+ ClientContainer user;
+ do {
+ user = AllUsers[new Random().Next(AllUsers.Count)];
+ } while (user.Client == null);
+
+ return user;
+ }
+
+ public async Task StartAsync(CancellationToken cancellationToken) {
+ Admins.ForEach(x => x.Auth.UserType = "admin");
+ Monitors.ForEach(x => x.Auth.UserType = "monitor");
+ var ss = new SemaphoreSlim(256, 256);
+ var tasks = ((ClientContainer[]) [..Users, ..Monitors, ..Admins]).Select(async container => {
+ await ss.WaitAsync();
+ await authService.Register(container.Auth);
+ // container.Client = new SafeNSoundClient(config, (await authService.Login(container.Auth)).AccessToken);
+ // container.WhoAmI = await container.Client.WhoAmI();
+ ss.Release();
+ }).ToList();
+ await Task.WhenAll(tasks);
+
+ var users = Users.ToArray();
+ tasks = Monitors.Select(async container => {
+ var items = Random.Shared.GetItems(users, Users.Count / Monitors.Count).DistinctBy(x=>x.WhoAmI!.UserId);
+ foreach (var user in items) {
+ await container.Client!.AddAssignedUser(user.WhoAmI!.UserId);
+ }
+ }).ToList();
+ await Task.WhenAll(tasks);
+ }
+
+ public async Task StopAsync(CancellationToken cancellationToken) {
+ await Task.WhenAll(Users.Select(Cleanup).ToList());
+ await Task.WhenAll(Monitors.Select(Cleanup).ToList());
+ await Task.WhenAll(Admins.Select(Cleanup).ToList());
+ }
+
+ private async Task Cleanup(ClientContainer container) {
+ if (container.Client == null) return;
+ try {
+ await container.Client.DeleteAccount(container.Auth);
+ }
+ catch {
+ Console.WriteLine("Failed to delete account for user: " + container.Auth.Username);
+ }
+ }
+
+ public class ClientContainer {
+ public RegisterDto Auth { get; set; } = new() {
+ Email = $"{Guid.NewGuid()}@example.com",
+ Username = $"user-{Guid.NewGuid()}",
+ Password = Guid.NewGuid().ToString(),
+ UserType = "user"
+ };
+
+ public SafeNSoundClient? Client { get; set; }
+ public WhoAmI? WhoAmI { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/testFrontend/SafeNSound.FakeUser/appsettings.json b/testFrontend/SafeNSound.FakeUser/appsettings.json
new file mode 100644
index 0000000..f77ef07
--- /dev/null
+++ b/testFrontend/SafeNSound.FakeUser/appsettings.json
@@ -0,0 +1,13 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Trace",
+ "System": "Information",
+ "Microsoft": "Information",
+ "ArcaneLibs.Blazor.Components.AuthorizedImage": "Information"
+ }
+ },
+ "SafeNSound": {
+ "BaseUrl": "http://localhost:3000"
+ }
+}
|