Publish from private repository

This commit is contained in:
Gitea Actions
2026-08-01 11:51:13 +00:00
commit 0304b03201
694 changed files with 69367 additions and 0 deletions
+65
View File
@@ -0,0 +1,65 @@
using MyOffice.Data.Models.Accounts;
using MyOffice.Services.Account;
using AccountEntity = MyOffice.Data.Models.Accounts.Account;
namespace MyOffice.Tests.Accounts;
public class AccountAclTests
{
private static AccountEntity CreateAccount(Guid userId, bool write, bool manage)
{
return new AccountEntity
{
Id = Guid.NewGuid(),
Name = "Cash",
AccessRights =
[
new AccountAccess
{
UserId = userId,
IsAllowRead = true,
IsAllowWrite = write,
IsAllowManage = manage,
}
]
};
}
[Fact]
public void CanWrite_true_when_flag_set()
{
var userId = Guid.NewGuid();
var account = CreateAccount(userId, write: true, manage: false);
Assert.True(AccountAcl.CanWrite(account, userId));
Assert.False(AccountAcl.CanManage(account, userId));
}
[Fact]
public void CanManage_true_when_flag_set()
{
var userId = Guid.NewGuid();
var account = CreateAccount(userId, write: false, manage: true);
Assert.False(AccountAcl.CanWrite(account, userId));
Assert.True(AccountAcl.CanManage(account, userId));
}
[Fact]
public void CanWrite_false_for_other_user()
{
var account = CreateAccount(Guid.NewGuid(), write: true, manage: true);
Assert.False(AccountAcl.CanWrite(account, Guid.NewGuid()));
Assert.False(AccountAcl.CanManage(account, Guid.NewGuid()));
}
[Fact]
public void CanWrite_false_when_access_rights_null()
{
var account = new AccountEntity { Id = Guid.NewGuid(), Name = "X", AccessRights = null };
Assert.False(AccountAcl.CanWrite(account, Guid.NewGuid()));
Assert.False(AccountAcl.CanManage(account, Guid.NewGuid()));
}
}
@@ -0,0 +1,30 @@
using MyOffice.Services.Account;
namespace MyOffice.Tests.Accounts;
public class AccountMotionBalancingTests
{
[Fact]
public void ResolveAmounts_income_primary_creates_expense_on_balancing()
{
var (plus, minus) = AccountMotionBalancing.ResolveAmounts(primaryPlus: 100m, amountBalancing: 100m);
Assert.Equal(0m, plus);
Assert.Equal(100m, minus);
}
[Fact]
public void ResolveAmounts_expense_primary_creates_income_on_balancing()
{
var (plus, minus) = AccountMotionBalancing.ResolveAmounts(primaryPlus: 0m, amountBalancing: 50m);
Assert.Equal(50m, plus);
Assert.Equal(0m, minus);
}
[Fact]
public void BalancingItemName_prefixes_account_name()
{
Assert.Equal("+Wallet", AccountMotionBalancing.BalancingItemName("Wallet"));
}
}
+173
View File
@@ -0,0 +1,173 @@
using AutoMapper;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using MyOffice.Data.Models.Accounts;
using MyOffice.Data.Models.Items;
using MyOffice.Data.Repositories.Account;
using MyOffice.Data.Repositories.Currency;
using MyOffice.Data.Repositories.Item;
using MyOffice.Services.Account;
using MyOffice.Services.Account.Domain;
using MyOffice.Services.Identity;
using MyOffice.Services.Item;
using AccountEntity = MyOffice.Data.Models.Accounts.Account;
namespace MyOffice.Tests.Accounts;
public class MotionAddAclTests
{
[Fact]
public async Task MotionAddAsync_returns_forbidden_without_write_access()
{
var userId = Guid.NewGuid();
var accountId = Guid.NewGuid();
var account = new AccountEntity
{
Id = accountId,
Name = "Cash",
AccessRights =
[
new AccountAccess
{
UserId = userId,
IsAllowRead = true,
IsAllowWrite = false,
IsAllowManage = false,
}
]
};
var accountRepo = new Mock<IAccountRepository>();
accountRepo
.Setup(x => x.GetAsync(userId, accountId, It.IsAny<CancellationToken>()))
.ReturnsAsync(account);
var service = CreateService(accountRepo.Object);
var exec = await service.MotionAddAsync(
userId,
accountId,
new MotionAddUpdate
{
Date = DateTime.UtcNow,
Item = "Coffee",
Plus = 0,
Minus = 10,
});
Assert.Equal(MotionAddStatus.forbidden, exec.Status);
}
[Fact]
public async Task MotionAddAsync_skips_balancing_when_counterparty_not_writable()
{
var userId = Guid.NewGuid();
var primaryId = Guid.NewGuid();
var balancingId = Guid.NewGuid();
var primary = new AccountEntity
{
Id = primaryId,
Name = "Cash",
AccessRights =
[
new AccountAccess { UserId = userId, IsAllowRead = true, IsAllowWrite = true }
]
};
var balancing = new AccountEntity
{
Id = balancingId,
Name = "Bank",
AccessRights =
[
new AccountAccess { UserId = userId, IsAllowRead = true, IsAllowWrite = false }
]
};
var accountRepo = new Mock<IAccountRepository>();
accountRepo
.Setup(x => x.GetAsync(userId, primaryId, It.IsAny<CancellationToken>()))
.ReturnsAsync(primary);
accountRepo
.Setup(x => x.GetAsync(userId, balancingId, It.IsAny<CancellationToken>()))
.ReturnsAsync(balancing);
var motionRepo = new Mock<IMotionRepository>();
motionRepo
.Setup(x => x.AddAsync(It.IsAny<Motion>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
var service = CreateService(accountRepo.Object, motionRepo.Object);
var exec = await service.MotionAddAsync(
userId,
primaryId,
new MotionAddUpdate
{
Date = DateTime.UtcNow,
Item = "Transfer",
Plus = 0,
Minus = 25,
AccountId = balancingId.ToString("N"),
AmountBalancing = 25,
});
Assert.Equal(MotionAddStatus.success, exec.Status);
Assert.NotNull(exec.Result);
Assert.Single(exec.Result!);
motionRepo.Verify(x => x.AddAsync(It.IsAny<Motion>(), It.IsAny<CancellationToken>()), Times.Once);
}
private static ItemService CreateItemServiceStub()
{
var nextId = 1;
var itemRepo = new Mock<IItemRepository>();
var itemGlobalRepo = new Mock<IItemGlobalRepository>();
itemGlobalRepo
.Setup(x => x.GetByNameAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync((ItemGlobal?)null);
itemGlobalRepo
.Setup(x => x.AddAsync(It.IsAny<ItemGlobal>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
itemRepo
.Setup(x => x.GetByGlobalAsync(It.IsAny<Guid>(), It.IsAny<Guid>(), It.IsAny<CancellationToken>()))
.ReturnsAsync((Item?)null);
itemRepo
.Setup(x => x.AddAsync(It.IsAny<Item>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(true)
.Callback<Item, CancellationToken>((i, _) => i.Id = nextId++);
return new ItemService(
Mock.Of<IItemCategoryRepository>(),
itemRepo.Object,
itemGlobalRepo.Object,
new MapperConfiguration(_ => { }, NullLoggerFactory.Instance).CreateMapper());
}
private static AccountService CreateService(
IAccountRepository accountRepository,
IMotionRepository? motionRepository = null)
{
var mapper = new MapperConfiguration(
cfg => cfg.AddProfile<MotionDtoProfile>(),
NullLoggerFactory.Instance).CreateMapper();
return new AccountService(
Mock.Of<ILogger<AccountService>>(),
mapper,
Mock.Of<IAccountCategoryRepository>(),
Mock.Of<IAccountAccessRepository>(),
Mock.Of<IAccountAccessInviteRepository>(),
accountRepository,
Mock.Of<ICurrencyRepository>(),
Mock.Of<IAccountAccountCategoryRepository>(),
motionRepository ?? Mock.Of<IMotionRepository>(),
Mock.Of<IItemRepository>(),
Mock.Of<IItemGlobalRepository>(),
CreateItemServiceStub(),
Mock.Of<IContextProvider>());
}
}
@@ -0,0 +1,22 @@
using MyOffice.Web.Auth;
namespace MyOffice.Tests.Auth;
public class OpenIddictRedirectUriTests
{
[Fact]
public void BuildRedirectUri_uses_localhost_when_host_empty()
{
var uri = OpenIddictSeeder.BuildRedirectUri(null);
Assert.Equal("http://localhost:4300/silent-refresh.html", uri.ToString());
}
[Fact]
public void BuildRedirectUri_appends_silent_refresh_and_trims_slash()
{
var uri = OpenIddictSeeder.BuildRedirectUri("https://app.example.com/");
Assert.Equal("https://app.example.com/silent-refresh.html", uri.ToString());
}
}
@@ -0,0 +1,56 @@
using Microsoft.AspNetCore.Identity;
using MyOffice.Web.Identity.Domain;
using MyOffice.Web.Identity.Repositories;
namespace MyOffice.Tests.Auth;
public class PasswordHasherTests
{
private readonly PasswordHasher _hasher = new();
private readonly ApplicationUser<Guid> _user = new() { Id = Guid.NewGuid(), UserName = "test@example.com" };
[Fact]
public void HashPassword_then_verify_succeeds_with_identity_format()
{
var hash = _hasher.HashPassword(_user, "P@ssw0rd!");
Assert.Equal(PasswordVerificationResult.Success, _hasher.VerifyHashedPassword(_user, hash, "P@ssw0rd!"));
}
[Fact]
public void VerifyHashedPassword_fails_for_wrong_password()
{
var hash = _hasher.HashPassword(_user, "P@ssw0rd!");
Assert.Equal(PasswordVerificationResult.Failed, _hasher.VerifyHashedPassword(_user, hash, "wrong"));
}
[Fact]
public void HashPassword_produces_distinct_hashes_due_to_salt()
{
var a = _hasher.HashPassword(_user, "same");
var b = _hasher.HashPassword(_user, "same");
Assert.NotEqual(a, b);
}
[Fact]
public void VerifyHashedPassword_legacy_hash_returns_rehash_needed()
{
var legacy = PasswordHasher.HashLegacyForTests("legacy-secret");
Assert.Equal(
PasswordVerificationResult.SuccessRehashNeeded,
_hasher.VerifyHashedPassword(_user, legacy, "legacy-secret"));
}
[Fact]
public void VerifyHashedPassword_legacy_wrong_password_fails()
{
var legacy = PasswordHasher.HashLegacyForTests("legacy-secret");
Assert.Equal(
PasswordVerificationResult.Failed,
_hasher.VerifyHashedPassword(_user, legacy, "nope"));
}
}
@@ -0,0 +1,54 @@
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using MyOffice.DbContext;
namespace MyOffice.Tests.Data;
public class DemoDataSeederTests
{
[Fact]
public async Task SeedIfEmpty_creates_demo_users_catalog_and_motions()
{
var path = Path.Combine(Path.GetTempPath(), $"myoffice-seed-{Guid.NewGuid():N}.db");
var connection = new ConnectionConfiguration("sqlite", $"Data Source={path}");
var options = new DbContextOptionsBuilder<AppDbContext>();
DbContextServiceCollectionExtensions.ConfigureDbContextOptions(options, connection);
await using (var db = new AppDbContext(AppDbContextProvidersEnum.sqlite, options.Options))
{
await DatabaseBootstrapper.InitializeAsync(db);
Assert.Equal(3, await db.Users.CountAsync());
Assert.Equal(9, await db.Currencies.CountAsync());
Assert.Equal(9, await db.CurrencyRates.CountAsync());
Assert.Equal(9, await db.AccountCategories.CountAsync());
Assert.Equal(27, await db.Accounts.CountAsync());
Assert.Equal(300, await db.Motions.CountAsync());
var uahUser = await db.Users.SingleAsync(x => x.Email == "user_UAH@user_UAH.userUAH");
Assert.Equal("UAH", uahUser.CurrencyId);
var uahRates = await db.CurrencyRates
.Include(x => x.Currency)
.Where(x => x.Currency!.UserId == uahUser.Id)
.ToListAsync();
Assert.Equal(1m, uahRates.Single(x => x.Currency!.CurrencyGlobalId == "UAH").Rate);
Assert.Equal(42m, uahRates.Single(x => x.Currency!.CurrencyGlobalId == "USD").Rate);
Assert.Equal(50m, uahRates.Single(x => x.Currency!.CurrencyGlobalId == "EUR").Rate);
Assert.Contains(await db.AccountCategories.Where(x => x.UserId == uahUser.Id).Select(x => x.Name).ToListAsync(),
x => x == "Готівка");
Assert.Contains(await db.ItemCategories.Where(x => x.UserId == uahUser.Id).Select(x => x.Name).ToListAsync(),
x => x == "Доходи");
await DemoDataSeeder.SeedIfEmptyAsync(db);
Assert.Equal(3, await db.Users.CountAsync());
}
SqliteConnection.ClearAllPools();
if (File.Exists(path))
{
File.Delete(path);
}
}
}
+29
View File
@@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AutoMapper" Version="16.2.0" />
<PackageReference Include="coverlet.collector" Version="6.0.4" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.10" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MyOffice.Services\MyOffice.Services.csproj" />
<ProjectReference Include="..\MyOffice.Web\MyOffice.Web.csproj" />
</ItemGroup>
</Project>