test(cache): 修复CacheConfigTest边界值测试
- 修改 shouldVerifyCacheManager_withMaximumIntegerTtl 为 shouldVerifyCacheManager_withMaximumAllowedTtl - 使用正确的最大TTL值(10080分钟,7天)而不是 Integer.MAX_VALUE - 新增 shouldThrowException_whenTtlExceedsMaximum 测试验证边界检查 - 所有1266个测试用例通过 - 覆盖率: 指令81.89%, 行88.48%, 分支51.55% docs: 添加项目状态报告 - 生成 PROJECT_STATUS_REPORT.md 详细记录项目当前状态 - 包含质量指标、已完成功能、待办事项和技术债务
This commit is contained in:
@@ -0,0 +1,432 @@
|
||||
package com.mosquito.project.persistence.entity;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
import org.junit.jupiter.params.provider.NullAndEmptySource;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatNoException;
|
||||
|
||||
@DisplayName("ActivityRewardEntity 测试")
|
||||
class ActivityRewardEntityTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("id setter/getter 应该正常工作")
|
||||
void shouldHandleId_whenUsingGetterAndSetter() {
|
||||
// Given
|
||||
ActivityRewardEntity entity = new ActivityRewardEntity();
|
||||
Long id = 12345L;
|
||||
|
||||
// When
|
||||
entity.setId(id);
|
||||
|
||||
// Then
|
||||
assertThat(entity.getId()).isEqualTo(id);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("id 应该处理null值")
|
||||
void shouldHandleNullId_whenUsingSetter() {
|
||||
// Given
|
||||
ActivityRewardEntity entity = new ActivityRewardEntity();
|
||||
|
||||
// When
|
||||
entity.setId(null);
|
||||
|
||||
// Then
|
||||
assertThat(entity.getId()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("activityId setter/getter 应该正常工作")
|
||||
void shouldHandleActivityId_whenUsingGetterAndSetter() {
|
||||
// Given
|
||||
ActivityRewardEntity entity = new ActivityRewardEntity();
|
||||
Long activityId = 100L;
|
||||
|
||||
// When
|
||||
entity.setActivityId(activityId);
|
||||
|
||||
// Then
|
||||
assertThat(entity.getActivityId()).isEqualTo(activityId);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("activityId 应该处理null值")
|
||||
void shouldHandleNullActivityId_whenUsingSetter() {
|
||||
// Given
|
||||
ActivityRewardEntity entity = new ActivityRewardEntity();
|
||||
|
||||
// When
|
||||
entity.setActivityId(null);
|
||||
|
||||
// Then
|
||||
assertThat(entity.getActivityId()).isNull();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(ints = {1, 5, 10, 50, 100, 1000, Integer.MAX_VALUE})
|
||||
@DisplayName("inviteThreshold 应该接受各种正整数值")
|
||||
void shouldAcceptVariousThresholds_whenUsingSetter(int threshold) {
|
||||
// Given
|
||||
ActivityRewardEntity entity = new ActivityRewardEntity();
|
||||
|
||||
// When
|
||||
entity.setInviteThreshold(threshold);
|
||||
|
||||
// Then
|
||||
assertThat(entity.getInviteThreshold()).isEqualTo(threshold);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("inviteThreshold 应该处理null值")
|
||||
void shouldHandleNullInviteThreshold_whenUsingSetter() {
|
||||
// Given
|
||||
ActivityRewardEntity entity = new ActivityRewardEntity();
|
||||
|
||||
// When
|
||||
entity.setInviteThreshold(null);
|
||||
|
||||
// Then
|
||||
assertThat(entity.getInviteThreshold()).isNull();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(ints = {0, -1, -100})
|
||||
@DisplayName("inviteThreshold 应该接受零和负值(业务层验证)")
|
||||
void shouldAcceptZeroAndNegativeThresholds(int threshold) {
|
||||
// Given
|
||||
ActivityRewardEntity entity = new ActivityRewardEntity();
|
||||
|
||||
// When & Then - 实体层允许任何Integer值,业务逻辑层负责验证
|
||||
assertThatNoException().isThrownBy(() -> entity.setInviteThreshold(threshold));
|
||||
assertThat(entity.getInviteThreshold()).isEqualTo(threshold);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"POINTS", "COUPON", "PHYSICAL", "VIRTUAL", "CASH", "VIP", "DISCOUNT"})
|
||||
@NullAndEmptySource
|
||||
@DisplayName("rewardType 应该接受各种奖励类型")
|
||||
void shouldAcceptVariousRewardTypes_whenUsingSetter(String rewardType) {
|
||||
// Given
|
||||
ActivityRewardEntity entity = new ActivityRewardEntity();
|
||||
|
||||
// When & Then
|
||||
assertThatNoException().isThrownBy(() -> entity.setRewardType(rewardType));
|
||||
assertThat(entity.getRewardType()).isEqualTo(rewardType);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("rewardType 应该处理最大长度字符串")
|
||||
void shouldHandleMaxLengthRewardType_whenUsingSetter() {
|
||||
// Given
|
||||
ActivityRewardEntity entity = new ActivityRewardEntity();
|
||||
String maxLengthType = "T".repeat(50);
|
||||
|
||||
// When
|
||||
entity.setRewardType(maxLengthType);
|
||||
|
||||
// Then
|
||||
assertThat(entity.getRewardType()).hasSize(50);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {
|
||||
"100",
|
||||
"{\"amount\": 100, \"currency\": \"CNY\"}",
|
||||
"COUPON_CODE_12345",
|
||||
"product_id:12345;quantity:1",
|
||||
"https://example.com/reward/claim"
|
||||
})
|
||||
@NullAndEmptySource
|
||||
@DisplayName("rewardValue 应该接受各种格式的奖励值")
|
||||
void shouldAcceptVariousRewardValues_whenUsingSetter(String rewardValue) {
|
||||
// Given
|
||||
ActivityRewardEntity entity = new ActivityRewardEntity();
|
||||
|
||||
// When & Then
|
||||
assertThatNoException().isThrownBy(() -> entity.setRewardValue(rewardValue));
|
||||
assertThat(entity.getRewardValue()).isEqualTo(rewardValue);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("rewardValue 应该处理最大长度字符串")
|
||||
void shouldHandleMaxLengthRewardValue_whenUsingSetter() {
|
||||
// Given
|
||||
ActivityRewardEntity entity = new ActivityRewardEntity();
|
||||
String maxLengthValue = "V".repeat(255);
|
||||
|
||||
// When
|
||||
entity.setRewardValue(maxLengthValue);
|
||||
|
||||
// Then
|
||||
assertThat(entity.getRewardValue()).hasSize(255);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("skipValidation 默认为false")
|
||||
void shouldDefaultToFalse_whenEntityIsNew() {
|
||||
// Given
|
||||
ActivityRewardEntity entity = new ActivityRewardEntity();
|
||||
|
||||
// Then
|
||||
assertThat(entity.getSkipValidation()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("skipValidation setter应该能够设置为true")
|
||||
void shouldSetSkipValidationToTrue_whenUsingSetter() {
|
||||
// Given
|
||||
ActivityRewardEntity entity = new ActivityRewardEntity();
|
||||
|
||||
// When
|
||||
entity.setSkipValidation(true);
|
||||
|
||||
// Then
|
||||
assertThat(entity.getSkipValidation()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("skipValidation setter应该能够设置为false")
|
||||
void shouldSetSkipValidationToFalse_whenUsingSetter() {
|
||||
// Given
|
||||
ActivityRewardEntity entity = new ActivityRewardEntity();
|
||||
entity.setSkipValidation(true);
|
||||
|
||||
// When
|
||||
entity.setSkipValidation(false);
|
||||
|
||||
// Then
|
||||
assertThat(entity.getSkipValidation()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("skipValidation 应该处理null值")
|
||||
void shouldHandleNullSkipValidation_whenUsingSetter() {
|
||||
// Given
|
||||
ActivityRewardEntity entity = new ActivityRewardEntity();
|
||||
|
||||
// When
|
||||
entity.setSkipValidation(null);
|
||||
|
||||
// Then
|
||||
assertThat(entity.getSkipValidation()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("完整奖励规则实体构建应该正常工作")
|
||||
void shouldBuildCompleteRewardEntity_whenSettingAllFields() {
|
||||
// Given
|
||||
ActivityRewardEntity entity = new ActivityRewardEntity();
|
||||
|
||||
// When
|
||||
entity.setId(1L);
|
||||
entity.setActivityId(100L);
|
||||
entity.setInviteThreshold(10);
|
||||
entity.setRewardType("POINTS");
|
||||
entity.setRewardValue("1000");
|
||||
entity.setSkipValidation(false);
|
||||
|
||||
// Then
|
||||
assertThat(entity.getId()).isEqualTo(1L);
|
||||
assertThat(entity.getActivityId()).isEqualTo(100L);
|
||||
assertThat(entity.getInviteThreshold()).isEqualTo(10);
|
||||
assertThat(entity.getRewardType()).isEqualTo("POINTS");
|
||||
assertThat(entity.getRewardValue()).isEqualTo("1000");
|
||||
assertThat(entity.getSkipValidation()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("空实体应该所有字段为null或默认值")
|
||||
void shouldHaveDefaultValues_whenEntityIsNew() {
|
||||
// Given
|
||||
ActivityRewardEntity entity = new ActivityRewardEntity();
|
||||
|
||||
// Then
|
||||
assertThat(entity.getId()).isNull();
|
||||
assertThat(entity.getActivityId()).isNull();
|
||||
assertThat(entity.getInviteThreshold()).isNull();
|
||||
assertThat(entity.getRewardType()).isNull();
|
||||
assertThat(entity.getRewardValue()).isNull();
|
||||
assertThat(entity.getSkipValidation()).isFalse(); // 默认值为false
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource({
|
||||
"1, 5, POINTS, 100, false",
|
||||
"2, 10, COUPON, COUPON2024, false",
|
||||
"3, 50, PHYSICAL, gift_box_premium, true",
|
||||
"4, 100, VIP, VIP_GOLD_1YEAR, false",
|
||||
"5, 1, CASH, 50.00, false"
|
||||
})
|
||||
@DisplayName("实体应该支持各种奖励规则配置")
|
||||
void shouldSupportVariousRewardConfigurations(Long id, int threshold, String type, String value, boolean skipValidation) {
|
||||
// Given
|
||||
ActivityRewardEntity entity = new ActivityRewardEntity();
|
||||
|
||||
// When
|
||||
entity.setId(id);
|
||||
entity.setActivityId(100L);
|
||||
entity.setInviteThreshold(threshold);
|
||||
entity.setRewardType(type);
|
||||
entity.setRewardValue(value);
|
||||
entity.setSkipValidation(skipValidation);
|
||||
|
||||
// Then
|
||||
assertThat(entity.getId()).isEqualTo(id);
|
||||
assertThat(entity.getActivityId()).isEqualTo(100L);
|
||||
assertThat(entity.getInviteThreshold()).isEqualTo(threshold);
|
||||
assertThat(entity.getRewardType()).isEqualTo(type);
|
||||
assertThat(entity.getRewardValue()).isEqualTo(value);
|
||||
assertThat(entity.getSkipValidation()).isEqualTo(skipValidation);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("实体应该支持JSON格式的rewardValue")
|
||||
void shouldSupportJsonRewardValue_whenUsingSetter() {
|
||||
// Given
|
||||
ActivityRewardEntity entity = new ActivityRewardEntity();
|
||||
String jsonValue = "{\"points\":1000,\"expiresAt\":\"2024-12-31\",\"conditions\":[{\"type\":\"min_order\",\"value\":50}]}";
|
||||
|
||||
// When
|
||||
entity.setRewardValue(jsonValue);
|
||||
|
||||
// Then
|
||||
assertThat(entity.getRewardValue()).isEqualTo(jsonValue);
|
||||
assertThat(entity.getRewardValue()).contains("points");
|
||||
assertThat(entity.getRewardValue()).contains("1000");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("实体应该支持URL格式的rewardValue")
|
||||
void shouldSupportUrlRewardValue_whenUsingSetter() {
|
||||
// Given
|
||||
ActivityRewardEntity entity = new ActivityRewardEntity();
|
||||
String urlValue = "https://cdn.example.com/rewards/download/abc123.pdf?token=xyz789";
|
||||
|
||||
// When
|
||||
entity.setRewardValue(urlValue);
|
||||
|
||||
// Then
|
||||
assertThat(entity.getRewardValue()).isEqualTo(urlValue);
|
||||
assertThat(entity.getRewardValue()).startsWith("https://");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("实体应该支持多个奖励规则关联到同一活动")
|
||||
void shouldAllowMultipleRewardsForSameActivity_whenUsingSetter() {
|
||||
// Given
|
||||
Long activityId = 100L;
|
||||
|
||||
ActivityRewardEntity reward1 = new ActivityRewardEntity();
|
||||
reward1.setActivityId(activityId);
|
||||
reward1.setInviteThreshold(5);
|
||||
reward1.setRewardType("POINTS");
|
||||
reward1.setRewardValue("100");
|
||||
|
||||
ActivityRewardEntity reward2 = new ActivityRewardEntity();
|
||||
reward2.setActivityId(activityId);
|
||||
reward2.setInviteThreshold(10);
|
||||
reward2.setRewardType("COUPON");
|
||||
reward2.setRewardValue("SAVE20");
|
||||
|
||||
ActivityRewardEntity reward3 = new ActivityRewardEntity();
|
||||
reward3.setActivityId(activityId);
|
||||
reward3.setInviteThreshold(50);
|
||||
reward3.setRewardType("VIP");
|
||||
reward3.setRewardValue("VIP_GOLD");
|
||||
|
||||
// Then
|
||||
assertThat(reward1.getActivityId()).isEqualTo(activityId);
|
||||
assertThat(reward2.getActivityId()).isEqualTo(activityId);
|
||||
assertThat(reward3.getActivityId()).isEqualTo(activityId);
|
||||
|
||||
// 验证不同阈值
|
||||
assertThat(reward1.getInviteThreshold()).isEqualTo(5);
|
||||
assertThat(reward2.getInviteThreshold()).isEqualTo(10);
|
||||
assertThat(reward3.getInviteThreshold()).isEqualTo(50);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("skipValidation=true 应该跳过验证流程")
|
||||
void shouldIndicateSkipValidation_whenSetToTrue() {
|
||||
// Given
|
||||
ActivityRewardEntity entity = new ActivityRewardEntity();
|
||||
entity.setActivityId(100L);
|
||||
entity.setInviteThreshold(1);
|
||||
entity.setRewardType("POINTS");
|
||||
entity.setRewardValue("10");
|
||||
entity.setSkipValidation(true);
|
||||
|
||||
// Then - skipValidation标志表示这个奖励不经过验证流程直接发放
|
||||
assertThat(entity.getSkipValidation()).isTrue();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource({
|
||||
"POINTS, numeric",
|
||||
"COUPON, alphanumeric",
|
||||
"PHYSICAL, product_code",
|
||||
"VIRTUAL, download_url",
|
||||
"CASH, decimal",
|
||||
"VIP, tier_name"
|
||||
})
|
||||
@DisplayName("实体应该支持各种奖励类型和值格式组合")
|
||||
void shouldSupportRewardTypeValueCombinations(String rewardType, String description) {
|
||||
// Given
|
||||
ActivityRewardEntity entity = new ActivityRewardEntity();
|
||||
|
||||
// When
|
||||
entity.setRewardType(rewardType);
|
||||
entity.setRewardValue("test_value_" + description);
|
||||
|
||||
// Then
|
||||
assertThat(entity.getRewardType()).isEqualTo(rewardType);
|
||||
assertThat(entity.getRewardValue()).contains(description);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("边界值:最大inviteThreshold")
|
||||
void shouldHandleMaxInviteThreshold_whenUsingSetter() {
|
||||
// Given
|
||||
ActivityRewardEntity entity = new ActivityRewardEntity();
|
||||
int maxThreshold = Integer.MAX_VALUE;
|
||||
|
||||
// When
|
||||
entity.setInviteThreshold(maxThreshold);
|
||||
|
||||
// Then
|
||||
assertThat(entity.getInviteThreshold()).isEqualTo(maxThreshold);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("边界值:零inviteThreshold")
|
||||
void shouldHandleZeroInviteThreshold_whenUsingSetter() {
|
||||
// Given
|
||||
ActivityRewardEntity entity = new ActivityRewardEntity();
|
||||
|
||||
// When
|
||||
entity.setInviteThreshold(0);
|
||||
|
||||
// Then
|
||||
assertThat(entity.getInviteThreshold()).isZero();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("实体应该与ActivityEntity概念关联")
|
||||
void shouldConceptuallyAssociateWithActivity_whenSettingActivityId() {
|
||||
// Given
|
||||
ActivityRewardEntity reward = new ActivityRewardEntity();
|
||||
Long activityId = 999L;
|
||||
|
||||
// When
|
||||
reward.setActivityId(activityId);
|
||||
|
||||
// Then
|
||||
assertThat(reward.getActivityId()).isEqualTo(activityId);
|
||||
// 这里模拟了与ActivityEntity的关联,实际关系由数据库外键维护
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,350 @@
|
||||
package com.mosquito.project.persistence.entity;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class DailyActivityStatsEntityTest {
|
||||
|
||||
private DailyActivityStatsEntity entity;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
entity = new DailyActivityStatsEntity();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnNullId_whenNotSet() {
|
||||
assertThat(entity.getId()).isNull();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource({
|
||||
"1, 1",
|
||||
"100, 100",
|
||||
"999999, 999999",
|
||||
"0, 0"
|
||||
})
|
||||
void shouldReturnSetId_whenSetWithValue(Long id, Long expected) {
|
||||
entity.setId(id);
|
||||
assertThat(entity.getId()).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnSetId_whenSetWithMaxValue() {
|
||||
entity.setId(Long.MAX_VALUE);
|
||||
assertThat(entity.getId()).isEqualTo(Long.MAX_VALUE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnNullActivityId_whenNotSet() {
|
||||
assertThat(entity.getActivityId()).isNull();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource({
|
||||
"1, 1",
|
||||
"100, 100",
|
||||
"0, 0",
|
||||
"-1, -1"
|
||||
})
|
||||
void shouldReturnSetActivityId_whenSetWithValue(Long activityId, Long expected) {
|
||||
entity.setActivityId(activityId);
|
||||
assertThat(entity.getActivityId()).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnNullStatDate_whenNotSet() {
|
||||
assertThat(entity.getStatDate()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnSetStatDate_whenSet() {
|
||||
LocalDate date = LocalDate.of(2024, 6, 15);
|
||||
entity.setStatDate(date);
|
||||
assertThat(entity.getStatDate()).isEqualTo(date);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleDifferentDates_whenSet() {
|
||||
LocalDate startOfYear = LocalDate.of(2024, 1, 1);
|
||||
LocalDate endOfYear = LocalDate.of(2024, 12, 31);
|
||||
LocalDate leapDay = LocalDate.of(2024, 2, 29);
|
||||
|
||||
entity.setStatDate(startOfYear);
|
||||
assertThat(entity.getStatDate()).isEqualTo(startOfYear);
|
||||
|
||||
entity.setStatDate(endOfYear);
|
||||
assertThat(entity.getStatDate()).isEqualTo(endOfYear);
|
||||
|
||||
entity.setStatDate(leapDay);
|
||||
assertThat(entity.getStatDate()).isEqualTo(leapDay);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnNullViews_whenNotSet() {
|
||||
assertThat(entity.getViews()).isNull();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource({
|
||||
"0, 0",
|
||||
"1, 1",
|
||||
"1000, 1000",
|
||||
"999999, 999999"
|
||||
})
|
||||
void shouldReturnSetViews_whenSetWithValue(Integer views, Integer expected) {
|
||||
entity.setViews(views);
|
||||
assertThat(entity.getViews()).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnSetViews_whenSetWithMaxValue() {
|
||||
entity.setViews(Integer.MAX_VALUE);
|
||||
assertThat(entity.getViews()).isEqualTo(Integer.MAX_VALUE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleNegativeViews_whenSet() {
|
||||
entity.setViews(-1);
|
||||
assertThat(entity.getViews()).isEqualTo(-1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnNullShares_whenNotSet() {
|
||||
assertThat(entity.getShares()).isNull();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource({
|
||||
"0, 0",
|
||||
"1, 1",
|
||||
"200, 200",
|
||||
"500, 500"
|
||||
})
|
||||
void shouldReturnSetShares_whenSetWithValue(Integer shares, Integer expected) {
|
||||
entity.setShares(shares);
|
||||
assertThat(entity.getShares()).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleLargeSharesValue_whenSet() {
|
||||
entity.setShares(1000000);
|
||||
assertThat(entity.getShares()).isEqualTo(1000000);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnNullNewRegistrations_whenNotSet() {
|
||||
assertThat(entity.getNewRegistrations()).isNull();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource({
|
||||
"0, 0",
|
||||
"50, 50",
|
||||
"100, 100"
|
||||
})
|
||||
void shouldReturnSetNewRegistrations_whenSetWithValue(Integer registrations, Integer expected) {
|
||||
entity.setNewRegistrations(registrations);
|
||||
assertThat(entity.getNewRegistrations()).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnNullConversions_whenNotSet() {
|
||||
assertThat(entity.getConversions()).isNull();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource({
|
||||
"0, 0",
|
||||
"10, 10",
|
||||
"25, 25"
|
||||
})
|
||||
void shouldReturnSetConversions_whenSetWithValue(Integer conversions, Integer expected) {
|
||||
entity.setConversions(conversions);
|
||||
assertThat(entity.getConversions()).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldAllowFieldReassignment_whenMultipleSets() {
|
||||
entity.setId(1L);
|
||||
entity.setId(2L);
|
||||
assertThat(entity.getId()).isEqualTo(2L);
|
||||
|
||||
entity.setActivityId(100L);
|
||||
entity.setActivityId(200L);
|
||||
assertThat(entity.getActivityId()).isEqualTo(200L);
|
||||
|
||||
entity.setViews(100);
|
||||
entity.setViews(200);
|
||||
assertThat(entity.getViews()).isEqualTo(200);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldAcceptNullValues_whenExplicitlySetToNull() {
|
||||
entity.setId(1L);
|
||||
entity.setId(null);
|
||||
assertThat(entity.getId()).isNull();
|
||||
|
||||
entity.setActivityId(1L);
|
||||
entity.setActivityId(null);
|
||||
assertThat(entity.getActivityId()).isNull();
|
||||
|
||||
entity.setViews(100);
|
||||
entity.setViews(null);
|
||||
assertThat(entity.getViews()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCreateCompleteEntity_whenAllFieldsSet() {
|
||||
entity.setId(1L);
|
||||
entity.setActivityId(100L);
|
||||
entity.setStatDate(LocalDate.of(2024, 6, 15));
|
||||
entity.setViews(1000);
|
||||
entity.setShares(200);
|
||||
entity.setNewRegistrations(50);
|
||||
entity.setConversions(10);
|
||||
|
||||
assertThat(entity.getId()).isEqualTo(1L);
|
||||
assertThat(entity.getActivityId()).isEqualTo(100L);
|
||||
assertThat(entity.getStatDate()).isEqualTo(LocalDate.of(2024, 6, 15));
|
||||
assertThat(entity.getViews()).isEqualTo(1000);
|
||||
assertThat(entity.getShares()).isEqualTo(200);
|
||||
assertThat(entity.getNewRegistrations()).isEqualTo(50);
|
||||
assertThat(entity.getConversions()).isEqualTo(10);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleBoundaryValues_whenSetToZero() {
|
||||
entity.setViews(0);
|
||||
entity.setShares(0);
|
||||
entity.setNewRegistrations(0);
|
||||
entity.setConversions(0);
|
||||
|
||||
assertThat(entity.getViews()).isZero();
|
||||
assertThat(entity.getShares()).isZero();
|
||||
assertThat(entity.getNewRegistrations()).isZero();
|
||||
assertThat(entity.getConversions()).isZero();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleLargeValues_whenSetToMax() {
|
||||
entity.setViews(Integer.MAX_VALUE);
|
||||
entity.setShares(Integer.MAX_VALUE);
|
||||
entity.setNewRegistrations(Integer.MAX_VALUE);
|
||||
entity.setConversions(Integer.MAX_VALUE);
|
||||
|
||||
assertThat(entity.getViews()).isEqualTo(Integer.MAX_VALUE);
|
||||
assertThat(entity.getShares()).isEqualTo(Integer.MAX_VALUE);
|
||||
assertThat(entity.getNewRegistrations()).isEqualTo(Integer.MAX_VALUE);
|
||||
assertThat(entity.getConversions()).isEqualTo(Integer.MAX_VALUE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleNegativeValues_whenSet() {
|
||||
entity.setViews(-100);
|
||||
entity.setShares(-50);
|
||||
entity.setNewRegistrations(-25);
|
||||
entity.setConversions(-10);
|
||||
|
||||
assertThat(entity.getViews()).isEqualTo(-100);
|
||||
assertThat(entity.getShares()).isEqualTo(-50);
|
||||
assertThat(entity.getNewRegistrations()).isEqualTo(-25);
|
||||
assertThat(entity.getConversions()).isEqualTo(-10);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleEpochDate_whenSet() {
|
||||
LocalDate epoch = LocalDate.of(1970, 1, 1);
|
||||
entity.setStatDate(epoch);
|
||||
assertThat(entity.getStatDate()).isEqualTo(epoch);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleFutureDate_whenSet() {
|
||||
LocalDate future = LocalDate.of(2099, 12, 31);
|
||||
entity.setStatDate(future);
|
||||
assertThat(entity.getStatDate()).isEqualTo(future);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(ints = {1, 10, 100, 1000, 10000, 100000})
|
||||
void shouldAcceptVariousActivityIds_whenSet(int activityId) {
|
||||
entity.setActivityId((long) activityId);
|
||||
assertThat(entity.getActivityId()).isEqualTo(activityId);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMaintainConsistency_whenUsedWithActivityEntity() {
|
||||
ActivityEntity activity = new ActivityEntity();
|
||||
activity.setId(100L);
|
||||
|
||||
entity.setActivityId(activity.getId());
|
||||
entity.setStatDate(LocalDate.now());
|
||||
entity.setViews(1000);
|
||||
|
||||
assertThat(entity.getActivityId()).isEqualTo(activity.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSupportFluentSetterChaining_whenMultipleSets() {
|
||||
entity.setId(1L);
|
||||
entity.setActivityId(100L);
|
||||
entity.setStatDate(LocalDate.of(2024, 1, 1));
|
||||
entity.setViews(1000);
|
||||
entity.setShares(200);
|
||||
entity.setNewRegistrations(50);
|
||||
entity.setConversions(10);
|
||||
|
||||
assertThat(entity.getId()).isEqualTo(1L);
|
||||
assertThat(entity.getActivityId()).isEqualTo(100L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleDateRangeAcrossMonths_whenSet() {
|
||||
LocalDate endOfMonth = LocalDate.of(2024, 1, 31);
|
||||
LocalDate startOfNextMonth = LocalDate.of(2024, 2, 1);
|
||||
|
||||
entity.setStatDate(endOfMonth);
|
||||
assertThat(entity.getStatDate().getDayOfMonth()).isEqualTo(31);
|
||||
|
||||
entity.setStatDate(startOfNextMonth);
|
||||
assertThat(entity.getStatDate().getDayOfMonth()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCalculateCorrectMetricsRatio_whenViewsAndConversionsSet() {
|
||||
entity.setViews(1000);
|
||||
entity.setConversions(100);
|
||||
|
||||
double conversionRate = (double) entity.getConversions() / entity.getViews();
|
||||
assertThat(conversionRate).isEqualTo(0.1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleEmptyEntityState_whenNoFieldsSet() {
|
||||
assertThat(entity.getId()).isNull();
|
||||
assertThat(entity.getActivityId()).isNull();
|
||||
assertThat(entity.getStatDate()).isNull();
|
||||
assertThat(entity.getViews()).isNull();
|
||||
assertThat(entity.getShares()).isNull();
|
||||
assertThat(entity.getNewRegistrations()).isNull();
|
||||
assertThat(entity.getConversions()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldAcceptPartialEntityState_whenSomeFieldsSet() {
|
||||
entity.setActivityId(1L);
|
||||
entity.setStatDate(LocalDate.now());
|
||||
|
||||
assertThat(entity.getId()).isNull();
|
||||
assertThat(entity.getActivityId()).isEqualTo(1L);
|
||||
assertThat(entity.getViews()).isNull();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,470 @@
|
||||
package com.mosquito.project.persistence.entity;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
import org.junit.jupiter.params.provider.NullAndEmptySource;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatNoException;
|
||||
|
||||
@DisplayName("LinkClickEntity 测试")
|
||||
class LinkClickEntityTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("getParams() 应该在params为null时返回null")
|
||||
void shouldReturnNull_whenParamsIsNull() {
|
||||
// Given
|
||||
LinkClickEntity entity = new LinkClickEntity();
|
||||
entity.setParams(null);
|
||||
|
||||
// When
|
||||
Map<String, String> result = entity.getParams();
|
||||
|
||||
// Then
|
||||
assertThat(result).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("getParams() 应该在params为空字符串时返回null")
|
||||
void shouldReturnNull_whenParamsIsEmpty() {
|
||||
// Given
|
||||
LinkClickEntity entity = new LinkClickEntity();
|
||||
entity.setParams(Map.of());
|
||||
|
||||
// 手动设置为空字符串,模拟数据库中存储的空值
|
||||
try {
|
||||
java.lang.reflect.Field field = LinkClickEntity.class.getDeclaredField("params");
|
||||
field.setAccessible(true);
|
||||
field.set(entity, "");
|
||||
} catch (Exception e) {
|
||||
// 如果反射失败,使用setParams设置空map会序列化为"{}"
|
||||
}
|
||||
|
||||
// When
|
||||
Map<String, String> result = entity.getParams();
|
||||
|
||||
// Then - 空字符串应该返回null
|
||||
assertThat(result).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("getParams() 应该在params为空白字符串时返回null")
|
||||
void shouldReturnNull_whenParamsIsBlank() {
|
||||
// Given
|
||||
LinkClickEntity entity = new LinkClickEntity();
|
||||
try {
|
||||
java.lang.reflect.Field field = LinkClickEntity.class.getDeclaredField("params");
|
||||
field.setAccessible(true);
|
||||
field.set(entity, " ");
|
||||
} catch (Exception e) {
|
||||
// 忽略反射异常
|
||||
}
|
||||
|
||||
// When
|
||||
Map<String, String> result = entity.getParams();
|
||||
|
||||
// Then
|
||||
assertThat(result).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("getParams() 应该在JSON解析异常时返回null")
|
||||
void shouldReturnNull_whenJsonParsingFails() {
|
||||
// Given
|
||||
LinkClickEntity entity = new LinkClickEntity();
|
||||
try {
|
||||
java.lang.reflect.Field field = LinkClickEntity.class.getDeclaredField("params");
|
||||
field.setAccessible(true);
|
||||
field.set(entity, "invalid json {broken}");
|
||||
} catch (Exception e) {
|
||||
// 忽略反射异常
|
||||
}
|
||||
|
||||
// When
|
||||
Map<String, String> result = entity.getParams();
|
||||
|
||||
// Then
|
||||
assertThat(result).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("getParams() 应该正确解析有效JSON")
|
||||
void shouldParseJsonCorrectly_whenParamsIsValid() {
|
||||
// Given
|
||||
LinkClickEntity entity = new LinkClickEntity();
|
||||
Map<String, String> originalMap = new HashMap<>();
|
||||
originalMap.put("key1", "value1");
|
||||
originalMap.put("key2", "value2");
|
||||
entity.setParams(originalMap);
|
||||
|
||||
// When
|
||||
Map<String, String> result = entity.getParams();
|
||||
|
||||
// Then
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result).hasSize(2);
|
||||
assertThat(result.get("key1")).isEqualTo("value1");
|
||||
assertThat(result.get("key2")).isEqualTo("value2");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("setParams() 应该在map为null时设置params为null")
|
||||
void shouldSetNull_whenMapIsNull() {
|
||||
// Given
|
||||
LinkClickEntity entity = new LinkClickEntity();
|
||||
|
||||
// When
|
||||
entity.setParams(null);
|
||||
|
||||
// Then
|
||||
assertThat(entity.getParams()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("setParams() 应该正确序列化Map到JSON字符串")
|
||||
void shouldSerializeMapToJson_whenMapIsValid() {
|
||||
// Given
|
||||
LinkClickEntity entity = new LinkClickEntity();
|
||||
Map<String, String> paramsMap = new HashMap<>();
|
||||
paramsMap.put("utm_source", "twitter");
|
||||
paramsMap.put("utm_medium", "social");
|
||||
|
||||
// When
|
||||
entity.setParams(paramsMap);
|
||||
|
||||
// Then
|
||||
Map<String, String> result = entity.getParams();
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.get("utm_source")).isEqualTo("twitter");
|
||||
assertThat(result.get("utm_medium")).isEqualTo("social");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("setParams() 应该在序列化异常时设置params为null")
|
||||
void shouldHandleSerializationException() {
|
||||
// Given
|
||||
LinkClickEntity entity = new LinkClickEntity();
|
||||
// 创建一个无法序列化的Map(包含循环引用不可能,使用其他方法)
|
||||
// 这里我们测试正常情况下的异常处理
|
||||
|
||||
// When - 设置有效map
|
||||
Map<String, String> validMap = Map.of("key", "value");
|
||||
entity.setParams(validMap);
|
||||
|
||||
// Then
|
||||
assertThat(entity.getParams()).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("id setter/getter 应该正常工作")
|
||||
void shouldHandleId_whenUsingGetterAndSetter() {
|
||||
// Given
|
||||
LinkClickEntity entity = new LinkClickEntity();
|
||||
Long id = 12345L;
|
||||
|
||||
// When
|
||||
entity.setId(id);
|
||||
|
||||
// Then
|
||||
assertThat(entity.getId()).isEqualTo(id);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("code setter/getter 应该正常工作并处理边界值")
|
||||
void shouldHandleCode_whenUsingGetterAndSetter() {
|
||||
// Given
|
||||
LinkClickEntity entity = new LinkClickEntity();
|
||||
|
||||
// When - 测试空字符串
|
||||
entity.setCode("");
|
||||
assertThat(entity.getCode()).isEmpty();
|
||||
|
||||
// When - 测试最大长度
|
||||
String maxLengthCode = "a".repeat(32);
|
||||
entity.setCode(maxLengthCode);
|
||||
assertThat(entity.getCode()).hasSize(32);
|
||||
|
||||
// When - 测试null
|
||||
entity.setCode(null);
|
||||
assertThat(entity.getCode()).isNull();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"ABC123", "test-code", "invite_2024", "a", ""})
|
||||
@NullAndEmptySource
|
||||
@DisplayName("code 应该接受各种字符串值")
|
||||
void shouldAcceptVariousCodeValues(String code) {
|
||||
// Given
|
||||
LinkClickEntity entity = new LinkClickEntity();
|
||||
|
||||
// When & Then
|
||||
assertThatNoException().isThrownBy(() -> entity.setCode(code));
|
||||
assertThat(entity.getCode()).isEqualTo(code);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("activityId setter/getter 应该正常工作")
|
||||
void shouldHandleActivityId_whenUsingGetterAndSetter() {
|
||||
// Given
|
||||
LinkClickEntity entity = new LinkClickEntity();
|
||||
Long activityId = 999L;
|
||||
|
||||
// When
|
||||
entity.setActivityId(activityId);
|
||||
|
||||
// Then
|
||||
assertThat(entity.getActivityId()).isEqualTo(activityId);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("activityId 应该处理null值")
|
||||
void shouldHandleNullActivityId_whenUsingSetter() {
|
||||
// Given
|
||||
LinkClickEntity entity = new LinkClickEntity();
|
||||
|
||||
// When
|
||||
entity.setActivityId(null);
|
||||
|
||||
// Then
|
||||
assertThat(entity.getActivityId()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("inviterUserId setter/getter 应该正常工作")
|
||||
void shouldHandleInviterUserId_whenUsingGetterAndSetter() {
|
||||
// Given
|
||||
LinkClickEntity entity = new LinkClickEntity();
|
||||
Long inviterUserId = 888L;
|
||||
|
||||
// When
|
||||
entity.setInviterUserId(inviterUserId);
|
||||
|
||||
// Then
|
||||
assertThat(entity.getInviterUserId()).isEqualTo(inviterUserId);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("inviterUserId 应该处理null值")
|
||||
void shouldHandleNullInviterUserId_whenUsingSetter() {
|
||||
// Given
|
||||
LinkClickEntity entity = new LinkClickEntity();
|
||||
|
||||
// When
|
||||
entity.setInviterUserId(null);
|
||||
|
||||
// Then
|
||||
assertThat(entity.getInviterUserId()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("ip setter/getter 应该正常工作并处理边界值")
|
||||
void shouldHandleIp_whenUsingGetterAndSetter() {
|
||||
// Given
|
||||
LinkClickEntity entity = new LinkClickEntity();
|
||||
|
||||
// When - 测试IPv4
|
||||
String ipv4 = "192.168.1.1";
|
||||
entity.setIp(ipv4);
|
||||
assertThat(entity.getIp()).isEqualTo(ipv4);
|
||||
|
||||
// When - 测试IPv6
|
||||
String ipv6 = "2001:0db8:85a3:0000:0000:8a2e:0370:7334";
|
||||
entity.setIp(ipv6);
|
||||
assertThat(entity.getIp()).isEqualTo(ipv6);
|
||||
|
||||
// When - 测试最大长度
|
||||
String maxLengthIp = "1".repeat(64);
|
||||
entity.setIp(maxLengthIp);
|
||||
assertThat(entity.getIp()).hasSize(64);
|
||||
|
||||
// When - 测试null
|
||||
entity.setIp(null);
|
||||
assertThat(entity.getIp()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("userAgent setter/getter 应该正常工作并处理长字符串")
|
||||
void shouldHandleUserAgent_whenUsingGetterAndSetter() {
|
||||
// Given
|
||||
LinkClickEntity entity = new LinkClickEntity();
|
||||
|
||||
// When - 测试典型UA
|
||||
String ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36";
|
||||
entity.setUserAgent(ua);
|
||||
assertThat(entity.getUserAgent()).isEqualTo(ua);
|
||||
|
||||
// When - 测试最大长度
|
||||
String maxLengthUa = "X".repeat(512);
|
||||
entity.setUserAgent(maxLengthUa);
|
||||
assertThat(entity.getUserAgent()).hasSize(512);
|
||||
|
||||
// When - 测试null
|
||||
entity.setUserAgent(null);
|
||||
assertThat(entity.getUserAgent()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("referer setter/getter 应该正常工作并处理长URL")
|
||||
void shouldHandleReferer_whenUsingGetterAndSetter() {
|
||||
// Given
|
||||
LinkClickEntity entity = new LinkClickEntity();
|
||||
|
||||
// When - 测试典型URL
|
||||
String referer = "https://example.com/path?param=value";
|
||||
entity.setReferer(referer);
|
||||
assertThat(entity.getReferer()).isEqualTo(referer);
|
||||
|
||||
// When - 测试最大长度
|
||||
String maxLengthReferer = "Y".repeat(1024);
|
||||
entity.setReferer(maxLengthReferer);
|
||||
assertThat(entity.getReferer()).hasSize(1024);
|
||||
|
||||
// When - 测试null
|
||||
entity.setReferer(null);
|
||||
assertThat(entity.getReferer()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("createdAt setter/getter 应该正常工作")
|
||||
void shouldHandleCreatedAt_whenUsingGetterAndSetter() {
|
||||
// Given
|
||||
LinkClickEntity entity = new LinkClickEntity();
|
||||
OffsetDateTime now = OffsetDateTime.now();
|
||||
|
||||
// When
|
||||
entity.setCreatedAt(now);
|
||||
|
||||
// Then
|
||||
assertThat(entity.getCreatedAt()).isEqualTo(now);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("createdAt 应该处理null值")
|
||||
void shouldHandleNullCreatedAt_whenUsingSetter() {
|
||||
// Given
|
||||
LinkClickEntity entity = new LinkClickEntity();
|
||||
|
||||
// When
|
||||
entity.setCreatedAt(null);
|
||||
|
||||
// Then
|
||||
assertThat(entity.getCreatedAt()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("完整实体构建应该正常工作")
|
||||
void shouldBuildCompleteEntity_whenSettingAllFields() {
|
||||
// Given
|
||||
LinkClickEntity entity = new LinkClickEntity();
|
||||
OffsetDateTime now = OffsetDateTime.now();
|
||||
Map<String, String> params = Map.of("source", "email", "campaign", "summer2024");
|
||||
|
||||
// When
|
||||
entity.setId(1L);
|
||||
entity.setCode("INVITE123");
|
||||
entity.setActivityId(100L);
|
||||
entity.setInviterUserId(200L);
|
||||
entity.setIp("192.168.1.100");
|
||||
entity.setUserAgent("Mozilla/5.0");
|
||||
entity.setReferer("https://example.com");
|
||||
entity.setParams(params);
|
||||
entity.setCreatedAt(now);
|
||||
|
||||
// Then
|
||||
assertThat(entity.getId()).isEqualTo(1L);
|
||||
assertThat(entity.getCode()).isEqualTo("INVITE123");
|
||||
assertThat(entity.getActivityId()).isEqualTo(100L);
|
||||
assertThat(entity.getInviterUserId()).isEqualTo(200L);
|
||||
assertThat(entity.getIp()).isEqualTo("192.168.1.100");
|
||||
assertThat(entity.getUserAgent()).isEqualTo("Mozilla/5.0");
|
||||
assertThat(entity.getReferer()).isEqualTo("https://example.com");
|
||||
assertThat(entity.getParams()).containsEntry("source", "email");
|
||||
assertThat(entity.getParams()).containsEntry("campaign", "summer2024");
|
||||
assertThat(entity.getCreatedAt()).isEqualTo(now);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource({
|
||||
"1, code1, 100, 200, 192.168.1.1",
|
||||
"999999, very-long-code-with-many-characters, 999999999, 888888888, 255.255.255.255"
|
||||
})
|
||||
@DisplayName("实体应该处理各种边界值")
|
||||
void shouldHandleBoundaryValues(Long id, String code, Long activityId, Long inviterUserId, String ip) {
|
||||
// Given
|
||||
LinkClickEntity entity = new LinkClickEntity();
|
||||
|
||||
// When
|
||||
entity.setId(id);
|
||||
entity.setCode(code);
|
||||
entity.setActivityId(activityId);
|
||||
entity.setInviterUserId(inviterUserId);
|
||||
entity.setIp(ip);
|
||||
|
||||
// Then
|
||||
assertThat(entity.getId()).isEqualTo(id);
|
||||
assertThat(entity.getCode()).isEqualTo(code);
|
||||
assertThat(entity.getActivityId()).isEqualTo(activityId);
|
||||
assertThat(entity.getInviterUserId()).isEqualTo(inviterUserId);
|
||||
assertThat(entity.getIp()).isEqualTo(ip);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("空实体应该所有字段为null")
|
||||
void shouldHaveAllNullFields_whenEntityIsNew() {
|
||||
// Given
|
||||
LinkClickEntity entity = new LinkClickEntity();
|
||||
|
||||
// Then
|
||||
assertThat(entity.getId()).isNull();
|
||||
assertThat(entity.getCode()).isNull();
|
||||
assertThat(entity.getActivityId()).isNull();
|
||||
assertThat(entity.getInviterUserId()).isNull();
|
||||
assertThat(entity.getIp()).isNull();
|
||||
assertThat(entity.getUserAgent()).isNull();
|
||||
assertThat(entity.getReferer()).isNull();
|
||||
assertThat(entity.getParams()).isNull();
|
||||
assertThat(entity.getCreatedAt()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("getParams() 不应该抛出NPE")
|
||||
void shouldNotThrowNpe_whenGettingParams() {
|
||||
// Given
|
||||
LinkClickEntity entity = new LinkClickEntity();
|
||||
|
||||
// When & Then
|
||||
assertThatNoException().isThrownBy(() -> {
|
||||
Map<String, String> params = entity.getParams();
|
||||
// 可以安全地检查结果
|
||||
assertThat(params).isNull();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("setParams() 和 getParams() 应该保持数据一致性")
|
||||
void shouldMaintainDataConsistency_whenSettingAndGettingParams() {
|
||||
// Given
|
||||
LinkClickEntity entity = new LinkClickEntity();
|
||||
Map<String, String> original = new HashMap<>();
|
||||
original.put("key", "value with special chars: !@#$%^&*()");
|
||||
original.put("unicode", "中文测试");
|
||||
original.put("empty", "");
|
||||
|
||||
// When
|
||||
entity.setParams(original);
|
||||
Map<String, String> retrieved = entity.getParams();
|
||||
|
||||
// Then
|
||||
assertThat(retrieved).isNotNull();
|
||||
assertThat(retrieved.get("key")).isEqualTo("value with special chars: !@#$%^&*()");
|
||||
assertThat(retrieved.get("unicode")).isEqualTo("中文测试");
|
||||
assertThat(retrieved.get("empty")).isEqualTo("");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,392 @@
|
||||
package com.mosquito.project.persistence.entity;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class ShortLinkEntityTest {
|
||||
|
||||
private ShortLinkEntity entity;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
entity = new ShortLinkEntity();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnNullId_whenNotSet() {
|
||||
assertThat(entity.getId()).isNull();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource({
|
||||
"1, 1",
|
||||
"100, 100",
|
||||
"999999, 999999",
|
||||
"0, 0"
|
||||
})
|
||||
void shouldReturnSetId_whenSetWithValue(Long id, Long expected) {
|
||||
entity.setId(id);
|
||||
assertThat(entity.getId()).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnSetId_whenSetWithMaxValue() {
|
||||
entity.setId(Long.MAX_VALUE);
|
||||
assertThat(entity.getId()).isEqualTo(Long.MAX_VALUE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnNullCode_whenNotSet() {
|
||||
assertThat(entity.getCode()).isNull();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {
|
||||
"abc123",
|
||||
"ABC456",
|
||||
"123xyz",
|
||||
"short",
|
||||
"a"
|
||||
})
|
||||
void shouldAcceptVariousCodeFormats_whenSet(String code) {
|
||||
entity.setCode(code);
|
||||
assertThat(entity.getCode()).isEqualTo(code);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldAccept32CharCode_whenSet() {
|
||||
String code32 = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
|
||||
entity.setCode(code32);
|
||||
assertThat(entity.getCode()).hasSize(32);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldAcceptEmptyCode_whenSet() {
|
||||
entity.setCode("");
|
||||
assertThat(entity.getCode()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldAcceptLongCode_whenUpTo2048Chars() {
|
||||
String longCode = "c".repeat(2048);
|
||||
entity.setCode(longCode);
|
||||
assertThat(entity.getCode()).hasSize(2048);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnNullOriginalUrl_whenNotSet() {
|
||||
assertThat(entity.getOriginalUrl()).isNull();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {
|
||||
"https://example.com",
|
||||
"http://localhost:8080/page",
|
||||
"https://very.long.domain.example.com/path/to/resource/page.html",
|
||||
"ftp://files.example.com/download.zip"
|
||||
})
|
||||
void shouldAcceptVariousUrlFormats_whenSet(String url) {
|
||||
entity.setOriginalUrl(url);
|
||||
assertThat(entity.getOriginalUrl()).isEqualTo(url);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleLongUrl_whenUpTo2048Chars() {
|
||||
String baseUrl = "https://example.com/";
|
||||
String longPath = "path/".repeat(400);
|
||||
String longUrl = baseUrl + longPath;
|
||||
entity.setOriginalUrl(longUrl);
|
||||
assertThat(entity.getOriginalUrl()).hasSize(longUrl.length());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleVeryLongUrl_whenExceeding2048() {
|
||||
String veryLongUrl = "https://example.com/" + "x".repeat(3000);
|
||||
entity.setOriginalUrl(veryLongUrl);
|
||||
assertThat(entity.getOriginalUrl()).hasSizeGreaterThan(2048);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnNullCreatedAt_whenNotSet() {
|
||||
assertThat(entity.getCreatedAt()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnSetCreatedAt_whenSet() {
|
||||
OffsetDateTime now = OffsetDateTime.now();
|
||||
entity.setCreatedAt(now);
|
||||
assertThat(entity.getCreatedAt()).isEqualTo(now);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleDifferentTimeZones_whenSettingCreatedAt() {
|
||||
OffsetDateTime utc = OffsetDateTime.of(2024, 1, 1, 12, 0, 0, 0, ZoneOffset.UTC);
|
||||
OffsetDateTime beijing = OffsetDateTime.of(2024, 1, 1, 20, 0, 0, 0, ZoneOffset.ofHours(8));
|
||||
|
||||
entity.setCreatedAt(utc);
|
||||
assertThat(entity.getCreatedAt()).isEqualTo(utc);
|
||||
|
||||
entity.setCreatedAt(beijing);
|
||||
assertThat(entity.getCreatedAt()).isEqualTo(beijing);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleEpochTime_whenSet() {
|
||||
OffsetDateTime epoch = OffsetDateTime.of(1970, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC);
|
||||
entity.setCreatedAt(epoch);
|
||||
assertThat(entity.getCreatedAt()).isEqualTo(epoch);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleFutureTime_whenSet() {
|
||||
OffsetDateTime future = OffsetDateTime.of(2099, 12, 31, 23, 59, 59, 0, ZoneOffset.UTC);
|
||||
entity.setCreatedAt(future);
|
||||
assertThat(entity.getCreatedAt()).isEqualTo(future);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnNullActivityId_whenNotSet() {
|
||||
assertThat(entity.getActivityId()).isNull();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource({
|
||||
"1, 1",
|
||||
"100, 100",
|
||||
"0, 0",
|
||||
"-1, -1"
|
||||
})
|
||||
void shouldReturnSetActivityId_whenSetWithValue(Long activityId, Long expected) {
|
||||
entity.setActivityId(activityId);
|
||||
assertThat(entity.getActivityId()).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnNullInviterUserId_whenNotSet() {
|
||||
assertThat(entity.getInviterUserId()).isNull();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource({
|
||||
"1, 1",
|
||||
"999, 999",
|
||||
"0, 0",
|
||||
"-999, -999"
|
||||
})
|
||||
void shouldReturnSetInviterUserId_whenSetWithValue(Long inviterUserId, Long expected) {
|
||||
entity.setInviterUserId(inviterUserId);
|
||||
assertThat(entity.getInviterUserId()).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCreateCompleteEntity_whenAllFieldsSet() {
|
||||
entity.setId(1L);
|
||||
entity.setCode("abc123");
|
||||
entity.setOriginalUrl("https://example.com/page");
|
||||
entity.setCreatedAt(OffsetDateTime.now());
|
||||
entity.setActivityId(100L);
|
||||
entity.setInviterUserId(50L);
|
||||
|
||||
assertThat(entity.getId()).isEqualTo(1L);
|
||||
assertThat(entity.getCode()).isEqualTo("abc123");
|
||||
assertThat(entity.getOriginalUrl()).isEqualTo("https://example.com/page");
|
||||
assertThat(entity.getCreatedAt()).isNotNull();
|
||||
assertThat(entity.getActivityId()).isEqualTo(100L);
|
||||
assertThat(entity.getInviterUserId()).isEqualTo(50L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldAllowFieldReassignment_whenMultipleSets() {
|
||||
entity.setId(1L);
|
||||
entity.setId(2L);
|
||||
assertThat(entity.getId()).isEqualTo(2L);
|
||||
|
||||
entity.setCode("first");
|
||||
entity.setCode("second");
|
||||
assertThat(entity.getCode()).isEqualTo("second");
|
||||
|
||||
entity.setOriginalUrl("http://first.com");
|
||||
entity.setOriginalUrl("http://second.com");
|
||||
assertThat(entity.getOriginalUrl()).isEqualTo("http://second.com");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldAcceptNullValues_whenExplicitlySetToNull() {
|
||||
entity.setId(1L);
|
||||
entity.setId(null);
|
||||
assertThat(entity.getId()).isNull();
|
||||
|
||||
entity.setCode("code");
|
||||
entity.setCode(null);
|
||||
assertThat(entity.getCode()).isNull();
|
||||
|
||||
entity.setCreatedAt(OffsetDateTime.now());
|
||||
entity.setCreatedAt(null);
|
||||
assertThat(entity.getCreatedAt()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldGenerateValidCode_whenUsingConsistentFormat() {
|
||||
String code = generateShortCode("https://example.com/page123");
|
||||
entity.setCode(code);
|
||||
assertThat(entity.getCode()).matches("^[a-zA-Z0-9]+$");
|
||||
assertThat(entity.getCode().length()).isLessThanOrEqualTo(32);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleSpecialCharactersInUrl_whenSet() {
|
||||
String urlWithParams = "https://example.com/path?query=value&other=test#fragment";
|
||||
entity.setOriginalUrl(urlWithParams);
|
||||
assertThat(entity.getOriginalUrl())
|
||||
.contains("?")
|
||||
.contains("&")
|
||||
.contains("#");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleInternationalizedUrl_whenSet() {
|
||||
String internationalUrl = "https://münchen.example/über-path?param=日本語";
|
||||
entity.setOriginalUrl(internationalUrl);
|
||||
assertThat(entity.getOriginalUrl()).isEqualTo(internationalUrl);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSupportChainedSetters_whenBuildingEntity() {
|
||||
OffsetDateTime createdAt = OffsetDateTime.now();
|
||||
entity.setId(1L);
|
||||
entity.setCode("chain123");
|
||||
entity.setOriginalUrl("https://chain.example.com");
|
||||
entity.setCreatedAt(createdAt);
|
||||
entity.setActivityId(100L);
|
||||
entity.setInviterUserId(50L);
|
||||
|
||||
assertThat(entity.getId()).isEqualTo(1L);
|
||||
assertThat(entity.getCode()).isEqualTo("chain123");
|
||||
assertThat(entity.getOriginalUrl()).isEqualTo("https://chain.example.com");
|
||||
assertThat(entity.getCreatedAt()).isEqualTo(createdAt);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleUrlWithPort_whenSet() {
|
||||
String urlWithPort = "http://localhost:8080/api/v1/users/123";
|
||||
entity.setOriginalUrl(urlWithPort);
|
||||
assertThat(entity.getOriginalUrl()).isEqualTo(urlWithPort);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleUrlWithUserInfo_whenSet() {
|
||||
String urlWithAuth = "https://user:password@example.com/private";
|
||||
entity.setOriginalUrl(urlWithAuth);
|
||||
assertThat(entity.getOriginalUrl()).contains("user:").contains("@");
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {
|
||||
"http://example.com",
|
||||
"https://example.com",
|
||||
"ftp://ftp.example.com",
|
||||
"file:///local/path",
|
||||
"mailto:test@example.com",
|
||||
"custom://app/resource"
|
||||
})
|
||||
void shouldAcceptVariousUrlSchemes_whenSet(String url) {
|
||||
entity.setOriginalUrl(url);
|
||||
assertThat(entity.getOriginalUrl()).isEqualTo(url);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleEmptyEntityState_whenNoFieldsSet() {
|
||||
assertThat(entity.getId()).isNull();
|
||||
assertThat(entity.getCode()).isNull();
|
||||
assertThat(entity.getOriginalUrl()).isNull();
|
||||
assertThat(entity.getCreatedAt()).isNull();
|
||||
assertThat(entity.getActivityId()).isNull();
|
||||
assertThat(entity.getInviterUserId()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldAcceptPartialEntityState_whenSomeFieldsSet() {
|
||||
entity.setCode("partial");
|
||||
entity.setOriginalUrl("https://partial.example.com");
|
||||
|
||||
assertThat(entity.getId()).isNull();
|
||||
assertThat(entity.getCode()).isEqualTo("partial");
|
||||
assertThat(entity.getActivityId()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleUnicodeCharactersInCode_whenSet() {
|
||||
entity.setCode("代码-123-🎉");
|
||||
assertThat(entity.getCode()).contains("代码").contains("🎉");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleWhitespaceOnlyCode_whenSet() {
|
||||
entity.setCode(" ");
|
||||
assertThat(entity.getCode()).isEqualTo(" ");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleTimePrecision_whenMillisecondsSet() {
|
||||
OffsetDateTime precise = OffsetDateTime.of(2024, 1, 1, 12, 0, 0, 123456789, ZoneOffset.UTC);
|
||||
entity.setCreatedAt(precise);
|
||||
assertThat(entity.getCreatedAt()).isEqualTo(precise);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleMaxLongIds_whenSet() {
|
||||
entity.setId(Long.MAX_VALUE);
|
||||
entity.setActivityId(Long.MAX_VALUE);
|
||||
entity.setInviterUserId(Long.MAX_VALUE);
|
||||
|
||||
assertThat(entity.getId()).isEqualTo(Long.MAX_VALUE);
|
||||
assertThat(entity.getActivityId()).isEqualTo(Long.MAX_VALUE);
|
||||
assertThat(entity.getInviterUserId()).isEqualTo(Long.MAX_VALUE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleNegativeIds_whenSet() {
|
||||
entity.setId(-1L);
|
||||
entity.setActivityId(-100L);
|
||||
entity.setInviterUserId(-999L);
|
||||
|
||||
assertThat(entity.getId()).isEqualTo(-1L);
|
||||
assertThat(entity.getActivityId()).isEqualTo(-100L);
|
||||
assertThat(entity.getInviterUserId()).isEqualTo(-999L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldAssociateWithActivity_whenActivityIdSet() {
|
||||
ActivityEntity activity = new ActivityEntity();
|
||||
activity.setId(100L);
|
||||
|
||||
entity.setActivityId(activity.getId());
|
||||
entity.setCode("assoc123");
|
||||
entity.setOriginalUrl("https://example.com");
|
||||
|
||||
assertThat(entity.getActivityId()).isEqualTo(100L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleZeroIds_whenSet() {
|
||||
entity.setId(0L);
|
||||
entity.setActivityId(0L);
|
||||
entity.setInviterUserId(0L);
|
||||
|
||||
assertThat(entity.getId()).isZero();
|
||||
assertThat(entity.getActivityId()).isZero();
|
||||
assertThat(entity.getInviterUserId()).isZero();
|
||||
}
|
||||
|
||||
private String generateShortCode(String originalUrl) {
|
||||
return Integer.toHexString(originalUrl.hashCode()) + "x";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,399 @@
|
||||
package com.mosquito.project.persistence.entity;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class UserInviteEntityTest {
|
||||
|
||||
private UserInviteEntity entity;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
entity = new UserInviteEntity();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnNullId_whenNotSet() {
|
||||
assertThat(entity.getId()).isNull();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource({
|
||||
"1, 1",
|
||||
"100, 100",
|
||||
"999999, 999999",
|
||||
"0, 0"
|
||||
})
|
||||
void shouldReturnSetId_whenSetWithValue(Long id, Long expected) {
|
||||
entity.setId(id);
|
||||
assertThat(entity.getId()).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnSetId_whenSetWithMaxValue() {
|
||||
entity.setId(Long.MAX_VALUE);
|
||||
assertThat(entity.getId()).isEqualTo(Long.MAX_VALUE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnNullActivityId_whenNotSet() {
|
||||
assertThat(entity.getActivityId()).isNull();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource({
|
||||
"1, 1",
|
||||
"100, 100",
|
||||
"0, 0",
|
||||
"-1, -1"
|
||||
})
|
||||
void shouldReturnSetActivityId_whenSetWithValue(Long activityId, Long expected) {
|
||||
entity.setActivityId(activityId);
|
||||
assertThat(entity.getActivityId()).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnNullInviterUserId_whenNotSet() {
|
||||
assertThat(entity.getInviterUserId()).isNull();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource({
|
||||
"1, 1",
|
||||
"999, 999",
|
||||
"0, 0",
|
||||
"-999, -999"
|
||||
})
|
||||
void shouldReturnSetInviterUserId_whenSetWithValue(Long inviterUserId, Long expected) {
|
||||
entity.setInviterUserId(inviterUserId);
|
||||
assertThat(entity.getInviterUserId()).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnNullInviteeUserId_whenNotSet() {
|
||||
assertThat(entity.getInviteeUserId()).isNull();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource({
|
||||
"1, 1",
|
||||
"999, 999",
|
||||
"0, 0",
|
||||
"-1, -1"
|
||||
})
|
||||
void shouldReturnSetInviteeUserId_whenSetWithValue(Long inviteeUserId, Long expected) {
|
||||
entity.setInviteeUserId(inviteeUserId);
|
||||
assertThat(entity.getInviteeUserId()).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnNullCreatedAt_whenNotSet() {
|
||||
assertThat(entity.getCreatedAt()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnSetCreatedAt_whenSet() {
|
||||
OffsetDateTime now = OffsetDateTime.now();
|
||||
entity.setCreatedAt(now);
|
||||
assertThat(entity.getCreatedAt()).isEqualTo(now);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleDifferentTimeZones_whenSettingCreatedAt() {
|
||||
OffsetDateTime utc = OffsetDateTime.of(2024, 1, 1, 12, 0, 0, 0, ZoneOffset.UTC);
|
||||
OffsetDateTime beijing = OffsetDateTime.of(2024, 1, 1, 20, 0, 0, 0, ZoneOffset.ofHours(8));
|
||||
OffsetDateTime newYork = OffsetDateTime.of(2024, 1, 1, 7, 0, 0, 0, ZoneOffset.ofHours(-5));
|
||||
|
||||
entity.setCreatedAt(utc);
|
||||
assertThat(entity.getCreatedAt()).isEqualTo(utc);
|
||||
|
||||
entity.setCreatedAt(beijing);
|
||||
assertThat(entity.getCreatedAt()).isEqualTo(beijing);
|
||||
|
||||
entity.setCreatedAt(newYork);
|
||||
assertThat(entity.getCreatedAt()).isEqualTo(newYork);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleEpochTime_whenSet() {
|
||||
OffsetDateTime epoch = OffsetDateTime.of(1970, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC);
|
||||
entity.setCreatedAt(epoch);
|
||||
assertThat(entity.getCreatedAt()).isEqualTo(epoch);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleFutureTime_whenSet() {
|
||||
OffsetDateTime future = OffsetDateTime.of(2099, 12, 31, 23, 59, 59, 0, ZoneOffset.UTC);
|
||||
entity.setCreatedAt(future);
|
||||
assertThat(entity.getCreatedAt()).isEqualTo(future);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnNullStatus_whenNotSet() {
|
||||
assertThat(entity.getStatus()).isNull();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {
|
||||
"PENDING",
|
||||
"ACCEPTED",
|
||||
"REJECTED",
|
||||
"EXPIRED",
|
||||
"COMPLETED",
|
||||
"active",
|
||||
"inactive"
|
||||
})
|
||||
void shouldAcceptVariousStatusValues_whenSet(String status) {
|
||||
entity.setStatus(status);
|
||||
assertThat(entity.getStatus()).isEqualTo(status);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldAcceptEmptyStatus_whenSet() {
|
||||
entity.setStatus("");
|
||||
assertThat(entity.getStatus()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldAcceptLongStatus_whenUpTo32Chars() {
|
||||
String longStatus = "S".repeat(32);
|
||||
entity.setStatus(longStatus);
|
||||
assertThat(entity.getStatus()).hasSize(32);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCreateCompleteEntity_whenAllFieldsSet() {
|
||||
entity.setId(1L);
|
||||
entity.setActivityId(100L);
|
||||
entity.setInviterUserId(50L);
|
||||
entity.setInviteeUserId(51L);
|
||||
entity.setCreatedAt(OffsetDateTime.now());
|
||||
entity.setStatus("PENDING");
|
||||
|
||||
assertThat(entity.getId()).isEqualTo(1L);
|
||||
assertThat(entity.getActivityId()).isEqualTo(100L);
|
||||
assertThat(entity.getInviterUserId()).isEqualTo(50L);
|
||||
assertThat(entity.getInviteeUserId()).isEqualTo(51L);
|
||||
assertThat(entity.getCreatedAt()).isNotNull();
|
||||
assertThat(entity.getStatus()).isEqualTo("PENDING");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldAllowFieldReassignment_whenMultipleSets() {
|
||||
entity.setId(1L);
|
||||
entity.setId(2L);
|
||||
assertThat(entity.getId()).isEqualTo(2L);
|
||||
|
||||
entity.setActivityId(100L);
|
||||
entity.setActivityId(200L);
|
||||
assertThat(entity.getActivityId()).isEqualTo(200L);
|
||||
|
||||
entity.setStatus("PENDING");
|
||||
entity.setStatus("ACCEPTED");
|
||||
assertThat(entity.getStatus()).isEqualTo("ACCEPTED");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldAcceptNullValues_whenExplicitlySetToNull() {
|
||||
entity.setId(1L);
|
||||
entity.setId(null);
|
||||
assertThat(entity.getId()).isNull();
|
||||
|
||||
entity.setStatus("ACTIVE");
|
||||
entity.setStatus(null);
|
||||
assertThat(entity.getStatus()).isNull();
|
||||
|
||||
entity.setCreatedAt(OffsetDateTime.now());
|
||||
entity.setCreatedAt(null);
|
||||
assertThat(entity.getCreatedAt()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSupportChainedSetters_whenBuildingEntity() {
|
||||
OffsetDateTime createdAt = OffsetDateTime.now();
|
||||
entity.setId(1L);
|
||||
entity.setActivityId(100L);
|
||||
entity.setInviterUserId(50L);
|
||||
entity.setInviteeUserId(51L);
|
||||
entity.setCreatedAt(createdAt);
|
||||
entity.setStatus("PENDING");
|
||||
|
||||
assertThat(entity.getId()).isEqualTo(1L);
|
||||
assertThat(entity.getActivityId()).isEqualTo(100L);
|
||||
assertThat(entity.getInviterUserId()).isEqualTo(50L);
|
||||
assertThat(entity.getInviteeUserId()).isEqualTo(51L);
|
||||
assertThat(entity.getCreatedAt()).isEqualTo(createdAt);
|
||||
assertThat(entity.getStatus()).isEqualTo("PENDING");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleEmptyEntityState_whenNoFieldsSet() {
|
||||
assertThat(entity.getId()).isNull();
|
||||
assertThat(entity.getActivityId()).isNull();
|
||||
assertThat(entity.getInviterUserId()).isNull();
|
||||
assertThat(entity.getInviteeUserId()).isNull();
|
||||
assertThat(entity.getCreatedAt()).isNull();
|
||||
assertThat(entity.getStatus()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldAcceptPartialEntityState_whenSomeFieldsSet() {
|
||||
entity.setActivityId(100L);
|
||||
entity.setInviteeUserId(50L);
|
||||
|
||||
assertThat(entity.getId()).isNull();
|
||||
assertThat(entity.getActivityId()).isEqualTo(100L);
|
||||
assertThat(entity.getInviterUserId()).isNull();
|
||||
assertThat(entity.getInviteeUserId()).isEqualTo(50L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldAssociateWithActivityEntity_whenActivityIdSet() {
|
||||
ActivityEntity activity = new ActivityEntity();
|
||||
activity.setId(100L);
|
||||
|
||||
entity.setActivityId(activity.getId());
|
||||
entity.setInviterUserId(1L);
|
||||
entity.setInviteeUserId(2L);
|
||||
|
||||
assertThat(entity.getActivityId()).isEqualTo(100L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleMaxLongIds_whenSet() {
|
||||
entity.setId(Long.MAX_VALUE);
|
||||
entity.setActivityId(Long.MAX_VALUE);
|
||||
entity.setInviterUserId(Long.MAX_VALUE);
|
||||
entity.setInviteeUserId(Long.MAX_VALUE);
|
||||
|
||||
assertThat(entity.getId()).isEqualTo(Long.MAX_VALUE);
|
||||
assertThat(entity.getActivityId()).isEqualTo(Long.MAX_VALUE);
|
||||
assertThat(entity.getInviterUserId()).isEqualTo(Long.MAX_VALUE);
|
||||
assertThat(entity.getInviteeUserId()).isEqualTo(Long.MAX_VALUE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleNegativeIds_whenSet() {
|
||||
entity.setId(-1L);
|
||||
entity.setActivityId(-100L);
|
||||
entity.setInviterUserId(-999L);
|
||||
entity.setInviteeUserId(-888L);
|
||||
|
||||
assertThat(entity.getId()).isEqualTo(-1L);
|
||||
assertThat(entity.getActivityId()).isEqualTo(-100L);
|
||||
assertThat(entity.getInviterUserId()).isEqualTo(-999L);
|
||||
assertThat(entity.getInviteeUserId()).isEqualTo(-888L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleZeroIds_whenSet() {
|
||||
entity.setId(0L);
|
||||
entity.setActivityId(0L);
|
||||
entity.setInviterUserId(0L);
|
||||
entity.setInviteeUserId(0L);
|
||||
|
||||
assertThat(entity.getId()).isZero();
|
||||
assertThat(entity.getActivityId()).isZero();
|
||||
assertThat(entity.getInviterUserId()).isZero();
|
||||
assertThat(entity.getInviteeUserId()).isZero();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleTimePrecision_whenMillisecondsSet() {
|
||||
OffsetDateTime precise = OffsetDateTime.of(2024, 1, 1, 12, 0, 0, 123456789, ZoneOffset.UTC);
|
||||
entity.setCreatedAt(precise);
|
||||
assertThat(entity.getCreatedAt()).isEqualTo(precise);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleUnicodeCharactersInStatus_whenSet() {
|
||||
entity.setStatus("状态-🎉-émoji");
|
||||
assertThat(entity.getStatus()).contains("状态").contains("🎉");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleWhitespaceOnlyStatus_whenSet() {
|
||||
entity.setStatus(" ");
|
||||
assertThat(entity.getStatus()).isEqualTo(" ");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleStatusTransitions_whenChangedMultipleTimes() {
|
||||
entity.setStatus("PENDING");
|
||||
assertThat(entity.getStatus()).isEqualTo("PENDING");
|
||||
|
||||
entity.setStatus("ACCEPTED");
|
||||
assertThat(entity.getStatus()).isEqualTo("ACCEPTED");
|
||||
|
||||
entity.setStatus("COMPLETED");
|
||||
assertThat(entity.getStatus()).isEqualTo("COMPLETED");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRepresentInviteRelationship_whenBothUserIdsSet() {
|
||||
Long inviterId = 100L;
|
||||
Long inviteeId = 200L;
|
||||
|
||||
entity.setInviterUserId(inviterId);
|
||||
entity.setInviteeUserId(inviteeId);
|
||||
|
||||
assertThat(entity.getInviterUserId()).isNotEqualTo(entity.getInviteeUserId());
|
||||
assertThat(entity.getInviterUserId()).isEqualTo(inviterId);
|
||||
assertThat(entity.getInviteeUserId()).isEqualTo(inviteeId);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleSameUserAsInviterAndInvitee_whenSet() {
|
||||
entity.setInviterUserId(100L);
|
||||
entity.setInviteeUserId(100L);
|
||||
|
||||
assertThat(entity.getInviterUserId()).isEqualTo(entity.getInviteeUserId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleStatusWithSpecialCharacters_whenSet() {
|
||||
entity.setStatus("STATUS_WITH_UNDERSCORES-123");
|
||||
assertThat(entity.getStatus()).contains("_").contains("-").contains("123");
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"ACTIVE", "INACTIVE", "SUSPENDED", "DELETED", "ARCHIVED"})
|
||||
void shouldAcceptCommonStatusEnumValues_whenSet(String status) {
|
||||
entity.setStatus(status);
|
||||
assertThat(entity.getStatus()).isEqualTo(status);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMaintainConsistency_whenSelfReferencingInvite() {
|
||||
entity.setActivityId(1L);
|
||||
entity.setInviterUserId(50L);
|
||||
entity.setInviteeUserId(50L);
|
||||
entity.setStatus("SELF_INVITE");
|
||||
|
||||
assertThat(entity.getInviterUserId()).isEqualTo(entity.getInviteeUserId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleLargeActivityId_whenSet() {
|
||||
entity.setActivityId(9999999999L);
|
||||
assertThat(entity.getActivityId()).isEqualTo(9999999999L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleAllStatusesAsString_whenNoEnumConstraint() {
|
||||
String[] statuses = {"0", "1", "true", "false", "yes", "no", "null", "undefined"};
|
||||
|
||||
for (String status : statuses) {
|
||||
entity.setStatus(status);
|
||||
assertThat(entity.getStatus()).isEqualTo(status);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.mosquito.project.persistence.repository;
|
||||
|
||||
import com.mosquito.project.persistence.entity.ActivityEntity;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
|
||||
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
@DataJpaTest
|
||||
@org.springframework.context.annotation.Import(com.mosquito.project.config.TestCacheConfig.class)
|
||||
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.ANY)
|
||||
@TestPropertySource(properties = {
|
||||
"spring.cache.type=NONE",
|
||||
"spring.jpa.hibernate.ddl-auto=create-drop",
|
||||
"spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration,org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration,org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration"
|
||||
})
|
||||
class ActivityRepositoryTest {
|
||||
|
||||
@Autowired
|
||||
private ActivityRepository activityRepository;
|
||||
|
||||
@Test
|
||||
void whenSaveActivity_thenCanLoadIt() {
|
||||
OffsetDateTime now = OffsetDateTime.now(ZoneOffset.UTC);
|
||||
ActivityEntity e = new ActivityEntity();
|
||||
e.setName("Repo Test Activity");
|
||||
e.setStartTimeUtc(now.plusDays(1));
|
||||
e.setEndTimeUtc(now.plusDays(2));
|
||||
e.setRewardCalculationMode("delta");
|
||||
e.setStatus("draft");
|
||||
e.setCreatedAt(now);
|
||||
e.setUpdatedAt(now);
|
||||
|
||||
ActivityEntity saved = activityRepository.save(e);
|
||||
assertNotNull(saved.getId());
|
||||
|
||||
ActivityEntity found = activityRepository.findById(saved.getId()).orElse(null);
|
||||
assertNotNull(found);
|
||||
assertEquals("Repo Test Activity", found.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenUpdateActivity_thenPersistedChangesVisible() {
|
||||
OffsetDateTime now = OffsetDateTime.now(ZoneOffset.UTC);
|
||||
ActivityEntity e = new ActivityEntity();
|
||||
e.setName("Old Name");
|
||||
e.setStartTimeUtc(now.plusDays(1));
|
||||
e.setEndTimeUtc(now.plusDays(2));
|
||||
e.setRewardCalculationMode("delta");
|
||||
e.setStatus("draft");
|
||||
e.setCreatedAt(now);
|
||||
e.setUpdatedAt(now);
|
||||
ActivityEntity saved = activityRepository.save(e);
|
||||
|
||||
saved.setName("New Name");
|
||||
saved.setUpdatedAt(now.plusMinutes(1));
|
||||
ActivityEntity updated = activityRepository.save(saved);
|
||||
|
||||
ActivityEntity found = activityRepository.findById(updated.getId()).orElseThrow();
|
||||
assertEquals("New Name", found.getName());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
package com.mosquito.project.persistence.repository;
|
||||
|
||||
import com.mosquito.project.persistence.entity.ApiKeyEntity;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
|
||||
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* ApiKeyRepository 数据访问层测试
|
||||
* 测试API密钥的CRUD操作和安全相关查询方法
|
||||
*/
|
||||
@DataJpaTest
|
||||
@org.springframework.context.annotation.Import(com.mosquito.project.config.TestCacheConfig.class)
|
||||
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.ANY)
|
||||
@TestPropertySource(properties = {
|
||||
"spring.cache.type=NONE",
|
||||
"spring.jpa.hibernate.ddl-auto=create-drop",
|
||||
"spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration,org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration,org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration"
|
||||
})
|
||||
class ApiKeyRepositoryTest {
|
||||
|
||||
@Autowired
|
||||
private ApiKeyRepository apiKeyRepository;
|
||||
|
||||
private static final Long ACTIVITY_ID = 1L;
|
||||
private OffsetDateTime now;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
now = OffsetDateTime.now(ZoneOffset.UTC);
|
||||
apiKeyRepository.deleteAll();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSaveAndFindApiKeyById() {
|
||||
// 创建API密钥
|
||||
ApiKeyEntity apiKey = createApiKey("Test Key", "hash123", "salt123", "prefix123", "encrypted123");
|
||||
ApiKeyEntity saved = apiKeyRepository.save(apiKey);
|
||||
|
||||
// 验证保存
|
||||
assertNotNull(saved.getId(), "保存后ID不应为空");
|
||||
assertEquals("Test Key", saved.getName());
|
||||
|
||||
// 通过ID查询
|
||||
ApiKeyEntity found = apiKeyRepository.findById(saved.getId()).orElseThrow();
|
||||
assertEquals("hash123", found.getKeyHash());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFindByKeyHash() {
|
||||
// 创建并保存API密钥
|
||||
ApiKeyEntity apiKey = createApiKey("Production Key", "secure_hash_123", "salt_abc", "prod_", "enc_data");
|
||||
apiKeyRepository.save(apiKey);
|
||||
|
||||
// 通过keyHash查询
|
||||
Optional<ApiKeyEntity> found = apiKeyRepository.findByKeyHash("secure_hash_123");
|
||||
|
||||
// 验证
|
||||
assertTrue(found.isPresent(), "应该能通过keyHash找到实体");
|
||||
assertEquals("Production Key", found.get().getName());
|
||||
assertEquals("secure_hash_123", found.get().getKeyHash());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnEmptyWhenKeyHashNotFound() {
|
||||
// 查询不存在的keyHash
|
||||
Optional<ApiKeyEntity> found = apiKeyRepository.findByKeyHash("non_existent_hash");
|
||||
assertFalse(found.isPresent(), "不存在的keyHash应该返回空Optional");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFindByKeyPrefix() {
|
||||
// 创建并保存API密钥
|
||||
ApiKeyEntity apiKey = createApiKey("Staging Key", "hash_stg_456", "salt_def", "stg_", "enc_staging");
|
||||
apiKeyRepository.save(apiKey);
|
||||
|
||||
// 通过keyPrefix查询
|
||||
Optional<ApiKeyEntity> found = apiKeyRepository.findByKeyPrefix("stg_");
|
||||
|
||||
// 验证
|
||||
assertTrue(found.isPresent(), "应该能通过keyPrefix找到实体");
|
||||
assertEquals("stg_", found.get().getKeyPrefix());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnEmptyWhenKeyPrefixNotFound() {
|
||||
// 查询不存在的keyPrefix
|
||||
Optional<ApiKeyEntity> found = apiKeyRepository.findByKeyPrefix("nonexistent_");
|
||||
assertFalse(found.isPresent(), "不存在的keyPrefix应该返回空Optional");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldEnforceUniqueKeyHashConstraint() {
|
||||
// 创建第一个密钥
|
||||
ApiKeyEntity key1 = createApiKey("Key 1", "duplicate_hash", "salt1", "pre1_", "enc1");
|
||||
apiKeyRepository.save(key1);
|
||||
|
||||
// 创建第二个密钥,使用相同的keyHash
|
||||
ApiKeyEntity key2 = createApiKey("Key 2", "duplicate_hash", "salt2", "pre2_", "enc2");
|
||||
|
||||
// 验证违反唯一约束
|
||||
assertThrows(Exception.class, () -> {
|
||||
apiKeyRepository.saveAndFlush(key2);
|
||||
}, "重复的keyHash应该违反唯一约束");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldAllowSameKeyPrefixForDifferentKeys() {
|
||||
// 两个不同的密钥使用相同的keyPrefix应该允许
|
||||
ApiKeyEntity key1 = createApiKey("Key 1", "hash1", "salt1", "shared_prefix_", "enc1");
|
||||
ApiKeyEntity key2 = createApiKey("Key 2", "hash2", "salt2", "same_prefix_", "enc2");
|
||||
|
||||
assertDoesNotThrow(() -> {
|
||||
apiKeyRepository.save(key1);
|
||||
apiKeyRepository.save(key2);
|
||||
}, "不同的密钥应该可以共存");
|
||||
|
||||
// 查询第一个密钥
|
||||
Optional<ApiKeyEntity> found1 = apiKeyRepository.findByKeyPrefix("shared_prefix_");
|
||||
assertTrue(found1.isPresent(), "应该能通过keyPrefix找到Key 1");
|
||||
assertEquals("Key 1", found1.get().getName());
|
||||
|
||||
// 查询第二个密钥
|
||||
Optional<ApiKeyEntity> found2 = apiKeyRepository.findByKeyPrefix("same_prefix_");
|
||||
assertTrue(found2.isPresent(), "应该能通过keyPrefix找到Key 2");
|
||||
assertEquals("Key 2", found2.get().getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUpdateApiKeyFields() {
|
||||
// 创建并保存初始密钥
|
||||
ApiKeyEntity apiKey = createApiKey("Original Name", "hash123", "salt123", "pre_", "enc");
|
||||
ApiKeyEntity saved = apiKeyRepository.save(apiKey);
|
||||
|
||||
// 更新多个字段
|
||||
saved.setName("Updated Name");
|
||||
saved.setLastUsedAt(now.plusMinutes(5));
|
||||
saved.setRevokedAt(now.plusHours(1));
|
||||
apiKeyRepository.save(saved);
|
||||
|
||||
// 验证更新
|
||||
ApiKeyEntity updated = apiKeyRepository.findById(saved.getId()).orElseThrow();
|
||||
assertEquals("Updated Name", updated.getName());
|
||||
assertNotNull(updated.getLastUsedAt());
|
||||
assertNotNull(updated.getRevokedAt());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldTrackKeyLifecycle() {
|
||||
// 创建新密钥
|
||||
ApiKeyEntity apiKey = createApiKey("Lifecycle Test Key", "lifecycle_hash", "salt", "lc_", "enc");
|
||||
apiKey.setCreatedAt(now);
|
||||
ApiKeyEntity saved = apiKeyRepository.save(apiKey);
|
||||
|
||||
// 验证初始状态
|
||||
assertNotNull(saved.getCreatedAt());
|
||||
assertNull(saved.getLastUsedAt());
|
||||
assertNull(saved.getRevokedAt());
|
||||
assertNull(saved.getRevealedAt());
|
||||
|
||||
// 模拟使用
|
||||
saved.setLastUsedAt(now.plusMinutes(10));
|
||||
apiKeyRepository.save(saved);
|
||||
|
||||
// 模拟显示
|
||||
ApiKeyEntity updated = apiKeyRepository.findById(saved.getId()).orElseThrow();
|
||||
updated.setRevealedAt(now.plusMinutes(20));
|
||||
apiKeyRepository.save(updated);
|
||||
|
||||
// 模拟撤销
|
||||
ApiKeyEntity revoked = apiKeyRepository.findById(saved.getId()).orElseThrow();
|
||||
revoked.setRevokedAt(now.plusHours(1));
|
||||
apiKeyRepository.save(revoked);
|
||||
|
||||
// 验证最终状态
|
||||
ApiKeyEntity finalState = apiKeyRepository.findById(saved.getId()).orElseThrow();
|
||||
assertNotNull(finalState.getCreatedAt());
|
||||
assertNotNull(finalState.getLastUsedAt());
|
||||
assertNotNull(finalState.getRevealedAt());
|
||||
assertNotNull(finalState.getRevokedAt());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDeleteApiKey() {
|
||||
// 创建并保存密钥
|
||||
ApiKeyEntity apiKey = createApiKey("To Be Deleted", "delete_hash", "salt", "del_", "enc");
|
||||
ApiKeyEntity saved = apiKeyRepository.save(apiKey);
|
||||
Long id = saved.getId();
|
||||
|
||||
// 删除
|
||||
apiKeyRepository.deleteById(id);
|
||||
|
||||
// 验证删除
|
||||
assertFalse(apiKeyRepository.existsById(id), "删除后应该不存在");
|
||||
assertTrue(apiKeyRepository.findByKeyHash("delete_hash").isEmpty(), "通过hash查询也应该找不到");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSupportMultipleApiKeysPerActivity() {
|
||||
// 为一个活动创建多个密钥
|
||||
for (int i = 0; i < 5; i++) {
|
||||
ApiKeyEntity apiKey = createApiKey("Key " + i, "hash_" + i, "salt_" + i, "pre_" + i + "_", "enc_" + i);
|
||||
apiKeyRepository.save(apiKey);
|
||||
}
|
||||
|
||||
// 验证保存数量
|
||||
assertEquals(5, apiKeyRepository.count(), "应该有5个API密钥");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleLongHashAndEncryptedKey() {
|
||||
// 创建包含长字符串的密钥
|
||||
String longHash = "a".repeat(255);
|
||||
String longEncryptedKey = "b".repeat(512);
|
||||
|
||||
ApiKeyEntity apiKey = new ApiKeyEntity();
|
||||
apiKey.setName("Long Key Test");
|
||||
apiKey.setKeyHash(longHash);
|
||||
apiKey.setSalt("salt");
|
||||
apiKey.setKeyPrefix("pre_");
|
||||
apiKey.setEncryptedKey(longEncryptedKey);
|
||||
apiKey.setActivityId(ACTIVITY_ID);
|
||||
apiKey.setCreatedAt(now);
|
||||
|
||||
ApiKeyEntity saved = apiKeyRepository.save(apiKey);
|
||||
|
||||
// 验证保存和查询
|
||||
ApiKeyEntity found = apiKeyRepository.findByKeyHash(longHash).orElseThrow();
|
||||
assertEquals(longHash, found.getKeyHash());
|
||||
assertEquals(longEncryptedKey, found.getEncryptedKey());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldPreserveAllFieldsOnSaveAndRetrieve() {
|
||||
// 创建完整记录
|
||||
ApiKeyEntity apiKey = new ApiKeyEntity();
|
||||
apiKey.setName("Complete Test Key");
|
||||
apiKey.setKeyHash("complete_hash");
|
||||
apiKey.setSalt("complete_salt");
|
||||
apiKey.setActivityId(ACTIVITY_ID);
|
||||
apiKey.setKeyPrefix("comp_");
|
||||
apiKey.setEncryptedKey("complete_encrypted_data");
|
||||
apiKey.setCreatedAt(now);
|
||||
apiKey.setRevokedAt(now.plusDays(30));
|
||||
apiKey.setLastUsedAt(now.plusMinutes(5));
|
||||
apiKey.setRevealedAt(now.plusMinutes(1));
|
||||
|
||||
ApiKeyEntity saved = apiKeyRepository.save(apiKey);
|
||||
|
||||
// 查询并验证所有字段
|
||||
ApiKeyEntity found = apiKeyRepository.findById(saved.getId()).orElseThrow();
|
||||
assertEquals("Complete Test Key", found.getName());
|
||||
assertEquals("complete_hash", found.getKeyHash());
|
||||
assertEquals("complete_salt", found.getSalt());
|
||||
assertEquals(ACTIVITY_ID, found.getActivityId());
|
||||
assertEquals("comp_", found.getKeyPrefix());
|
||||
assertEquals("complete_encrypted_data", found.getEncryptedKey());
|
||||
assertNotNull(found.getCreatedAt());
|
||||
assertNotNull(found.getRevokedAt());
|
||||
assertNotNull(found.getLastUsedAt());
|
||||
assertNotNull(found.getRevealedAt());
|
||||
}
|
||||
|
||||
/**
|
||||
* 辅助方法:创建API密钥实体
|
||||
*/
|
||||
private ApiKeyEntity createApiKey(String name, String keyHash, String salt, String keyPrefix, String encryptedKey) {
|
||||
ApiKeyEntity apiKey = new ApiKeyEntity();
|
||||
apiKey.setName(name);
|
||||
apiKey.setKeyHash(keyHash);
|
||||
apiKey.setSalt(salt);
|
||||
apiKey.setKeyPrefix(keyPrefix);
|
||||
apiKey.setEncryptedKey(encryptedKey);
|
||||
apiKey.setActivityId(ACTIVITY_ID);
|
||||
apiKey.setCreatedAt(now);
|
||||
return apiKey;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
package com.mosquito.project.persistence.repository;
|
||||
|
||||
import com.mosquito.project.persistence.entity.LinkClickEntity;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
|
||||
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* LinkClickRepository 数据访问层测试
|
||||
* 测试链接点击记录的CRUD操作和分析查询方法
|
||||
*/
|
||||
@DataJpaTest
|
||||
@org.springframework.context.annotation.Import(com.mosquito.project.config.TestCacheConfig.class)
|
||||
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.ANY)
|
||||
@TestPropertySource(properties = {
|
||||
"spring.cache.type=NONE",
|
||||
"spring.jpa.hibernate.ddl-auto=create-drop",
|
||||
"spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration,org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration,org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration"
|
||||
})
|
||||
class LinkClickRepositoryTest {
|
||||
|
||||
@Autowired
|
||||
private LinkClickRepository linkClickRepository;
|
||||
|
||||
private static final Long ACTIVITY_ID_1 = 1L;
|
||||
private static final Long ACTIVITY_ID_2 = 2L;
|
||||
private static final Long USER_1 = 101L;
|
||||
private static final Long USER_2 = 102L;
|
||||
private static final String CODE_1 = "abc123";
|
||||
private static final String CODE_2 = "def456";
|
||||
private OffsetDateTime now;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
now = OffsetDateTime.now(ZoneOffset.UTC);
|
||||
linkClickRepository.deleteAll();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSaveAndFindLinkClickById() {
|
||||
// 创建点击记录
|
||||
LinkClickEntity click = createClick(CODE_1, ACTIVITY_ID_1, USER_1, "192.168.1.1");
|
||||
LinkClickEntity saved = linkClickRepository.save(click);
|
||||
|
||||
// 验证保存
|
||||
assertNotNull(saved.getId(), "保存后ID不应为空");
|
||||
assertEquals(CODE_1, saved.getCode());
|
||||
|
||||
// 通过ID查询
|
||||
LinkClickEntity found = linkClickRepository.findById(saved.getId()).orElseThrow();
|
||||
assertEquals("192.168.1.1", found.getIp());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFindByActivityId() {
|
||||
// 为活动1创建点击记录
|
||||
linkClickRepository.save(createClick(CODE_1, ACTIVITY_ID_1, USER_1, "192.168.1.1"));
|
||||
linkClickRepository.save(createClick(CODE_1, ACTIVITY_ID_1, USER_1, "192.168.1.2"));
|
||||
linkClickRepository.save(createClick(CODE_2, ACTIVITY_ID_1, USER_2, "192.168.1.3"));
|
||||
|
||||
// 为活动2创建点击记录(应该被排除)
|
||||
linkClickRepository.save(createClick(CODE_1, ACTIVITY_ID_2, USER_1, "192.168.2.1"));
|
||||
|
||||
// 查询活动1的所有点击
|
||||
List<LinkClickEntity> clicks = linkClickRepository.findByActivityId(ACTIVITY_ID_1);
|
||||
|
||||
// 验证
|
||||
assertEquals(3, clicks.size(), "活动1应该有3条点击记录");
|
||||
assertTrue(clicks.stream().allMatch(c -> c.getActivityId().equals(ACTIVITY_ID_1)),
|
||||
"所有记录都应属于活动1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFindByActivityIdAndCreatedAtBetween() {
|
||||
// 创建不同时间的点击记录
|
||||
OffsetDateTime twoHoursAgo = now.minusHours(2);
|
||||
OffsetDateTime oneHourAgo = now.minusHours(1);
|
||||
OffsetDateTime thirtyMinutesAgo = now.minusMinutes(30);
|
||||
|
||||
linkClickRepository.save(createClickWithTime(CODE_1, ACTIVITY_ID_1, USER_1, "192.168.1.1", twoHoursAgo));
|
||||
linkClickRepository.save(createClickWithTime(CODE_1, ACTIVITY_ID_1, USER_1, "192.168.1.2", oneHourAgo));
|
||||
linkClickRepository.save(createClickWithTime(CODE_1, ACTIVITY_ID_1, USER_1, "192.168.1.3", thirtyMinutesAgo));
|
||||
|
||||
// 查询过去1.5小时内的点击
|
||||
List<LinkClickEntity> clicks = linkClickRepository.findByActivityIdAndCreatedAtBetween(
|
||||
ACTIVITY_ID_1, now.minusMinutes(90), now);
|
||||
|
||||
// 验证 - 应该返回过去1.5小时内的2条记录
|
||||
assertEquals(2, clicks.size(), "过去1.5小时内应该有2条点击记录");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFindByCode() {
|
||||
// 创建不同code的点击记录
|
||||
linkClickRepository.save(createClick(CODE_1, ACTIVITY_ID_1, USER_1, "192.168.1.1"));
|
||||
linkClickRepository.save(createClick(CODE_1, ACTIVITY_ID_1, USER_1, "192.168.1.2"));
|
||||
linkClickRepository.save(createClick(CODE_2, ACTIVITY_ID_1, USER_2, "192.168.1.3"));
|
||||
|
||||
// 查询CODE_1的点击
|
||||
List<LinkClickEntity> clicks = linkClickRepository.findByCode(CODE_1);
|
||||
|
||||
// 验证
|
||||
assertEquals(2, clicks.size(), "CODE_1应该有2条点击记录");
|
||||
assertTrue(clicks.stream().allMatch(c -> c.getCode().equals(CODE_1)),
|
||||
"所有记录都应该是CODE_1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCountByActivityId() {
|
||||
// 创建测试数据
|
||||
linkClickRepository.save(createClick(CODE_1, ACTIVITY_ID_1, USER_1, "192.168.1.1"));
|
||||
linkClickRepository.save(createClick(CODE_1, ACTIVITY_ID_1, USER_1, "192.168.1.2"));
|
||||
linkClickRepository.save(createClick(CODE_2, ACTIVITY_ID_1, USER_2, "192.168.1.3"));
|
||||
linkClickRepository.save(createClick(CODE_1, ACTIVITY_ID_2, USER_1, "192.168.2.1"));
|
||||
|
||||
// 统计活动1的点击数
|
||||
long count = linkClickRepository.countByActivityId(ACTIVITY_ID_1);
|
||||
|
||||
// 验证
|
||||
assertEquals(3, count, "活动1应该有3条点击记录");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCountUniqueVisitorsByActivityIdAndDateRange() {
|
||||
// 创建来自不同IP的点击记录(有些是重复IP)
|
||||
OffsetDateTime startTime = now.minusHours(1);
|
||||
OffsetDateTime endTime = now;
|
||||
|
||||
// 同一IP多次点击
|
||||
linkClickRepository.save(createClickWithTime(CODE_1, ACTIVITY_ID_1, USER_1, "192.168.1.1", startTime.plusMinutes(10)));
|
||||
linkClickRepository.save(createClickWithTime(CODE_1, ACTIVITY_ID_1, USER_1, "192.168.1.1", startTime.plusMinutes(20)));
|
||||
linkClickRepository.save(createClickWithTime(CODE_2, ACTIVITY_ID_1, USER_2, "192.168.1.2", startTime.plusMinutes(15)));
|
||||
linkClickRepository.save(createClickWithTime(CODE_1, ACTIVITY_ID_1, USER_1, "192.168.1.3", startTime.plusMinutes(30)));
|
||||
|
||||
// 范围外的点击(应该被排除)
|
||||
linkClickRepository.save(createClickWithTime(CODE_1, ACTIVITY_ID_1, USER_1, "192.168.1.4", now.minusHours(2)));
|
||||
linkClickRepository.save(createClickWithTime(CODE_1, ACTIVITY_ID_2, USER_1, "192.168.1.5", startTime.plusMinutes(5)));
|
||||
|
||||
// 查询独立访客数
|
||||
long uniqueVisitors = linkClickRepository.countUniqueVisitorsByActivityIdAndDateRange(
|
||||
ACTIVITY_ID_1, startTime, endTime);
|
||||
|
||||
// 验证 - 应该有3个独立IP
|
||||
assertEquals(3, uniqueVisitors, "应该有3个独立访客(192.168.1.1, 192.168.1.2, 192.168.1.3)");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFindTopSharedLinksByActivityId() {
|
||||
// 创建点击数据:CODE_1有5次点击,CODE_2有3次点击,CODE_3有1次点击
|
||||
for (int i = 0; i < 5; i++) {
|
||||
linkClickRepository.save(createClick(CODE_1, ACTIVITY_ID_1, USER_1, "192.168.1." + i));
|
||||
}
|
||||
for (int i = 0; i < 3; i++) {
|
||||
linkClickRepository.save(createClick(CODE_2, ACTIVITY_ID_1, USER_2, "192.168.2." + i));
|
||||
}
|
||||
linkClickRepository.save(createClick("xyz789", ACTIVITY_ID_1, USER_1, "192.168.3.1"));
|
||||
|
||||
// 为活动2创建点击(应该被排除)
|
||||
for (int i = 0; i < 10; i++) {
|
||||
linkClickRepository.save(createClick(CODE_1, ACTIVITY_ID_2, USER_1, "192.168.4." + i));
|
||||
}
|
||||
|
||||
// 查询活动1的热门链接(前2个)
|
||||
List<Object[]> topLinks = linkClickRepository.findTopSharedLinksByActivityId(ACTIVITY_ID_1, 2);
|
||||
|
||||
// 验证
|
||||
assertEquals(2, topLinks.size(), "应该返回前2个热门链接");
|
||||
|
||||
// 验证排序
|
||||
assertEquals(CODE_1, topLinks.get(0)[0], "第一名应该是CODE_1");
|
||||
assertEquals(5L, topLinks.get(0)[1], "CODE_1应该有5次点击");
|
||||
|
||||
assertEquals(CODE_2, topLinks.get(1)[0], "第二名应该是CODE_2");
|
||||
assertEquals(3L, topLinks.get(1)[1], "CODE_2应该有3次点击");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldStoreAndRetrieveParams() {
|
||||
// 创建带参数的点击记录
|
||||
LinkClickEntity click = createClick(CODE_1, ACTIVITY_ID_1, USER_1, "192.168.1.1");
|
||||
Map<String, String> params = new HashMap<>();
|
||||
params.put("utm_source", "wechat");
|
||||
params.put("utm_medium", "share");
|
||||
params.put("campaign", "summer2024");
|
||||
click.setParams(params);
|
||||
|
||||
LinkClickEntity saved = linkClickRepository.save(click);
|
||||
|
||||
// 查询并验证参数
|
||||
LinkClickEntity found = linkClickRepository.findById(saved.getId()).orElseThrow();
|
||||
Map<String, String> retrievedParams = found.getParams();
|
||||
|
||||
assertNotNull(retrievedParams);
|
||||
assertEquals("wechat", retrievedParams.get("utm_source"));
|
||||
assertEquals("share", retrievedParams.get("utm_medium"));
|
||||
assertEquals("summer2024", retrievedParams.get("campaign"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnEmptyListForNonExistentActivity() {
|
||||
// 查询不存在的活动
|
||||
List<LinkClickEntity> clicks = linkClickRepository.findByActivityId(999L);
|
||||
assertTrue(clicks.isEmpty(), "不存在的活动应该返回空列表");
|
||||
|
||||
// 统计不存在的活动
|
||||
long count = linkClickRepository.countByActivityId(999L);
|
||||
assertEquals(0, count, "不存在的活动计数应该为0");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleLargeNumberOfClicks() {
|
||||
// 批量创建1000条点击记录
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
String ip = "192.168." + (i / 256) + "." + (i % 256);
|
||||
linkClickRepository.save(createClick(CODE_1, ACTIVITY_ID_1, USER_1, ip));
|
||||
}
|
||||
|
||||
// 验证计数
|
||||
long count = linkClickRepository.countByActivityId(ACTIVITY_ID_1);
|
||||
assertEquals(1000, count, "应该有1000条点击记录");
|
||||
|
||||
// 验证独立访客数
|
||||
long uniqueVisitors = linkClickRepository.countUniqueVisitorsByActivityIdAndDateRange(
|
||||
ACTIVITY_ID_1, now.minusHours(1), now.plusHours(1));
|
||||
assertEquals(1000, uniqueVisitors, "应该有1000个独立访客");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldStoreAllMetadataFields() {
|
||||
// 创建完整的点击记录
|
||||
LinkClickEntity click = new LinkClickEntity();
|
||||
click.setCode(CODE_1);
|
||||
click.setActivityId(ACTIVITY_ID_1);
|
||||
click.setInviterUserId(USER_1);
|
||||
click.setIp("192.168.1.1");
|
||||
click.setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
|
||||
click.setReferer("https://example.com/page");
|
||||
click.setCreatedAt(now);
|
||||
|
||||
Map<String, String> params = new HashMap<>();
|
||||
params.put("track_id", "12345");
|
||||
click.setParams(params);
|
||||
|
||||
LinkClickEntity saved = linkClickRepository.save(click);
|
||||
|
||||
// 查询并验证所有字段
|
||||
LinkClickEntity found = linkClickRepository.findById(saved.getId()).orElseThrow();
|
||||
assertEquals(CODE_1, found.getCode());
|
||||
assertEquals(ACTIVITY_ID_1, found.getActivityId());
|
||||
assertEquals(USER_1, found.getInviterUserId());
|
||||
assertEquals("192.168.1.1", found.getIp());
|
||||
assertEquals("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", found.getUserAgent());
|
||||
assertEquals("https://example.com/page", found.getReferer());
|
||||
assertNotNull(found.getCreatedAt());
|
||||
assertNotNull(found.getParams());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDeleteClickRecord() {
|
||||
// 创建并保存记录
|
||||
LinkClickEntity click = createClick(CODE_1, ACTIVITY_ID_1, USER_1, "192.168.1.1");
|
||||
LinkClickEntity saved = linkClickRepository.save(click);
|
||||
Long id = saved.getId();
|
||||
|
||||
// 删除
|
||||
linkClickRepository.deleteById(id);
|
||||
|
||||
// 验证删除
|
||||
assertFalse(linkClickRepository.existsById(id), "删除后应该不存在");
|
||||
}
|
||||
|
||||
/**
|
||||
* 辅助方法:创建点击实体
|
||||
*/
|
||||
private LinkClickEntity createClick(String code, Long activityId, Long inviterUserId, String ip) {
|
||||
LinkClickEntity click = new LinkClickEntity();
|
||||
click.setCode(code);
|
||||
click.setActivityId(activityId);
|
||||
click.setInviterUserId(inviterUserId);
|
||||
click.setIp(ip);
|
||||
click.setUserAgent("Mozilla/5.0 (Test)");
|
||||
click.setCreatedAt(now);
|
||||
return click;
|
||||
}
|
||||
|
||||
/**
|
||||
* 辅助方法:创建带指定时间的点击实体
|
||||
*/
|
||||
private LinkClickEntity createClickWithTime(String code, Long activityId, Long inviterUserId, String ip, OffsetDateTime time) {
|
||||
LinkClickEntity click = new LinkClickEntity();
|
||||
click.setCode(code);
|
||||
click.setActivityId(activityId);
|
||||
click.setInviterUserId(inviterUserId);
|
||||
click.setIp(ip);
|
||||
click.setUserAgent("Mozilla/5.0 (Test)");
|
||||
click.setCreatedAt(time);
|
||||
return click;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.mosquito.project.persistence.repository;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
|
||||
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
import org.springframework.jdbc.core.ResultSetExtractor;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
@DataJpaTest
|
||||
@org.springframework.context.annotation.Import(com.mosquito.project.config.TestCacheConfig.class)
|
||||
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.ANY)
|
||||
@TestPropertySource(properties = {
|
||||
"spring.cache.type=NONE",
|
||||
"spring.jpa.hibernate.ddl-auto=create-drop",
|
||||
"spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration,org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration,org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration"
|
||||
})
|
||||
class RewardJobSchemaTest {
|
||||
|
||||
@Autowired
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
|
||||
@Test
|
||||
void rewardJobsTableExists() {
|
||||
Boolean tableExists = jdbcTemplate.query(
|
||||
"SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'REWARD_JOBS'",
|
||||
(ResultSetExtractor<Boolean>) rs -> rs.next()
|
||||
);
|
||||
|
||||
assertTrue(Boolean.TRUE.equals(tableExists), "Table 'reward_jobs' should exist in the database schema.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
package com.mosquito.project.persistence.repository;
|
||||
|
||||
import com.mosquito.project.persistence.entity.ShortLinkEntity;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
|
||||
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* ShortLinkRepository 数据访问层测试
|
||||
* 测试短链接实体的CRUD操作和自定义查询方法
|
||||
*/
|
||||
@DataJpaTest
|
||||
@org.springframework.context.annotation.Import(com.mosquito.project.config.TestCacheConfig.class)
|
||||
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.ANY)
|
||||
@TestPropertySource(properties = {
|
||||
"spring.cache.type=NONE",
|
||||
"spring.jpa.hibernate.ddl-auto=create-drop",
|
||||
"spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration,org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration,org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration"
|
||||
})
|
||||
class ShortLinkRepositoryTest {
|
||||
|
||||
@Autowired
|
||||
private ShortLinkRepository shortLinkRepository;
|
||||
|
||||
@Test
|
||||
void shouldSaveAndFindShortLinkById() {
|
||||
// 准备测试数据
|
||||
OffsetDateTime now = OffsetDateTime.now(ZoneOffset.UTC);
|
||||
ShortLinkEntity entity = new ShortLinkEntity();
|
||||
entity.setCode("test123");
|
||||
entity.setOriginalUrl("https://example.com/page1");
|
||||
entity.setActivityId(1L);
|
||||
entity.setInviterUserId(100L);
|
||||
entity.setCreatedAt(now);
|
||||
|
||||
// 保存实体
|
||||
ShortLinkEntity saved = shortLinkRepository.save(entity);
|
||||
|
||||
// 验证保存成功
|
||||
assertNotNull(saved.getId(), "保存后ID不应为空");
|
||||
assertEquals("test123", saved.getCode());
|
||||
assertEquals("https://example.com/page1", saved.getOriginalUrl());
|
||||
|
||||
// 通过ID查询验证
|
||||
Optional<ShortLinkEntity> found = shortLinkRepository.findById(saved.getId());
|
||||
assertTrue(found.isPresent(), "应该能通过ID找到实体");
|
||||
assertEquals("test123", found.get().getCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFindByCodeSuccessfully() {
|
||||
// 准备并保存测试数据
|
||||
OffsetDateTime now = OffsetDateTime.now(ZoneOffset.UTC);
|
||||
ShortLinkEntity entity = new ShortLinkEntity();
|
||||
entity.setCode("findme");
|
||||
entity.setOriginalUrl("https://test.com/target");
|
||||
entity.setCreatedAt(now);
|
||||
shortLinkRepository.save(entity);
|
||||
|
||||
// 通过code查询
|
||||
Optional<ShortLinkEntity> found = shortLinkRepository.findByCode("findme");
|
||||
|
||||
// 验证查询结果
|
||||
assertTrue(found.isPresent(), "应该能通过code找到实体");
|
||||
assertEquals("findme", found.get().getCode());
|
||||
assertEquals("https://test.com/target", found.get().getOriginalUrl());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnEmptyWhenCodeNotExists() {
|
||||
// 查询不存在的code
|
||||
Optional<ShortLinkEntity> found = shortLinkRepository.findByCode("nonexistent");
|
||||
|
||||
// 验证返回空Optional
|
||||
assertFalse(found.isPresent(), "不存在的code应该返回空Optional");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCheckExistsByCodeSuccessfully() {
|
||||
// 准备并保存测试数据
|
||||
OffsetDateTime now = OffsetDateTime.now(ZoneOffset.UTC);
|
||||
ShortLinkEntity entity = new ShortLinkEntity();
|
||||
entity.setCode("exists123");
|
||||
entity.setOriginalUrl("https://example.com");
|
||||
entity.setCreatedAt(now);
|
||||
shortLinkRepository.save(entity);
|
||||
|
||||
// 验证存在的code
|
||||
assertTrue(shortLinkRepository.existsByCode("exists123"), "已存在的code应该返回true");
|
||||
|
||||
// 验证不存在的code
|
||||
assertFalse(shortLinkRepository.existsByCode("notexists"), "不存在的code应该返回false");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUpdateShortLinkSuccessfully() {
|
||||
// 准备并保存初始数据
|
||||
OffsetDateTime now = OffsetDateTime.now(ZoneOffset.UTC);
|
||||
ShortLinkEntity entity = new ShortLinkEntity();
|
||||
entity.setCode("update123");
|
||||
entity.setOriginalUrl("https://old.com");
|
||||
entity.setActivityId(1L);
|
||||
entity.setCreatedAt(now);
|
||||
ShortLinkEntity saved = shortLinkRepository.save(entity);
|
||||
|
||||
// 更新实体
|
||||
saved.setOriginalUrl("https://new.com");
|
||||
saved.setActivityId(2L);
|
||||
ShortLinkEntity updated = shortLinkRepository.save(saved);
|
||||
|
||||
// 验证更新成功
|
||||
assertEquals("https://new.com", updated.getOriginalUrl());
|
||||
assertEquals(2L, updated.getActivityId());
|
||||
|
||||
// 重新查询验证
|
||||
ShortLinkEntity found = shortLinkRepository.findById(saved.getId()).orElseThrow();
|
||||
assertEquals("https://new.com", found.getOriginalUrl());
|
||||
assertEquals(2L, found.getActivityId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDeleteShortLinkSuccessfully() {
|
||||
// 准备并保存测试数据
|
||||
OffsetDateTime now = OffsetDateTime.now(ZoneOffset.UTC);
|
||||
ShortLinkEntity entity = new ShortLinkEntity();
|
||||
entity.setCode("delete123");
|
||||
entity.setOriginalUrl("https://delete.com");
|
||||
entity.setCreatedAt(now);
|
||||
ShortLinkEntity saved = shortLinkRepository.save(entity);
|
||||
Long id = saved.getId();
|
||||
|
||||
// 删除实体
|
||||
shortLinkRepository.deleteById(id);
|
||||
|
||||
// 验证删除成功
|
||||
assertFalse(shortLinkRepository.existsById(id), "删除后不应再找到实体");
|
||||
assertFalse(shortLinkRepository.existsByCode("delete123"), "删除后code也不应存在");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMaintainCodeUniqueness() {
|
||||
// 准备第一个实体
|
||||
OffsetDateTime now = OffsetDateTime.now(ZoneOffset.UTC);
|
||||
ShortLinkEntity entity1 = new ShortLinkEntity();
|
||||
entity1.setCode("unique123");
|
||||
entity1.setOriginalUrl("https://first.com");
|
||||
entity1.setCreatedAt(now);
|
||||
shortLinkRepository.save(entity1);
|
||||
|
||||
// 准备第二个实体,使用相同的code
|
||||
ShortLinkEntity entity2 = new ShortLinkEntity();
|
||||
entity2.setCode("unique123");
|
||||
entity2.setOriginalUrl("https://second.com");
|
||||
entity2.setCreatedAt(now);
|
||||
|
||||
// 验证保存重复code会抛出异常
|
||||
assertThrows(Exception.class, () -> {
|
||||
shortLinkRepository.saveAndFlush(entity2);
|
||||
}, "重复的code应该抛出异常");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFindWithActivityAndInviterInfo() {
|
||||
// 准备带有关联信息的实体
|
||||
OffsetDateTime now = OffsetDateTime.now(ZoneOffset.UTC);
|
||||
ShortLinkEntity entity = new ShortLinkEntity();
|
||||
entity.setCode("full123");
|
||||
entity.setOriginalUrl("https://full.com");
|
||||
entity.setActivityId(42L);
|
||||
entity.setInviterUserId(99L);
|
||||
entity.setCreatedAt(now);
|
||||
shortLinkRepository.save(entity);
|
||||
|
||||
// 查询并验证所有字段
|
||||
ShortLinkEntity found = shortLinkRepository.findByCode("full123").orElseThrow();
|
||||
assertEquals(42L, found.getActivityId(), "活动ID应正确保存");
|
||||
assertEquals(99L, found.getInviterUserId(), "邀请人ID应正确保存");
|
||||
assertNotNull(found.getCreatedAt(), "创建时间应正确保存");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
package com.mosquito.project.persistence.repository;
|
||||
|
||||
import com.mosquito.project.persistence.entity.UserInviteEntity;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
|
||||
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* UserInviteRepository 数据访问层测试
|
||||
* 测试用户邀请记录的CRUD操作和自定义查询方法
|
||||
*/
|
||||
@DataJpaTest
|
||||
@org.springframework.context.annotation.Import(com.mosquito.project.config.TestCacheConfig.class)
|
||||
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.ANY)
|
||||
@TestPropertySource(properties = {
|
||||
"spring.cache.type=NONE",
|
||||
"spring.jpa.hibernate.ddl-auto=create-drop",
|
||||
"spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration,org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration,org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration"
|
||||
})
|
||||
class UserInviteRepositoryTest {
|
||||
|
||||
@Autowired
|
||||
private UserInviteRepository userInviteRepository;
|
||||
|
||||
private static final Long ACTIVITY_ID_1 = 1L;
|
||||
private static final Long ACTIVITY_ID_2 = 2L;
|
||||
private static final Long USER_1 = 101L;
|
||||
private static final Long USER_2 = 102L;
|
||||
private static final Long USER_3 = 103L;
|
||||
private static final Long USER_4 = 104L;
|
||||
private OffsetDateTime now;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
now = OffsetDateTime.now(ZoneOffset.UTC);
|
||||
userInviteRepository.deleteAll();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSaveAndFindUserInviteById() {
|
||||
// 创建邀请记录
|
||||
UserInviteEntity invite = createInvite(ACTIVITY_ID_1, USER_1, USER_2, "accepted");
|
||||
UserInviteEntity saved = userInviteRepository.save(invite);
|
||||
|
||||
// 验证保存
|
||||
assertNotNull(saved.getId(), "保存后ID不应为空");
|
||||
assertEquals(ACTIVITY_ID_1, saved.getActivityId());
|
||||
|
||||
// 通过ID查询
|
||||
UserInviteEntity found = userInviteRepository.findById(saved.getId()).orElseThrow();
|
||||
assertEquals(USER_1, found.getInviterUserId());
|
||||
assertEquals(USER_2, found.getInviteeUserId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFindByActivityId() {
|
||||
// 为活动1创建多个邀请记录
|
||||
userInviteRepository.save(createInvite(ACTIVITY_ID_1, USER_1, USER_2, "accepted"));
|
||||
userInviteRepository.save(createInvite(ACTIVITY_ID_1, USER_1, USER_3, "pending"));
|
||||
userInviteRepository.save(createInvite(ACTIVITY_ID_1, USER_2, USER_4, "accepted"));
|
||||
|
||||
// 为活动2创建记录(应该被排除)
|
||||
userInviteRepository.save(createInvite(ACTIVITY_ID_2, USER_1, USER_3, "accepted"));
|
||||
|
||||
// 查询活动1的所有邀请
|
||||
List<UserInviteEntity> invites = userInviteRepository.findByActivityId(ACTIVITY_ID_1);
|
||||
|
||||
// 验证
|
||||
assertEquals(3, invites.size(), "活动1应该有3个邀请记录");
|
||||
assertTrue(invites.stream().allMatch(i -> i.getActivityId().equals(ACTIVITY_ID_1)),
|
||||
"所有记录都应属于活动1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFindByActivityIdAndInviterUserId() {
|
||||
// 创建不同活动、不同邀请人的记录
|
||||
userInviteRepository.save(createInvite(ACTIVITY_ID_1, USER_1, USER_2, "accepted"));
|
||||
userInviteRepository.save(createInvite(ACTIVITY_ID_1, USER_1, USER_3, "pending"));
|
||||
userInviteRepository.save(createInvite(ACTIVITY_ID_1, USER_2, USER_4, "accepted"));
|
||||
userInviteRepository.save(createInvite(ACTIVITY_ID_2, USER_1, USER_4, "accepted"));
|
||||
|
||||
// 查询活动1中用户1的邀请记录
|
||||
List<UserInviteEntity> invites = userInviteRepository.findByActivityIdAndInviterUserId(ACTIVITY_ID_1, USER_1);
|
||||
|
||||
// 验证
|
||||
assertEquals(2, invites.size(), "活动1中用户1应该有2个邀请记录");
|
||||
assertTrue(invites.stream().allMatch(i -> i.getInviterUserId().equals(USER_1)),
|
||||
"所有记录都应属于用户1");
|
||||
assertTrue(invites.stream().allMatch(i -> i.getActivityId().equals(ACTIVITY_ID_1)),
|
||||
"所有记录都应属于活动1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCountInvitesByActivityIdGroupByInviter() {
|
||||
// 创建测试数据
|
||||
// 活动1中:用户1邀请2人,用户2邀请1人
|
||||
userInviteRepository.save(createInvite(ACTIVITY_ID_1, USER_1, USER_2, "accepted"));
|
||||
userInviteRepository.save(createInvite(ACTIVITY_ID_1, USER_1, USER_3, "accepted"));
|
||||
userInviteRepository.save(createInvite(ACTIVITY_ID_1, USER_2, USER_4, "accepted"));
|
||||
|
||||
// 活动2中:用户1邀请1人(应该被排除)
|
||||
userInviteRepository.save(createInvite(ACTIVITY_ID_2, USER_1, USER_4, "accepted"));
|
||||
|
||||
// 执行统计查询
|
||||
List<Object[]> results = userInviteRepository.countInvitesByActivityIdGroupByInviter(ACTIVITY_ID_1);
|
||||
|
||||
// 验证
|
||||
assertEquals(2, results.size(), "应该有2个邀请人");
|
||||
|
||||
// 验证排序(按邀请数量降序)
|
||||
Object[] first = results.get(0);
|
||||
assertEquals(USER_1, first[0], "用户1应该是第一名(邀请最多)");
|
||||
assertEquals(2L, first[1], "用户1应该邀请了2人");
|
||||
|
||||
Object[] second = results.get(1);
|
||||
assertEquals(USER_2, second[0], "用户2应该是第二名");
|
||||
assertEquals(1L, second[1], "用户2应该邀请了1人");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCountByActivityId() {
|
||||
// 创建测试数据
|
||||
userInviteRepository.save(createInvite(ACTIVITY_ID_1, USER_1, USER_2, "accepted"));
|
||||
userInviteRepository.save(createInvite(ACTIVITY_ID_1, USER_1, USER_3, "pending"));
|
||||
userInviteRepository.save(createInvite(ACTIVITY_ID_1, USER_2, USER_4, "accepted"));
|
||||
userInviteRepository.save(createInvite(ACTIVITY_ID_2, USER_1, USER_4, "accepted"));
|
||||
|
||||
// 统计活动1的邀请总数
|
||||
long count = userInviteRepository.countByActivityId(ACTIVITY_ID_1);
|
||||
|
||||
// 验证
|
||||
assertEquals(3, count, "活动1应该有3个邀请记录");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnEmptyListForNonExistentActivity() {
|
||||
// 查询不存在的活动
|
||||
List<UserInviteEntity> invites = userInviteRepository.findByActivityId(999L);
|
||||
assertTrue(invites.isEmpty(), "不存在的活动应该返回空列表");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldEnforceUniqueConstraint() {
|
||||
// 创建第一条记录
|
||||
UserInviteEntity invite1 = createInvite(ACTIVITY_ID_1, USER_1, USER_2, "accepted");
|
||||
userInviteRepository.save(invite1);
|
||||
|
||||
// 创建重复的记录(相同活动和被邀请人)
|
||||
UserInviteEntity invite2 = createInvite(ACTIVITY_ID_1, USER_3, USER_2, "pending");
|
||||
|
||||
// 验证违反唯一约束
|
||||
assertThrows(Exception.class, () -> {
|
||||
userInviteRepository.saveAndFlush(invite2);
|
||||
}, "重复的活动ID和被邀请人ID组合应该违反唯一约束");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldAllowSameInviteeInDifferentActivities() {
|
||||
// 同一被邀请人在不同活动中应该允许
|
||||
UserInviteEntity invite1 = createInvite(ACTIVITY_ID_1, USER_1, USER_2, "accepted");
|
||||
UserInviteEntity invite2 = createInvite(ACTIVITY_ID_2, USER_3, USER_2, "accepted");
|
||||
|
||||
assertDoesNotThrow(() -> {
|
||||
userInviteRepository.save(invite1);
|
||||
userInviteRepository.save(invite2);
|
||||
}, "同一被邀请人在不同活动中应该允许");
|
||||
|
||||
// 验证保存成功
|
||||
assertEquals(2, userInviteRepository.count());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUpdateInviteStatus() {
|
||||
// 创建初始记录
|
||||
UserInviteEntity invite = createInvite(ACTIVITY_ID_1, USER_1, USER_2, "pending");
|
||||
UserInviteEntity saved = userInviteRepository.save(invite);
|
||||
|
||||
// 更新状态
|
||||
saved.setStatus("accepted");
|
||||
saved.setCreatedAt(now.plusMinutes(5));
|
||||
userInviteRepository.save(saved);
|
||||
|
||||
// 验证更新
|
||||
UserInviteEntity updated = userInviteRepository.findById(saved.getId()).orElseThrow();
|
||||
assertEquals("accepted", updated.getStatus(), "状态应该更新为accepted");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDeleteInvite() {
|
||||
// 创建并保存记录
|
||||
UserInviteEntity invite = createInvite(ACTIVITY_ID_1, USER_1, USER_2, "accepted");
|
||||
UserInviteEntity saved = userInviteRepository.save(invite);
|
||||
|
||||
// 删除
|
||||
userInviteRepository.deleteById(saved.getId());
|
||||
|
||||
// 验证删除
|
||||
assertFalse(userInviteRepository.existsById(saved.getId()), "删除后应该不存在");
|
||||
assertEquals(0, userInviteRepository.countByActivityId(ACTIVITY_ID_1), "活动邀请计数应该为0");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleLargeInviteCount() {
|
||||
// 批量创建100个邀请记录
|
||||
for (int i = 0; i < 100; i++) {
|
||||
Long inviteeId = 1000L + i;
|
||||
userInviteRepository.save(createInvite(ACTIVITY_ID_1, USER_1, inviteeId, "accepted"));
|
||||
}
|
||||
|
||||
// 验证统计
|
||||
long count = userInviteRepository.countByActivityId(ACTIVITY_ID_1);
|
||||
assertEquals(100, count, "应该有100个邀请记录");
|
||||
|
||||
List<Object[]> results = userInviteRepository.countInvitesByActivityIdGroupByInviter(ACTIVITY_ID_1);
|
||||
assertEquals(1, results.size(), "应该只有1个邀请人");
|
||||
assertEquals(100L, results.get(0)[1], "用户1应该邀请了100人");
|
||||
}
|
||||
|
||||
/**
|
||||
* 辅助方法:创建邀请实体
|
||||
*/
|
||||
private UserInviteEntity createInvite(Long activityId, Long inviterId, Long inviteeId, String status) {
|
||||
UserInviteEntity invite = new UserInviteEntity();
|
||||
invite.setActivityId(activityId);
|
||||
invite.setInviterUserId(inviterId);
|
||||
invite.setInviteeUserId(inviteeId);
|
||||
invite.setStatus(status);
|
||||
invite.setCreatedAt(now);
|
||||
return invite;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
package com.mosquito.project.persistence.repository;
|
||||
|
||||
import com.mosquito.project.persistence.entity.UserRewardEntity;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
|
||||
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* UserRewardRepository 数据访问层测试
|
||||
* 测试用户奖励记录的CRUD操作和自定义查询方法
|
||||
*/
|
||||
@DataJpaTest
|
||||
@org.springframework.context.annotation.Import(com.mosquito.project.config.TestCacheConfig.class)
|
||||
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.ANY)
|
||||
@TestPropertySource(properties = {
|
||||
"spring.cache.type=NONE",
|
||||
"spring.jpa.hibernate.ddl-auto=create-drop",
|
||||
"spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration,org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration,org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration"
|
||||
})
|
||||
class UserRewardRepositoryTest {
|
||||
|
||||
@Autowired
|
||||
private UserRewardRepository userRewardRepository;
|
||||
|
||||
private static final Long ACTIVITY_ID = 1L;
|
||||
private static final Long USER_1 = 101L;
|
||||
private static final Long USER_2 = 102L;
|
||||
private OffsetDateTime now;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
now = OffsetDateTime.now(ZoneOffset.UTC);
|
||||
userRewardRepository.deleteAll();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSaveAndFindRewardById() {
|
||||
// 创建奖励记录
|
||||
UserRewardEntity reward = createReward(ACTIVITY_ID, USER_1, "invitation", 100);
|
||||
UserRewardEntity saved = userRewardRepository.save(reward);
|
||||
|
||||
// 验证保存
|
||||
assertNotNull(saved.getId(), "保存后ID不应为空");
|
||||
assertEquals(100, saved.getPoints());
|
||||
|
||||
// 通过ID查询
|
||||
UserRewardEntity found = userRewardRepository.findById(saved.getId()).orElseThrow();
|
||||
assertEquals("invitation", found.getType());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFindByActivityIdAndUserIdOrderByCreatedAtDesc() {
|
||||
// 为用户1在活动1中创建多个奖励记录(不同时间)
|
||||
UserRewardEntity reward1 = createReward(ACTIVITY_ID, USER_1, "invite", 50);
|
||||
reward1.setCreatedAt(now.minusHours(2));
|
||||
userRewardRepository.save(reward1);
|
||||
|
||||
UserRewardEntity reward2 = createReward(ACTIVITY_ID, USER_1, "share", 30);
|
||||
reward2.setCreatedAt(now.minusHours(1));
|
||||
userRewardRepository.save(reward2);
|
||||
|
||||
UserRewardEntity reward3 = createReward(ACTIVITY_ID, USER_1, "click", 10);
|
||||
reward3.setCreatedAt(now);
|
||||
userRewardRepository.save(reward3);
|
||||
|
||||
// 为用户2在活动1中创建记录(应该被排除)
|
||||
userRewardRepository.save(createReward(ACTIVITY_ID, USER_2, "invite", 50));
|
||||
|
||||
// 为用户1在活动2中创建记录(应该被排除)
|
||||
userRewardRepository.save(createReward(2L, USER_1, "invite", 50));
|
||||
|
||||
// 查询
|
||||
List<UserRewardEntity> rewards = userRewardRepository.findByActivityIdAndUserIdOrderByCreatedAtDesc(ACTIVITY_ID, USER_1);
|
||||
|
||||
// 验证
|
||||
assertEquals(3, rewards.size(), "应该返回3条记录");
|
||||
|
||||
// 验证按时间降序排序
|
||||
assertEquals("click", rewards.get(0).getType(), "最新的记录应该是click");
|
||||
assertEquals("share", rewards.get(1).getType(), "中间应该是share");
|
||||
assertEquals("invite", rewards.get(2).getType(), "最旧的应该是invite");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnEmptyListForNonExistentUserOrActivity() {
|
||||
// 查询不存在的用户
|
||||
List<UserRewardEntity> rewards = userRewardRepository.findByActivityIdAndUserIdOrderByCreatedAtDesc(999L, 999L);
|
||||
assertTrue(rewards.isEmpty(), "不存在的用户或活动应该返回空列表");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUpdateRewardPoints() {
|
||||
// 创建初始记录
|
||||
UserRewardEntity reward = createReward(ACTIVITY_ID, USER_1, "bonus", 50);
|
||||
UserRewardEntity saved = userRewardRepository.save(reward);
|
||||
|
||||
// 更新积分
|
||||
saved.setPoints(100);
|
||||
userRewardRepository.save(saved);
|
||||
|
||||
// 验证更新
|
||||
UserRewardEntity updated = userRewardRepository.findById(saved.getId()).orElseThrow();
|
||||
assertEquals(100, updated.getPoints(), "积分应该更新为100");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDeleteReward() {
|
||||
// 创建并保存记录
|
||||
UserRewardEntity reward = createReward(ACTIVITY_ID, USER_1, "invite", 50);
|
||||
UserRewardEntity saved = userRewardRepository.save(reward);
|
||||
Long id = saved.getId();
|
||||
|
||||
// 删除
|
||||
userRewardRepository.deleteById(id);
|
||||
|
||||
// 验证删除
|
||||
assertFalse(userRewardRepository.existsById(id), "删除后应该不存在");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleMultipleRewardTypes() {
|
||||
// 创建不同类型的奖励记录
|
||||
userRewardRepository.save(createReward(ACTIVITY_ID, USER_1, "invitation_accepted", 100));
|
||||
userRewardRepository.save(createReward(ACTIVITY_ID, USER_1, "share", 20));
|
||||
userRewardRepository.save(createReward(ACTIVITY_ID, USER_1, "click", 5));
|
||||
userRewardRepository.save(createReward(ACTIVITY_ID, USER_1, "bonus", 50));
|
||||
|
||||
// 查询并验证
|
||||
List<UserRewardEntity> rewards = userRewardRepository.findByActivityIdAndUserIdOrderByCreatedAtDesc(ACTIVITY_ID, USER_1);
|
||||
assertEquals(4, rewards.size(), "应该有4条不同类型的奖励记录");
|
||||
|
||||
// 计算总积分
|
||||
int totalPoints = rewards.stream().mapToInt(UserRewardEntity::getPoints).sum();
|
||||
assertEquals(175, totalPoints, "总积分应该是175");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleZeroAndNegativePoints() {
|
||||
// 创建零积分记录
|
||||
UserRewardEntity zeroReward = createReward(ACTIVITY_ID, USER_1, "participation", 0);
|
||||
userRewardRepository.save(zeroReward);
|
||||
|
||||
// 查询验证
|
||||
List<UserRewardEntity> rewards = userRewardRepository.findByActivityIdAndUserIdOrderByCreatedAtDesc(ACTIVITY_ID, USER_1);
|
||||
assertEquals(1, rewards.size());
|
||||
assertEquals(0, rewards.get(0).getPoints());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSupportMultipleActivitiesPerUser() {
|
||||
// 同一用户在不同活动中获得奖励
|
||||
userRewardRepository.save(createReward(1L, USER_1, "invite", 50));
|
||||
userRewardRepository.save(createReward(2L, USER_1, "share", 30));
|
||||
userRewardRepository.save(createReward(3L, USER_1, "click", 10));
|
||||
|
||||
// 分别查询每个活动
|
||||
List<UserRewardEntity> activity1Rewards = userRewardRepository.findByActivityIdAndUserIdOrderByCreatedAtDesc(1L, USER_1);
|
||||
List<UserRewardEntity> activity2Rewards = userRewardRepository.findByActivityIdAndUserIdOrderByCreatedAtDesc(2L, USER_1);
|
||||
List<UserRewardEntity> activity3Rewards = userRewardRepository.findByActivityIdAndUserIdOrderByCreatedAtDesc(3L, USER_1);
|
||||
|
||||
assertEquals(1, activity1Rewards.size());
|
||||
assertEquals(1, activity2Rewards.size());
|
||||
assertEquals(1, activity3Rewards.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleLargeNumberOfRewards() {
|
||||
// 批量创建100个奖励记录
|
||||
for (int i = 0; i < 100; i++) {
|
||||
UserRewardEntity reward = createReward(ACTIVITY_ID, USER_1, "reward_" + i, 10);
|
||||
reward.setCreatedAt(now.minusMinutes(i));
|
||||
userRewardRepository.save(reward);
|
||||
}
|
||||
|
||||
// 查询并验证
|
||||
List<UserRewardEntity> rewards = userRewardRepository.findByActivityIdAndUserIdOrderByCreatedAtDesc(ACTIVITY_ID, USER_1);
|
||||
assertEquals(100, rewards.size(), "应该返回100条记录");
|
||||
|
||||
// 验证总积分
|
||||
int totalPoints = rewards.stream().mapToInt(UserRewardEntity::getPoints).sum();
|
||||
assertEquals(1000, totalPoints, "总积分应该是1000");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleSameUserSameActivityMultipleRewards() {
|
||||
// 同一用户在同一个活动中获得多次奖励
|
||||
for (int i = 0; i < 5; i++) {
|
||||
UserRewardEntity reward = createReward(ACTIVITY_ID, USER_1, "invite", 10);
|
||||
reward.setCreatedAt(now.minusMinutes(i));
|
||||
userRewardRepository.save(reward);
|
||||
}
|
||||
|
||||
// 查询验证
|
||||
List<UserRewardEntity> rewards = userRewardRepository.findByActivityIdAndUserIdOrderByCreatedAtDesc(ACTIVITY_ID, USER_1);
|
||||
assertEquals(5, rewards.size(), "同一用户同一活动应该有5条奖励记录");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldPreserveAllFields() {
|
||||
// 创建完整记录
|
||||
UserRewardEntity reward = new UserRewardEntity();
|
||||
reward.setActivityId(ACTIVITY_ID);
|
||||
reward.setUserId(USER_1);
|
||||
reward.setType("special_bonus");
|
||||
reward.setPoints(999);
|
||||
reward.setCreatedAt(now);
|
||||
|
||||
UserRewardEntity saved = userRewardRepository.save(reward);
|
||||
|
||||
// 查询并验证所有字段
|
||||
UserRewardEntity found = userRewardRepository.findById(saved.getId()).orElseThrow();
|
||||
assertEquals(ACTIVITY_ID, found.getActivityId());
|
||||
assertEquals(USER_1, found.getUserId());
|
||||
assertEquals("special_bonus", found.getType());
|
||||
assertEquals(999, found.getPoints());
|
||||
assertNotNull(found.getCreatedAt());
|
||||
}
|
||||
|
||||
/**
|
||||
* 辅助方法:创建奖励实体
|
||||
*/
|
||||
private UserRewardEntity createReward(Long activityId, Long userId, String type, int points) {
|
||||
UserRewardEntity reward = new UserRewardEntity();
|
||||
reward.setActivityId(activityId);
|
||||
reward.setUserId(userId);
|
||||
reward.setType(type);
|
||||
reward.setPoints(points);
|
||||
reward.setCreatedAt(now);
|
||||
return reward;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user