diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 92a365a..2e23839 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,89 +1,26 @@ -# 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 analyze-graphics. -# Optional: SSH deploy when CI/CD variable ENABLE_SSH_DEPLOY=true and deploy secrets are set. -# -# CI/CD variables (Settings → CI/CD → Variables): -# GHCR_USERNAME, GHCR_TOKEN — for docker push (GitHub user + PAT with write:packages) -# ENABLE_SSH_DEPLOY = true (optional; omit or false to skip deploy job) -# DEPLOY_HOST, DEPLOY_USER, DEPLOY_SSH_KEY, DEPLOY_SCRIPT — optional deploy +image: eclipse-temurin:21-jdk variables: MAVEN_OPTS: "-Dmaven.repo.local=$CI_PROJECT_DIR/.m2/repository" -default: - tags: - - marketing-parser - -workflow: - rules: - # Merge requests whose target branch is analyze-graphics - - if: $CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "analyze-graphics" - # Direct pushes to analyze-graphics - - if: $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "analyze-graphics" - # Manual pipeline (Run pipeline in UI) - - if: $CI_PIPELINE_SOURCE == "web" +stages: + - test cache: key: ${CI_COMMIT_REF_SLUG} paths: - .m2/repository -stages: - - test - - publish - - deploy - -build-and-test: +test: stage: test - image: maven:3.9.9-eclipse-temurin-21 + before_script: + - chmod +x mvnw script: - - 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 + - ./mvnw --batch-mode clean verify artifacts: when: always - expire_in: 7 days paths: - - target/site/jacoco - - target/surefire-reports + - target/site/jacoco/ reports: junit: - target/surefire-reports/TEST-*.xml - -docker-build-push: - stage: publish - needs: [build-and-test] - rules: - - if: $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "analyze-graphics" && $GHCR_USERNAME && $GHCR_TOKEN - image: docker:24-cli - services: - - docker:24-dind - variables: - DOCKER_HOST: tcp://docker:2375 - DOCKER_TLS_CERTDIR: "" - before_script: - - until docker info; do sleep 1; done - - echo "$GHCR_TOKEN" | docker login ghcr.io -u "$GHCR_USERNAME" --password-stdin - script: - - IMG=$(echo "$CI_PROJECT_PATH" | tr '[:upper:]' '[:lower:]') - - docker build -t "ghcr.io/${IMG}:latest" -t "ghcr.io/${IMG}:${CI_COMMIT_SHA}" . - - docker push "ghcr.io/${IMG}:latest" - - docker push "ghcr.io/${IMG}:${CI_COMMIT_SHA}" - -deploy: - stage: deploy - needs: [docker-build-push] - rules: - - if: $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "analyze-graphics" && $ENABLE_SSH_DEPLOY == "true" && $GHCR_USERNAME && $GHCR_TOKEN - image: alpine:3.20 - before_script: - - apk add --no-cache openssh-client - - mkdir -p ~/.ssh - - echo "$DEPLOY_SSH_KEY" | tr -d '\r' > ~/.ssh/id_rsa - - chmod 600 ~/.ssh/id_rsa - script: - - ssh -o StrictHostKeyChecking=no "$DEPLOY_USER@$DEPLOY_HOST" "$DEPLOY_SCRIPT" diff --git a/pom.xml b/pom.xml index 5371ed7..9b0f97b 100644 --- a/pom.xml +++ b/pom.xml @@ -228,7 +228,7 @@ - check + qg01-coverage-check verify check diff --git a/src/test/java/kz/konturai/parser/service/JwtServiceTest.java b/src/test/java/kz/konturai/parser/service/JwtServiceTest.java index 68b3eb2..306b334 100644 --- a/src/test/java/kz/konturai/parser/service/JwtServiceTest.java +++ b/src/test/java/kz/konturai/parser/service/JwtServiceTest.java @@ -64,6 +64,11 @@ class JwtServiceTest { assertThrows(JwtException.class, () -> jwtService.parseAndValidate(token)); } + @Test + void parseAndValidateShouldThrowForMalformedToken() { + assertThrows(JwtException.class, () -> jwtService.parseAndValidate("not-a-jwt")); + } + @Test void constructorWithoutConfiguredSecretShouldUseDefaultSigningKey() { JwtService serviceWithDefaultKey = new JwtService(""); @@ -76,6 +81,7 @@ class JwtServiceTest { void extractTokenFromHeaderShouldHandleBearerPrefixBlankAndRawToken() { assertNull(jwtService.extractTokenFromHeader(null)); assertNull(jwtService.extractTokenFromHeader(" ")); + assertEquals("", jwtService.extractTokenFromHeader("Bearer ")); assertEquals("sample-token", jwtService.extractTokenFromHeader("Bearer sample-token")); assertEquals("raw-token", jwtService.extractTokenFromHeader("raw-token")); } @@ -114,6 +120,17 @@ class JwtServiceTest { assertNull(jwtService.extractUserIdFromToken("not-a-jwt")); } + @Test + void extractUserIdFromTokenShouldReturnNullForInvalidSignature() { + 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", 123L)); + + assertNull(jwtService.extractUserIdFromToken(token)); + } + @Test void extractUserIdFromHeaderShouldExtractFromBearerToken() { String token = createToken(TEST_SECRET, "qa@example.com", Instant.now().plusSeconds(3600), Map.of("uid", 501L)); @@ -131,6 +148,7 @@ class JwtServiceTest { @Test void extractEmailFromTokenShouldReturnNullForInvalidToken() { assertNull(jwtService.extractEmailFromToken("broken-token")); + assertNull(jwtService.extractEmailFromToken(" ")); } @Test diff --git a/src/test/java/kz/konturai/parser/service/PostingTaskServiceTest.java b/src/test/java/kz/konturai/parser/service/PostingTaskServiceTest.java index 66b4c15..06b3a08 100644 --- a/src/test/java/kz/konturai/parser/service/PostingTaskServiceTest.java +++ b/src/test/java/kz/konturai/parser/service/PostingTaskServiceTest.java @@ -350,8 +350,8 @@ class PostingTaskServiceTest { } @Test - void executeTaskShouldFailWhenCredentialsAreMissing() { - PostingTask task = task("task-1", "user-1", "facebook", "pending"); + void executeTaskShouldFailWhenCredentialsAreMissingForNonFacebookPlatforms() { + PostingTask task = task("task-1", "user-1", "linkedin", "pending"); List savedStatuses = new ArrayList<>(); when(taskRepository.findById("task-1")).thenReturn(Optional.of(task)); @@ -360,13 +360,14 @@ class PostingTaskServiceTest { savedStatuses.add(savedTask.getStatus()); return savedTask; }); - when(credentialsService.getCredentials("user-1", "facebook")).thenReturn(null); + when(credentialsService.getCredentials("user-1", "linkedin")).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()); + assertEquals("Credentials not found for platform: linkedin", task.getErrorMessage()); + verifyNoInteractions(linkedInPostingService); } @Test @@ -383,6 +384,47 @@ class PostingTaskServiceTest { assertEquals("Platform not supported: instagram", task.getErrorMessage()); } + @Test + void executeTaskShouldCompleteWhenFacebookReturnsNullPostId() throws Exception { + PostingTask task = task("task-1", "user-1", "facebook", "pending"); + task.setPostText("Silent post"); + task.setHashtags(List.of("#silent")); + + when(taskRepository.findById("task-1")).thenReturn(Optional.of(task)); + when(taskRepository.save(any(PostingTask.class))).thenAnswer(invocation -> invocation.getArgument(0)); + when(facebookPostingService.postToPage("Silent post", List.of("#silent"))).thenReturn(null); + + postingTaskService.executeTask("task-1"); + + assertEquals("completed", task.getStatus()); + assertNull(task.getErrorMessage()); + verify(facebookPostingService).postToPage("Silent post", List.of("#silent")); + verify(facebookPostingService, never()).sendPrivateMessage(anyString(), anyString()); + verify(facebookPostingService, never()).sendPrivateImageMessage(anyString(), any()); + } + + @Test + void executeTaskShouldFailWhenTelegramPublishingTimesOut() { + PostingTask task = task("task-1", "user-1", "telegram", "pending"); + task.setPostText("Timeout post"); + task.setHashtags(List.of("#timeout")); + + 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\"}", + "Timeout post", + List.of("#timeout"))) + .thenThrow(new RuntimeException("Network timeout")); + + postingTaskService.executeTask("task-1"); + + assertEquals("failed", task.getStatus()); + assertEquals("Network timeout", task.getErrorMessage()); + assertNotNull(task.getExecutedAt()); + } + @Test void executeTaskShouldHandleFacebookExpiredToken() throws Exception { PostingTask task = task("task-1", "user-1", "facebook", "pending");