add(test):添加单元测试

This commit is contained in:
西街长安 2025-12-06 19:37:11 +08:00
commit cbff80ba80
170 changed files with 14260 additions and 18 deletions

View File

@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.22" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="xunit" Version="2.5.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\IM_API\IM_API.csproj" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,205 @@
using Xunit;
using Microsoft.EntityFrameworkCore;
using Moq;
using AutoMapper;
using IM_API.Services;
using IM_API.Models;
using IM_API.Dtos;
using IM_API.Exceptions;
using IM_API.Tools;
using Microsoft.Extensions.Logging;
using System;
using System.Threading;
using System.Threading.Tasks;
public class AuthServiceTests
{
// ---------- 辅助:创建独立的 InMemory DbContext ----------
private ImContext CreateDbContext()
{
var options = new DbContextOptionsBuilder<ImContext>()
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.Options;
var context = new ImContext(options);
return context;
}
// ---------- 可用于模拟 SaveChanges 抛异常的 DbContext ----------
private class ThrowOnSaveImContext : ImContext
{
private readonly bool _throw;
public ThrowOnSaveImContext(DbContextOptions<ImContext> options, bool throwOnSave) : base(options)
{
_throw = throwOnSave;
}
public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
if (_throw) throw new InvalidOperationException("Simulated DB save error");
return base.SaveChangesAsync(cancellationToken);
}
}
// ---------- 默认 AutoMapper简易映射配置 ----------
private IMapper CreateMapper()
{
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<RegisterRequestDto, User>();
cfg.CreateMap<User, UserInfoDto>();
});
return config.CreateMapper();
}
// ---------- 创建 Service允许注入自定义 mapper ----------
private AuthService CreateService(ImContext context, IMapper mapper = null)
{
var loggerMock = new Mock<ILogger<AuthService>>();
var mapperToUse = mapper ?? CreateMapper();
return new AuthService(context, loggerMock.Object, mapperToUse);
}
// --------------------- 测试用例 ---------------------
[Fact]
public async Task LoginAsync_ShouldReturnUser_WhenValidCredentials()
{
var context = CreateDbContext();
context.Users.Add(new User { Id = 1, Username = "Tom", Password = "123456", NickName = "测试用户" });
await context.SaveChangesAsync();
var service = CreateService(context);
var result = await service.LoginAsync(new LoginRequestDto
{
Username = "Tom",
Password = "123456"
});
Assert.NotNull(result);
Assert.Equal(1, result.Id);
Assert.Equal("Tom", result.Username);
}
[Fact]
public async Task LoginAsync_ShouldThrowException_WhenInvalidCredentials()
{
var context = CreateDbContext();
var service = CreateService(context);
await Assert.ThrowsAsync<BaseException>(async () =>
{
await service.LoginAsync(new LoginRequestDto
{
Username = "NotExist",
Password = "wrong"
});
});
}
[Fact]
public async Task LoginAsync_IsCaseSensitive_ForUsername()
{
// 说明:当前实现是精确匹配 => 区分大小写
var context = CreateDbContext();
context.Users.Add(new User { Id = 10, Username = "Tom", Password = "p" });
await context.SaveChangesAsync();
var service = CreateService(context);
// 使用小写用户名应失败(表明当前逻辑区分大小写)
await Assert.ThrowsAsync<BaseException>(async () =>
{
await service.LoginAsync(new LoginRequestDto { Username = "tom", Password = "p" });
});
}
[Fact]
public async Task RegisterAsync_ShouldPersistUserAndReturnDto_WhenNewUser()
{
var context = CreateDbContext();
var service = CreateService(context);
var request = new RegisterRequestDto
{
Username = "Jerry",
Password = "123456"
};
var result = await service.RegisterAsync(request);
// 返回值正确
Assert.NotNull(result);
Assert.Equal("Jerry", result.Username);
Assert.True(result.Id > 0);
// DB 中确实存在该用户
var persisted = await context.Users.FirstOrDefaultAsync(u => u.Username == "Jerry");
Assert.NotNull(persisted);
Assert.Equal("Jerry", persisted.Username);
}
[Fact]
public async Task RegisterAsync_ShouldThrowException_WhenUserExists()
{
var context = CreateDbContext();
context.Users.Add(new User { Username = "Tom", Password = "12223" });
await context.SaveChangesAsync();
var service = CreateService(context);
var request = new RegisterRequestDto { Username = "Tom", Password = "123" };
await Assert.ThrowsAsync<BaseException>(() => service.RegisterAsync(request));
}
[Fact]
public async Task RegisterAsync_ShouldThrow_WhenMapperThrows()
{
var context = CreateDbContext();
var mapperMock = new Mock<IMapper>();
mapperMock.Setup(m => m.Map<User>(It.IsAny<RegisterRequestDto>()))
.Throws(new Exception("mapper failure"));
var service = CreateService(context, mapperMock.Object);
var req = new RegisterRequestDto { Username = "A", Password = "B" };
var ex = await Assert.ThrowsAsync<Exception>(() => service.RegisterAsync(req));
Assert.Contains("mapper failure", ex.Message);
}
[Fact]
public async Task RegisterAsync_ShouldPropagateSaveException_WhenSaveFails()
{
var options = new DbContextOptionsBuilder<ImContext>()
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.Options;
// 使用自定义上下文在 SaveChanges 时抛异常
var throwingContext = new ThrowOnSaveImContext(options, throwOnSave: true);
var service = CreateService(throwingContext);
var req = new RegisterRequestDto { Username = "X", Password = "P" };
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => service.RegisterAsync(req));
Assert.Contains("Simulated DB save error", ex.Message);
}
[Fact]
public async Task RegisterAsync_NullDto_ThrowsNullReferenceException_CurrentBehavior()
{
// 说明:当前实现没有对 dto 为 null 做检查,会触发 NullReferenceException。
// 建议:在生产代码中改为抛 ArgumentNullException 或者进行参数校验并返回明确错误。
var context = CreateDbContext();
var service = CreateService(context);
await Assert.ThrowsAsync<NullReferenceException>(async () =>
{
await service.RegisterAsync(null);
});
}
}

