Compare commits

..

127 Commits

Author SHA1 Message Date
8d952578de 修改 2026-04-07 20:14:07 +08:00
cf8be0bff5 add 2026-04-07 20:12:56 +08:00
f7772a9c5a add 2026-03-29 12:03:15 +08:00
feed24f378 修改 2026-03-24 17:50:57 +08:00
20013ba6bd Merge branch 'feature-nxdev' of https://gitea.nxsir.cn/code/IM into feature-nxdev 2026-03-18 21:48:56 +08:00
536b360127 Merge branch 'feature-nxdev' of https://gitea.nxsir.cn/code/IM into feature-nxdev 2026-03-18 21:48:47 +08:00
9e7609d6b4 Merge branch 'feature-nxdev' of https://gitea.nxsir.cn/code/IM into feature-nxdev 2026-03-15 22:31:23 +08:00
d1db5e6490 后端:
完善注册页面
2026-03-15 22:27:34 +08:00
eb8455e141 后端:
add(GroupNotification):合并群通知表
2026-03-15 14:55:29 +08:00
f7223dc590 Merge branch 'feature-nxdev' of https://gitea.nxsir.cn/code/IM into feature-nxdev 2026-03-09 14:44:44 +08:00
e38c15ed92 Merge branch 'feature-nxdev' of https://gitea.nxsir.cn/code/IM into feature-nxdev 2026-03-09 14:44:37 +08:00
faaaaaa1fa Merge branch 'feature-nxdev' of https://gitea.nxsir.cn/code/IM into feature-nxdev 2026-03-09 13:16:25 +08:00
fef4bbda70 Merge branch 'feature-nxdev' of https://gitea.nxsir.cn/code/IM into feature-nxdev 2026-03-09 13:16:16 +08:00
0f42ecb5dc Merge branch 'feature-nxdev' of https://gitea.nxsir.cn/code/IM into feature-nxdev 2026-03-07 14:11:50 +08:00
c53b896128 后端:
新增群成员接口
2026-03-07 14:11:46 +08:00
e6af0a98cd 后端:
新增群成员接口
2026-03-07 13:41:25 +08:00
e43f7d6365 前端:
优化electron效果
2026-03-07 13:40:06 +08:00
2ecaa28091 前端:
优化electron效果
2026-02-25 20:54:52 +08:00
333391d16f 前端:
适配客户端部分特性
2026-02-24 21:56:34 +08:00
123fa6a7aa add:
更新
2026-02-23 18:52:32 +08:00
d429560511 Revert "提交"
This reverts commit b96d895809.
2026-02-12 21:59:08 +08:00
b96d895809 提交 2026-02-12 21:53:26 +08:00
77db20dc38 前端:
1、完善创建群聊逻辑
后端:
1、完善群聊相关接口
2026-02-11 22:44:28 +08:00
dc6ecf224d fix:修复已知问题 2026-02-09 15:46:40 +08:00
10f79fb537 Merge branch 'feature-nxdev' of https://gitea.nxsir.cn/code/IM into feature-nxdev 2026-02-08 18:43:33 +08:00
eff83a9f68 222 2026-02-08 18:43:20 +08:00
46472f03e6 222 2026-02-08 15:13:55 +08:00
ed95bdddac 111 2026-02-07 22:58:25 +08:00
72cdf96241 Merge branch 'feature-nxdev' of https://gitea.nxsir.cn/code/IM into feature-nxdev 2026-02-07 22:54:20 +08:00
19fec61cff Merge branch 'feature-nxdev' of https://gitea.nxsir.cn/code/IM into feature-nxdev 2026-02-07 22:54:16 +08:00
139037085b Merge branch 'feature-nxdev' of https://gitea.nxsir.cn/code/IM into feature-nxdev 2026-02-07 22:38:23 +08:00
58bc8b4b5a 前端:
1、优化消息排序逻辑
2、新增加载历史消息
3、修复已知问题
后端:
1、优化消息排序逻辑
2、增加用户信息缓存机制
3、修改日期类型为DateTimeOffset改善时区信息丢失问题
3、修复了已知问题
数据库:
1、新增SequenceId字段用于消息排序
2、新增ClientMsgId字段用于客户端消息回执
2026-02-07 22:37:56 +08:00
07bb01808f Merge branch 'feature-nxdev' of https://gitea.nxsir.cn/code/IM into feature-nxdev 2026-02-03 22:46:55 +08:00
6d747327df Merge branch 'feature-nxdev' of https://gitea.nxsir.cn/code/IM into feature-nxdev 2026-02-03 22:46:34 +08:00
22038345c3 Merge branch 'feature-nxdev' of https://gitea.nxsir.cn/code/IM into feature-nxdev 2026-02-01 13:22:12 +08:00
136199290b 后端:
新增好友请求事件和好友已添加事件
2026-02-01 13:21:21 +08:00
83be063e7f Merge pull request 'main' (#56) from main into feature-nxdev
Reviewed-on: #56
2026-01-31 14:43:20 +08:00
a5e1b103b5 Merge pull request 'feature-nxdev' (#55) from feature-nxdev into main
Reviewed-on: #55
2026-01-30 22:14:50 +08:00
6e0d11cb24 Merge branch 'feature-nxdev' of https://gitea.nxsir.cn/code/IM into feature-nxdev 2026-01-30 22:14:05 +08:00
2d22b4bd27 后端:添加添加好友处理事件
文档:添加测试环境文档
2026-01-30 22:13:56 +08:00
c265868a51 Merge pull request '前端:修复wss连接失败的问题' (#53) from feature-nxdev into main
Reviewed-on: #53
2026-01-29 23:44:46 +08:00
7ebf23ddd8 前端:修复wss连接失败的问题 2026-01-29 23:43:50 +08:00
8938dd40d4 Merge pull request '前端:修复多分支部署时静态资源的路径问题' (#52) from feature-nxdev into main
Reviewed-on: #52
2026-01-29 22:54:48 +08:00
a590aa0a26 前端:修复多分支部署时静态资源的路径问题 2026-01-29 22:54:12 +08:00
98b5cad586 Merge pull request 'feature-nxdev' (#51) from feature-nxdev into main
Reviewed-on: #51
2026-01-29 22:31:28 +08:00
3d9335b97b 前端:修复多分支部署时静态资源的路径问题 2026-01-29 22:30:35 +08:00
1df3a0f30e 1111 2026-01-28 18:27:13 +08:00
e65eeb5ff7 Merge branch 'feature-nxdev' of https://gitea.nxsir.cn/code/IM into feature-nxdev 2026-01-28 18:27:08 +08:00
e05497a478 1111 2026-01-28 18:26:19 +08:00
ae99d3290f 后端:
1、事件订阅增加rabbitmq中间件
移动端:
1、完善移动端目录结构和代码框架
2026-01-28 18:19:45 +08:00
6cab42679c Merge pull request '前端:修复一堆BUG' (#50) from feature-nxdev into main
Reviewed-on: #50
2026-01-24 14:39:52 +08:00
f9cc69698c Merge branch 'feature-nxdev' of https://gitea.nxsir.cn/code/IM 2026-01-24 14:37:53 +08:00
f41e7137fe 前端:修复一堆BUG 2026-01-23 17:22:07 +08:00
9d942b38c7 Merge pull request 'feature-nxdev' (#49) from feature-nxdev into main
Reviewed-on: #49
2026-01-22 15:29:17 +08:00
435db45a52 更新 2026-01-22 15:28:42 +08:00
a31735dbe2 前端:
修复了已知问题
后端:
修复了已知问题
2026-01-22 15:25:54 +08:00
11840a2f30 Merge pull request '前端:' (#48) from feature-nxdev into main
Reviewed-on: #48
2026-01-21 22:32:37 +08:00
f7e68555b1 前端:
新增添加好友功能
修复给联系人发消息时跳转空会话的情况
修复了已知问题
后端:
修复了已知问题
2026-01-21 22:30:51 +08:00
cca84f88c0 Merge pull request '前端:' (#47) from feature-nxdev into main
Reviewed-on: #47
2026-01-21 18:50:42 +08:00
77744015de 前端:
1、更新界面图标
2、修改消息缓存逻辑
3、新增清空未读消息数
后端:
1、修复不同时区导致的时间显示问题
2、新增清空未读消息数接口
2026-01-21 18:48:05 +08:00
e18fb9db48 Merge pull request '前端:' (#46) from feature-nxdev into main
Reviewed-on: #46
2026-01-20 23:11:48 +08:00
bedcf97c9d 前端:
增加会话缓存
后端:
增加触发消息创建事件
增加消息创建事件触发后的处理函数
2026-01-20 23:09:46 +08:00
df044e23f1 Merge pull request '前端&后端:' (#45) from feature-nxdev into main
Reviewed-on: #45
2026-01-20 20:26:34 +08:00
be621e9ae2 前端&后端:
互发消息流程打通
2026-01-20 20:25:08 +08:00
204853beb9 Merge pull request '前端:' (#44) from feature-nxdev into main
Reviewed-on: #44
2026-01-19 19:39:00 +08:00
507788f6a3 前端:
引用signalr三方包
2026-01-19 19:37:47 +08:00
ac54af3ff8 Merge pull request '前端:' (#43) from feature-nxdev into main
Reviewed-on: #43
2026-01-18 22:38:39 +08:00
e7dbb651a2 前端:
1、会话列表、消息界面展示与后端打通
后端:
1、修复会话和消息服务现存问题
2、会话对象不再返回Message对象,而是使用MessageBaseDto替代
3、修改查询会话列表和会话信息的逻辑
4、新增消息列表查询
文档:
后端代码规范文档新增从数据库同步到模型的命令
2026-01-18 22:32:55 +08:00
f565e9d8a3 Merge pull request 'feature-nxdev' (#42) from feature-nxdev into main
Reviewed-on: #42
2026-01-13 22:08:35 +08:00
d855c8f8fb 前端:完善无感刷新
后端:修复刷新token接口异常报错
2026-01-13 22:07:00 +08:00
3b5e8b8eba Merge branch 'feature-nxdev' of https://gitea.nxsir.cn/code/IM into feature-nxdev 2026-01-13 21:01:21 +08:00
3b126f17fc Merge branch 'feature-nxdev' of https://gitea.nxsir.cn/code/IM into feature-nxdev 2026-01-13 21:01:13 +08:00
448cf25538 Merge pull request 'feature-nxdev' (#41) from feature-nxdev into main
Reviewed-on: #41
2026-01-13 20:15:59 +08:00
2b2d018658 Merge branch 'feature-nxdev' of https://gitea.nxsir.cn/code/IM into feature-nxdev 2026-01-13 20:14:31 +08:00
816b01f13a Merge pull request 'main' (#40) from main into feature-nxdev
Reviewed-on: #40
2026-01-13 20:14:27 +08:00
da401a20c0 Merge pull request 'main' (#40) from main into feature-nxdev
Reviewed-on: #40
2026-01-13 19:56:57 +08:00
9e826153a4 Merge pull request '修改侧栏,添加联系人跳转消息' (#39) from y1 into main
Reviewed-on: #39
2026-01-12 17:40:56 +08:00
ca4e2b7dd5 修改侧栏,添加联系人跳转消息 2026-01-12 16:44:32 +08:00
57deb5c9bf Merge pull request 'feature-nxdev' (#38) from feature-nxdev into main
Reviewed-on: #38
2026-01-03 22:14:44 +08:00
5323c29549 Merge branch 'feature-nxdev' of https://gitea.nxsir.cn/code/IM into feature-nxdev 2026-01-03 22:13:38 +08:00
41963dde70 add(conversation):完善私聊好友会话服务 2026-01-03 22:13:28 +08:00
7d5699ad53 Merge pull request 'feature-nxdev' (#37) from feature-nxdev into main
Reviewed-on: #37
2025-12-30 19:19:35 +08:00
5274f0d22d Merge branch 'feature-nxdev' of https://gitea.nxsir.cn/code/IM into feature-nxdev 2025-12-30 19:19:08 +08:00
e981ee190f Merge branch 'feature-nxdev' of https://gitea.nxsir.cn/code/IM into feature-nxdev 2025-12-30 19:18:58 +08:00
85144dbf6f Merge pull request 'feature-nxdev' (#36) from feature-nxdev into main
Reviewed-on: #36
2025-12-30 17:59:50 +08:00
f42e990c45 Merge branch 'feature-nxdev' of https://gitea.nxsir.cn/code/IM into feature-nxdev 2025-12-30 17:59:02 +08:00
705c96a230 Merge branch 'feature-nxdev' of https://gitea.nxsir.cn/code/IM into feature-nxdev 2025-12-30 17:58:59 +08:00
885b4012ed Merge pull request 'feature-nxdev' (#35) from feature-nxdev into main
Reviewed-on: #35
2025-12-30 17:22:51 +08:00
2565678b22 Merge branch 'feature-nxdev' of https://gitea.nxsir.cn/code/IM into feature-nxdev 2025-12-30 17:21:20 +08:00
f89007f52b Merge branch 'feature-nxdev' of https://gitea.nxsir.cn/code/IM into feature-nxdev 2025-12-30 17:21:11 +08:00
36f442a1c2 Merge pull request 'feature-nxdev' (#34) from feature-nxdev into main
Reviewed-on: #34
2025-12-29 22:03:13 +08:00
0ea560694b Merge branch 'feature-nxdev' of https://gitea.nxsir.cn/code/IM into feature-nxdev 2025-12-29 22:02:58 +08:00
c764f0eb03 Merge branch 'feature-nxdev' of https://gitea.nxsir.cn/code/IM into feature-nxdev 2025-12-29 22:02:55 +08:00
600272486b Merge pull request 'feature-nxdev' (#33) from feature-nxdev into main
Reviewed-on: #33
2025-12-29 21:57:59 +08:00
0a440d2914 Merge branch 'feature-nxdev' of https://gitea.nxsir.cn/code/IM into feature-nxdev 2025-12-29 21:57:40 +08:00
d641cb3275 Merge pull request 'main' (#32) from main into feature-nxdev
Reviewed-on: #32
2025-12-29 21:57:33 +08:00
26a13c4165 Merge pull request 'main' (#32) from main into feature-nxdev
Reviewed-on: #32
2025-12-29 20:46:10 +08:00
6dd93529c2 Merge pull request 'feat/auth-ui-refinement' (#31) from feat/auth-ui-refinement into main
Reviewed-on: #31
2025-12-29 20:45:30 +08:00
86841867f3 Merge pull request 'feature-nxdev' (#30) from feature-nxdev into main
Reviewed-on: #30
2025-12-29 20:41:46 +08:00
191c05461a Merge branch 'feature-nxdev' of https://gitea.nxsir.cn/code/IM into feature-nxdev 2025-12-29 20:41:19 +08:00
cf09984056 add(conversationSerivice):后端新增消息会话服务add(main):完善主面板 2025-12-29 20:39:29 +08:00
a40b768f3b style: update App.vue and IconInput.vue for consistent auth UI 2025-12-29 20:37:14 +08:00
ad0ab87747 style: refine login and register pages with green/blue themes and pill-shaped buttons 2025-12-29 20:32:40 +08:00
afe2c38f74 Merge pull request 'add(main):完善主面板' (#29) from feature-nxdev into main
Reviewed-on: #29
2025-12-29 16:16:03 +08:00
964b129e5e add(main):完善主面板 2025-12-29 16:14:50 +08:00
4cc1d20b7f Merge pull request 'feature-nxdev' (#28) from feature-nxdev into main
Reviewed-on: #28
2025-12-28 22:14:18 +08:00
06410a75e7 Merge branch 'feature-nxdev' of https://gitea.nxsir.cn/code/IM into feature-nxdev 2025-12-28 22:13:39 +08:00
6c564ceba3 Merge branch 'feature-nxdev' of https://gitea.nxsir.cn/code/IM into feature-nxdev 2025-12-28 22:13:35 +08:00
9c108de175 Merge pull request 'feature-nxdev' (#27) from feature-nxdev into main
Reviewed-on: #27
2025-12-28 22:02:09 +08:00
ceae1a0cd8 Merge branch 'feature-nxdev' of https://gitea.nxsir.cn/code/IM into feature-nxdev 2025-12-28 22:01:27 +08:00
62642ee86a modify(login): 修改登录返回对象,新增返回用户信息 2025-12-28 22:01:24 +08:00
f8fd54b953 Merge pull request 'add(filter): 新增全局错误拦截器' (#26) from feature-nxdev into main
Reviewed-on: #26
2025-12-15 17:32:04 +08:00
3725af7952 add(filter): 新增全局错误拦截器 2025-12-15 17:30:58 +08:00
465431ee98 Merge pull request 'feature-nxdev' (#25) from feature-nxdev into main
Reviewed-on: #25
2025-12-14 21:41:32 +08:00
82652efed7 add(login):完善登录逻辑 2025-12-14 21:40:34 +08:00
6ca7a8275c add(MyButton): 新增按钮组件 2025-12-14 14:12:47 +08:00
d55fc351ad Merge pull request 'add(components): 添加MyInput组件' (#24) from feature-nxdev into main
Reviewed-on: #24
2025-12-12 22:43:59 +08:00
fe4a9173a4 add(components): 添加MyInput组件 2025-12-12 22:42:47 +08:00
9ea6b1d50e Merge pull request 'feature-nxdev' (#23) from feature-nxdev into main
Reviewed-on: #23
2025-12-07 15:45:31 +08:00
d90366e304 Merge branch 'feature-nxdev' of https://gitea.nxsir.cn/code/IM into feature-nxdev 2025-12-07 15:45:00 +08:00
cbff80ba80 add(test):添加单元测试 2025-12-07 15:44:51 +08:00
8203dbe6ec Merge pull request 'feature-nxdev' (#22) from feature-nxdev into main
Reviewed-on: #22
2025-12-06 19:59:37 +08:00
1d633c3909 Merge pull request 'feature-nxdev' (#21) from feature-nxdev into main
Reviewed-on: #21
2025-12-01 20:47:51 +08:00
4c7bdbe9fc Merge pull request 'feature-nxdev' (#20) from feature-nxdev into main
Reviewed-on: #20
2025-11-27 16:56:37 +08:00
7c0cf6941f Merge pull request 'feature-nxdev' (#19) from feature-nxdev into main
Reviewed-on: #19
2025-11-16 16:21:49 +08:00
0675e1807b Merge pull request 'feature-nxdev' (#18) from feature-nxdev into main
Reviewed-on: #18
2025-11-16 10:56:41 +08:00
1807c6905f Merge pull request 'feature-nxdev' (#17) from feature-nxdev into main
Reviewed-on: #17
2025-11-03 15:09:09 +08:00
638 changed files with 70189 additions and 1657 deletions

View 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
View File

@ -0,0 +1,3 @@
bin/
obj/
.vs/

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,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);
});
}
}

View 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 状态的
}
}

View 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);
}
}
}

View 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.

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,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,
}
}

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