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:
Your Name
2026-03-02 13:31:54 +08:00
parent 32d6449ea4
commit 91a0b77f7a
2272 changed files with 221995 additions and 503 deletions

View File

@@ -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的关联实际关系由数据库外键维护
}
}

View File

@@ -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();
}
}

View File

@@ -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("");
}
}

View File

@@ -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";
}
}

View File

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

View File

@@ -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());
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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.");
}
}

View File

@@ -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(), "创建时间应正确保存");
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}