Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
d27b87f
chore(deps): Bump @angular/common in /eform-client (#7993)
dependabot[bot] Sep 6, 2026
a744367
chore(deps): Bump @angular/core from 20.3.17 to 20.3.27 in /eform-cli…
dependabot[bot] Sep 6, 2026
86021f1
chore(deps): Bump immutable from 5.1.5 to 5.1.9 in /eform-client (#7985)
dependabot[bot] Sep 6, 2026
721cdaa
chore(deps): Bump sigstore from 4.1.0 to 4.1.1 in /eform-client (#7964)
dependabot[bot] Sep 6, 2026
38c1f27
chore(deps): Bump form-data from 4.0.5 to 4.0.6 in /eform-client (#7955)
dependabot[bot] Sep 6, 2026
d554d31
chore(deps): Bump launch-editor from 2.13.0 to 2.14.1 in /eform-clien…
dependabot[bot] Sep 6, 2026
105009d
chore(deps): Bump ip-address from 10.2.0 to 10.4.0 in /eform-client (…
dependabot[bot] Sep 6, 2026
f7916ee
chore(deps): Bump tar from 7.5.11 to 7.5.22 in /eform-client (#7988)
dependabot[bot] Sep 6, 2026
7488e7c
chore(deps): Bump tmp from 0.2.5 to 0.2.7 in /eform-client (#7913)
dependabot[bot] Sep 6, 2026
871d77b
Bump Sentry from 6.9.0 to 6.10.0 (#8034)
dependabot[bot] Sep 6, 2026
b13c4b5
chore(deps): Bump browserslist from 4.28.1 to 4.28.8 in /eform-client…
dependabot[bot] Sep 6, 2026
3cdc9be
fix(eform-files): make picture upload atomic and idempotent; localize…
renemadsen Sep 6, 2026
b603924
Merge pull request #8039 from microting/fix/8036-picture-upload-atomic
renemadsen Sep 6, 2026
c7aeca3
ci: point the Jest job at test:ci, and correct TESTING.md
renemadsen Sep 9, 2026
9839c71
Merge pull request #8045 from microting/ci/jest-ci-invocation
renemadsen Sep 9, 2026
7f4c34a
fix(i18n): restore the {{value}} token in 'Generate report' for da, f…
renemadsen Sep 11, 2026
5b7dac9
feat(device-users): only the first user can delete a device user (#8059)
renemadsen Sep 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/dotnet-core-master.yml
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ jobs:
- name: Install dependencies
run: cd eform-client && yarn install --frozen-lockfile
- name: Run Jest unit tests
run: cd eform-client && npm run test:unit
run: cd eform-client && npm run test:ci
test-dotnet:
runs-on: ubuntu-latest
steps:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/dotnet-core-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ jobs:
- name: Install dependencies
run: cd eform-client && yarn install --frozen-lockfile
- name: Run Jest unit tests
run: cd eform-client && npm run test:unit
run: cd eform-client && npm run test:ci
test-dotnet:
runs-on: ubuntu-latest
steps:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

using System;
using System.Collections.Generic;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Threading.Tasks;
using NUnit.Framework;
using eFormAPI.Web.Services;
using Microsoft.Extensions.Logging;
Expand All @@ -48,6 +53,10 @@ public class AuthServiceTests : DbTestFixture
private ILocalizationService _localizationService;
private IClaimsService _claimsService;
private IAuthCacheService _authCacheService;
#pragma warning disable NUnit1032
private UserManager<EformUser> _userManager;
private RoleManager<EformRole> _roleManager;
#pragma warning restore NUnit1032
private AuthService _authService;

public override void DoSetup()
Expand All @@ -60,15 +69,15 @@ public override void DoSetup()
_claimsService = Substitute.For<IClaimsService>();
_authCacheService = Substitute.For<IAuthCacheService>();

var roleManager = Substitute.For<RoleManager<EformRole>>(
_roleManager = Substitute.For<RoleManager<EformRole>>(
Substitute.For<IRoleStore<EformRole>>(), null, null, null, null);
var signInManager = Substitute.For<SignInManager<EformUser>>(
Substitute.For<UserManager<EformUser>>(
Substitute.For<IUserStore<EformUser>>(), null, null, null, null, null, null, null, null),
Substitute.For<Microsoft.AspNetCore.Http.IHttpContextAccessor>(),
Substitute.For<IUserClaimsPrincipalFactory<EformUser>>(),
null, null, null, null);
var userManager = Substitute.For<UserManager<EformUser>>(
_userManager = Substitute.For<UserManager<EformUser>>(
Substitute.For<IUserStore<EformUser>>(), null, null, null, null, null, null, null, null);

_localizationService.GetString(Arg.Any<string>())
Expand All @@ -81,9 +90,9 @@ public override void DoSetup()
_tokenOptions,
_logger,
_appSettings,
roleManager,
_roleManager,
signInManager,
userManager,
_userManager,
_userService,
_localizationService,
_claimsService,
Expand All @@ -96,5 +105,42 @@ public void AuthService_InitializesCorrectly()
// Assert
Assert.That(_authService, Is.Not.Null);
}

// A refreshed token must say whether the caller is the first user, exactly as Login
// does: the client replaces its stored user with the refresh result, so a missing
// flag would drop the first user's first-user-only actions after every refresh.
[TestCase(1, true)]
[TestCase(2, false)]
public async Task RefreshToken_ReportsWhetherTheCallerIsTheFirstUser(int userId, bool expectedIsFirstUser)
{
// Arrange
const int firstUserId = 1;
var user = new EformUser
{
Id = userId,
UserName = $"user{userId}@example.com",
Email = $"user{userId}@example.com"
};
_userService.UserId.Returns(userId);
_userService.GetByIdAsync(userId).Returns(user);
_userService.GetFirstUserIdInDb().Returns(firstUserId);
_userManager.GetRolesAsync(user).Returns(new List<string> { EformRole.Admin });
_userManager.GetClaimsAsync(user).Returns(new List<Claim>());
_roleManager.FindByNameAsync(Arg.Any<string>()).Returns((EformRole)null);
_claimsService.GetUserPermissions(userId, Arg.Any<bool>()).Returns(new List<Claim>());
// HMAC-SHA256 needs a key of at least 256 bits; generate one per run.
_tokenOptions.Value.Returns(new EformTokenOptions
{
Issuer = "eform-tests",
SigningKey = Convert.ToHexString(RandomNumberGenerator.GetBytes(32))
});

// Act
var result = await _authService.RefreshToken();

// Assert
Assert.That(result.Success, Is.True, result.Message);
Assert.That(result.Model.IsFirstUser, Is.EqualTo(expectedIsFirstUser));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
The MIT License (MIT)

Copyright (c) 2007 - 2021 Microting A/S

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

using System.Threading.Tasks;
using NUnit.Framework;
using eFormAPI.Web.Services;
using Microsoft.Extensions.Logging;
using NSubstitute;
using Microting.eFormApi.BasePn.Infrastructure.Models.API;

namespace eFormAPI.Web.Integration.Tests.Services
{
/// <summary>
/// The first-user delete rule on DELETE api/device-users/delete/{id}.
/// </summary>
[TestFixture]
public class DeviceUsersServiceDeleteTests : FirstUserOnlyDeleteTestsBase
{
protected override Task<OperationResult> DeleteAsync(int id) =>
new DeviceUsersService(
LocalizationService,
CoreHelper,
UserService,
Substitute.For<ILogger<DeviceUsersService>>()).Delete(id);

protected override string SdkFailureMessageKey => "DeviceUserParamCouldNotBeDeleted";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ public override void DoSetup()
{
_localizationService = Substitute.For<ILocalizationService>();
_coreHelper = Substitute.For<IEFormCoreService>();
var userService = Substitute.For<IUserService>();
_logger = Substitute.For<ILogger<DeviceUsersService>>();

_localizationService.GetString(Arg.Any<string>())
Expand All @@ -52,6 +53,7 @@ public override void DoSetup()
_deviceUsersService = new DeviceUsersService(
_localizationService,
_coreHelper,
userService,
_logger);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/*
The MIT License (MIT)

Copyright (c) 2007 - 2021 Microting A/S

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

using System;
using System.Threading.Tasks;
using NUnit.Framework;
using NSubstitute;
using Microting.eFormApi.BasePn.Abstractions;
using Microting.eFormApi.BasePn.Infrastructure.Models.API;
using eFormAPI.Web.Abstractions;

namespace eFormAPI.Web.Integration.Tests.Services
{
/// <summary>
/// Only the first user (lowest AspNetUsers Id) may delete a device user, on every path
/// that removes its Site row; everyone else, admins included, is refused before the SDK
/// is touched. Each subclass points <see cref="DeleteAsync"/> at one delete path and
/// inherits these tests. They never touch the database, so they avoid <c>DbTestFixture</c>.
/// </summary>
public abstract class FirstUserOnlyDeleteTestsBase
{
private const int FirstUserId = 1;
private const int OtherUserId = 2;
private const int SiteId = 42;

protected ILocalizationService LocalizationService;
protected IEFormCoreService CoreHelper;
protected IUserService UserService;

/// <summary>Calls the delete path under test with fresh service wiring.</summary>
protected abstract Task<OperationResult> DeleteAsync(int id);

/// <summary>The key the service reports when the (faked) SDK delete fails.</summary>
protected abstract string SdkFailureMessageKey { get; }

[SetUp]
public void SetUpSubstitutes()
{
LocalizationService = Substitute.For<ILocalizationService>();
CoreHelper = Substitute.For<IEFormCoreService>();
UserService = Substitute.For<IUserService>();

LocalizationService.GetString(Arg.Any<string>())
.Returns(args => args.Arg<string>());
LocalizationService.GetStringWithFormat(Arg.Any<string>(), Arg.Any<object[]>())
.Returns(args => args.ArgAt<string>(0));

UserService.GetFirstUserIdInDb().Returns(FirstUserId);

// Every SDK call in Delete goes through the core instance, so a GetCore that
// throws marks the point where the SDK path is entered.
CoreHelper.GetCore()
.Returns<eFormCore.Core>(_ => throw new InvalidOperationException("SDK reached"));
}

[Test]
public async Task Delete_ByAdminWhoIsNotTheFirstUser_IsRefusedWithoutReachingTheSdk()
{
// Arrange
UserService.UserId.Returns(OtherUserId);
// Admin status is deliberately irrelevant: an admin who is not the first user is refused.
UserService.IsAdmin().Returns(true);

// Act
var result = await DeleteAsync(SiteId);

// Assert
Assert.That(result.Success, Is.False);
Assert.That(result.Message, Is.EqualTo("OnlyTheFirstUserCanDeleteWorkers"));
// The SDK delete is only reachable through GetCore.
await CoreHelper.DidNotReceive().GetCore();
}

[Test]
public async Task Delete_WithoutAUserIdAgainstAnEmptyUsersTable_IsRefusedWithoutReachingTheSdk()
{
// Arrange: no signed-in user (UserId 0) and no users, so GetFirstUserIdInDb is 0 too.
UserService.UserId.Returns(0);
UserService.GetFirstUserIdInDb().Returns(0);

// Act
var result = await DeleteAsync(SiteId);

// Assert
Assert.That(result.Success, Is.False);
Assert.That(result.Message, Is.EqualTo("OnlyTheFirstUserCanDeleteWorkers"));
await CoreHelper.DidNotReceive().GetCore();
}

[Test]
public async Task Delete_ByTheFirstUser_PassesTheCheckAndReachesTheSdk()
{
// Arrange
UserService.UserId.Returns(FirstUserId);

// Act
var result = await DeleteAsync(SiteId);

// Assert
await UserService.Received(1).GetFirstUserIdInDb();
await CoreHelper.Received(1).GetCore();
// The faked SDK fails, so the result is the SDK-failure message, not the refusal.
Assert.That(result.Success, Is.False);
Assert.That(result.Message, Is.EqualTo(SdkFailureMessageKey));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
The MIT License (MIT)

Copyright (c) 2007 - 2021 Microting A/S

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

using System.Threading.Tasks;
using NUnit.Framework;
using eFormAPI.Web.Services;
using Microsoft.Extensions.Logging;
using NSubstitute;
using Microting.eFormApi.BasePn.Infrastructure.Models.API;

namespace eFormAPI.Web.Integration.Tests.Services
{
/// <summary>
/// The first-user delete rule on Advanced &gt; Sites, which removes the same Site row.
/// </summary>
[TestFixture]
public class SitesServiceDeleteTests : FirstUserOnlyDeleteTestsBase
{
protected override Task<OperationResult> DeleteAsync(int id) =>
new SitesService(
CoreHelper,
LocalizationService,
UserService,
Substitute.For<ILogger<SitesService>>()).Delete(id);

protected override string SdkFailureMessageKey => "SiteParamCouldNotBeDeleted";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ public override void DoSetup()
{
_coreHelper = Substitute.For<IEFormCoreService>();
_localizationService = Substitute.For<ILocalizationService>();
var userService = Substitute.For<IUserService>();
_logger = Substitute.For<ILogger<SitesService>>();

_localizationService.GetString(Arg.Any<string>())
Expand All @@ -51,6 +52,7 @@ public override void DoSetup()
_sitesService = new SitesService(
_coreHelper,
_localizationService,
userService,
_logger);
}

Expand Down
Loading
Loading