Compare commits
127 Commits
0.0.1
...
feature-nx
| Author | SHA1 | Date | |
|---|---|---|---|
| 8d952578de | |||
| cf8be0bff5 | |||
| f7772a9c5a | |||
| feed24f378 | |||
| 20013ba6bd | |||
| 536b360127 | |||
| 9e7609d6b4 | |||
| d1db5e6490 | |||
| eb8455e141 | |||
| f7223dc590 | |||
| e38c15ed92 | |||
| faaaaaa1fa | |||
| fef4bbda70 | |||
| 0f42ecb5dc | |||
| c53b896128 | |||
| e6af0a98cd | |||
| e43f7d6365 | |||
| 2ecaa28091 | |||
| 333391d16f | |||
| 123fa6a7aa | |||
| d429560511 | |||
| b96d895809 | |||
| 77db20dc38 | |||
| dc6ecf224d | |||
| 10f79fb537 | |||
| eff83a9f68 | |||
| 46472f03e6 | |||
| ed95bdddac | |||
| 72cdf96241 | |||
| 19fec61cff | |||
| 139037085b | |||
| 58bc8b4b5a | |||
| 07bb01808f | |||
| 6d747327df | |||
| 22038345c3 | |||
| 136199290b | |||
| 83be063e7f | |||
| a5e1b103b5 | |||
| 6e0d11cb24 | |||
| 2d22b4bd27 | |||
| c265868a51 | |||
| 7ebf23ddd8 | |||
| 8938dd40d4 | |||
| a590aa0a26 | |||
| 98b5cad586 | |||
| 3d9335b97b | |||
| 1df3a0f30e | |||
| e65eeb5ff7 | |||
| e05497a478 | |||
| ae99d3290f | |||
| 6cab42679c | |||
| f9cc69698c | |||
| f41e7137fe | |||
| 9d942b38c7 | |||
| 435db45a52 | |||
| a31735dbe2 | |||
| 11840a2f30 | |||
| f7e68555b1 | |||
| cca84f88c0 | |||
| 77744015de | |||
| e18fb9db48 | |||
| bedcf97c9d | |||
| df044e23f1 | |||
| be621e9ae2 | |||
| 204853beb9 | |||
| 507788f6a3 | |||
| ac54af3ff8 | |||
| e7dbb651a2 | |||
| f565e9d8a3 | |||
| d855c8f8fb | |||
| 3b5e8b8eba | |||
| 3b126f17fc | |||
| 448cf25538 | |||
| 2b2d018658 | |||
| 816b01f13a | |||
| da401a20c0 | |||
| 9e826153a4 | |||
| ca4e2b7dd5 | |||
| 57deb5c9bf | |||
| 5323c29549 | |||
| 41963dde70 | |||
| 7d5699ad53 | |||
| 5274f0d22d | |||
| e981ee190f | |||
| 85144dbf6f | |||
| f42e990c45 | |||
| 705c96a230 | |||
| 885b4012ed | |||
| 2565678b22 | |||
| f89007f52b | |||
| 36f442a1c2 | |||
| 0ea560694b | |||
| c764f0eb03 | |||
| 600272486b | |||
| 0a440d2914 | |||
| d641cb3275 | |||
| 26a13c4165 | |||
| 6dd93529c2 | |||
| 86841867f3 | |||
| 191c05461a | |||
| cf09984056 | |||
| a40b768f3b | |||
| ad0ab87747 | |||
| afe2c38f74 | |||
| 964b129e5e | |||
| 4cc1d20b7f | |||
| 06410a75e7 | |||
| 6c564ceba3 | |||
| 9c108de175 | |||
| ceae1a0cd8 | |||
| 62642ee86a | |||
| f8fd54b953 | |||
| 3725af7952 | |||
| 465431ee98 | |||
| 82652efed7 | |||
| 6ca7a8275c | |||
| d55fc351ad | |||
| fe4a9173a4 | |||
| 9ea6b1d50e | |||
| d90366e304 | |||
| cbff80ba80 | |||
| 8203dbe6ec | |||
| 1d633c3909 | |||
| 4c7bdbe9fc | |||
| 7c0cf6941f | |||
| 0675e1807b | |||
| 1807c6905f |
28
.gitea/ISSUE_TEMPLATE/bug.yaml
Normal file
28
.gitea/ISSUE_TEMPLATE/bug.yaml
Normal file
@ -0,0 +1,28 @@
|
||||
name: "IM 缺陷报告"
|
||||
description: "记录聊天消息、SignalR 或缓存相关的问题"
|
||||
title: "[BUG]简要描述问题"
|
||||
labels: ["bug", "high-priority"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "请详细填写 Bug 信息,这将有助于快速定位后端 Redis 或 SignalR 的问题。"
|
||||
- type: input
|
||||
id: stream_key
|
||||
attributes:
|
||||
label: "相关 StreamKey / 群组 ID"
|
||||
placeholder: "例如:group:123"
|
||||
- type: textarea
|
||||
id: steps
|
||||
attributes:
|
||||
label: "复现步骤"
|
||||
value: |
|
||||
1. 登录用户 A 和用户 B
|
||||
2. 用户 A 向群组发送消息
|
||||
3. 用户 B 界面没有反应
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: "控制台错误日志 (Console Log)"
|
||||
placeholder: "在此粘贴浏览器或后端的报错信息..."
|
||||
3
backend/IMTest/.gitignore
vendored
Normal file
3
backend/IMTest/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
bin/
|
||||
obj/
|
||||
.vs/
|
||||
29
backend/IMTest/IMTest.csproj
Normal file
29
backend/IMTest/IMTest.csproj
Normal 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>
|
||||
208
backend/IMTest/Service/AuthServiceTest.cs
Normal file
208
backend/IMTest/Service/AuthServiceTest.cs
Normal file
@ -0,0 +1,208 @@
|
||||
using AutoMapper;
|
||||
using IM_API.Dtos.Auth;
|
||||
using IM_API.Dtos.User;
|
||||
using IM_API.Exceptions;
|
||||
using IM_API.Interface.Services;
|
||||
using IM_API.Models;
|
||||
using IM_API.Services;
|
||||
using IM_API.Tools;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
|
||||
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();
|
||||
var mockCache = new Mock<ICacheService>();
|
||||
return new AuthService(context, loggerMock.Object, mapperToUse,mockCache.Object);
|
||||
}
|
||||
|
||||
// --------------------- 测试用例 ---------------------
|
||||
|
||||
[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);
|
||||
});
|
||||
}
|
||||
}
|
||||
149
backend/IMTest/Service/FriendServiceTest.cs
Normal file
149
backend/IMTest/Service/FriendServiceTest.cs
Normal file
@ -0,0 +1,149 @@
|
||||
using AutoMapper;
|
||||
using IM_API.Domain.Events;
|
||||
using IM_API.Dtos.Friend;
|
||||
using IM_API.Exceptions;
|
||||
using IM_API.Models;
|
||||
using IM_API.Services;
|
||||
using MassTransit; // 必须引入
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
|
||||
public class FriendServiceTests
|
||||
{
|
||||
private readonly Mock<IPublishEndpoint> _mockEndpoint = new();
|
||||
private readonly Mock<ILogger<FriendService>> _mockLogger = new();
|
||||
|
||||
#region 辅助构造方法
|
||||
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<FriendRequest, Friend>()
|
||||
.ForMember(d => d.UserId, o => o.MapFrom(s => s.ResponseUser))
|
||||
.ForMember(d => d.FriendId, o => o.MapFrom(s => s.RequestUser));
|
||||
});
|
||||
return config.CreateMapper();
|
||||
}
|
||||
|
||||
private FriendService CreateService(ImContext context)
|
||||
{
|
||||
// 注入 Mock 对象和真实的 Mapper/Context
|
||||
return new FriendService(context, _mockLogger.Object, CreateMapper(), _mockEndpoint.Object);
|
||||
}
|
||||
#endregion
|
||||
|
||||
[Fact]
|
||||
public async Task SendFriendRequestAsync_Success_ShouldSaveAndPublish()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateDbContext();
|
||||
context.Users.AddRange(
|
||||
new User { Id = 1, Username = "Sender", Password = "..." },
|
||||
new User { Id = 2, Username = "Receiver", Password = "..." }
|
||||
);
|
||||
await context.SaveChangesAsync();
|
||||
var service = CreateService(context);
|
||||
var dto = new FriendRequestDto { ToUserId = 2, FromUserId = 2, Description = "Hello" , RemarkName = "测试备注" };
|
||||
|
||||
// Act
|
||||
//var result = await service.SendFriendRequestAsync(dto);
|
||||
|
||||
// Assert
|
||||
//Assert.True(result);
|
||||
//Assert.Single(context.FriendRequests);
|
||||
|
||||
// 验证事件是否发布到了 MQ
|
||||
/*
|
||||
_mockEndpoint.Verify(x => x.Publish(
|
||||
It.Is<RequestFriendEvent>(e => e.FromUserId == 1 && e.ToUserId == 2),
|
||||
It.IsAny<CancellationToken>()),
|
||||
Times.Once);
|
||||
*/
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendFriendRequestAsync_UserNotFound_ShouldThrow()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateDbContext();
|
||||
var service = CreateService(context);
|
||||
var dto = new FriendRequestDto { ToUserId = 99 }; // 不存在的用户
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<BaseException>(() => service.SendFriendRequestAsync(dto));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendFriendRequestAsync_AlreadyExists_ShouldThrow()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateDbContext();
|
||||
context.Users.Add(new User { Id = 2, Password = "123", Username = "111" });
|
||||
context.FriendRequests.Add(new FriendRequest
|
||||
{
|
||||
RequestUser = 1,
|
||||
ResponseUser = 2,
|
||||
State = (sbyte)FriendRequestState.Pending,
|
||||
RemarkName = "测试备注"
|
||||
});
|
||||
await context.SaveChangesAsync();
|
||||
var service = CreateService(context);
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<BaseException>(() => service.SendFriendRequestAsync(new FriendRequestDto { ToUserId = 2 }));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BlockFriendAsync_ValidId_ShouldUpdateStatus()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateDbContext();
|
||||
var friend = new Friend { Id = 50, UserId = 1, FriendId = 2, StatusEnum = FriendStatus.Added,RemarkName = "测试备注" };
|
||||
context.Friends.Add(friend);
|
||||
await context.SaveChangesAsync();
|
||||
var service = CreateService(context);
|
||||
|
||||
// Act
|
||||
await service.BlockeFriendAsync(50);
|
||||
|
||||
// Assert
|
||||
var updated = await context.Friends.FindAsync(50);
|
||||
Assert.Equal(FriendStatus.Blocked, updated.StatusEnum);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetFriendListAsync_ShouldFilterByStatus()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateDbContext();
|
||||
context.Friends.AddRange(
|
||||
new Friend { UserId = 1, FriendId = 2, StatusEnum = FriendStatus.Added,RemarkName = "111" },
|
||||
new Friend { UserId = 1, FriendId = 3, StatusEnum = FriendStatus.Blocked,RemarkName = "222" }
|
||||
);
|
||||
await context.SaveChangesAsync();
|
||||
var service = CreateService(context);
|
||||
|
||||
// Act
|
||||
var result = await service.GetFriendListAsync(1, 1, 10, false);
|
||||
|
||||
// Assert
|
||||
//Assert.Single(result); // 只应该拿到 Added 状态的
|
||||
}
|
||||
}
|
||||
94
backend/IMTest/Service/GroupServiceTest.cs
Normal file
94
backend/IMTest/Service/GroupServiceTest.cs
Normal file
@ -0,0 +1,94 @@
|
||||
using AutoMapper;
|
||||
using IM_API.Dtos.Group;
|
||||
using IM_API.Models;
|
||||
using IM_API.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using MassTransit;
|
||||
using IM_API.Configs;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
|
||||
namespace IM_API.Tests.Services
|
||||
{
|
||||
public class GroupServiceTests
|
||||
{
|
||||
private readonly ImContext _context;
|
||||
private readonly IMapper _mapper;
|
||||
private readonly Mock<ILogger<GroupService>> _loggerMock = new();
|
||||
private readonly Mock<IPublishEndpoint> _publishMock = new();
|
||||
|
||||
public GroupServiceTests()
|
||||
{
|
||||
// 1. 配置内存数据库
|
||||
var options = new DbContextOptionsBuilder<ImContext>()
|
||||
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
|
||||
.Options;
|
||||
_context = new ImContext(options);
|
||||
|
||||
// 2. 配置真实的 AutoMapper (ProjectTo 必须使用真实配置,不能用 Mock)
|
||||
var config = new MapperConfiguration(cfg =>
|
||||
{
|
||||
// 替换为你项目中真实的 Profile 类名
|
||||
cfg.AddProfile<MapperConfig>();
|
||||
// 如果有多个 Profile,可以继续添加或者加载整个程序集
|
||||
// cfg.AddMaps(typeof(GroupProfile).Assembly);
|
||||
});
|
||||
_mapper = config.CreateMapper();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetGroupList_ShouldReturnPagedAndOrderedData()
|
||||
{
|
||||
// Arrange (准备数据)
|
||||
var userId = 1;
|
||||
var groups = new List<Group>
|
||||
{
|
||||
new Group { Id = 101, Name = "Group A", Avatar = "1111" },
|
||||
new Group { Id = 102, Name = "Group B" , Avatar = "1111"},
|
||||
new Group { Id = 103, Name = "Group C", Avatar = "1111" }
|
||||
};
|
||||
|
||||
_context.Groups.AddRange(groups);
|
||||
_context.GroupMembers.AddRange(new List<GroupMember>
|
||||
{
|
||||
new GroupMember { UserId = userId, GroupId = 101 },
|
||||
new GroupMember { UserId = userId, GroupId = 102 },
|
||||
new GroupMember { UserId = userId, GroupId = 103 }
|
||||
});
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var userService = new Mock<UserService>();
|
||||
|
||||
var service = new GroupService(_context, _mapper, _loggerMock.Object, _publishMock.Object,userService.Object);
|
||||
|
||||
// Act (执行测试: 第1页,每页2条,倒序)
|
||||
var result = await service.GetGroupListAsync(userId, page: 1, limit: 2, desc: true);
|
||||
|
||||
// Assert (断言)
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.Equal(103, result[0].Id); // 倒序最大的是 103
|
||||
Assert.Equal(102, result[1].Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetGroupList_ShouldReturnEmpty_WhenUserHasNoGroups()
|
||||
{
|
||||
|
||||
var userSerivce = new Mock<UserService>();
|
||||
// Arrange
|
||||
var service = new GroupService(_context, _mapper, _loggerMock.Object, _publishMock.Object, userSerivce.Object);
|
||||
|
||||
// Act
|
||||
var result = await service.GetGroupListAsync(userId: 999, page: 1, limit: 10, desc: false);
|
||||
|
||||
// Assert
|
||||
Assert.Empty(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
186
backend/IMTest/Service/UserServiceTest.cs
Normal file
186
backend/IMTest/Service/UserServiceTest.cs
Normal file
@ -0,0 +1,186 @@
|
||||
using AutoMapper;
|
||||
using IM_API.Dtos.User;
|
||||
using IM_API.Exceptions;
|
||||
using IM_API.Interface.Services;
|
||||
using IM_API.Models;
|
||||
using IM_API.Services;
|
||||
using IM_API.Tools;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using StackExchange.Redis;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
|
||||
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();
|
||||
var mockCache = new Mock<ICacheService>();
|
||||
var res = new Mock<IConnectionMultiplexer>();
|
||||
return new UserService(context, loggerMock.Object, mapper , mockCache.Object, res.Object);
|
||||
}
|
||||
|
||||
// ========== 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.
BIN
backend/IMTest/bin/Debug/net8.0/AutoMapper.dll
Normal file
BIN
backend/IMTest/bin/Debug/net8.0/AutoMapper.dll
Normal file
Binary file not shown.
BIN
backend/IMTest/bin/Debug/net8.0/Castle.Core.dll
Normal file
BIN
backend/IMTest/bin/Debug/net8.0/Castle.Core.dll
Normal file
Binary file not shown.
Binary file not shown.
2905
backend/IMTest/bin/Debug/net8.0/IMTest.deps.json
Normal file
2905
backend/IMTest/bin/Debug/net8.0/IMTest.deps.json
Normal file
File diff suppressed because it is too large
Load Diff
BIN
backend/IMTest/bin/Debug/net8.0/IMTest.dll
Normal file
BIN
backend/IMTest/bin/Debug/net8.0/IMTest.dll
Normal file
Binary file not shown.
BIN
backend/IMTest/bin/Debug/net8.0/IMTest.pdb
Normal file
BIN
backend/IMTest/bin/Debug/net8.0/IMTest.pdb
Normal file
Binary file not shown.
19
backend/IMTest/bin/Debug/net8.0/IMTest.runtimeconfig.json
Normal file
19
backend/IMTest/bin/Debug/net8.0/IMTest.runtimeconfig.json
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
1759
backend/IMTest/bin/Debug/net8.0/IM_API.deps.json
Normal file
1759
backend/IMTest/bin/Debug/net8.0/IM_API.deps.json
Normal file
File diff suppressed because it is too large
Load Diff
BIN
backend/IMTest/bin/Debug/net8.0/IM_API.dll
Normal file
BIN
backend/IMTest/bin/Debug/net8.0/IM_API.dll
Normal file
Binary file not shown.
BIN
backend/IMTest/bin/Debug/net8.0/IM_API.exe
Normal file
BIN
backend/IMTest/bin/Debug/net8.0/IM_API.exe
Normal file
Binary file not shown.
BIN
backend/IMTest/bin/Debug/net8.0/IM_API.pdb
Normal file
BIN
backend/IMTest/bin/Debug/net8.0/IM_API.pdb
Normal file
Binary file not shown.
20
backend/IMTest/bin/Debug/net8.0/IM_API.runtimeconfig.json
Normal file
20
backend/IMTest/bin/Debug/net8.0/IM_API.runtimeconfig.json
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
backend/IMTest/bin/Debug/net8.0/Microsoft.Extensions.Logging.dll
Normal file
BIN
backend/IMTest/bin/Debug/net8.0/Microsoft.Extensions.Logging.dll
Normal file
Binary file not shown.
Binary file not shown.
BIN
backend/IMTest/bin/Debug/net8.0/Microsoft.Extensions.Options.dll
Normal file
BIN
backend/IMTest/bin/Debug/net8.0/Microsoft.Extensions.Options.dll
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
backend/IMTest/bin/Debug/net8.0/Microsoft.OpenApi.dll
Normal file
BIN
backend/IMTest/bin/Debug/net8.0/Microsoft.OpenApi.dll
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
backend/IMTest/bin/Debug/net8.0/Moq.dll
Normal file
BIN
backend/IMTest/bin/Debug/net8.0/Moq.dll
Normal file
Binary file not shown.
BIN
backend/IMTest/bin/Debug/net8.0/MySqlConnector.dll
Normal file
BIN
backend/IMTest/bin/Debug/net8.0/MySqlConnector.dll
Normal file
Binary file not shown.
BIN
backend/IMTest/bin/Debug/net8.0/Newtonsoft.Json.dll
Normal file
BIN
backend/IMTest/bin/Debug/net8.0/Newtonsoft.Json.dll
Normal file
Binary file not shown.
BIN
backend/IMTest/bin/Debug/net8.0/NuGet.Frameworks.dll
Normal file
BIN
backend/IMTest/bin/Debug/net8.0/NuGet.Frameworks.dll
Normal file
Binary file not shown.
BIN
backend/IMTest/bin/Debug/net8.0/Pipelines.Sockets.Unofficial.dll
Normal file
BIN
backend/IMTest/bin/Debug/net8.0/Pipelines.Sockets.Unofficial.dll
Normal file
Binary file not shown.
Binary file not shown.
BIN
backend/IMTest/bin/Debug/net8.0/StackExchange.Redis.dll
Normal file
BIN
backend/IMTest/bin/Debug/net8.0/StackExchange.Redis.dll
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
30
backend/IMTest/bin/Debug/net8.0/appsettings.json
Normal file
30
backend/IMTest/bin/Debug/net8.0/appsettings.json
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"Jwt": {
|
||||
"Key": "YourSuperSecretKey123456784124214190!",
|
||||
"Issuer": "IMDemo",
|
||||
"Audience": "IMClients",
|
||||
"AccessTokenMinutes": 30,
|
||||
"RefreshTokenDays": 30
|
||||
},
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Server=192.168.5.100;Port=3306;Database=IM;User=product;Password=12345678;",
|
||||
"Redis": "192.168.5.100:6379"
|
||||
},
|
||||
"RabbitMQOptions": {
|
||||
"Host": "192.168.5.100",
|
||||
"Port": 5672,
|
||||
"Username": "test",
|
||||
"Password": "123456"
|
||||
},
|
||||
"FileUploadOptions": {
|
||||
"DefaultStorage": "Local",
|
||||
"ChunkSize": 5000000,
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user