This commit is contained in:
Magzhan Zhumabayev
2026-04-03 01:24:01 +05:00
parent 1cbca9ecfc
commit ecc05d9ad2
5 changed files with 990 additions and 5 deletions
+18 -3
View File
@@ -1,5 +1,6 @@
# CI: Maven build + unit tests on merge requests to main/master and on push to those branches.
# CD: Build Docker image and push to GitHub Container Registry (ghcr.io) on push to main/master only.
# GitLab CI pipeline for marketing-parser.
# CI: runs Maven unit tests, enforces JaCoCo coverage, and publishes reports on merge requests and protected-branch pushes.
# CD: existing publish job pushes the Docker image to ghcr.io on push to main/master.
# Optional: SSH deploy when CI/CD variable ENABLE_SSH_DEPLOY=true and deploy secrets are set.
#
# CI/CD variables (Settings → CI/CD → Variables):
@@ -33,7 +34,21 @@ build-and-test:
stage: test
image: maven:3.9.9-eclipse-temurin-21
script:
- mvn -B -ntp verify
- mvn -B -ntp clean verify
after_script:
- |
if [ "$CI_JOB_STATUS" != "success" ]; then
echo "ALERT: marketing-parser unit tests or JaCoCo coverage threshold failed."
fi
artifacts:
when: always
expire_in: 7 days
paths:
- target/site/jacoco
- target/surefire-reports
reports:
junit:
- target/surefire-reports/TEST-*.xml
docker-build-push:
stage: publish
+53 -1
View File
@@ -29,6 +29,7 @@
</scm>
<properties>
<java.version>21</java.version>
<jacoco.version>0.8.12</jacoco.version>
</properties>
<dependencies>
<dependency>
@@ -202,7 +203,58 @@
</excludes>
</configuration>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<excludedGroups>integration</excludedGroups>
</configuration>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>${jacoco.version}</version>
<executions>
<execution>
<id>prepare-agent</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>verify</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
<execution>
<id>check</id>
<phase>verify</phase>
<goals>
<goal>check</goal>
</goals>
<configuration>
<rules>
<rule>
<element>CLASS</element>
<includes>
<include>kz.konturai.parser.service.PostingTaskService</include>
<include>kz.konturai.parser.service.JwtService</include>
<include>kz.konturai.parser.service.SocialMediaCredentialsService</include>
</includes>
<limits>
<limit>
<counter>LINE</counter>
<value>COVEREDRATIO</value>
<minimum>0.80</minimum>
</limit>
</limits>
</rule>
</rules>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
@@ -0,0 +1,174 @@
package kz.konturai.parser.service;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.Base64;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
class JwtServiceTest {
private static final String TEST_SECRET = Base64.getEncoder().encodeToString(
"0123456789abcdef0123456789abcdef".getBytes(StandardCharsets.UTF_8));
private static final String DEFAULT_SECRET =
"dGVzdC1zZWNyZXQta2V5LWZvci1kZXZlbG9wbWVudC1vbmx5LWRvLW5vdC11c2UtaW4tcHJvZHVjdGlvbg==";
private JwtService jwtService;
@BeforeEach
void setUp() {
jwtService = new JwtService(TEST_SECRET);
}
@Test
void parseAndValidateShouldReturnClaimsForValidToken() {
String token = createToken(TEST_SECRET, "qa@example.com", Instant.now().plusSeconds(3600), Map.of(
"uid", 42L,
"roles", "ADMIN, USER"));
Claims claims = jwtService.parseAndValidate(token);
assertEquals("qa@example.com", claims.getSubject());
assertEquals(42L, claims.get("uid", Long.class));
assertEquals("ADMIN, USER", claims.get("roles", String.class));
}
@Test
void parseAndValidateShouldThrowForInvalidSignature() {
String token = createToken(
Base64.getEncoder().encodeToString("another-valid-secret-key-1234567".getBytes(StandardCharsets.UTF_8)),
"qa@example.com",
Instant.now().plusSeconds(3600),
Map.of("uid", 99L));
assertThrows(JwtException.class, () -> jwtService.parseAndValidate(token));
}
@Test
void constructorWithoutConfiguredSecretShouldUseDefaultSigningKey() {
JwtService serviceWithDefaultKey = new JwtService("");
String token = createToken(DEFAULT_SECRET, "fallback@example.com", Instant.now().plusSeconds(3600), Map.of("uid", 7L));
assertEquals("7", serviceWithDefaultKey.extractUserIdFromToken(token));
}
@Test
void extractTokenFromHeaderShouldHandleBearerPrefixBlankAndRawToken() {
assertNull(jwtService.extractTokenFromHeader(null));
assertNull(jwtService.extractTokenFromHeader(" "));
assertEquals("sample-token", jwtService.extractTokenFromHeader("Bearer sample-token"));
assertEquals("raw-token", jwtService.extractTokenFromHeader("raw-token"));
}
@Test
void extractUserIdFromTokenShouldPreferUidClaim() {
String token = createToken(TEST_SECRET, "qa@example.com", Instant.now().plusSeconds(3600), Map.of("uid", 12345L));
assertEquals("12345", jwtService.extractUserIdFromToken(token));
}
@ParameterizedTest
@MethodSource("legacyUserIdClaims")
void extractUserIdFromTokenShouldUseLegacyClaims(String claimName, Object claimValue, String expectedValue) {
String token = createToken(TEST_SECRET, "qa@example.com", Instant.now().plusSeconds(3600), Map.of(claimName, claimValue));
assertEquals(expectedValue, jwtService.extractUserIdFromToken(token));
}
@Test
void extractUserIdFromTokenShouldUseSubjectAsLastFallback() {
String token = createToken(TEST_SECRET, "subject@example.com", Instant.now().plusSeconds(3600), Map.of());
assertEquals("subject@example.com", jwtService.extractUserIdFromToken(token));
}
@Test
void extractUserIdFromTokenShouldReturnNullForExpiredToken() {
String token = createToken(TEST_SECRET, "qa@example.com", Instant.now().minusSeconds(60), Map.of("uid", 10L));
assertNull(jwtService.extractUserIdFromToken(token));
}
@Test
void extractUserIdFromTokenShouldReturnNullForMalformedToken() {
assertNull(jwtService.extractUserIdFromToken("not-a-jwt"));
}
@Test
void extractUserIdFromHeaderShouldExtractFromBearerToken() {
String token = createToken(TEST_SECRET, "qa@example.com", Instant.now().plusSeconds(3600), Map.of("uid", 501L));
assertEquals("501", jwtService.extractUserIdFromHeader("Bearer " + token));
}
@Test
void extractEmailFromTokenShouldReturnSubject() {
String token = createToken(TEST_SECRET, "qa@example.com", Instant.now().plusSeconds(3600), Map.of("uid", 12L));
assertEquals("qa@example.com", jwtService.extractEmailFromToken(token));
}
@Test
void extractEmailFromTokenShouldReturnNullForInvalidToken() {
assertNull(jwtService.extractEmailFromToken("broken-token"));
}
@Test
void extractRolesFromTokenShouldSplitAndTrimRoleList() {
String token = createToken(TEST_SECRET, "qa@example.com", Instant.now().plusSeconds(3600), Map.of(
"uid", 1L,
"roles", "ADMIN, USER , ,MANAGER"));
assertEquals(List.of("ADMIN", "USER", "MANAGER"), jwtService.extractRolesFromToken(token));
}
@Test
void extractRolesFromTokenShouldReturnEmptyListWhenRolesAreMissingOrTokenInvalid() {
String tokenWithoutRoles = createToken(TEST_SECRET, "qa@example.com", Instant.now().plusSeconds(3600), Map.of("uid", 1L));
assertTrue(jwtService.extractRolesFromToken(tokenWithoutRoles).isEmpty());
assertTrue(jwtService.extractRolesFromToken("invalid").isEmpty());
assertTrue(jwtService.extractRolesFromToken(null).isEmpty());
}
private static Stream<Arguments> legacyUserIdClaims() {
return Stream.of(
Arguments.of("userId", "legacy-user", "legacy-user"),
Arguments.of("id", 55L, "55"),
Arguments.of("user_id", "external-user", "external-user"));
}
private String createToken(String base64Secret, String subject, Instant expiration, Map<String, Object> customClaims) {
SecretKey key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(base64Secret));
Map<String, Object> claims = new HashMap<>(customClaims);
var builder = Jwts.builder()
.subject(subject)
.issuedAt(Date.from(Instant.now().minusSeconds(30)))
.expiration(Date.from(expiration))
.signWith(key);
claims.forEach(builder::claim);
return builder.compact();
}
}
@@ -0,0 +1,538 @@
package kz.konturai.parser.service;
import kz.konturai.parser.exception.FacebookTokenExpiredException;
import kz.konturai.parser.exception.LinkedInTokenExpiredException;
import kz.konturai.parser.exception.TelegramTokenExpiredException;
import kz.konturai.parser.model.MarketingStrategy;
import kz.konturai.parser.model.PostingTask;
import kz.konturai.parser.repository.MarketingStrategyRepository;
import kz.konturai.parser.repository.PostingTaskRepository;
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 java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.aryEq;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class PostingTaskServiceTest {
private static final String TEST_RECIPIENT_ID = "25769470256007187";
@Mock
private PostingTaskRepository taskRepository;
@Mock
private MarketingStrategyRepository strategyRepository;
@Mock
private SocialMediaCredentialsService credentialsService;
@Mock
private FacebookPostingService facebookPostingService;
@Mock
private LinkedInPostingService linkedInPostingService;
@Mock
private TelegramPostingService telegramPostingService;
@Mock
private MinIOService minIOService;
private PostingTaskService postingTaskService;
@BeforeEach
void setUp() {
postingTaskService = spy(new PostingTaskService(
taskRepository,
strategyRepository,
credentialsService,
facebookPostingService,
linkedInPostingService,
telegramPostingService,
minIOService));
}
@Test
void createTasksFromStrategyShouldThrowWhenStrategyIsMissing() {
when(strategyRepository.findById("strategy-1")).thenReturn(Optional.empty());
IllegalArgumentException exception = assertThrows(
IllegalArgumentException.class,
() -> postingTaskService.createTasksFromStrategy("strategy-1"));
assertEquals("Strategy not found: strategy-1", exception.getMessage());
verifyNoInteractions(taskRepository, credentialsService);
}
@Test
void createTasksFromStrategyShouldThrowWhenStrategyIsNotCompleted() {
MarketingStrategy strategy = new MarketingStrategy();
strategy.setStatus("processing");
when(strategyRepository.findById("strategy-1")).thenReturn(Optional.of(strategy));
IllegalStateException exception = assertThrows(
IllegalStateException.class,
() -> postingTaskService.createTasksFromStrategy("strategy-1"));
assertEquals("Strategy is not completed yet. Status: processing", exception.getMessage());
verifyNoInteractions(credentialsService, taskRepository);
}
@Test
void createTasksFromStrategyShouldReturnEmptyListWhenCalendarIsMissing() {
MarketingStrategy strategy = new MarketingStrategy();
strategy.setStatus("completed");
strategy.setPostCalendar(List.of());
when(strategyRepository.findById("strategy-1")).thenReturn(Optional.of(strategy));
List<PostingTask> tasks = postingTaskService.createTasksFromStrategy("strategy-1");
assertTrue(tasks.isEmpty());
verifyNoInteractions(credentialsService);
verify(taskRepository, never()).saveAll(anyList());
}
@Test
void createTasksFromStrategyShouldThrowWhenCredentialsAreMissing() {
MarketingStrategy strategy = completedStrategy("user-1", List.of(
calendarItem("facebook", LocalDateTime.now().plusDays(1), "Post", List.of("#marketing"))));
when(strategyRepository.findById("strategy-1")).thenReturn(Optional.of(strategy));
when(credentialsService.hasCredentials("user-1", "facebook")).thenReturn(false);
IllegalStateException exception = assertThrows(
IllegalStateException.class,
() -> postingTaskService.createTasksFromStrategy("strategy-1"));
assertEquals(
"Credentials not found for platform: facebook. Please configure credentials first.",
exception.getMessage());
verify(taskRepository, never()).saveAll(anyList());
}
@Test
void createTasksFromStrategyShouldReturnExistingTasksWhenAlreadyCreated() {
MarketingStrategy strategy = completedStrategy("user-1", List.of(
calendarItem("facebook", LocalDateTime.now().plusDays(1), "Post", List.of("#marketing"))));
List<PostingTask> existingTasks = List.of(task("task-1", "user-1", "facebook", "pending"));
when(strategyRepository.findById("strategy-1")).thenReturn(Optional.of(strategy));
when(credentialsService.hasCredentials("user-1", "facebook")).thenReturn(true);
when(taskRepository.findByStrategyId("strategy-1")).thenReturn(existingTasks);
List<PostingTask> result = postingTaskService.createTasksFromStrategy("strategy-1");
assertSame(existingTasks, result);
verify(taskRepository, never()).saveAll(anyList());
}
@Test
void createTasksFromStrategyShouldCreateTasksAndTriggerImmediateExecutionForPastDates() {
MarketingStrategy.PostCalendarItem pastItem = calendarItem(
"facebook",
LocalDateTime.now().minusHours(1),
"Past post",
List.of("#past"));
pastItem.setImageUrl("https://cdn.example.com/post.png");
pastItem.setImageFilename("post.png");
MarketingStrategy.PostCalendarItem futureItem = calendarItem(
"telegram",
LocalDateTime.now().plusHours(2),
"Future post",
List.of("#future"));
MarketingStrategy strategy = completedStrategy("user-1", List.of(pastItem, futureItem));
when(strategyRepository.findById("strategy-1")).thenReturn(Optional.of(strategy));
when(credentialsService.hasCredentials("user-1", "facebook")).thenReturn(true);
when(credentialsService.hasCredentials("user-1", "telegram")).thenReturn(true);
when(taskRepository.findByStrategyId("strategy-1")).thenReturn(List.of());
when(taskRepository.saveAll(anyList())).thenAnswer(invocation -> {
List<PostingTask> tasks = invocation.getArgument(0);
tasks.get(0).setId("past-task");
tasks.get(1).setId("future-task");
return tasks;
});
doNothing().when(postingTaskService).executeTaskAsync(anyString());
List<PostingTask> savedTasks = postingTaskService.createTasksFromStrategy("strategy-1");
assertEquals(2, savedTasks.size());
assertEquals("post.png", savedTasks.get(0).getImageFilename());
assertEquals("https://cdn.example.com/post.png", savedTasks.get(0).getImageUrl());
assertEquals("telegram", savedTasks.get(1).getPlatform());
verify(postingTaskService).executeTaskAsync("past-task");
verify(postingTaskService, never()).executeTaskAsync("future-task");
}
@Test
void getPendingTasksShouldDelegateToRepository() {
LocalDateTime beforeDate = LocalDateTime.now();
List<PostingTask> expectedTasks = List.of(task("task-1", "user-1", "facebook", "pending"));
when(taskRepository.findByStatusAndPublishDateLessThanEqual("pending", beforeDate)).thenReturn(expectedTasks);
List<PostingTask> actualTasks = postingTaskService.getPendingTasks(beforeDate);
assertSame(expectedTasks, actualTasks);
}
@Test
void executeTaskShouldIgnoreMissingTask() {
when(taskRepository.findById("task-1")).thenReturn(Optional.empty());
postingTaskService.executeTask("task-1");
verify(taskRepository, never()).save(any());
verifyNoInteractions(credentialsService, facebookPostingService, linkedInPostingService, telegramPostingService);
}
@Test
void executeTaskShouldSkipTaskWhenStatusIsNotPending() {
PostingTask task = task("task-1", "user-1", "facebook", "completed");
when(taskRepository.findById("task-1")).thenReturn(Optional.of(task));
postingTaskService.executeTask("task-1");
verify(taskRepository, never()).save(any());
verifyNoInteractions(credentialsService, facebookPostingService, linkedInPostingService, telegramPostingService);
}
@Test
void executeTaskShouldCompleteFacebookTaskWithoutImage() throws Exception {
PostingTask task = task("task-1", "user-1", "facebook", "pending");
task.setPostText("Product launch");
task.setHashtags(List.of("#launch"));
List<String> savedStatuses = new ArrayList<>();
when(taskRepository.findById("task-1")).thenReturn(Optional.of(task));
when(taskRepository.save(any(PostingTask.class))).thenAnswer(invocation -> {
PostingTask savedTask = invocation.getArgument(0);
savedStatuses.add(savedTask.getStatus());
return savedTask;
});
when(credentialsService.getCredentials("user-1", "facebook")).thenReturn("fb-token");
when(facebookPostingService.postToPage("fb-token", "Product launch", List.of("#launch")))
.thenReturn("fb-post-1");
postingTaskService.executeTask("task-1");
assertEquals(List.of("processing", "completed"), savedStatuses);
assertEquals("completed", task.getStatus());
assertNull(task.getErrorMessage());
assertNotNull(task.getExecutedAt());
verify(facebookPostingService).postToPage("fb-token", "Product launch", List.of("#launch"));
verify(facebookPostingService).sendPrivateMessage(
eq("fb-token"),
eq(TEST_RECIPIENT_ID),
argThat(message -> message.contains("Product launch") && message.contains("https://facebook.com/fb-post-1")));
verify(facebookPostingService, never()).sendPrivateImageMessage(anyString(), anyString(), any());
verify(minIOService, never()).downloadFile(anyString());
}
@Test
void executeTaskShouldCompleteFacebookTaskWithImageAndIgnoreNotificationFailure() throws Exception {
PostingTask task = task("task-1", "user-1", "facebook", "pending");
task.setPostText("Image post");
task.setHashtags(List.of("#image"));
task.setImageFilename("banner.png");
byte[] imageBytes = "binary-image".getBytes(StandardCharsets.UTF_8);
when(taskRepository.findById("task-1")).thenReturn(Optional.of(task));
when(taskRepository.save(any(PostingTask.class))).thenAnswer(invocation -> invocation.getArgument(0));
when(credentialsService.getCredentials("user-1", "facebook")).thenReturn("fb-token");
when(minIOService.downloadFile("banner.png")).thenReturn(streamOf(imageBytes));
when(facebookPostingService.postToPageWithImage(
eq("fb-token"),
eq("Image post"),
eq(List.of("#image")),
aryEq(imageBytes)))
.thenReturn("fb-post-2");
doThrow(new RuntimeException("Messenger unavailable"))
.when(facebookPostingService)
.sendPrivateMessage(eq("fb-token"), eq(TEST_RECIPIENT_ID), anyString());
postingTaskService.executeTask("task-1");
assertEquals("completed", task.getStatus());
assertNull(task.getErrorMessage());
verify(facebookPostingService).postToPageWithImage(
eq("fb-token"),
eq("Image post"),
eq(List.of("#image")),
aryEq(imageBytes));
verify(facebookPostingService, never()).sendPrivateImageMessage(anyString(), anyString(), any());
}
@Test
void executeTaskShouldCompleteLinkedInTaskWithImage() {
PostingTask task = task("task-1", "user-1", "linkedin", "pending");
task.setPostText("B2B update");
task.setHashtags(List.of("#b2b"));
task.setImageFilename("banner.png");
byte[] imageBytes = "binary-image".getBytes(StandardCharsets.UTF_8);
when(taskRepository.findById("task-1")).thenReturn(Optional.of(task));
when(taskRepository.save(any(PostingTask.class))).thenAnswer(invocation -> invocation.getArgument(0));
when(credentialsService.getCredentials("user-1", "linkedin")).thenReturn("linkedin-token");
when(minIOService.downloadFile("banner.png")).thenReturn(streamOf(imageBytes));
when(linkedInPostingService.postToLinkedInWithImage(
eq("linkedin-token"),
eq("B2B update"),
eq(List.of("#b2b")),
aryEq(imageBytes),
eq("image/png")))
.thenReturn("linkedin-post-1");
postingTaskService.executeTask("task-1");
assertEquals("completed", task.getStatus());
assertNull(task.getErrorMessage());
verify(linkedInPostingService).postToLinkedInWithImage(
eq("linkedin-token"),
eq("B2B update"),
eq(List.of("#b2b")),
aryEq(imageBytes),
eq("image/png"));
verify(linkedInPostingService, never()).postToLinkedIn(anyString(), anyString(), anyList());
}
@Test
void executeTaskShouldContinueWithoutImageWhenImageDownloadFails() {
PostingTask task = task("task-1", "user-1", "telegram", "pending");
task.setPostText("Telegram post");
task.setHashtags(List.of("#telegram"));
task.setImageFilename("missing.png");
when(taskRepository.findById("task-1")).thenReturn(Optional.of(task));
when(taskRepository.save(any(PostingTask.class))).thenAnswer(invocation -> invocation.getArgument(0));
when(credentialsService.getCredentials("user-1", "telegram")).thenReturn("{\"botToken\":\"t\",\"chatId\":\"1\"}");
when(minIOService.downloadFile("missing.png")).thenThrow(new RuntimeException("Missing object"));
when(telegramPostingService.postToTelegram(
"{\"botToken\":\"t\",\"chatId\":\"1\"}",
"Telegram post",
List.of("#telegram")))
.thenReturn("telegram-message-1");
postingTaskService.executeTask("task-1");
assertEquals("completed", task.getStatus());
verify(telegramPostingService).postToTelegram(
"{\"botToken\":\"t\",\"chatId\":\"1\"}",
"Telegram post",
List.of("#telegram"));
verify(telegramPostingService, never()).postToTelegramWithImage(anyString(), anyString(), anyList(), any(), anyString());
}
@Test
void executeTaskShouldFailWhenCredentialsAreMissing() {
PostingTask task = task("task-1", "user-1", "facebook", "pending");
List<String> savedStatuses = new ArrayList<>();
when(taskRepository.findById("task-1")).thenReturn(Optional.of(task));
when(taskRepository.save(any(PostingTask.class))).thenAnswer(invocation -> {
PostingTask savedTask = invocation.getArgument(0);
savedStatuses.add(savedTask.getStatus());
return savedTask;
});
when(credentialsService.getCredentials("user-1", "facebook")).thenReturn(null);
postingTaskService.executeTask("task-1");
assertEquals(List.of("processing", "failed"), savedStatuses);
assertEquals("failed", task.getStatus());
assertEquals("Credentials not found for platform: facebook", task.getErrorMessage());
}
@Test
void executeTaskShouldFailWhenPlatformIsUnsupported() {
PostingTask task = task("task-1", "user-1", "instagram", "pending");
when(taskRepository.findById("task-1")).thenReturn(Optional.of(task));
when(taskRepository.save(any(PostingTask.class))).thenAnswer(invocation -> invocation.getArgument(0));
when(credentialsService.getCredentials("user-1", "instagram")).thenReturn("ig-token");
postingTaskService.executeTask("task-1");
assertEquals("failed", task.getStatus());
assertEquals("Platform not supported: instagram", task.getErrorMessage());
}
@Test
void executeTaskShouldHandleFacebookExpiredToken() throws Exception {
PostingTask task = task("task-1", "user-1", "facebook", "pending");
when(taskRepository.findById("task-1")).thenReturn(Optional.of(task));
when(taskRepository.save(any(PostingTask.class))).thenAnswer(invocation -> invocation.getArgument(0));
when(credentialsService.getCredentials("user-1", "facebook")).thenReturn("fb-token");
when(facebookPostingService.postToPage("fb-token", task.getPostText(), task.getHashtags()))
.thenThrow(new FacebookTokenExpiredException("Expired", "code 190", 190, 463));
postingTaskService.executeTask("task-1");
assertEquals("failed", task.getStatus());
assertEquals("Facebook token expired: code 190", task.getErrorMessage());
}
@Test
void executeTaskShouldHandleLinkedInExpiredToken() {
PostingTask task = task("task-1", "user-1", "linkedin", "pending");
when(taskRepository.findById("task-1")).thenReturn(Optional.of(task));
when(taskRepository.save(any(PostingTask.class))).thenAnswer(invocation -> invocation.getArgument(0));
when(credentialsService.getCredentials("user-1", "linkedin")).thenReturn("linkedin-token");
when(linkedInPostingService.postToLinkedIn("linkedin-token", task.getPostText(), task.getHashtags()))
.thenThrow(new LinkedInTokenExpiredException("Expired", "status 401", 401));
postingTaskService.executeTask("task-1");
assertEquals("failed", task.getStatus());
assertEquals("LinkedIn token expired: status 401", task.getErrorMessage());
}
@Test
void executeTaskShouldHandleTelegramExpiredToken() {
PostingTask task = task("task-1", "user-1", "telegram", "pending");
when(taskRepository.findById("task-1")).thenReturn(Optional.of(task));
when(taskRepository.save(any(PostingTask.class))).thenAnswer(invocation -> invocation.getArgument(0));
when(credentialsService.getCredentials("user-1", "telegram")).thenReturn("{\"botToken\":\"t\",\"chatId\":\"1\"}");
when(telegramPostingService.postToTelegram("{\"botToken\":\"t\",\"chatId\":\"1\"}", task.getPostText(), task.getHashtags()))
.thenThrow(new TelegramTokenExpiredException("Expired", "status 401", 401));
postingTaskService.executeTask("task-1");
assertEquals("failed", task.getStatus());
assertEquals("Telegram token expired: status 401", task.getErrorMessage());
}
@Test
void executeTaskManuallyShouldThrowWhenTaskIsMissing() {
when(taskRepository.findById("task-1")).thenReturn(Optional.empty());
IllegalArgumentException exception = assertThrows(
IllegalArgumentException.class,
() -> postingTaskService.executeTaskManually("task-1"));
assertEquals("Task not found: task-1", exception.getMessage());
}
@Test
void executeTaskManuallyShouldThrowWhenTaskStatusIsNotAllowed() {
PostingTask task = task("task-1", "user-1", "facebook", "completed");
when(taskRepository.findById("task-1")).thenReturn(Optional.of(task));
IllegalStateException exception = assertThrows(
IllegalStateException.class,
() -> postingTaskService.executeTaskManually("task-1"));
assertEquals("Task cannot be executed manually. Current status: completed", exception.getMessage());
}
@Test
void executeTaskManuallyShouldResetFailedTaskBeforeExecution() {
PostingTask task = task("task-1", "user-1", "facebook", "failed");
task.setErrorMessage("Old error");
when(taskRepository.findById("task-1")).thenReturn(Optional.of(task));
when(taskRepository.save(any(PostingTask.class))).thenAnswer(invocation -> invocation.getArgument(0));
doNothing().when(postingTaskService).executeTask("task-1");
postingTaskService.executeTaskManually("task-1");
assertEquals("pending", task.getStatus());
assertNull(task.getErrorMessage());
verify(taskRepository).save(task);
verify(postingTaskService).executeTask("task-1");
}
@Test
void executeTaskManuallyShouldExecutePendingTaskWithoutReset() {
PostingTask task = task("task-1", "user-1", "facebook", "pending");
when(taskRepository.findById("task-1")).thenReturn(Optional.of(task));
doNothing().when(postingTaskService).executeTask("task-1");
postingTaskService.executeTaskManually("task-1");
verify(taskRepository, never()).save(any());
verify(postingTaskService).executeTask("task-1");
}
@Test
void queryMethodsShouldDelegateToRepository() {
PostingTask task = task("task-1", "user-1", "facebook", "pending");
when(taskRepository.findByUserId("user-1")).thenReturn(List.of(task));
when(taskRepository.findByStrategyId("strategy-1")).thenReturn(List.of(task));
when(taskRepository.findById("task-1")).thenReturn(Optional.of(task));
assertEquals(List.of(task), postingTaskService.getUserTasks("user-1"));
assertEquals(List.of(task), postingTaskService.getStrategyTasks("strategy-1"));
assertEquals(Optional.of(task), postingTaskService.getTaskById("task-1"));
}
private MarketingStrategy completedStrategy(String userId, List<MarketingStrategy.PostCalendarItem> calendarItems) {
MarketingStrategy strategy = new MarketingStrategy();
strategy.setStatus("completed");
strategy.setUserId(userId);
strategy.setPostCalendar(calendarItems);
return strategy;
}
private MarketingStrategy.PostCalendarItem calendarItem(
String platform,
LocalDateTime publishDate,
String postText,
List<String> hashtags) {
MarketingStrategy.PostCalendarItem item = new MarketingStrategy.PostCalendarItem();
item.setPlatform(platform);
item.setPublishDate(publishDate);
item.setPostText(postText);
item.setHashtags(hashtags);
return item;
}
private PostingTask task(String id, String userId, String platform, String status) {
PostingTask task = new PostingTask();
task.setId(id);
task.setUserId(userId);
task.setStrategyId("strategy-1");
task.setPlatform(platform);
task.setPostText("Default post");
task.setHashtags(List.of("#default"));
task.setPublishDate(LocalDateTime.now().plusMinutes(5));
task.setStatus(status);
return task;
}
private InputStream streamOf(byte[] data) {
return new ByteArrayInputStream(data);
}
}
@@ -0,0 +1,206 @@
package kz.konturai.parser.service;
import kz.konturai.parser.model.SocialMediaCredentials;
import kz.konturai.parser.repository.SocialMediaCredentialsRepository;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.List;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class SocialMediaCredentialsServiceTest {
@Mock
private SocialMediaCredentialsRepository repository;
@Mock
private EncryptionService encryptionService;
@InjectMocks
private SocialMediaCredentialsService service;
@Test
void saveCredentialsShouldThrowWhenInputIsInvalid() {
assertThrows(IllegalArgumentException.class, () -> service.saveCredentials(null, "facebook", "token"));
assertThrows(IllegalArgumentException.class, () -> service.saveCredentials("user-1", null, "token"));
assertThrows(IllegalArgumentException.class, () -> service.saveCredentials("user-1", "facebook", ""));
}
@Test
void saveCredentialsShouldCreateNewEntityAndEncryptCredentials() {
when(repository.findByUserIdAndPlatform("user-1", "Facebook")).thenReturn(Optional.empty());
when(encryptionService.encrypt("plain-token")).thenReturn("encrypted-token");
when(repository.save(any(SocialMediaCredentials.class))).thenAnswer(invocation -> invocation.getArgument(0));
SocialMediaCredentials saved = service.saveCredentials("user-1", "Facebook", "plain-token");
ArgumentCaptor<SocialMediaCredentials> captor = ArgumentCaptor.forClass(SocialMediaCredentials.class);
verify(repository).save(captor.capture());
SocialMediaCredentials persisted = captor.getValue();
assertEquals("user-1", persisted.getUserId());
assertEquals("facebook", persisted.getPlatform());
assertEquals("encrypted-token", persisted.getEncryptedCredentials());
assertSame(persisted, saved);
}
@Test
void saveCredentialsShouldUpdateExistingEntity() {
SocialMediaCredentials existing = new SocialMediaCredentials("user-1", "facebook", "old-encrypted");
when(repository.findByUserIdAndPlatform("user-1", "facebook")).thenReturn(Optional.of(existing));
when(encryptionService.encrypt("new-token")).thenReturn("new-encrypted");
when(repository.save(any(SocialMediaCredentials.class))).thenAnswer(invocation -> invocation.getArgument(0));
SocialMediaCredentials saved = service.saveCredentials("user-1", "facebook", "new-token");
assertSame(existing, saved);
assertEquals("new-encrypted", existing.getEncryptedCredentials());
assertEquals("facebook", existing.getPlatform());
}
@Test
void getCredentialsShouldReturnNullWhenInputIsInvalid() {
assertNull(service.getCredentials(null, "facebook"));
assertNull(service.getCredentials("user-1", null));
}
@Test
void getCredentialsShouldReturnNullWhenCredentialsAreMissing() {
when(repository.findByUserIdAndPlatform("user-1", "facebook")).thenReturn(Optional.empty());
assertNull(service.getCredentials("user-1", "FACEBOOK"));
}
@Test
void getCredentialsShouldDecryptStringCredentials() {
SocialMediaCredentials entity = new SocialMediaCredentials("user-1", "facebook", "encrypted-token");
when(repository.findByUserIdAndPlatform("user-1", "facebook")).thenReturn(Optional.of(entity));
when(encryptionService.decrypt("encrypted-token")).thenReturn("plain-token");
assertEquals("plain-token", service.getCredentials("user-1", "FACEBOOK"));
}
@Test
void getCredentialsShouldDecryptNonStringCredentialsViaToString() {
SocialMediaCredentials entity = new SocialMediaCredentials("user-1", "telegram", new StringBuilder("encrypted-json"));
when(repository.findByUserIdAndPlatform("user-1", "telegram")).thenReturn(Optional.of(entity));
when(encryptionService.decrypt("encrypted-json")).thenReturn("{\"botToken\":\"t\"}");
assertEquals("{\"botToken\":\"t\"}", service.getCredentials("user-1", "telegram"));
}
@Test
void getCredentialsShouldWrapDecryptionFailures() {
SocialMediaCredentials entity = new SocialMediaCredentials("user-1", "facebook", "encrypted-token");
when(repository.findByUserIdAndPlatform("user-1", "facebook")).thenReturn(Optional.of(entity));
when(encryptionService.decrypt("encrypted-token")).thenThrow(new RuntimeException("decrypt failed"));
RuntimeException exception = assertThrows(
RuntimeException.class,
() -> service.getCredentials("user-1", "facebook"));
assertEquals("Failed to decrypt credentials", exception.getMessage());
assertNotNull(exception.getCause());
}
@Test
void hasCredentialsShouldReturnFalseForInvalidInput() {
assertTrue(!service.hasCredentials(null, "facebook"));
assertTrue(!service.hasCredentials("user-1", null));
}
@Test
void hasCredentialsShouldCheckRepositoryUsingLowercasePlatform() {
when(repository.findByUserIdAndPlatform("user-1", "facebook"))
.thenReturn(Optional.of(new SocialMediaCredentials("user-1", "facebook", "encrypted")));
assertTrue(service.hasCredentials("user-1", "FACEBOOK"));
}
@Test
void deleteCredentialsShouldThrowForInvalidInput() {
assertThrows(IllegalArgumentException.class, () -> service.deleteCredentials(null, "facebook"));
assertThrows(IllegalArgumentException.class, () -> service.deleteCredentials("user-1", null));
}
@Test
void deleteCredentialsShouldUseLowercasePlatform() {
service.deleteCredentials("user-1", "FACEBOOK");
verify(repository).deleteByUserIdAndPlatform("user-1", "facebook");
}
@Test
void getUserPlatformsShouldReturnEmptyListForNullUserId() {
assertEquals(List.of(), service.getUserPlatforms(null));
verify(repository, never()).findByUserId(any());
}
@Test
void getUserPlatformsShouldReturnAllPlatforms() {
when(repository.findByUserId("user-1")).thenReturn(List.of(
new SocialMediaCredentials("user-1", "facebook", "e1"),
new SocialMediaCredentials("user-1", "telegram", "e2")));
assertEquals(List.of("facebook", "telegram"), service.getUserPlatforms("user-1"));
}
@Test
void getUserCredentialsShouldReturnEmptyListForNullUserId() {
assertEquals(List.of(), service.getUserCredentials(null));
verify(repository, never()).findByUserId(any());
}
@Test
void getUserCredentialsShouldDelegateToRepository() {
List<SocialMediaCredentials> credentials = List.of(new SocialMediaCredentials("user-1", "facebook", "encrypted"));
when(repository.findByUserId("user-1")).thenReturn(credentials);
assertSame(credentials, service.getUserCredentials("user-1"));
}
@Test
void updateAdAccountIdShouldThrowForInvalidInput() {
assertThrows(IllegalArgumentException.class, () -> service.updateAdAccountId(null, "facebook", "act-1"));
assertThrows(IllegalArgumentException.class, () -> service.updateAdAccountId("user-1", null, "act-1"));
assertThrows(IllegalArgumentException.class, () -> service.updateAdAccountId("user-1", "facebook", null));
}
@Test
void updateAdAccountIdShouldThrowWhenCredentialsAreMissing() {
when(repository.findByUserIdAndPlatform("user-1", "facebook")).thenReturn(Optional.empty());
IllegalArgumentException exception = assertThrows(
IllegalArgumentException.class,
() -> service.updateAdAccountId("user-1", "FACEBOOK", "act-1"));
assertEquals("Credentials not found for user user-1 and platform FACEBOOK", exception.getMessage());
}
@Test
void updateAdAccountIdShouldUpdateAndSaveCredentials() {
SocialMediaCredentials credentials = new SocialMediaCredentials("user-1", "facebook", "encrypted");
when(repository.findByUserIdAndPlatform("user-1", "facebook")).thenReturn(Optional.of(credentials));
when(repository.save(any(SocialMediaCredentials.class))).thenAnswer(invocation -> invocation.getArgument(0));
service.updateAdAccountId("user-1", "FACEBOOK", "act-123");
assertEquals("act-123", credentials.getAdAccountId());
verify(repository).save(credentials);
}
}