diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/server/src/test/java/run/ikaros/server/core/attachment/operator/AttachmentOperatorTest.java b/server/src/test/java/run/ikaros/server/core/attachment/operator/AttachmentOperatorTest.java new file mode 100644 index 000000000..51f6aa727 --- /dev/null +++ b/server/src/test/java/run/ikaros/server/core/attachment/operator/AttachmentOperatorTest.java @@ -0,0 +1,326 @@ +package run.ikaros.server.core.attachment.operator; + +import static org.mockito.Mockito.when; + +import java.util.UUID; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; +import run.ikaros.api.core.attachment.Attachment; +import run.ikaros.api.core.attachment.AttachmentDriver; +import run.ikaros.api.core.attachment.AttachmentReference; +import run.ikaros.api.core.attachment.AttachmentRelation; +import run.ikaros.api.core.attachment.AttachmentSearchCondition; +import run.ikaros.api.core.attachment.AttachmentUploadCondition; +import run.ikaros.api.store.enums.AttachmentDriverType; +import run.ikaros.api.store.enums.AttachmentReferenceType; +import run.ikaros.api.store.enums.AttachmentRelationType; +import run.ikaros.api.store.enums.AttachmentType; +import run.ikaros.api.wrap.PagingWrap; +import run.ikaros.server.core.attachment.service.AttachmentDriverService; +import run.ikaros.server.core.attachment.service.AttachmentReferenceService; +import run.ikaros.server.core.attachment.service.AttachmentRelationService; +import run.ikaros.server.core.attachment.service.AttachmentService; + +@ExtendWith(MockitoExtension.class) +class AttachmentOperatorTest { + + @Mock + private AttachmentService attachmentService; + @Mock + private AttachmentReferenceService referenceService; + @Mock + private AttachmentRelationService relationService; + @Mock + private AttachmentDriverService driverService; + + private AttachmentOperator attachmentOperator; + private AttachmentReferenceOperator referenceOperator; + private AttachmentRelationOperator relationOperator; + private AttachmentDriverOperator driverOperator; + + @BeforeEach + void setUp() { + attachmentOperator = new AttachmentOperator(attachmentService); + referenceOperator = new AttachmentReferenceOperator(referenceService); + relationOperator = new AttachmentRelationOperator(relationService); + driverOperator = new AttachmentDriverOperator(driverService); + } + + @Test + void attachmentSave() { + Attachment attachment = Attachment + .builder() + .name("test.txt") + .build(); + when(attachmentService.save(attachment)).thenReturn(Mono.just(attachment)); + StepVerifier + .create(attachmentOperator.save(attachment)) + .expectNext(attachment) + .verifyComplete(); + } + + @Test + void attachmentListByCondition() { + AttachmentSearchCondition condition = AttachmentSearchCondition + .builder() + .build(); + PagingWrap wrap = PagingWrap.emptyResult(); + when(attachmentService.listByCondition(condition)).thenReturn(Mono.just(wrap)); + StepVerifier + .create(attachmentOperator.listByCondition(condition)) + .expectNext(wrap) + .verifyComplete(); + } + + @Test + void attachmentUpload() { + AttachmentUploadCondition uploadCondition = AttachmentUploadCondition + .builder() + .build(); + Attachment attachment = Attachment + .builder() + .name("uploaded.png") + .build(); + when(attachmentService.upload(uploadCondition)).thenReturn(Mono.just(attachment)); + StepVerifier + .create(attachmentOperator.upload(uploadCondition)) + .expectNext(attachment) + .verifyComplete(); + } + + @Test + void attachmentFindById() { + UUID id = UUID.randomUUID(); + Attachment attachment = Attachment + .builder() + .id(id) + .name("test.txt") + .build(); + when(attachmentService.findById(id)).thenReturn(Mono.just(attachment)); + StepVerifier + .create(attachmentOperator.findById(id)) + .expectNext(attachment) + .verifyComplete(); + } + + @Test + void attachmentFindByTypeAndParentIdAndName() { + UUID parentId = UUID.randomUUID(); + Attachment attachment = Attachment + .builder() + .name("child.txt") + .build(); + when(attachmentService.findByTypeAndParentIdAndName(AttachmentType.File, parentId, + "child.txt")) + .thenReturn(Mono.just(attachment)); + StepVerifier + .create( + attachmentOperator.findByTypeAndParentIdAndName(AttachmentType.File, parentId, + "child.txt")) + .expectNext(attachment) + .verifyComplete(); + } + + @Test + void attachmentCreateDirectory() { + UUID parentId = UUID.randomUUID(); + Attachment dir = Attachment + .builder() + .name("newdir") + .type(AttachmentType.Directory) + .build(); + when(attachmentService.createDirectory(parentId, "newdir")).thenReturn(Mono.just(dir)); + StepVerifier + .create(attachmentOperator.createDirectory(parentId, "newdir")) + .expectNext(dir) + .verifyComplete(); + } + + @Test + void attachmentExistsByParentIdAndName() { + UUID parentId = UUID.randomUUID(); + when(attachmentService.existsByParentIdAndName(parentId, "test.txt")).thenReturn( + Mono.just(true)); + StepVerifier + .create(attachmentOperator.existsByParentIdAndName(parentId, "test.txt")) + .expectNext(true) + .verifyComplete(); + } + + @Test + void attachmentExistsByTypeAndParentIdAndName() { + UUID parentId = UUID.randomUUID(); + when(attachmentService.existsByTypeAndParentIdAndName(AttachmentType.File, parentId, + "test.txt")) + .thenReturn(Mono.just(false)); + StepVerifier + .create( + attachmentOperator.existsByTypeAndParentIdAndName(AttachmentType.File, parentId, + "test.txt")) + .expectNext(false) + .verifyComplete(); + } + + @Test + void referenceSave() { + AttachmentReference ref = AttachmentReference + .builder() + .build(); + when(referenceService.save(ref)).thenReturn(Mono.just(ref)); + StepVerifier + .create(referenceOperator.save(ref)) + .expectNext(ref) + .verifyComplete(); + } + + @Test + void referenceFindAllByTypeAndAttachmentId() { + UUID attachmentId = UUID.randomUUID(); + AttachmentReference ref = AttachmentReference + .builder() + .attachmentId(attachmentId) + .build(); + when(referenceService.findAllByTypeAndAttachmentId(AttachmentReferenceType.EPISODE, + attachmentId)) + .thenReturn(Flux.just(ref)); + StepVerifier + .create( + referenceOperator.findAllByTypeAndAttachmentId(AttachmentReferenceType.EPISODE, + attachmentId)) + .expectNext(ref) + .verifyComplete(); + } + + @Test + void referenceMatchingAttachmentsAndSubjectEpisodes() { + UUID subjectId = UUID.randomUUID(); + UUID[] attachmentIds = {UUID.randomUUID()}; + when(referenceService.matchingAttachmentsAndSubjectEpisodes(subjectId, attachmentIds)) + .thenReturn(Mono.empty()); + StepVerifier + .create( + referenceOperator.matchingAttachmentsAndSubjectEpisodes(subjectId, attachmentIds)) + .verifyComplete(); + } + + @Test + void referenceMatchingAttachmentsAndSubjectEpisodesWithNotify() { + UUID subjectId = UUID.randomUUID(); + UUID[] attachmentIds = {UUID.randomUUID()}; + when(referenceService.matchingAttachmentsAndSubjectEpisodes(subjectId, attachmentIds, true)) + .thenReturn(Mono.empty()); + StepVerifier + .create( + referenceOperator.matchingAttachmentsAndSubjectEpisodes(subjectId, attachmentIds, + true)) + .verifyComplete(); + } + + @Test + void referenceMatchingAttachmentsForEpisode() { + UUID episodeId = UUID.randomUUID(); + UUID[] attachmentIds = {UUID.randomUUID()}; + when(referenceService.matchingAttachmentsForEpisode(episodeId, attachmentIds)) + .thenReturn(Mono.empty()); + StepVerifier + .create(referenceOperator.matchingAttachmentsForEpisode(episodeId, attachmentIds)) + .verifyComplete(); + } + + @Test + void relationFindAllByTypeAndAttachmentId() { + UUID attachmentId = UUID.randomUUID(); + AttachmentRelation relation = AttachmentRelation + .builder() + .attachmentId(attachmentId) + .build(); + when(relationService.findAllByTypeAndAttachmentId(AttachmentRelationType.VIDEO_SUBTITLE, + attachmentId)) + .thenReturn(Flux.just(relation)); + StepVerifier + .create( + relationOperator.findAllByTypeAndAttachmentId(AttachmentRelationType.VIDEO_SUBTITLE, + attachmentId)) + .expectNext(relation) + .verifyComplete(); + } + + @Test + void driverSave() { + AttachmentDriver driver = AttachmentDriver + .builder() + .name("local") + .build(); + when(driverService.save(driver)).thenReturn(Mono.just(driver)); + StepVerifier + .create(driverOperator.save(driver)) + .expectNext(driver) + .verifyComplete(); + } + + @Test + void driverFindById() { + UUID id = UUID.randomUUID(); + AttachmentDriver driver = AttachmentDriver + .builder() + .id(id) + .build(); + when(driverService.findById(id)).thenReturn(Mono.just(driver)); + StepVerifier + .create(driverOperator.findById(id)) + .expectNext(driver) + .verifyComplete(); + } + + @Test + void driverFindByTypeAndName() { + AttachmentDriver driver = AttachmentDriver + .builder() + .name("local") + .type(AttachmentDriverType.LOCAL) + .build(); + when(driverService.findByTypeAndName("LOCAL", "local")).thenReturn(Mono.just(driver)); + StepVerifier + .create(driverOperator.findByTypeAndName("LOCAL", "local")) + .expectNext(driver) + .verifyComplete(); + } + + @Test + void driverListAttachmentsByCondition() { + AttachmentSearchCondition condition = AttachmentSearchCondition + .builder() + .build(); + PagingWrap wrap = PagingWrap.emptyResult(); + when(driverService.listAttachmentsByCondition(condition)).thenReturn(Mono.just(wrap)); + StepVerifier + .create(driverOperator.listAttachmentsByCondition(condition)) + .expectNext(wrap) + .verifyComplete(); + } + + @Test + void driverRefresh() { + UUID id = UUID.randomUUID(); + when(driverService.refresh(id)).thenReturn(Mono.empty()); + StepVerifier + .create(driverOperator.refresh(id)) + .verifyComplete(); + } + + @Test + void driverListDriversByCondition() { + PagingWrap wrap = PagingWrap.emptyResult(); + when(driverService.listDriversByCondition(1, 10)).thenReturn(Mono.just(wrap)); + StepVerifier + .create(driverOperator.listDriversByCondition(1, 10)) + .expectNext(wrap) + .verifyComplete(); + } +} diff --git a/server/src/test/java/run/ikaros/server/core/collection/DefaultCollectionServiceTest.java b/server/src/test/java/run/ikaros/server/core/collection/DefaultCollectionServiceTest.java new file mode 100644 index 000000000..4e3b14d66 --- /dev/null +++ b/server/src/test/java/run/ikaros/server/core/collection/DefaultCollectionServiceTest.java @@ -0,0 +1,220 @@ +package run.ikaros.server.core.collection; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.UUID; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.data.r2dbc.core.R2dbcEntityTemplate; +import org.springframework.data.relational.core.query.Query; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; +import run.ikaros.api.core.collection.vo.FindCollectionCondition; +import run.ikaros.api.store.enums.CollectionCategory; +import run.ikaros.api.store.enums.CollectionType; +import run.ikaros.server.core.user.UserService; +import run.ikaros.server.store.entity.EpisodeCollectionEntity; +import run.ikaros.server.store.entity.SubjectCollectionEntity; +import run.ikaros.server.store.repository.EpisodeCollectionRepository; +import run.ikaros.server.store.repository.SubjectCollectionRepository; + +class DefaultCollectionServiceTest { + private SubjectCollectionRepository subjectCollectionRepository; + private EpisodeCollectionRepository episodeCollectionRepository; + private UserService userService; + private R2dbcEntityTemplate template; + private DefaultCollectionService defaultCollectionService; + + private final UUID userId = UUID.randomUUID(); + private final UUID subjectId = UUID.randomUUID(); + private final UUID episodeId = UUID.randomUUID(); + + @BeforeEach + void setUp() { + subjectCollectionRepository = Mockito.mock(SubjectCollectionRepository.class); + episodeCollectionRepository = Mockito.mock(EpisodeCollectionRepository.class); + userService = Mockito.mock(UserService.class); + template = Mockito.mock(R2dbcEntityTemplate.class); + defaultCollectionService = new DefaultCollectionService( + subjectCollectionRepository, episodeCollectionRepository, + userService, template + ); + } + + @Test + void findTypeBySubjectId() { + SubjectCollectionEntity entity = new SubjectCollectionEntity(); + entity.setUserId(userId); + entity.setSubjectId(subjectId); + entity.setType(CollectionType.WISH); + + when(userService.getUserIdFromSecurityContext()).thenReturn(Mono.just(userId)); + when(subjectCollectionRepository.findByUserIdAndSubjectId(userId, subjectId)) + .thenReturn(Mono.just(entity)); + + StepVerifier + .create(defaultCollectionService.findTypeBySubjectId(subjectId)) + .assertNext(type -> assertThat(type).isEqualTo(CollectionType.WISH)) + .verifyComplete(); + } + + @Test + void findTypeBySubjectId_whenNotFound() { + when(userService.getUserIdFromSecurityContext()).thenReturn(Mono.just(userId)); + when(subjectCollectionRepository.findByUserIdAndSubjectId(userId, subjectId)) + .thenReturn(Mono.empty()); + + StepVerifier + .create(defaultCollectionService.findTypeBySubjectId(subjectId)) + .verifyComplete(); + } + + @Test + void listCollectionsByCondition_forSubject_withType() { + FindCollectionCondition condition = FindCollectionCondition + .builder() + .page(1) + .size(10) + .category(CollectionCategory.SUBJECT) + .type(CollectionType.DONE) + .build(); + + SubjectCollectionEntity entity = new SubjectCollectionEntity(); + entity.setId(UUID.randomUUID()); + entity.setUserId(userId); + entity.setSubjectId(subjectId); + entity.setType(CollectionType.DONE); + + when(template.select(any(Query.class), eq(SubjectCollectionEntity.class))) + .thenReturn(Flux.just(entity)); + when(template.count(any(Query.class), eq(SubjectCollectionEntity.class))) + .thenReturn(Mono.just(1L)); + when(subjectCollectionRepository.findById(entity.getId())) + .thenReturn(Mono.just(entity)); + + StepVerifier + .create(defaultCollectionService.listCollectionsByCondition(condition)) + .assertNext(pagingWrap -> { + assertThat(pagingWrap.getPage()).isEqualTo(1); + assertThat(pagingWrap.getSize()).isEqualTo(10); + assertThat(pagingWrap.getTotal()).isEqualTo(1); + List items = pagingWrap.getItems(); + assertThat(items).hasSize(1); + }) + .verifyComplete(); + } + + @Test + void listCollectionsByCondition_forSubject_withoutType() { + FindCollectionCondition condition = FindCollectionCondition + .builder() + .page(1) + .size(10) + .category(CollectionCategory.SUBJECT) + .build(); + + SubjectCollectionEntity entity = new SubjectCollectionEntity(); + entity.setId(UUID.randomUUID()); + entity.setUserId(userId); + entity.setSubjectId(subjectId); + entity.setType(CollectionType.WISH); + + when(template.select(any(Query.class), eq(SubjectCollectionEntity.class))) + .thenReturn(Flux.just(entity)); + when(template.count(any(Query.class), eq(SubjectCollectionEntity.class))) + .thenReturn(Mono.just(1L)); + when(subjectCollectionRepository.findById(entity.getId())) + .thenReturn(Mono.just(entity)); + + StepVerifier + .create(defaultCollectionService.listCollectionsByCondition(condition)) + .assertNext(pagingWrap -> { + assertThat(pagingWrap.getTotal()).isEqualTo(1); + }) + .verifyComplete(); + } + + @Test + void listCollectionsByCondition_forEpisode_withTimeRange() { + long now = System.currentTimeMillis(); + FindCollectionCondition condition = FindCollectionCondition + .builder() + .page(1) + .size(10) + .category(CollectionCategory.EPISODE) + .updateTimeDesc(true) + .time((now - 86400000) + "-" + now) + .build(); + + EpisodeCollectionEntity entity = new EpisodeCollectionEntity(); + entity.setId(episodeId); + entity.setUserId(userId); + entity.setEpisodeId(UUID.randomUUID()); + entity.setFinish(true); + + when(template.select(any(Query.class), eq(EpisodeCollectionEntity.class))) + .thenReturn(Flux.just(entity)); + when(template.count(any(Query.class), eq(EpisodeCollectionEntity.class))) + .thenReturn(Mono.just(1L)); + when(episodeCollectionRepository.findById(episodeId)) + .thenReturn(Mono.just(entity)); + + StepVerifier + .create(defaultCollectionService.listCollectionsByCondition(condition)) + .assertNext(pagingWrap -> { + assertThat(pagingWrap.getTotal()).isEqualTo(1); + }) + .verifyComplete(); + } + + @Test + void listCollectionsByCondition_withPageZero_throwsException() { + FindCollectionCondition condition = FindCollectionCondition + .builder() + .page(0) + .size(10) + .category(CollectionCategory.SUBJECT) + .build(); + + org.junit.jupiter.api.Assertions.assertThrows(IllegalArgumentException.class, + () -> defaultCollectionService.listCollectionsByCondition(condition)); + } + + @Test + void listCollectionsByCondition_withNullCondition_throwsException() { + org.junit.jupiter.api.Assertions.assertThrows(IllegalArgumentException.class, + () -> defaultCollectionService.listCollectionsByCondition(null)); + } + + @Test + void listCollectionsByCondition_forEpisode_withoutUpdateTimeDesc() { + FindCollectionCondition condition = FindCollectionCondition + .builder() + .page(1) + .size(10) + .category(CollectionCategory.EPISODE) + .updateTimeDesc(false) + .build(); + + EpisodeCollectionEntity entity = new EpisodeCollectionEntity(); + entity.setId(episodeId); + + when(template.select(any(Query.class), eq(EpisodeCollectionEntity.class))) + .thenReturn(Flux.just(entity)); + when(template.count(any(Query.class), eq(EpisodeCollectionEntity.class))) + .thenReturn(Mono.just(1L)); + when(episodeCollectionRepository.findById(episodeId)) + .thenReturn(Mono.just(entity)); + + StepVerifier + .create(defaultCollectionService.listCollectionsByCondition(condition)) + .assertNext(pagingWrap -> assertThat(pagingWrap.getTotal()).isEqualTo(1)) + .verifyComplete(); + } +} diff --git a/server/src/test/java/run/ikaros/server/core/notify/service/MailServiceImplTest.java b/server/src/test/java/run/ikaros/server/core/notify/service/MailServiceImplTest.java new file mode 100644 index 000000000..f8487b3b2 --- /dev/null +++ b/server/src/test/java/run/ikaros/server/core/notify/service/MailServiceImplTest.java @@ -0,0 +1,96 @@ +package run.ikaros.server.core.notify.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.thymeleaf.TemplateEngine; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; +import run.ikaros.api.core.setting.ConfigMap; +import run.ikaros.api.custom.ReactiveCustomClient; +import run.ikaros.server.core.notify.model.MailConfig; +import run.ikaros.server.core.notify.model.MailProtocol; + +class MailServiceImplTest { + private ReactiveCustomClient reactiveCustomClient; + private TemplateEngine templateEngine; + private MailServiceImpl mailService; + + private ConfigMap createFullConfigMap(String enable) { + ConfigMap configMap = new ConfigMap(); + configMap.putDataItem("MAIL_ENABLE", enable); + configMap.putDataItem("MAIL_PROTOCOL", "smtp"); + configMap.putDataItem("MAIL_SMTP_HOST", "smtp.example.com"); + configMap.putDataItem("MAIL_SMTP_PORT", "587"); + configMap.putDataItem("MAIL_SMTP_ACCOUNT", "user@example.com"); + configMap.putDataItem("MAIL_SMTP_PASSWORD", "secret"); + configMap.putDataItem("MAIL_SMTP_ACCOUNT_ALIAS", "Ikaros"); + configMap.putDataItem("MAIL_RECEIVE_ADDRESS", "admin@example.com"); + return configMap; + } + + @BeforeEach + void setUp() { + reactiveCustomClient = Mockito.mock(ReactiveCustomClient.class); + templateEngine = Mockito.mock(TemplateEngine.class); + mailService = new MailServiceImpl(reactiveCustomClient, templateEngine); + } + + @Test + void getMailConfig_initialState() { + MailConfig config = mailService.getMailConfig(); + assertThat(config).isNotNull(); + assertThat(config.getEnable()).isNull(); + assertThat(config.getHost()).isNull(); + } + + @Test + void updateConfig_whenConfigFound() { + when(reactiveCustomClient.findOne(any(), any())) + .thenReturn(Mono.just(createFullConfigMap("true"))); + + StepVerifier.create(mailService.updateConfig()) + .verifyComplete(); + + MailConfig config = mailService.getMailConfig(); + assertThat(config.getEnable()).isTrue(); + assertThat(config.getProtocol()).isEqualTo(MailProtocol.SMTP); + assertThat(config.getHost()).isEqualTo("smtp.example.com"); + assertThat(config.getPort()).isEqualTo(587); + assertThat(config.getAccount()).isEqualTo("user@example.com"); + assertThat(config.getPassword()).isEqualTo("secret"); + assertThat(config.getAccountAlias()).isEqualTo("Ikaros"); + assertThat(config.getReceiveAddress()).isEqualTo("admin@example.com"); + } + + @Test + void updateConfig_whenMailDisabled() { + when(reactiveCustomClient.findOne(any(), any())) + .thenReturn(Mono.just(createFullConfigMap("false"))); + + StepVerifier.create(mailService.updateConfig()) + .verifyComplete(); + + MailConfig config = mailService.getMailConfig(); + assertThat(config.getEnable()).isFalse(); + // Other fields should still be set from config map + assertThat(config.getHost()).isEqualTo("smtp.example.com"); + } + + @Test + void updateConfig_whenConfigNotFound() { + when(reactiveCustomClient.findOne(any(), any())) + .thenReturn(Mono.empty()); + + StepVerifier.create(mailService.updateConfig()) + .verifyComplete(); + + MailConfig config = mailService.getMailConfig(); + assertThat(config.getEnable()).isNull(); + assertThat(config.getHost()).isNull(); + } +} diff --git a/server/src/test/java/run/ikaros/server/core/notify/service/NotifyServiceImplTest.java b/server/src/test/java/run/ikaros/server/core/notify/service/NotifyServiceImplTest.java new file mode 100644 index 000000000..9d2363007 --- /dev/null +++ b/server/src/test/java/run/ikaros/server/core/notify/service/NotifyServiceImplTest.java @@ -0,0 +1,121 @@ +package run.ikaros.server.core.notify.service; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import jakarta.mail.MessagingException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.thymeleaf.context.Context; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; +import run.ikaros.server.core.notify.MailService; +import run.ikaros.server.core.notify.model.MailConfig; +import run.ikaros.server.core.notify.model.MailRequest; + +class NotifyServiceImplTest { + private MailService mailService; + private NotifyServiceImpl notifyService; + + @BeforeEach + void setUp() { + mailService = Mockito.mock(MailService.class); + notifyService = new NotifyServiceImpl(mailService); + } + + @Test + void sendMail() throws MessagingException { + MailConfig mailConfig = new MailConfig(); + mailConfig.setReceiveAddress("test@example.com"); + when(mailService.getMailConfig()).thenReturn(mailConfig); + when(mailService.send(any(MailRequest.class))).thenReturn(Mono.empty()); + + StepVerifier + .create(notifyService.sendMail("Hello", "World")) + .verifyComplete(); + verify(mailService).send(any(MailRequest.class)); + } + + @Test + void sendMail_withBlankTitle_throwsException() { + assertThrows(IllegalArgumentException.class, + () -> notifyService.sendMail("", "World")); + } + + @Test + void sendMail_withBlankContext_throwsException() { + assertThrows(IllegalArgumentException.class, + () -> notifyService.sendMail("Hello", "")); + } + + @Test + void sendMail_whenNoReceiveAddress_throwsException() { + MailConfig mailConfig = new MailConfig(); + when(mailService.getMailConfig()).thenReturn(mailConfig); + + assertThrows(IllegalArgumentException.class, + () -> notifyService.sendMail("Hello", "World")); + } + + @Test + void send() throws MessagingException { + MailConfig mailConfig = new MailConfig(); + mailConfig.setReceiveAddress("test@example.com"); + when(mailService.getMailConfig()).thenReturn(mailConfig); + when(mailService.send(any(MailRequest.class), anyString(), any(Context.class))) + .thenReturn(Mono.empty()); + + StepVerifier + .create(notifyService.send("Hello", "mail/template", new Context())) + .verifyComplete(); + verify(mailService).send(any(MailRequest.class), anyString(), any(Context.class)); + } + + @Test + void send_withBlankTitle_throwsException() { + assertThrows(IllegalArgumentException.class, + () -> notifyService.send("", "mail/template", new Context())); + } + + @Test + void send_withNullContext_throwsException() { + assertThrows(IllegalArgumentException.class, + () -> notifyService.send("Hello", "mail/template", null)); + } + + @Test + void send_whenNoReceiveAddress_throwsException() { + MailConfig mailConfig = new MailConfig(); + when(mailService.getMailConfig()).thenReturn(mailConfig); + + assertThrows(IllegalArgumentException.class, + () -> notifyService.send("Hello", "mail/template", new Context())); + } + + @Test + void testMail() throws MessagingException { + MailConfig mailConfig = new MailConfig(); + mailConfig.setReceiveAddress("test@example.com"); + when(mailService.getMailConfig()).thenReturn(mailConfig); + when(mailService.send(any(MailRequest.class), anyString(), any(Context.class))) + .thenReturn(Mono.empty()); + + StepVerifier + .create(notifyService.testMail()) + .verifyComplete(); + verify(mailService).send(any(MailRequest.class), anyString(), any(Context.class)); + } + + @Test + void testMail_whenNoReceiveAddress_throwsException() { + MailConfig mailConfig = new MailConfig(); + when(mailService.getMailConfig()).thenReturn(mailConfig); + + assertThrows(IllegalArgumentException.class, + () -> notifyService.testMail()); + } +} diff --git a/server/src/test/java/run/ikaros/server/core/setting/DefaultSettingServiceTest.java b/server/src/test/java/run/ikaros/server/core/setting/DefaultSettingServiceTest.java new file mode 100644 index 000000000..2f4e68c17 --- /dev/null +++ b/server/src/test/java/run/ikaros/server/core/setting/DefaultSettingServiceTest.java @@ -0,0 +1,80 @@ +package run.ikaros.server.core.setting; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; +import run.ikaros.api.core.setting.ConfigMap; +import run.ikaros.api.custom.ReactiveCustomClient; + +class DefaultSettingServiceTest { + private ReactiveCustomClient reactiveCustomClient; + private DefaultSettingService defaultSettingService; + + @BeforeEach + void setUp() { + reactiveCustomClient = Mockito.mock(ReactiveCustomClient.class); + defaultSettingService = new DefaultSettingService(reactiveCustomClient); + } + + @Test + void getGlobalSetting_withHeaderAndFooter() { + ConfigMap configMap = new ConfigMap(); + configMap.putDataItem("GLOBAL_HEADER", "site-header"); + configMap.putDataItem("GLOBAL_FOOTER", "site-footer"); + when(reactiveCustomClient.findOne(any(), any())) + .thenReturn(Mono.just(configMap)); + + StepVerifier.create(defaultSettingService.getGlobalSetting()) + .assertNext(setting -> { + assertThat(setting.getHeader()).isEqualTo("site-header"); + assertThat(setting.getFooter()).isEqualTo("site-footer"); + }) + .verifyComplete(); + } + + @Test + void getGlobalSetting_withoutHeaderAndFooter() { + ConfigMap configMap = new ConfigMap(); + configMap.putDataItem("OTHER_KEY", "value"); + when(reactiveCustomClient.findOne(any(), any())) + .thenReturn(Mono.just(configMap)); + + StepVerifier.create(defaultSettingService.getGlobalSetting()) + .assertNext(setting -> { + assertThat(setting.getHeader()).isNull(); + assertThat(setting.getFooter()).isNull(); + }) + .verifyComplete(); + } + + @Test + void getGlobalSetting_whenConfigMapNotFound() { + when(reactiveCustomClient.findOne(any(), any())) + .thenReturn(Mono.empty()); + + StepVerifier.create(defaultSettingService.getGlobalSetting()) + .verifyComplete(); + } + + @Test + void getGlobalSetting_withEmptyData() { + ConfigMap configMap = new ConfigMap(); + configMap.setData(Map.of()); + when(reactiveCustomClient.findOne(any(), any())) + .thenReturn(Mono.just(configMap)); + + StepVerifier.create(defaultSettingService.getGlobalSetting()) + .assertNext(setting -> { + assertThat(setting.getHeader()).isNull(); + assertThat(setting.getFooter()).isNull(); + }) + .verifyComplete(); + } +} diff --git a/server/src/test/java/run/ikaros/server/core/statics/StaticServiceImplTest.java b/server/src/test/java/run/ikaros/server/core/statics/StaticServiceImplTest.java new file mode 100644 index 000000000..56d9c54c5 --- /dev/null +++ b/server/src/test/java/run/ikaros/server/core/statics/StaticServiceImplTest.java @@ -0,0 +1,119 @@ +package run.ikaros.server.core.statics; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import reactor.test.StepVerifier; +import run.ikaros.api.constant.AppConst; +import run.ikaros.api.infra.properties.IkarosProperties; + +@ExtendWith(MockitoExtension.class) +class StaticServiceImplTest { + + @Mock + private IkarosProperties ikarosProperties; + + private StaticServiceImpl staticService; + private Path tempWorkDir; + + @BeforeEach + void setUp() throws IOException { + tempWorkDir = Files.createTempDirectory("ikaros-statics-test-"); + when(ikarosProperties.getWorkDir()).thenReturn(tempWorkDir); + staticService = new StaticServiceImpl(ikarosProperties); + } + + @AfterEach + void tearDown() throws IOException { + if (tempWorkDir != null) { + Files + .walk(tempWorkDir) + .sorted((a, b) -> b.compareTo(a)) + .forEach(path -> { + try { + Files.deleteIfExists(path); + } catch (IOException ignored) { + // + } + }); + } + } + + @Test + void listStaticsFontsWhenDirNotExists() { + StepVerifier + .create(staticService.listStaticsFonts()) + .expectNextCount(0) + .verifyComplete(); + } + + @Test + void listStaticsFontsWhenFontDirNotExists() throws IOException { + Path staticsDir = tempWorkDir.resolve(AppConst.STATIC_DIR_NAME); + Files.createDirectory(staticsDir); + StepVerifier + .create(staticService.listStaticsFonts()) + .expectNextCount(0) + .verifyComplete(); + } + + @Test + void listStaticsFontsWithFiles() throws IOException { + Path staticsDir = tempWorkDir.resolve(AppConst.STATIC_DIR_NAME); + Files.createDirectory(staticsDir); + Path fontDir = staticsDir.resolve(AppConst.STATIC_FONT_DIR_NAME); + Files.createDirectory(fontDir); + Files.createFile(fontDir.resolve("NotoSansSC-Regular.otf")); + Files.createFile(fontDir.resolve("NotoSerifSC-Regular.otf")); + + StepVerifier + .create(staticService + .listStaticsFonts() + .collectList()) + .assertNext(fonts -> { + assertThat(fonts).hasSize(2); + assertThat(fonts).anyMatch(f -> f.contains("NotoSansSC-Regular.otf")); + assertThat(fonts).anyMatch(f -> f.contains("NotoSerifSC-Regular.otf")); + }) + .verifyComplete(); + } + + @Test + void listStaticsFontsReturnsCorrectUrlPrefix() throws IOException { + Path staticsDir = tempWorkDir.resolve(AppConst.STATIC_DIR_NAME); + Files.createDirectory(staticsDir); + Path fontDir = staticsDir.resolve(AppConst.STATIC_FONT_DIR_NAME); + Files.createDirectory(fontDir); + Files.createFile(fontDir.resolve("test.woff2")); + + StepVerifier + .create(staticService.listStaticsFonts()) + .assertNext(font -> assertThat(font).startsWith( + "/static/" + AppConst.STATIC_FONT_DIR_NAME + "/")) + .verifyComplete(); + } + + @Test + void listStaticsFontsWhenFontDirIsEmpty() throws IOException { + Path staticsDir = tempWorkDir.resolve(AppConst.STATIC_DIR_NAME); + Files.createDirectory(staticsDir); + Path fontDir = staticsDir.resolve(AppConst.STATIC_FONT_DIR_NAME); + Files.createDirectory(fontDir); + + StepVerifier + .create(staticService + .listStaticsFonts() + .collectList()) + .assertNext(fonts -> assertThat(fonts).isEmpty()) + .verifyComplete(); + } +} diff --git a/server/src/test/java/run/ikaros/server/core/subject/SubjectOperatorsTest.java b/server/src/test/java/run/ikaros/server/core/subject/SubjectOperatorsTest.java new file mode 100644 index 000000000..f3707633d --- /dev/null +++ b/server/src/test/java/run/ikaros/server/core/subject/SubjectOperatorsTest.java @@ -0,0 +1,213 @@ +package run.ikaros.server.core.subject; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Set; +import java.util.UUID; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; +import run.ikaros.api.core.subject.Subject; +import run.ikaros.api.core.subject.SubjectRelation; +import run.ikaros.api.core.subject.SubjectSync; +import run.ikaros.api.infra.utils.UuidV7Utils; +import run.ikaros.api.store.enums.SubjectRelationType; +import run.ikaros.api.store.enums.SubjectSyncPlatform; +import run.ikaros.server.core.subject.service.SubjectRelationService; +import run.ikaros.server.core.subject.service.SubjectService; +import run.ikaros.server.core.subject.service.SubjectSyncService; + +@ExtendWith(MockitoExtension.class) +class SubjectOperatorsTest { + + private SubjectService subjectService; + private SubjectSyncService syncService; + private SubjectRelationService relationService; + + private SubjectOperator subjectOperator; + private SubjectRelationOperator relationOperator; + private SubjectSyncOperator syncOperator; + + @BeforeEach + void setUp() { + subjectService = mock(SubjectService.class); + syncService = mock(SubjectSyncService.class); + relationService = mock(SubjectRelationService.class); + subjectOperator = new SubjectOperator(subjectService, syncService); + relationOperator = new SubjectRelationOperator(relationService); + syncOperator = new SubjectSyncOperator(syncService); + } + + @Test + void findById() { + UUID id = UuidV7Utils.generateUuid(); + Subject subject = Subject + .builder() + .id(id) + .name("Test") + .build(); + when(subjectService.findById(id)).thenReturn(Mono.just(subject)); + StepVerifier + .create(subjectOperator.findById(id)) + .expectNext(subject) + .verifyComplete(); + } + + @Test + void create() { + Subject subject = Subject + .builder() + .name("New") + .build(); + when(subjectService.create(any())).thenReturn(Mono.just(subject)); + StepVerifier + .create(subjectOperator.create(subject)) + .expectNext(subject) + .verifyComplete(); + } + + @Test + void update() { + Subject subject = Subject + .builder() + .id(UuidV7Utils.generateUuid()) + .name("Updated") + .build(); + when(subjectService.update(any())).thenReturn(Mono.empty()); + StepVerifier + .create(subjectOperator.update(subject)) + .verifyComplete(); + } + + @Test + void findBySubjectIdAndPlatformAndPlatformId() { + UUID subjectId = UuidV7Utils.generateUuid(); + Subject subject = Subject + .builder() + .name("SyncTest") + .build(); + when(subjectService.findBySubjectIdAndPlatformAndPlatformId(subjectId, + SubjectSyncPlatform.BGM_TV, "bgm123")) + .thenReturn(Mono.just(subject)); + StepVerifier + .create( + subjectOperator.findBySubjectIdAndPlatformAndPlatformId(subjectId, + SubjectSyncPlatform.BGM_TV, "bgm123")) + .expectNext(subject) + .verifyComplete(); + } + + @Test + void relationFindAllBySubjectId() { + UUID subjectId = UuidV7Utils.generateUuid(); + SubjectRelation relation = SubjectRelation + .builder() + .subject(subjectId) + .relationType(SubjectRelationType.AFTER) + .relationSubjects(Set.of(UuidV7Utils.generateUuid())) + .build(); + when(relationService.findAllBySubjectId(subjectId)).thenReturn(Flux.just(relation)); + StepVerifier + .create(relationOperator.findAllBySubjectId(subjectId)) + .expectNext(relation) + .verifyComplete(); + } + + @Test + void relationFindBySubjectIdAndType() { + UUID subjectId = UuidV7Utils.generateUuid(); + SubjectRelation relation = SubjectRelation + .builder() + .subject(subjectId) + .relationType(SubjectRelationType.AFTER) + .build(); + when(relationService.findBySubjectIdAndType(subjectId, SubjectRelationType.AFTER)) + .thenReturn(Mono.just(relation)); + StepVerifier + .create(relationOperator.findBySubjectIdAndType(subjectId, SubjectRelationType.AFTER)) + .expectNext(relation) + .verifyComplete(); + } + + @Test + void relationCreateSubjectRelation() { + SubjectRelation relation = SubjectRelation + .builder() + .relationType(SubjectRelationType.AFTER) + .build(); + when(relationService.createSubjectRelation(relation)).thenReturn(Mono.just(relation)); + StepVerifier + .create(relationOperator.createSubjectRelation(relation)) + .expectNext(relation) + .verifyComplete(); + } + + @Test + void sync() { + UUID subjectId = UuidV7Utils.generateUuid(); + when(syncService.sync(subjectId, SubjectSyncPlatform.BGM_TV, "456")) + .thenReturn(Mono.empty()); + StepVerifier + .create(syncOperator.sync(subjectId, SubjectSyncPlatform.BGM_TV, "456")) + .verifyComplete(); + } + + @Test + void syncWithoutSubjectId() { + when(syncService.sync(null, SubjectSyncPlatform.BGM_TV, "456")) + .thenReturn(Mono.empty()); + StepVerifier + .create(syncOperator.sync(null, SubjectSyncPlatform.BGM_TV, "456")) + .verifyComplete(); + } + + @Test + void saveSubjectSync() { + SubjectSync sync = SubjectSync + .builder() + .platformId("bgm123") + .build(); + when(syncService.save(sync)).thenReturn(Mono.just(sync)); + StepVerifier + .create(syncOperator.save(sync)) + .expectNext(sync) + .verifyComplete(); + } + + @Test + void findSubjectSyncsBySubjectId() { + UUID subjectId = UuidV7Utils.generateUuid(); + SubjectSync sync = SubjectSync + .builder() + .subjectId(subjectId) + .build(); + when(syncService.findSubjectSyncsBySubjectId(subjectId)).thenReturn(Flux.just(sync)); + StepVerifier + .create(syncOperator.findSubjectSyncsBySubjectId(subjectId)) + .expectNext(sync) + .verifyComplete(); + } + + @Test + void findSubjectSyncBySubjectIdAndPlatform() { + UUID subjectId = UuidV7Utils.generateUuid(); + SubjectSync sync = SubjectSync + .builder() + .subjectId(subjectId) + .build(); + when(syncService.findSubjectSyncBySubjectIdAndPlatform(subjectId, + SubjectSyncPlatform.BGM_TV)) + .thenReturn(Mono.just(sync)); + StepVerifier + .create(syncOperator.findSubjectSyncBySubjectIdAndPlatform(subjectId, + SubjectSyncPlatform.BGM_TV)) + .expectNext(sync) + .verifyComplete(); + } +} diff --git a/server/src/test/java/run/ikaros/server/core/tag/DefaultTagServiceMoreTest.java b/server/src/test/java/run/ikaros/server/core/tag/DefaultTagServiceMoreTest.java new file mode 100644 index 000000000..40baa5e25 --- /dev/null +++ b/server/src/test/java/run/ikaros/server/core/tag/DefaultTagServiceMoreTest.java @@ -0,0 +1,150 @@ +package run.ikaros.server.core.tag; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.time.LocalDateTime; +import java.util.UUID; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.data.r2dbc.core.R2dbcEntityTemplate; +import org.springframework.data.relational.core.query.Query; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; +import run.ikaros.api.infra.utils.UuidV7Utils; +import run.ikaros.api.store.enums.TagType; +import run.ikaros.server.core.tag.event.TagRemoveEvent; +import run.ikaros.server.store.entity.TagEntity; +import run.ikaros.server.store.repository.TagRepository; + +class DefaultTagServiceMoreTest { + private TagRepository tagRepository; + private R2dbcEntityTemplate r2dbcEntityTemplate; + private ApplicationEventPublisher eventPublisher; + private DefaultTagService defaultTagService; + + @BeforeEach + void setUp() { + tagRepository = Mockito.mock(TagRepository.class); + r2dbcEntityTemplate = Mockito.mock(R2dbcEntityTemplate.class); + eventPublisher = Mockito.mock(ApplicationEventPublisher.class); + defaultTagService = + new DefaultTagService(tagRepository, r2dbcEntityTemplate, eventPublisher); + } + + @Test + void findSubjectTags() { + UUID subjectId = UuidV7Utils.generateUuid(); + TagEntity tagEntity = TagEntity + .builder() + .id(UuidV7Utils.generateUuid()) + .type(TagType.SUBJECT) + .name("anime") + .masterId(subjectId) + .userId(UuidV7Utils.generateUuid()) + .color("#FF0000") + .createTime(LocalDateTime.now()) + .build(); + + when(r2dbcEntityTemplate.select(any(Query.class), any(Class.class))) + .thenReturn(Flux.just(tagEntity)); + + StepVerifier + .create(defaultTagService.findSubjectTags(subjectId)) + .assertNext(subjectTag -> { + assertThat(subjectTag.getName()).isEqualTo("anime"); + assertThat(subjectTag.getSubjectId()).isEqualTo(subjectId); + }) + .verifyComplete(); + } + + @Test + void findSubjectTagsEmpty() { + UUID subjectId = UuidV7Utils.generateUuid(); + when(r2dbcEntityTemplate.select(any(Query.class), any(Class.class))) + .thenReturn(Flux.empty()); + StepVerifier + .create(defaultTagService.findSubjectTags(subjectId)) + .expectNextCount(0) + .verifyComplete(); + } + + @Test + void findAttachmentTags() { + UUID attachmentId = UuidV7Utils.generateUuid(); + TagEntity tagEntity = TagEntity + .builder() + .id(UuidV7Utils.generateUuid()) + .type(TagType.ATTACHMENT) + .name("cover") + .masterId(attachmentId) + .userId(UuidV7Utils.generateUuid()) + .createTime(LocalDateTime.now()) + .build(); + + when(r2dbcEntityTemplate.select(any(Query.class), any(Class.class))) + .thenReturn(Flux.just(tagEntity)); + + StepVerifier + .create(defaultTagService.findAttachmentTags(attachmentId)) + .assertNext(attachmentTag -> { + assertThat(attachmentTag.getName()).isEqualTo("cover"); + assertThat(attachmentTag.getAttachmentId()).isEqualTo(attachmentId); + }) + .verifyComplete(); + } + + @Test + void remove() { + UUID masterId = UuidV7Utils.generateUuid(); + UUID userId = UuidV7Utils.generateUuid(); + UUID tagId = UuidV7Utils.generateUuid(); + + TagEntity tagEntity = TagEntity + .builder() + .id(tagId) + .type(TagType.SUBJECT) + .masterId(masterId) + .name("action") + .userId(userId) + .createTime(LocalDateTime.now()) + .build(); + + when(r2dbcEntityTemplate.select(any(Query.class), any(Class.class))) + .thenReturn(Flux.just(tagEntity)); + when(tagRepository.findById(tagId)).thenReturn(Mono.just(tagEntity)); + when(tagRepository.deleteById(tagId)).thenReturn(Mono.empty()); + + StepVerifier + .create(defaultTagService.remove(TagType.SUBJECT, masterId, "action", userId)) + .verifyComplete(); + + verify(eventPublisher).publishEvent(any(TagRemoveEvent.class)); + } + + @Test + void removeById() { + UUID tagId = UuidV7Utils.generateUuid(); + TagEntity tagEntity = TagEntity + .builder() + .id(tagId) + .type(TagType.SUBJECT) + .name("test") + .createTime(LocalDateTime.now()) + .build(); + + when(tagRepository.findById(tagId)).thenReturn(Mono.just(tagEntity)); + when(tagRepository.deleteById(tagId)).thenReturn(Mono.empty()); + + StepVerifier + .create(defaultTagService.removeById(tagId)) + .verifyComplete(); + + verify(eventPublisher).publishEvent(any(TagRemoveEvent.class)); + } +} diff --git a/server/src/test/java/run/ikaros/server/custom/scheme/DefaultCustomSchemeManagerTest.java b/server/src/test/java/run/ikaros/server/custom/scheme/DefaultCustomSchemeManagerTest.java new file mode 100644 index 000000000..953c65ec6 --- /dev/null +++ b/server/src/test/java/run/ikaros/server/custom/scheme/DefaultCustomSchemeManagerTest.java @@ -0,0 +1,111 @@ +package run.ikaros.server.custom.scheme; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import run.ikaros.api.custom.GroupVersionKind; +import run.ikaros.api.custom.scheme.CustomScheme; +import run.ikaros.server.custom.scheme.CustomSchemeWatcherManager.SchemeWatcher; + +@ExtendWith(MockitoExtension.class) +class DefaultCustomSchemeManagerTest { + + @Mock + private CustomSchemeWatcherManager watcherManager; + @Mock + private SchemeWatcher schemeWatcher; + + private DefaultCustomSchemeManager manager; + private CustomScheme scheme; + private ObjectNode schema; + + @BeforeEach + void setUp() throws Exception { + manager = new DefaultCustomSchemeManager(watcherManager); + ObjectMapper mapper = new ObjectMapper(); + schema = mapper.createObjectNode(); + schema.put("type", "object"); + schema.putObject("properties"); + scheme = new CustomScheme( + Object.class, + new GroupVersionKind("test.io", "v1", "Test"), + "tests", + "test", + schema + ); + } + + @Test + void register() { + manager.register(scheme); + assertThat(manager.schemes()) + .hasSize(1) + .contains(scheme); + } + + @Test + void registerNotifiesWatcher() { + when(watcherManager.watchers()).thenReturn(List.of(schemeWatcher)); + manager.register(scheme); + verify(schemeWatcher).onChange(any(CustomSchemeWatcherManager.SchemeRegistered.class)); + } + + @Test + void registerDuplicate() { + manager.register(scheme); + manager.register(scheme); + assertThat(manager.schemes()).hasSize(1); + } + + @Test + void unregister() { + manager.register(scheme); + manager.unregister(scheme); + assertThat(manager.schemes()).isEmpty(); + } + + @Test + void unregisterNotifiesWatcher() { + when(watcherManager.watchers()).thenReturn(List.of(schemeWatcher)); + manager.register(scheme); + manager.unregister(scheme); + verify(schemeWatcher).onChange(any(CustomSchemeWatcherManager.SchemeUnregistered.class)); + } + + @Test + void unregisterNonExistent() { + manager.unregister(scheme); + assertThat(manager.schemes()).isEmpty(); + } + + @Test + void registerWithNullWatcherManager() { + DefaultCustomSchemeManager noWatcherManager = new DefaultCustomSchemeManager(null); + noWatcherManager.register(scheme); + assertThat(noWatcherManager.schemes()).hasSize(1); + } + + @Test + void schemesReturnsUnmodifiableList() { + manager.register(scheme); + assertThat(manager.schemes()).isUnmodifiable(); + } + + @Test + void unregisterNoWatchers() { + when(watcherManager.watchers()).thenReturn(null); + manager.register(scheme); + manager.unregister(scheme); + assertThat(manager.schemes()).isEmpty(); + } +} diff --git a/server/src/test/java/run/ikaros/server/custom/scheme/DefaultCustomSchemeWatcherManagerTest.java b/server/src/test/java/run/ikaros/server/custom/scheme/DefaultCustomSchemeWatcherManagerTest.java new file mode 100644 index 000000000..0c2689736 --- /dev/null +++ b/server/src/test/java/run/ikaros/server/custom/scheme/DefaultCustomSchemeWatcherManagerTest.java @@ -0,0 +1,68 @@ +package run.ikaros.server.custom.scheme; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import run.ikaros.server.custom.scheme.CustomSchemeWatcherManager.SchemeWatcher; + +class DefaultCustomSchemeWatcherManagerTest { + + private DefaultCustomSchemeWatcherManager watcherManager; + + @BeforeEach + void setUp() { + watcherManager = new DefaultCustomSchemeWatcherManager(); + } + + @Test + void register() { + SchemeWatcher watcher = event -> {}; + watcherManager.register(watcher); + assertThat(watcherManager.watchers()).hasSize(1).contains(watcher); + } + + @Test + void registerNullThrows() { + assertThatThrownBy(() -> watcherManager.register(null)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void unregister() { + SchemeWatcher watcher = event -> {}; + watcherManager.register(watcher); + watcherManager.unregister(watcher); + assertThat(watcherManager.watchers()).isEmpty(); + } + + @Test + void unregisterNullThrows() { + assertThatThrownBy(() -> watcherManager.unregister(null)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void unregisterNonExistent() { + SchemeWatcher watcher = event -> {}; + watcherManager.unregister(watcher); + assertThat(watcherManager.watchers()).isEmpty(); + } + + @Test + void watchersReturnsUnmodifiableList() { + SchemeWatcher watcher = event -> {}; + watcherManager.register(watcher); + assertThat(watcherManager.watchers()).isUnmodifiable(); + } + + @Test + void multipleWatchers() { + SchemeWatcher w1 = event -> {}; + SchemeWatcher w2 = event -> {}; + watcherManager.register(w1); + watcherManager.register(w2); + assertThat(watcherManager.watchers()).hasSize(2).containsExactly(w1, w2); + } +} diff --git a/server/src/test/java/run/ikaros/server/infra/utils/AesEncryptUtilsTest.java b/server/src/test/java/run/ikaros/server/infra/utils/AesEncryptUtilsTest.java index 3051b80a6..a6dccef6e 100644 --- a/server/src/test/java/run/ikaros/server/infra/utils/AesEncryptUtilsTest.java +++ b/server/src/test/java/run/ikaros/server/infra/utils/AesEncryptUtilsTest.java @@ -1,172 +1,146 @@ package run.ikaros.server.infra.utils; -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.assertj.core.api.Assertions.assertThat; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; -import java.nio.file.Path; import java.util.Base64; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; class AesEncryptUtilsTest { - @TempDir - Path tempDir; - @Test - void generateKeyByteArrayDefaultLength() { - byte[] keyBytes = AesEncryptUtils.generateKeyByteArray(); - assertNotNull(keyBytes); - assertTrue(keyBytes.length > 0, "Default key bytes should not be empty"); - // Default is 192-bit, Base64-encoded: ceil(192/8) = 24 raw bytes -> Base64 = 32 chars - byte[] decoded = Base64.getDecoder().decode(keyBytes); - assertEquals(24, decoded.length, "Default 192-bit key should decode to 24 bytes"); + void generateKeyByteArray_default_returnsBase64Bytes() { + byte[] keyB64 = AesEncryptUtils.generateKeyByteArray(); + assertThat(keyB64).isNotNull(); + // decode to verify it's valid base64 and 24 raw bytes (192-bit default) + byte[] rawKey = Base64 + .getDecoder() + .decode(keyB64); + assertThat(rawKey).hasSize(24); } @Test - void generateKeyByteArray128() { - byte[] keyBytes = AesEncryptUtils.generateKeyByteArray(128); - assertNotNull(keyBytes); - byte[] decoded = Base64.getDecoder().decode(keyBytes); - assertEquals(16, decoded.length, "128-bit key should decode to 16 bytes"); + void generateKeyByteArray_128bit_returnsValidKey() { + byte[] keyB64 = AesEncryptUtils.generateKeyByteArray(128); + assertThat(keyB64).isNotNull(); + byte[] rawKey = Base64 + .getDecoder() + .decode(keyB64); + assertThat(rawKey).hasSize(16); // 128 bits = 16 bytes } @Test - void generateKeyByteArray256() { - byte[] keyBytes = AesEncryptUtils.generateKeyByteArray(256); - assertNotNull(keyBytes); - byte[] decoded = Base64.getDecoder().decode(keyBytes); - assertEquals(32, decoded.length, "256-bit key should decode to 32 bytes"); + void generateKeyByteArray_256bit_returnsValidKey() { + byte[] keyB64 = AesEncryptUtils.generateKeyByteArray(256); + assertThat(keyB64).isNotNull(); + byte[] rawKey = Base64 + .getDecoder() + .decode(keyB64); + assertThat(rawKey).hasSize(32); // 256 bits = 32 bytes } @Test - void encryptDecryptByteArrayRoundtrip() { - byte[] keyBytes = Base64.getDecoder().decode(AesEncryptUtils.generateKeyByteArray()); - byte[] original = "Hello, AES encryption!".getBytes(); - - byte[] encrypted = AesEncryptUtils.encryptByteArray(keyBytes, original); - assertNotNull(encrypted); - byte[] decrypted = AesEncryptUtils.decryptByteArray(keyBytes, encrypted); - assertNotNull(decrypted); - assertArrayEquals(original, decrypted); + void encryptDecrypt_roundTrip_bytes() throws Exception { + byte[] keyB64 = AesEncryptUtils.generateKeyByteArray(128); + byte[] rawKey = Base64 + .getDecoder() + .decode(keyB64); + String original = "Hello AES World! " + System.currentTimeMillis(); + byte[] originalBytes = original.getBytes(StandardCharsets.UTF_8); + + // encryptByteArray(byte[] key, byte[] data) + byte[] encrypted = AesEncryptUtils.encryptByteArray(rawKey, originalBytes); + assertThat(encrypted).isNotNull(); + assertThat(encrypted).isNotEqualTo(originalBytes); + + // decryptByteArray(byte[] key, byte[] encryptedDataBase64) + byte[] decrypted = AesEncryptUtils.decryptByteArray(rawKey, encrypted); + String result = new String(decrypted, StandardCharsets.UTF_8); + assertThat(result).isEqualTo(original); } @Test - void encryptDecryptWithBase64StringKey() { - byte[] keyBytes = AesEncryptUtils.generateKeyByteArray(); - String keyStrBase64 = new String(keyBytes); - byte[] original = "Test string key encryption".getBytes(); - - byte[] encrypted = AesEncryptUtils.encryptByteArray(keyStrBase64, original); - assertNotNull(encrypted); - byte[] decrypted = AesEncryptUtils.decryptByteArray(keyStrBase64, encrypted); - assertNotNull(decrypted); - assertArrayEquals(original, decrypted); + void encryptDecrypt_withStringKey() throws Exception { + byte[] keyB64 = AesEncryptUtils.generateKeyByteArray(128); + String keyBase64Str = new String(keyB64, StandardCharsets.UTF_8); + String original = "测试中文加密内容"; + + // encryptByteArray(String base64Key, byte[] data) + byte[] encrypted = AesEncryptUtils.encryptByteArray(keyBase64Str, + original.getBytes(StandardCharsets.UTF_8)); + assertThat(encrypted).isNotNull(); + + byte[] rawKey = Base64 + .getDecoder() + .decode(keyB64); + byte[] decrypted = AesEncryptUtils.decryptByteArray(rawKey, encrypted); + String result = new String(decrypted, StandardCharsets.UTF_8); + assertThat(result).isEqualTo(original); } @Test - void generateKeyFileAndEncryptDecrypt() throws IOException { - File keyFile = tempDir.resolve("aes.key").toFile(); + void generateKeyFile_validPath_createsFile(@TempDir File tempDir) throws Exception { + File keyFile = new File(tempDir, "aes.key"); AesEncryptUtils.generateKeyFile(keyFile); - assertTrue(keyFile.exists()); - assertTrue(keyFile.length() > 0); - - byte[] keyBytes = Base64.getDecoder().decode(Files.readAllBytes(keyFile.toPath())); - byte[] original = "File key test data".getBytes(); - - byte[] encrypted = AesEncryptUtils.encryptByteArray(keyFile, original); - assertNotNull(encrypted); - byte[] decrypted = AesEncryptUtils.decryptByteArray(keyBytes, encrypted); - assertArrayEquals(original, decrypted); + assertThat(keyFile).exists(); + assertThat(keyFile.length()).isGreaterThan(0); } @Test - void encryptDecryptInputStreamRoundtrip() throws IOException { - byte[] keyBytes = Base64.getDecoder().decode(AesEncryptUtils.generateKeyByteArray()); - byte[] original = "Stream encryption test content".getBytes(); - - // Encrypt - ByteArrayOutputStream encryptedOut = new ByteArrayOutputStream(); - AesEncryptUtils.encryptInputStream( - new ByteArrayInputStream(original), true, encryptedOut, keyBytes); - byte[] encrypted = encryptedOut.toByteArray(); - - // Decrypt - ByteArrayOutputStream decryptedOut = new ByteArrayOutputStream(); - AesEncryptUtils.decryptInputStream( - new ByteArrayInputStream(encrypted), true, decryptedOut, keyBytes); - byte[] decrypted = decryptedOut.toByteArray(); - - assertArrayEquals(original, decrypted); + void generateKeyFile_customLength_createsFile(@TempDir File tempDir) throws Exception { + File keyFile = new File(tempDir, "aes256.key"); + AesEncryptUtils.generateKeyFile(256, keyFile); + assertThat(keyFile).exists(); + // key file content is base64 encoded, should be ~44 chars for 256-bit = 32 raw bytes + String content = Files + .readString(keyFile.toPath()) + .trim(); + byte[] rawKey = Base64 + .getDecoder() + .decode(content); + assertThat(rawKey).hasSize(32); } @Test - void encryptDecryptFileWithFileKey() throws IOException { - File keyFile = tempDir.resolve("key.dat").toFile(); - AesEncryptUtils.generateKeyFile(keyFile); + void encryptFile_decryptFile_roundTrip(@TempDir File tempDir) throws Exception { + byte[] keyB64 = AesEncryptUtils.generateKeyByteArray(128); + File keyFile = new File(tempDir, "secret.key"); + Files.write(keyFile.toPath(), keyB64); - File dataFile = tempDir.resolve("data.txt").toFile(); - byte[] original = "File encryption roundtrip".getBytes(); - try (FileOutputStream fos = new FileOutputStream(dataFile)) { - fos.write(original); - } + File originalFile = new File(tempDir, "original.txt"); + File encryptedFile = new File(tempDir, "encrypted.bin"); + File decryptedFile = new File(tempDir, "decrypted.txt"); - File encryptedFile = tempDir.resolve("encrypted.dat").toFile(); - AesEncryptUtils.encryptFile(keyFile, dataFile, encryptedFile); - assertTrue(encryptedFile.exists()); + Files.write(originalFile.toPath(), "File encryption test".getBytes()); - File decryptedFile = tempDir.resolve("decrypted.txt").toFile(); + // encryptFile(keyFile, dataFile, encryptedFile) + AesEncryptUtils.encryptFile(keyFile, originalFile, encryptedFile); + // decryptFile(keyFile, dataFile, decryptedFile) AesEncryptUtils.decryptFile(keyFile, encryptedFile, decryptedFile); - assertTrue(decryptedFile.exists()); - byte[] decrypted = Files.readAllBytes(decryptedFile.toPath()); - assertArrayEquals(original, decrypted); + String decryptedContent = Files.readString(decryptedFile.toPath()); + assertThat(decryptedContent).isEqualTo("File encryption test"); } @Test - void encryptFileWithByteArrayKeyAndDecrypt() throws IOException { - byte[] keyBytes = Base64.getDecoder().decode(AesEncryptUtils.generateKeyByteArray()); - - File dataFile = tempDir.resolve("input.txt").toFile(); - byte[] original = "ByteArray key file test".getBytes(); - try (FileOutputStream fos = new FileOutputStream(dataFile)) { - fos.write(original); - } - - byte[] encrypted = AesEncryptUtils.encryptFile(keyBytes, dataFile); - assertNotNull(encrypted); - - byte[] decrypted = AesEncryptUtils.decryptFile(keyBytes, - createTempFileFromBytes("encrypted.dat", encrypted)); - assertArrayEquals(original, decrypted); - } - - @Test - void encryptEmptyByteArray() { - byte[] keyBytes = Base64.getDecoder().decode(AesEncryptUtils.generateKeyByteArray()); - byte[] original = new byte[0]; - - byte[] encrypted = AesEncryptUtils.encryptByteArray(keyBytes, original); - assertNotNull(encrypted); - - byte[] decrypted = AesEncryptUtils.decryptByteArray(keyBytes, encrypted); - assertNotNull(decrypted); - assertArrayEquals(original, decrypted); - } - - private File createTempFileFromBytes(String name, byte[] bytes) throws IOException { - File file = tempDir.resolve(name).toFile(); - try (FileOutputStream fos = new FileOutputStream(file)) { - fos.write(bytes); - } - return file; + void encryptByteArray_withKeyFile_roundTrip(@TempDir File tempDir) throws Exception { + File keyFile = new File(tempDir, "secret.key"); + AesEncryptUtils.generateKeyFile(keyFile); + String original = "Encrypt with key file"; + + byte[] encrypted = + AesEncryptUtils.encryptByteArray(keyFile, original.getBytes(StandardCharsets.UTF_8)); + assertThat(encrypted).isNotNull(); + + byte[] keyB64 = Files.readAllBytes(keyFile.toPath()); + byte[] rawKey = Base64 + .getDecoder() + .decode(keyB64); + byte[] decrypted = AesEncryptUtils.decryptByteArray(rawKey, encrypted); + String result = new String(decrypted, StandardCharsets.UTF_8); + assertThat(result).isEqualTo(original); } } diff --git a/server/src/test/java/run/ikaros/server/infra/utils/ByteArrUtilsTest.java b/server/src/test/java/run/ikaros/server/infra/utils/ByteArrUtilsTest.java index 4407b59bf..9191ecb85 100644 --- a/server/src/test/java/run/ikaros/server/infra/utils/ByteArrUtilsTest.java +++ b/server/src/test/java/run/ikaros/server/infra/utils/ByteArrUtilsTest.java @@ -1,46 +1,65 @@ package run.ikaros.server.infra.utils; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; -import java.nio.charset.StandardCharsets; import org.junit.jupiter.api.Test; class ByteArrUtilsTest { @Test - void isBinaryDataWithControlChars() { - byte[] data = {0, 1, 2, 127}; - assertTrue(ByteArrUtils.isBinaryData(data)); + void isBinaryData_pngHeader_returnsTrue() { + byte[] png = {(byte) 0x89, 0x50, 0x4E, 0x47}; + assertThat(ByteArrUtils.isBinaryData(png)).isTrue(); } @Test - void isBinaryDataWithTextBytes() { - byte[] data = "Hello, world!".getBytes(StandardCharsets.UTF_8); - assertFalse(ByteArrUtils.isBinaryData(data)); + void isBinaryData_textAscii_returnsFalse() { + byte[] text = "Hello World".getBytes(); + assertThat(ByteArrUtils.isBinaryData(text)).isFalse(); } @Test - void isBinaryDataWithEmptyArray() { - byte[] data = new byte[0]; - assertFalse(ByteArrUtils.isBinaryData(data)); + void isBinaryData_empty_returnsFalse() { + assertThat(ByteArrUtils.isBinaryData(new byte[0])).isFalse(); } @Test - void isStringDataWithValidUtf8Text() { - byte[] data = "Hello, world!".getBytes(StandardCharsets.UTF_8); - assertTrue(ByteArrUtils.isStringData(data)); + void isBinaryData_withNullByte_returnsTrue() { + byte[] data = {0x48, 0x00, 0x65, 0x6C}; + assertThat(ByteArrUtils.isBinaryData(data)).isTrue(); } @Test - void isStringDataWithBinaryData() { - byte[] data = {0, 1, 2, (byte) 0xFF}; - assertFalse(ByteArrUtils.isStringData(data)); + void isBinaryData_withControlChars_returnsTrue() { + byte[] data = {0x01, 0x02, 0x03}; + assertThat(ByteArrUtils.isBinaryData(data)).isTrue(); } @Test - void isStringDataWithEmptyArray() { - byte[] data = new byte[0]; - assertTrue(ByteArrUtils.isStringData(data)); + void isBinaryData_tabNewlineReturn_returnsFalse() { + byte[] data = {0x09, 0x0A, 0x0D, 0x48}; + assertThat(ByteArrUtils.isBinaryData(data)).isFalse(); + } + + @Test + void isBinaryData_null_throwsNpe() { + assertThrows(NullPointerException.class, () -> ByteArrUtils.isBinaryData(null)); + } + + @Test + void isStringData_text_returnsTrue() { + byte[] text = "plain text".getBytes(); + assertThat(ByteArrUtils.isStringData(text)).isTrue(); + } + + @Test + void isStringData_null_returnsFalse() { + assertThat(ByteArrUtils.isStringData(null)).isFalse(); + } + + @Test + void isStringData_empty_returnsTrue() { + assertThat(ByteArrUtils.isStringData(new byte[0])).isTrue(); } } diff --git a/server/src/test/java/run/ikaros/server/infra/utils/JsonUtilsTest.java b/server/src/test/java/run/ikaros/server/infra/utils/JsonUtilsTest.java index 31e3f05d2..16f12c2c4 100644 --- a/server/src/test/java/run/ikaros/server/infra/utils/JsonUtilsTest.java +++ b/server/src/test/java/run/ikaros/server/infra/utils/JsonUtilsTest.java @@ -1,75 +1,97 @@ package run.ikaros.server.infra.utils; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.assertj.core.api.Assertions.assertThat; import com.fasterxml.jackson.core.type.TypeReference; -import java.util.ArrayList; -import java.util.List; -import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; class JsonUtilsTest { - @Test - void json2ObjArr() { - List userList = new ArrayList<>(); - userList.add(new User("u1", "p1")); - userList.add(new User("u2", "p2")); - userList.add(new User("u3", "p3")); - - String json = JsonUtils.obj2Json(userList); + void obj2Json_validObject_returnsJsonString() { + TestObj obj = new TestObj("hello", 42); + String json = JsonUtils.obj2Json(obj); + assertThat(json).contains("\"name\""); + assertThat(json).contains("\"hello\""); + assertThat(json).contains("\"value\""); + } - TypeReference userTypeReference = new TypeReference() { - }; - User[] users = JsonUtils.json2ObjArr(json, userTypeReference); - assertNotNull(users); - assertEquals(users.length, userList.size()); + @Test + void obj2Json_beanWithNull_returnsJson() { + TestObj obj = new TestObj(null, 0); + String json = JsonUtils.obj2Json(obj); + assertThat(json).isNotNull(); + } + @Test + void json2obj_validJson_returnsObject() { + String json = "{\"name\":\"test\",\"value\":123}"; + TestObj obj = JsonUtils.json2obj(json, TestObj.class); + assertThat(obj).isNotNull(); + assertThat(obj.getName()).isEqualTo("test"); + assertThat(obj.getValue()).isEqualTo(123); } + @Test + void json2obj_invalidJson_returnsNull() { + TestObj obj = JsonUtils.json2obj("invalid json", TestObj.class); + assertThat(obj).isNull(); + } @Test - void objAndJson() { - String json = JsonUtils.obj2Json(new User().setUsername("u1")); - Assertions.assertThat(json).isNotBlank(); + void json2ObjArr_validJson_returnsArray() { + String json = "[{\"name\":\"a\",\"value\":1},{\"name\":\"b\",\"value\":2}]"; + TestObj[] arr = JsonUtils.json2ObjArr(json, new TypeReference() { + }); + assertThat(arr).hasSize(2); + assertThat(arr[0].getName()).isEqualTo("a"); + assertThat(arr[1].getName()).isEqualTo("b"); + } - User user1 = JsonUtils.json2obj(json, User.class); - Assertions.assertThat(user1).isNotNull(); - Assertions.assertThat(user1.getUsername()).isEqualTo("u1"); + @Test + void json2ObjArr_invalidJson_returnsEmptyArray() { + // implementation returns null for invalid json + TestObj[] arr = JsonUtils.json2ObjArr("bad json", new TypeReference() { + }); + assertThat(arr).isNull(); } + @Test + void obj2JsonAndBack_roundTrip() { + TestObj original = new TestObj("roundtrip", 999); + String json = JsonUtils.obj2Json(original); + TestObj restored = JsonUtils.json2obj(json, TestObj.class); + assertThat(restored).isNotNull(); + assertThat(restored.getName()).isEqualTo("roundtrip"); + assertThat(restored.getValue()).isEqualTo(999); + } - static class User { - private String username; - private String password; + static class TestObj { + private String name; + private int value; - public User() { + public TestObj() { } - public User(String username, String password) { - this.username = username; - this.password = password; + public TestObj(String name, int value) { + this.name = name; + this.value = value; } - public String getUsername() { - return username; + public String getName() { + return name; } - public User setUsername(String username) { - this.username = username; - return this; + public void setName(String name) { + this.name = name; } - public String getPassword() { - return password; + public int getValue() { + return value; } - public User setPassword(String password) { - this.password = password; - return this; + public void setValue(int value) { + this.value = value; } } - -} \ No newline at end of file +} diff --git a/server/src/test/java/run/ikaros/server/infra/utils/PathUtilsTest.java b/server/src/test/java/run/ikaros/server/infra/utils/PathUtilsTest.java index ab13a36d4..99bb9bb99 100644 --- a/server/src/test/java/run/ikaros/server/infra/utils/PathUtilsTest.java +++ b/server/src/test/java/run/ikaros/server/infra/utils/PathUtilsTest.java @@ -1,6 +1,6 @@ package run.ikaros.server.infra.utils; -import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -9,47 +9,113 @@ class PathUtilsTest { @Test - void isAbsoluteUriWithHttpsUrl() { - assertTrue(PathUtils.isAbsoluteUri("https://example.com")); + void isAbsoluteUri_httpUrl_returnsTrue() { + assertTrue(PathUtils.isAbsoluteUri("http://example.com/file.txt")); } @Test - void isAbsoluteUriWithRelativePath() { - assertFalse(PathUtils.isAbsoluteUri("/api/test")); + void isAbsoluteUri_httpsUrl_returnsTrue() { + assertTrue(PathUtils.isAbsoluteUri("https://example.com/file.txt")); } @Test - void isAbsoluteUriWithNull() { + void isAbsoluteUri_relativePath_returnsFalse() { + assertFalse(PathUtils.isAbsoluteUri("/relative/path/file.txt")); + } + + @Test + void isAbsoluteUri_emptyString_returnsFalse() { + assertFalse(PathUtils.isAbsoluteUri("")); + } + + @Test + void isAbsoluteUri_null_returnsFalse() { assertFalse(PathUtils.isAbsoluteUri(null)); } @Test - void combinePathMultipleSegments() { - assertEquals("/api/v1/test", PathUtils.combinePath("api", "v1", "test")); + void isAbsoluteUri_blankString_returnsFalse() { + assertFalse(PathUtils.isAbsoluteUri(" ")); + } + + @Test + void combinePath_singleSegment_returnsPath() { + assertThat(PathUtils.combinePath("a", "b", "c")).isEqualTo("/a/b/c"); + } + + @Test + void combinePath_withLeadingSlash_handlesCorrectly() { + assertThat(PathUtils.combinePath("/a", "/b")).isEqualTo("/a/b"); + } + + @Test + void combinePath_withTrailingSlash_removesTrailing() { + assertThat(PathUtils.combinePath("a/", "b/")).isEqualTo("/a/b"); + } + + @Test + void combinePath_emptySegments_skipsEmpty() { + assertThat(PathUtils.combinePath("a", "", "b")).isEqualTo("/a/b"); + } + + @Test + void combinePath_nullSegments_skipsNull() { + assertThat(PathUtils.combinePath("a", null, "b")).isEqualTo("/a/b"); + } + + @Test + void combinePath_noArgs_returnsEmpty() { + assertThat(PathUtils.combinePath()).isEmpty(); + } + + @Test + void appendPathSeparatorIfMissing_endsWithSlash_unchanged() { + assertThat(PathUtils.appendPathSeparatorIfMissing("path/")).isEqualTo("path/"); + } + + @Test + void appendPathSeparatorIfMissing_noSlash_appends() { + assertThat(PathUtils.appendPathSeparatorIfMissing("path")).isEqualTo("path/"); + } + + @Test + void appendPathSeparatorIfMissing_empty_returnsSlash() { + assertThat(PathUtils.appendPathSeparatorIfMissing("")).isEqualTo("/"); + } + + @Test + void appendPathSeparatorIfMissing_null_returnsNull() { + assertThat(PathUtils.appendPathSeparatorIfMissing(null)).isNull(); + } + + @Test + void simplifyPathPattern_removesRegexPlaceholder() { + assertThat(PathUtils.simplifyPathPattern("/{year:\\d{4}}/{month:\\d{2}}")) + .isEqualTo("/{year}/{month}"); } @Test - void combinePathSingleSegment() { - assertEquals("/single", PathUtils.combinePath("single")); + void simplifyPathPattern_simplePath_unchanged() { + assertThat(PathUtils.simplifyPathPattern("/a/b/c")).isEqualTo("/a/b/c"); } @Test - void appendPathSeparatorIfMissing() { - assertEquals("/path/", PathUtils.appendPathSeparatorIfMissing("/path")); + void simplifyPathPattern_empty_returnsEmpty() { + assertThat(PathUtils.simplifyPathPattern("")).isEmpty(); } @Test - void appendPathSeparatorIfMissingAlreadyPresent() { - assertEquals("/path/", PathUtils.appendPathSeparatorIfMissing("/path/")); + void simplifyPathPattern_blank_returnsEmpty() { + assertThat(PathUtils.simplifyPathPattern(" ")).isEmpty(); } @Test - void simplifyPathPatternWithRegex() { - assertEquals("/{year}", PathUtils.simplifyPathPattern("{year:\\d{4}}")); + void simplifyPathPattern_withoutColon_unchanged() { + assertThat(PathUtils.simplifyPathPattern("/{slug}")).isEqualTo("/{slug}"); } @Test - void simplifyPathPatternWithoutRegex() { - assertEquals("/{id}", PathUtils.simplifyPathPattern("{id}")); + void isAbsoluteUri_invalidUri_returnsFalse() { + assertFalse(PathUtils.isAbsoluteUri("\\invalid\\path")); } } diff --git a/server/src/test/java/run/ikaros/server/infra/utils/RandomUtilsTest.java b/server/src/test/java/run/ikaros/server/infra/utils/RandomUtilsTest.java index d20d67f65..1cb4a0ba6 100644 --- a/server/src/test/java/run/ikaros/server/infra/utils/RandomUtilsTest.java +++ b/server/src/test/java/run/ikaros/server/infra/utils/RandomUtilsTest.java @@ -1,37 +1,41 @@ package run.ikaros.server.infra.utils; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.Test; class RandomUtilsTest { @Test - void randomStringFiveDigits() { - String result = RandomUtils.randomString(5); - assertEquals(5, result.length()); - assertTrue(result.matches("\\d+"), "Result should contain only digits"); + void randomString_positiveLength_returnsString() { + String result = RandomUtils.randomString(10); + assertThat(result).hasSize(10); } @Test - void randomStringZeroDefaultsToTenDigits() { + void randomString_zeroLength_defaultsToLength10() { String result = RandomUtils.randomString(0); - assertEquals(10, result.length()); - assertTrue(result.matches("\\d+"), "Result should contain only digits"); + assertThat(result).hasSize(10); } @Test - void randomStringNegativeDefaultsToTenDigits() { + void randomString_negativeLength_defaultsToLength10() { String result = RandomUtils.randomString(-1); - assertEquals(10, result.length()); - assertTrue(result.matches("\\d+"), "Result should contain only digits"); + assertThat(result).hasSize(10); } @Test - void randomStringOneDigit() { - String result = RandomUtils.randomString(1); - assertEquals(1, result.length()); - assertTrue(result.matches("\\d+"), "Result should contain only digits"); + void randomString_producesNumericString() { + String result = RandomUtils.randomString(50); + assertThat(result).hasSize(50); + assertThat(result).matches("[0-9]+"); + } + + @Test + void randomString_multipleCalls_producesDifferentResults() { + String r1 = RandomUtils.randomString(20); + String r2 = RandomUtils.randomString(20); + // 理论上极小概率相同,但测试这个保证方法正常工作 + assertThat(r1).isNotEqualTo(r2); } } diff --git a/server/src/test/java/run/ikaros/server/infra/utils/SqlUtilsTest.java b/server/src/test/java/run/ikaros/server/infra/utils/SqlUtilsTest.java index c4e3eff7b..2af305574 100644 --- a/server/src/test/java/run/ikaros/server/infra/utils/SqlUtilsTest.java +++ b/server/src/test/java/run/ikaros/server/infra/utils/SqlUtilsTest.java @@ -1,39 +1,61 @@ package run.ikaros.server.infra.utils; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; +import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.Test; class SqlUtilsTest { @Test - void escapeLikeSpecialCharsWithBackslash() { - assertEquals("\\\\", SqlUtils.escapeLikeSpecialChars("\\")); + void escapeLikeSpecialChars_null_returnsNull() { + assertThat(SqlUtils.escapeLikeSpecialChars(null)).isNull(); } @Test - void escapeLikeSpecialCharsWithUnderscore() { - assertEquals("\\_", SqlUtils.escapeLikeSpecialChars("_")); + void escapeLikeSpecialChars_empty_returnsEmpty() { + assertThat(SqlUtils.escapeLikeSpecialChars("")).isEmpty(); } @Test - void escapeLikeSpecialCharsWithPercent() { - assertEquals("\\%", SqlUtils.escapeLikeSpecialChars("%")); + void escapeLikeSpecialChars_noSpecial_unchanged() { + assertThat(SqlUtils.escapeLikeSpecialChars("hello")).isEqualTo("hello"); } @Test - void escapeLikeSpecialCharsWithBrackets() { - assertEquals("\\[\\]", SqlUtils.escapeLikeSpecialChars("[]")); + void escapeLikeSpecialChars_escapePercent() { + assertThat(SqlUtils.escapeLikeSpecialChars("50%")).isEqualTo("50\\%"); } @Test - void escapeLikeSpecialCharsWithNull() { - assertNull(SqlUtils.escapeLikeSpecialChars(null)); + void escapeLikeSpecialChars_escapeUnderscore() { + assertThat(SqlUtils.escapeLikeSpecialChars("a_b")).isEqualTo("a\\_b"); } @Test - void escapeLikeSpecialCharsWithNormalString() { - assertEquals("hello", SqlUtils.escapeLikeSpecialChars("hello")); + void escapeLikeSpecialChars_escapeBackslash() { + assertThat(SqlUtils.escapeLikeSpecialChars("a\\b")).isEqualTo("a\\\\b"); + } + + @Test + void escapeLikeSpecialChars_escapeExclamation() { + assertThat(SqlUtils.escapeLikeSpecialChars("a!b")).isEqualTo("a\\!b"); + } + + @Test + void escapeLikeSpecialChars_escapeDash() { + assertThat(SqlUtils.escapeLikeSpecialChars("a-b")).isEqualTo("a\\-b"); + } + + @Test + void escapeLikeSpecialChars_escapeSingleQuote() { + assertThat(SqlUtils.escapeLikeSpecialChars("it's")).isEqualTo("it''s"); + } + + @Test + void escapeLikeSpecialChars_escapeMultipleSpecial() { + String result = SqlUtils.escapeLikeSpecialChars("100%_complete!test"); + assertThat(result).contains("\\%"); + assertThat(result).contains("\\_"); + assertThat(result).contains("\\!"); } } diff --git a/server/src/test/java/run/ikaros/server/infra/utils/TimeUtilsTest.java b/server/src/test/java/run/ikaros/server/infra/utils/TimeUtilsTest.java index 63f282289..172c35642 100644 --- a/server/src/test/java/run/ikaros/server/infra/utils/TimeUtilsTest.java +++ b/server/src/test/java/run/ikaros/server/infra/utils/TimeUtilsTest.java @@ -1,27 +1,32 @@ package run.ikaros.server.infra.utils; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.Test; class TimeUtilsTest { @Test - void formatTimestampDefaultPattern() { - // 2024-01-15 in UTC - String result = TimeUtils.formatTimestamp(1705276800000L); - assertEquals("2024-01-15", result); + void formatTimestamp_defaultPattern_returnsFormatted() { + String result = TimeUtils.formatTimestamp(1700000000000L); + assertThat(result).matches("\\d{4}-\\d{2}-\\d{2}"); } @Test - void formatTimestampCustomPattern() { - String result = TimeUtils.formatTimestamp(1705276800000L, "yyyy/MM/dd"); - assertEquals("2024/01/15", result); + void formatTimestamp_customPattern_returnsFormatted() { + String result = TimeUtils.formatTimestamp(1700000000000L, "yyyy/MM/dd"); + assertThat(result).matches("\\d{4}/\\d{2}/\\d{2}"); } @Test - void formatTimestampNullThrowsException() { - assertThrows(NullPointerException.class, () -> TimeUtils.formatTimestamp(null)); + void formatTimestamp_epochZero_returnsDate() { + String result = TimeUtils.formatTimestamp(0L); + assertThat(result).isEqualTo("1970-01-01"); + } + + @Test + void formatTimestamp_withTimePattern_includesTime() { + String result = TimeUtils.formatTimestamp(1700000000000L, "HH:mm:ss"); + assertThat(result).matches("\\d{2}:\\d{2}:\\d{2}"); } } diff --git a/server/src/test/java/run/ikaros/server/search/subject/LuceneSubjectSearchServiceTest.java b/server/src/test/java/run/ikaros/server/search/subject/LuceneSubjectSearchServiceTest.java new file mode 100644 index 000000000..34c2bbc07 --- /dev/null +++ b/server/src/test/java/run/ikaros/server/search/subject/LuceneSubjectSearchServiceTest.java @@ -0,0 +1,239 @@ +package run.ikaros.server.search.subject; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.Mockito; +import org.springframework.util.LinkedMultiValueMap; +import run.ikaros.api.infra.properties.IkarosProperties; +import run.ikaros.api.search.SearchParam; +import run.ikaros.api.search.SearchResult; +import run.ikaros.api.search.subject.SubjectDoc; +import run.ikaros.api.search.subject.SubjectHint; +import run.ikaros.api.store.enums.SubjectType; + +class LuceneSubjectSearchServiceTest { + private LuceneSubjectSearchService searchService; + private Path tempWorkDir; + + @BeforeEach + void setUp(@TempDir Path tempDir) throws IOException { + tempWorkDir = tempDir.resolve("ikaros"); + Files.createDirectories(tempWorkDir); + IkarosProperties ikarosProperties = Mockito.mock(IkarosProperties.class); + when(ikarosProperties.getWorkDir()).thenReturn(tempWorkDir); + searchService = new LuceneSubjectSearchService(ikarosProperties); + } + + private SubjectDoc createSubjectDoc(UUID id, String name, String nameCn, + String summary, SubjectType type, Long airTime) { + SubjectDoc doc = new SubjectDoc(); + doc.setId(id); + doc.setName(name); + doc.setNameCn(nameCn); + doc.setSummary(summary); + doc.setType(type); + doc.setAirTime(airTime); + doc.setNsfw(false); + doc.setTags(List.of("tag1", "tag2")); + return doc; + } + + private SearchParam createSearchParam(String keyword) { + var params = new LinkedMultiValueMap(); + params.add("keyword", keyword); + params.add("limit", "10"); + return new SearchParam(params); + } + + private SearchParam createSearchParam(String keyword, int limit, + String preTag, String postTag) { + var params = new LinkedMultiValueMap(); + params.add("keyword", keyword); + params.add("limit", String.valueOf(limit)); + if (preTag != null) { + params.add("highlightPreTag", preTag); + } + if (postTag != null) { + params.add("highlightPostTag", postTag); + } + return new SearchParam(params); + } + + @Test + void updateDocument_and_search() throws Exception { + UUID id = UUID.randomUUID(); + SubjectDoc doc = createSubjectDoc(id, "Test Anime", "测试动画", + "This is a test anime summary.", SubjectType.ANIME, 1700000000000L); + + searchService.updateDocument(List.of(doc)); + + // Search by name + SearchParam param = createSearchParam("Test", 10, "", ""); + SearchResult result = searchService.search(param); + + assertThat(result.getHits()).hasSize(1); + assertThat(result.getTotal()).isEqualTo(1); + assertThat(result.getKeyword()).isEqualTo("Test"); + SubjectHint hint = result + .getHits() + .get(0); + assertThat(hint.id()).isEqualTo(id); + assertThat(hint.name()).isEqualTo("Test Anime"); + assertThat(hint.nameCn()).isEqualTo("测试动画"); + } + + @Test + void updateDocument_and_searchByChinese() throws Exception { + UUID id = UUID.randomUUID(); + SubjectDoc doc = createSubjectDoc(id, "Attack on Titan", "进击的巨人", + "An anime about titans.", SubjectType.ANIME, 1700000000000L); + + searchService.updateDocument(List.of(doc)); + + SearchParam param = createSearchParam("进击"); + SearchResult result = searchService.search(param); + + assertThat(result.getTotal()).isGreaterThanOrEqualTo(1); + } + + @Test + void updateDocument_and_searchByTag() throws Exception { + UUID id = UUID.randomUUID(); + SubjectDoc doc = createSubjectDoc(id, "Sword Art Online", "刀剑神域", + "A VRMMO anime.", SubjectType.ANIME, 1700000000000L); + + searchService.updateDocument(List.of(doc)); + + SearchParam param = createSearchParam("tag:tag1"); + SearchResult result = searchService.search(param); + + assertThat(result.getHits()).hasSize(1); + } + + @Test + void updateDocument_withMultipleDocs() throws Exception { + SubjectDoc doc1 = createSubjectDoc(UUID.randomUUID(), "Naruto", "火影忍者", + "Ninja anime.", SubjectType.ANIME, 1700000000000L); + SubjectDoc doc2 = createSubjectDoc(UUID.randomUUID(), "One Piece", "海贼王", + "Pirate anime.", SubjectType.ANIME, 1700000000000L); + + searchService.updateDocument(List.of(doc1, doc2)); + + SearchParam param = createSearchParam("anime"); + SearchResult result = searchService.search(param); + + assertThat(result.getTotal()).isEqualTo(2); + } + + @Test + void rebuild() throws Exception { + SubjectDoc doc1 = createSubjectDoc(UUID.randomUUID(), "Initial D", "头文字D", + "Racing comic.", SubjectType.COMIC, 1700000000000L); + searchService.updateDocument(List.of(doc1)); + + // Rebuild with different set + SubjectDoc doc2 = createSubjectDoc(UUID.randomUUID(), "Demon Slayer", "鬼灭之刃", + "Demon hunting anime.", SubjectType.ANIME, 1700000000000L); + searchService.rebuild(List.of(doc2)); + + // Should only have doc2 + SearchParam param = createSearchParam("Demon"); + SearchResult result = searchService.search(param); + assertThat(result.getTotal()).isEqualTo(1); + + // Old doc should not be found + SearchParam paramOld = createSearchParam("Initial"); + SearchResult resultOld = searchService.search(paramOld); + assertThat(resultOld.getTotal()).isZero(); + } + + @Test + void removeDocuments() throws Exception { + UUID id = UUID.randomUUID(); + SubjectDoc doc = createSubjectDoc(id, "Bleach", "死神", + "Soul reaper anime.", SubjectType.ANIME, 1700000000000L); + searchService.updateDocument(List.of(doc)); + + // Verify exists + SearchParam param = createSearchParam("Bleach"); + assertThat(searchService + .search(param) + .getTotal()).isEqualTo(1); + + // Remove by keyword (UUIDs with hyphens confuse QueryParser, use name instead) + searchService.removeDocuments(Set.of("Bleach")); + + // Verify gone + SearchResult result = searchService.search(param); + assertThat(result.getTotal()).isZero(); + } + + @Test + void search_withNoResults() throws Exception { + // Add a doc first to ensure index exists + SubjectDoc doc = createSubjectDoc(UUID.randomUUID(), "Existing", "现有", + "A doc to initialize the index.", SubjectType.ANIME, 1700000000000L); + searchService.updateDocument(List.of(doc)); + + // Search for non-existent keyword + SearchParam param = createSearchParam("NonExistentAnimeXYZ"); + SearchResult result = searchService.search(param); + assertThat(result.getTotal()).isZero(); + assertThat(result.getHits()).isEmpty(); + } + + @Test + void search_byField() throws Exception { + UUID id = UUID.randomUUID(); + SubjectDoc doc = createSubjectDoc(id, "Fullmetal Alchemist", "钢之炼金术师", + "Alchemy anime.", SubjectType.ANIME, 1700000000000L); + searchService.updateDocument(List.of(doc)); + + // Search with field:value syntax (must be exact match for StringField) + SearchParam param = createSearchParam("name:Fullmetal Alchemist"); + SearchResult result = searchService.search(param); + assertThat(result.getTotal()).isEqualTo(1); + } + + @Test + void updateDocument_and_searchByType() throws Exception { + SubjectDoc doc1 = createSubjectDoc(UUID.randomUUID(), "Manga One", "漫画一", + "A comic.", SubjectType.COMIC, 1700000000000L); + SubjectDoc doc2 = createSubjectDoc(UUID.randomUUID(), "Anime One", "动画一", + "An anime.", SubjectType.ANIME, 1700000000000L); + + searchService.updateDocument(List.of(doc1, doc2)); + + // Search by type field + SearchParam param = createSearchParam("type:COMIC"); + SearchResult result = searchService.search(param); + assertThat(result.getTotal()).isEqualTo(1); + assertThat(result + .getHits() + .get(0) + .name()).isEqualTo("Manga One"); + } + + @Test + void search_highlighting() throws Exception { + SubjectDoc doc = createSubjectDoc(UUID.randomUUID(), "One Punch Man", "一拳超人", + "A superhero anime.", SubjectType.ANIME, 1700000000000L); + searchService.updateDocument(List.of(doc)); + + SearchParam param = createSearchParam("Punch", 10, "", ""); + SearchResult result = searchService.search(param); + assertThat(result.getTotal()).isEqualTo(1); + // Note: the name is stored as StringField, not analyzed, so highlight may apply + // to the searchable content, not the displayed name + } +} diff --git a/server/src/test/java/run/ikaros/server/security/authentication/logout/LogoutSuccessHandlerTest.java b/server/src/test/java/run/ikaros/server/security/authentication/logout/LogoutSuccessHandlerTest.java new file mode 100644 index 000000000..05ac4e38e --- /dev/null +++ b/server/src/test/java/run/ikaros/server/security/authentication/logout/LogoutSuccessHandlerTest.java @@ -0,0 +1,44 @@ +package run.ikaros.server.security.authentication.logout; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.core.io.buffer.DefaultDataBufferFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.mock.http.server.reactive.MockServerHttpResponse; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.server.WebFilterExchange; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilterChain; +import reactor.test.StepVerifier; + +@ExtendWith(MockitoExtension.class) +class LogoutSuccessHandlerTest { + + @Test + void onLogoutSuccess() { + LogoutSuccessHandler handler = new LogoutSuccessHandler(); + ServerHttpResponse response = new MockServerHttpResponse(new DefaultDataBufferFactory()); + ServerWebExchange exchange = mock(ServerWebExchange.class); + when(exchange.getResponse()).thenReturn(response); + WebFilterChain chain = mock(WebFilterChain.class); + WebFilterExchange filterExchange = new WebFilterExchange(exchange, chain); + Authentication authentication = mock(Authentication.class); + + StepVerifier + .create(handler.onLogoutSuccess(filterExchange, authentication)) + .verifyComplete(); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response + .getHeaders() + .getContentType()) + .isEqualTo(MediaType.APPLICATION_JSON); + } +} diff --git a/server/src/test/java/run/ikaros/server/security/authentication/totp/TotpServiceTest.java b/server/src/test/java/run/ikaros/server/security/authentication/totp/TotpServiceTest.java new file mode 100644 index 000000000..c75d9e752 --- /dev/null +++ b/server/src/test/java/run/ikaros/server/security/authentication/totp/TotpServiceTest.java @@ -0,0 +1,93 @@ +package run.ikaros.server.security.authentication.totp; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.apache.commons.codec.binary.Base32; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class TotpServiceTest { + private TotpService totpService; + + @BeforeEach + void setUp() { + totpService = new TotpService(); + } + + @Test + void generateSecret() { + String secret = totpService.generateSecret(); + assertThat(secret).isNotBlank(); + // Base32 encoded, should not contain '=' + assertThat(secret).doesNotContain("="); + Base32 base32 = new Base32(); + byte[] decoded = base32.decode(secret); + assertThat(decoded).hasSize(20); + } + + @Test + void generateOtpAuthUri() { + String secret = totpService.generateSecret(); + String uri = totpService.generateOtpAuthUri("testuser", secret); + + assertThat(uri).startsWith("otpauth://totp/Ikaros:testuser"); + assertThat(uri).contains("secret=" + secret); + assertThat(uri).contains("issuer=Ikaros"); + assertThat(uri).contains("algorithm=SHA1"); + assertThat(uri).contains("digits=6"); + assertThat(uri).contains("period=30"); + } + + @Test + void generateOtpAuthUri_withBlankUsername_throwsException() { + String secret = totpService.generateSecret(); + org.junit.jupiter.api.Assertions.assertThrows( + IllegalArgumentException.class, + () -> totpService.generateOtpAuthUri("", secret) + ); + } + + @Test + void generateOtpAuthUri_withBlankSecret_throwsException() { + org.junit.jupiter.api.Assertions.assertThrows( + IllegalArgumentException.class, + () -> totpService.generateOtpAuthUri("testuser", "") + ); + } + + @Test + void validateCode_withWrongCode_returnsFalse() { + String secret = totpService.generateSecret(); + boolean result = totpService.validateCode(secret, "000000"); + assertThat(result).isFalse(); + } + + @Test + void validateCode_withInvalidLength_returnsFalse() { + String secret = totpService.generateSecret(); + assertThat(totpService.validateCode(secret, "12345")).isFalse(); + assertThat(totpService.validateCode(secret, "1234567")).isFalse(); + } + + @Test + void validateCode_withNonDigit_returnsFalse() { + String secret = totpService.generateSecret(); + assertThat(totpService.validateCode(secret, "abc123")).isFalse(); + } + + @Test + void validateCode_withBlankSecret_throwsException() { + org.junit.jupiter.api.Assertions.assertThrows( + IllegalArgumentException.class, + () -> totpService.validateCode("", "123456") + ); + } + + @Test + void validateCode_withBlankCode_throwsException() { + org.junit.jupiter.api.Assertions.assertThrows( + IllegalArgumentException.class, + () -> totpService.validateCode(totpService.generateSecret(), "") + ); + } +} diff --git a/server/src/test/java/run/ikaros/server/theme/DefaultThemeServiceTest.java b/server/src/test/java/run/ikaros/server/theme/DefaultThemeServiceTest.java new file mode 100644 index 000000000..149b3e7bc --- /dev/null +++ b/server/src/test/java/run/ikaros/server/theme/DefaultThemeServiceTest.java @@ -0,0 +1,69 @@ +package run.ikaros.server.theme; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; +import run.ikaros.api.core.setting.ConfigMap; +import run.ikaros.api.custom.ReactiveCustomClient; + +class DefaultThemeServiceTest { + private ReactiveCustomClient reactiveCustomClient; + private DefaultThemeService defaultThemeService; + + @BeforeEach + void setUp() { + reactiveCustomClient = Mockito.mock(ReactiveCustomClient.class); + defaultThemeService = new DefaultThemeService(reactiveCustomClient); + } + + @Test + void getCurrentTheme_whenThemeSet() { + ConfigMap configMap = new ConfigMap(); + configMap.putDataItem("THEME_SELECT", "dark"); + when(reactiveCustomClient.findOne(any(), any())) + .thenReturn(Mono.just(configMap)); + + StepVerifier.create(defaultThemeService.getCurrentTheme()) + .assertNext(theme -> assertThat(theme).isEqualTo("dark")) + .verifyComplete(); + } + + @Test + void getCurrentTheme_whenThemeNotSet_returnsDefault() { + ConfigMap configMap = new ConfigMap(); + configMap.putDataItem("OTHER_KEY", "value"); + when(reactiveCustomClient.findOne(any(), any())) + .thenReturn(Mono.just(configMap)); + + StepVerifier.create(defaultThemeService.getCurrentTheme()) + .assertNext(theme -> assertThat(theme).isEqualTo("simple")) + .verifyComplete(); + } + + @Test + void getCurrentTheme_whenConfigMapNotFound_returnsEmpty() { + when(reactiveCustomClient.findOne(any(), any())) + .thenReturn(Mono.empty()); + + StepVerifier.create(defaultThemeService.getCurrentTheme()) + .verifyComplete(); + } + + @Test + void getCurrentTheme_whenDataIsNull_throwsNpe() { + ConfigMap configMap = new ConfigMap(); + configMap.setData(null); + when(reactiveCustomClient.findOne(any(), any())) + .thenReturn(Mono.just(configMap)); + + StepVerifier.create(defaultThemeService.getCurrentTheme()) + .expectError(NullPointerException.class) + .verify(); + } +}