View File

@ -0,0 +1,158 @@
using Xunit;
using Microsoft.EntityFrameworkCore;
using Moq;
using AutoMapper;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Threading.Tasks;
using System;
using IM_API.Services;
using IM_API.Models;
using IM_API.Dtos;
using IM_API.Exceptions;
using IM_API.Tools;
public class FriendServiceTests
{
private ImContext CreateDbContext()
{
var options = new DbContextOptionsBuilder<ImContext>()
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.Options;
return new ImContext(options);
}
private IMapper CreateMapper()
{
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<Friend, FriendInfoDto>();
cfg.CreateMap<FriendRequestDto, FriendRequest>();
cfg.CreateMap<HandleFriendRequestDto, FriendRequest>();
cfg.CreateMap<FriendRequest, Friend>()
.ForMember(dest => dest.UserId, opt => opt.MapFrom(src => src.ResponseUser))
.ForMember(dest => dest.FriendId, opt => opt.MapFrom(src => src.RequestUser))
.ForMember(dest => dest.RemarkName, opt => opt.MapFrom(src => "AutoAdded"));
});
return config.CreateMapper();
}
private FriendService CreateService(ImContext context)
{
var logger = new Mock<ILogger<FriendService>>();
return new FriendService(context, logger.Object, CreateMapper());
}
// --------------------------- 测试 BlockFriendAsync ---------------------------
[Fact]
public async Task BlockFriendAsync_Should_Set_Status_To_Blocked()
{
var context = CreateDbContext();
context.Friends.Add(new Friend
{
Id = 1,
UserId = 10,
FriendId = 20,
StatusEnum = FriendStatus.Added,
RemarkName = "test remark"
});
await context.SaveChangesAsync();
var service = CreateService(context);
var result = await service.BlockeFriendAsync(1);
Assert.True(result);
Assert.Equal(FriendStatus.Blocked, context.Friends.Find(1).StatusEnum);
}
[Fact]
public async Task BlockFriendAsync_Should_Throw_When_NotFound()
{
var service = CreateService(CreateDbContext());
await Assert.ThrowsAsync<BaseException>(() => service.BlockeFriendAsync(99));
}
// --------------------------- 删除好友关系 ---------------------------
[Fact]
public async Task DeleteFriendAsync_Should_Remove_Friend()
{
var context = CreateDbContext();
context.Friends.Add(new Friend
{
Id = 2,
UserId = 1,
FriendId = 3,
RemarkName = "remark",
StatusEnum = FriendStatus.Added
});
await context.SaveChangesAsync();
var service = CreateService(context);
var result = await service.DeleteFriendAsync(2);
Assert.True(result);
Assert.Empty(context.Friends);
}
// --------------------------- 获取好友列表 ---------------------------
[Fact]
public async Task GetFriendListAsync_Should_Return_Only_Added_Friends()
{
var context = CreateDbContext();
context.Friends.AddRange(new List<Friend>
{
new Friend{ UserId = 1, FriendId = 2, RemarkName ="a1", StatusEnum = FriendStatus.Added },
new Friend{ UserId = 1, FriendId = 3, RemarkName ="a2", StatusEnum = FriendStatus.Blocked }
});
await context.SaveChangesAsync();
var service = CreateService(context);
var result = await service.GetFriendListAsync(1, 1, 10, false);
Assert.Single(result);
}
// --------------------------- 发起好友请求 ---------------------------
[Fact]
public async Task SendFriendRequestAsync_Should_Succeed()
{
var context = CreateDbContext();
context.Users.Add(new User { Id = 10, Username = "A", Password = "123" });
context.Users.Add(new User { Id = 20, Username = "B", Password = "123" });
await context.SaveChangesAsync();
var service = CreateService(context);
var result = await service.SendFriendRequestAsync(new FriendRequestDto
{
FromUserId = 10,
ToUserId = 20
});
Assert.True(result);
Assert.Single(context.FriendRequests);
Assert.Single(context.Friends);
}
[Fact]
public async Task SendFriendRequestAsync_Should_Throw_When_User_NotFound()
{
var context = CreateDbContext();
context.Users.Add(new User { Id = 10, Username = "A", Password = "123" });
await context.SaveChangesAsync();
var service = CreateService(context);
await Assert.ThrowsAsync<BaseException>(() => service.SendFriendRequestAsync(new FriendRequestDto
{
FromUserId = 10,
ToUserId = 99
}));
}
}

View File

@ -0,0 +1,182 @@
using Xunit;
using Microsoft.EntityFrameworkCore;
using Moq;
using AutoMapper;
using IM_API.Services;
using IM_API.Models;
using IM_API.Dtos;
using IM_API.Exceptions;
using IM_API.Tools;
using Microsoft.Extensions.Logging;
using System;
using System.Threading.Tasks;
public class UserServiceTests
{
private ImContext CreateDbContext()
{
var options = new DbContextOptionsBuilder<ImContext>()
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.Options;
return new ImContext(options);
}
private IMapper CreateMapper()
{
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<User, UserInfoDto>();
cfg.CreateMap<UpdateUserDto, User>();
});
return config.CreateMapper();
}
private UserService CreateService(ImContext context)
{
var loggerMock = new Mock<ILogger<UserService>>();
var mapper = CreateMapper();
return new UserService(context, loggerMock.Object, mapper);
}
// ========== GetUserInfoAsync ==========
[Fact]
public async Task GetUserInfoAsync_ShouldReturnUser_WhenExists()
{
var context = CreateDbContext();
context.Users.Add(new User { Id = 1, Username = "Tom", Password = "123456" });
await context.SaveChangesAsync();
var service = CreateService(context);
var result = await service.GetUserInfoAsync(1);
Assert.NotNull(result);
Assert.Equal("Tom", result.Username);
}
[Fact]
public async Task GetUserInfoAsync_ShouldThrow_WhenNotFound()
{
var service = CreateService(CreateDbContext());
await Assert.ThrowsAsync<BaseException>(() =>
service.GetUserInfoAsync(999)
);
}
// ========== GetUserInfoByUsernameAsync ==========
[Fact]
public async Task GetUserInfoByUsernameAsync_ShouldReturn_WhenExists()
{
var context = CreateDbContext();
context.Users.Add(new User { Id = 2, Username = "Jerry", Password = "aaa" });
await context.SaveChangesAsync();
var service = CreateService(context);
var result = await service.GetUserInfoByUsernameAsync("Jerry");
Assert.NotNull(result);
Assert.Equal(2, result.Id);
}
[Fact]
public async Task GetUserInfoByUsernameAsync_ShouldThrow_WhenNotFound()
{
var service = CreateService(CreateDbContext());
await Assert.ThrowsAsync<BaseException>(() =>
service.GetUserInfoByUsernameAsync("Nobody")
);
}
// ========== ResetPasswordAsync ==========
[Fact]
public async Task ResetPasswordAsync_ShouldReset_WhenCorrectPassword()
{
var context = CreateDbContext();
context.Users.Add(new User { Id = 3, Username = "test", Password = "old" });
await context.SaveChangesAsync();
var service = CreateService(context);
var result = await service.ResetPasswordAsync(3, "old", "new");
Assert.True(result);
Assert.Equal("new", context.Users.Find(3).Password);
}
[Fact]
public async Task ResetPasswordAsync_ShouldThrow_WhenWrongPassword()
{
var context = CreateDbContext();
context.Users.Add(new User { Id = 4, Username = "test", Password = "oldPwd" });
await context.SaveChangesAsync();
var service = CreateService(context);
await Assert.ThrowsAsync<BaseException>(() =>
service.ResetPasswordAsync(4, "wrong", "new")
);
}
[Fact]
public async Task ResetPasswordAsync_ShouldThrow_WhenUserNotFound()
{
var service = CreateService(CreateDbContext());
await Assert.ThrowsAsync<BaseException>(() =>
service.ResetPasswordAsync(123, "anything", "new")
);
}
// ========== UpdateOlineStatusAsync ==========
[Fact]
public async Task UpdateOlineStatusAsync_ShouldUpdateStatus()
{
var context = CreateDbContext();
context.Users.Add(new User { Id = 5, Username = "test", Password = "p", OnlineStatusEnum = UserOnlineStatus.Offline });
await context.SaveChangesAsync();
var service = CreateService(context);
var result = await service.UpdateOlineStatusAsync(5, UserOnlineStatus.Online);
Assert.True(result);
Assert.Equal(UserOnlineStatus.Online, context.Users.Find(5).OnlineStatusEnum);
}
[Fact]
public async Task UpdateOlineStatusAsync_ShouldThrow_WhenUserNotFound()
{
var service = CreateService(CreateDbContext());
await Assert.ThrowsAsync<BaseException>(() =>
service.UpdateOlineStatusAsync(999, UserOnlineStatus.Online)
);
}
// ========== UpdateUserAsync ==========
[Fact]
public async Task UpdateUserAsync_ShouldUpdateUserFields()
{
var context = CreateDbContext();
context.Users.Add(new User { Id = 6, Username = "test", Password = "p", NickName = "OldName" });
await context.SaveChangesAsync();
var service = CreateService(context);
var dto = new UpdateUserDto { NickName = "NewName" };
var result = await service.UpdateUserAsync(6, dto);
Assert.NotNull(result);
Assert.Equal("NewName", context.Users.Find(6).NickName);
}
[Fact]
public async Task UpdateUserAsync_ShouldThrow_WhenNotFound()
{
var service = CreateService(CreateDbContext());
await Assert.ThrowsAsync<BaseException>(() =>
service.UpdateUserAsync(123, new UpdateUserDto { NickName = "Test" })
);
}
}

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,19 @@
{
"runtimeOptions": {
"tfm": "net8.0",
"frameworks": [
{
"name": "Microsoft.NETCore.App",
"version": "8.0.0"
},
{
"name": "Microsoft.AspNetCore.App",
"version": "8.0.0"
}
],
"configProperties": {
"System.Reflection.NullabilityInfoContext.IsSupported": true,
"System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": false
}
}
}

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,20 @@
{
"runtimeOptions": {
"tfm": "net8.0",
"frameworks": [
{
"name": "Microsoft.NETCore.App",
"version": "8.0.0"
},
{
"name": "Microsoft.AspNetCore.App",
"version": "8.0.0"
}
],
"configProperties": {
"System.GC.Server": true,
"System.Reflection.NullabilityInfoContext.IsSupported": true,
"System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": false
}
}
}

View File

@ -0,0 +1 @@
{"Version":1,"ManifestType":"Build","Endpoints":[]}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@ -0,0 +1,20 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"Jwt": {
"Key": "YourSuperSecretKey123456784124214190!",
"Issuer": "IMDemo",
"Audience": "IMClients",
"AccessTokenMinutes": 15,
"RefreshTokenDays": 30
},
"ConnectionStrings": {
"DefaultConnection": "Server=frp-era.com;Port=26582;Database=IM;User=product;Password=12345678;",
"Redis": "192.168.5.100:6379"
}
}

Some files were not shown because too many files have changed in this diff Show More