diff --git a/src/main/java/kz/konturai/parser/controller/FacebookConfigController.java b/src/main/java/kz/konturai/parser/controller/FacebookConfigController.java new file mode 100644 index 0000000..ea2ffd3 --- /dev/null +++ b/src/main/java/kz/konturai/parser/controller/FacebookConfigController.java @@ -0,0 +1,65 @@ +package kz.konturai.parser.controller; + +import kz.konturai.parser.dto.ApiResponse; +import kz.konturai.parser.service.FacebookPostingService; +import kz.konturai.parser.service.JwtService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +/** + * Контроллер для настройки Facebook (обмен токенов и т.д.) + */ +@RestController +@RequestMapping("/api/facebook/config") +@RequiredArgsConstructor +public class FacebookConfigController { + + private final FacebookPostingService facebookPostingService; + private final JwtService jwtService; + + private String extractUserIdFromHeader(String authHeader) { + if (authHeader == null || authHeader.isEmpty()) { + return null; + } + try { + return jwtService.extractUserIdFromHeader(authHeader); + } catch (Exception e) { + return null; + } + } + + /** + * Помощник для обмена временного токена на долгосрочный. + * + * POST /api/facebook/config/exchange-token + * Body: { "shortToken": "...", "pageId": "..." } + */ + @PostMapping("/exchange-token") + public ResponseEntity exchangeToken( + @RequestHeader(value = "Authorization", required = false) String authHeader, + @RequestBody Map request) { + + String userId = extractUserIdFromHeader(authHeader); + if (userId == null) { + return ResponseEntity.status(401).body(ApiResponse.error("Не авторизован", null)); + } + + String shortToken = request.get("shortToken"); + String pageId = request.get("pageId"); + + if (shortToken == null || pageId == null) { + return ResponseEntity.badRequest().body(ApiResponse.error("shortToken и pageId обязательны", null)); + } + + try { + String longLivedToken = facebookPostingService.exchangeForLongLivedPageToken(shortToken, pageId); + return ResponseEntity.ok(ApiResponse.success("Токен успешно обменян. Сохраните его в настройках.", + Map.of("longLivedToken", longLivedToken))); + } catch (Exception e) { + return ResponseEntity.status(500).body(ApiResponse.error("Ошибка обмена: " + e.getMessage(), null)); + } + } +} diff --git a/src/main/java/kz/konturai/parser/controller/TargetingCampaignController.java b/src/main/java/kz/konturai/parser/controller/TargetingCampaignController.java index e201c57..b69f8a6 100644 --- a/src/main/java/kz/konturai/parser/controller/TargetingCampaignController.java +++ b/src/main/java/kz/konturai/parser/controller/TargetingCampaignController.java @@ -286,8 +286,8 @@ public class TargetingCampaignController { // ── Инсайты ─────────────────────────────────────────────────────────────── - @GetMapping("/campaigns/{id}/insights") - public ResponseEntity getInsights( + @GetMapping("/campaigns/{id}/insights/raw") + public ResponseEntity getRawInsights( @RequestHeader(value = "X-User-Id", required = false) String userIdHeader, @RequestHeader(value = "Authorization", required = false) String authHeader, @PathVariable String id) { diff --git a/src/main/java/kz/konturai/parser/model/SocialMediaCredentials.java b/src/main/java/kz/konturai/parser/model/SocialMediaCredentials.java index ef5ee13..fd39835 100644 --- a/src/main/java/kz/konturai/parser/model/SocialMediaCredentials.java +++ b/src/main/java/kz/konturai/parser/model/SocialMediaCredentials.java @@ -24,6 +24,9 @@ public class SocialMediaCredentials { @Field("ad_account_id") private String adAccountId; + @Field("page_id") + private String pageId; + @Field("created_at") private LocalDateTime createdAt; @@ -84,6 +87,15 @@ public class SocialMediaCredentials { this.updatedAt = LocalDateTime.now(); } + public String getPageId() { + return pageId; + } + + public void setPageId(String pageId) { + this.pageId = pageId; + this.updatedAt = LocalDateTime.now(); + } + public LocalDateTime getCreatedAt() { return createdAt; } diff --git a/src/main/java/kz/konturai/parser/service/AiTargetingSystemService.java b/src/main/java/kz/konturai/parser/service/AiTargetingSystemService.java index c9f741a..89fa09e 100644 --- a/src/main/java/kz/konturai/parser/service/AiTargetingSystemService.java +++ b/src/main/java/kz/konturai/parser/service/AiTargetingSystemService.java @@ -485,13 +485,13 @@ public class AiTargetingSystemService { if (imageBytes != null) { facebookPostId = facebookPostingService.postToPageWithImage( - null, post.getPostText(), post.getHashtags(), imageBytes); + post.getPostText(), post.getHashtags(), imageBytes); imagePosted = true; facebookStatus = "PUBLISHED"; logger.info("Facebook post published WITH image. postId={}", facebookPostId); } else { facebookPostId = facebookPostingService.postToPage( - null, post.getPostText(), post.getHashtags()); + post.getPostText(), post.getHashtags()); facebookStatus = "TEXT_ONLY"; logger.info("Facebook post published TEXT ONLY. postId={}", facebookPostId); } diff --git a/src/main/java/kz/konturai/parser/service/FacebookPostingService.java b/src/main/java/kz/konturai/parser/service/FacebookPostingService.java index 7489ee9..159a984 100644 --- a/src/main/java/kz/konturai/parser/service/FacebookPostingService.java +++ b/src/main/java/kz/konturai/parser/service/FacebookPostingService.java @@ -1,11 +1,9 @@ package kz.konturai.parser.service; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import kz.konturai.parser.exception.FacebookTokenExpiredException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.io.ByteArrayResource; import org.springframework.http.MediaType; @@ -19,27 +17,42 @@ import org.springframework.web.reactive.function.client.WebClientResponseExcepti import reactor.netty.http.client.HttpClient; import java.time.Duration; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.stream.Collectors; +/** + * Сервис публикации постов в Facebook. + * + * Использует надежную схему с Long-Lived Page Access Token. + * Настройки приоритетно берутся из application.properties. + */ @Service +@Slf4j public class FacebookPostingService { - private static final Logger logger = LoggerFactory.getLogger(FacebookPostingService.class); private static final String FACEBOOK_GRAPH_API_BASE = "https://graph.facebook.com/v19.0"; - private static final String HARDCODED_PAGE_TOKEN = "EAAaocqgT3JoBQhROtnPQ3echpBNU7pnrRxmj7XOoyFzDzOeJIZCjjck4ilraxYEBNXgDZCWfvUZBjDWPLGKrvF7ZCc1ZAmnDj1RHnW06I69ZBvCVbpoiKzsMqPihLZAwZB7WJykDAlaCVbhSBFmOY2YdMlf1fwi3cUq0tvKAaiJesKJbfOuXgMV0c7lSFKNz2yxsWiRfTzpMjhjk8Y9EsYpM8FW0HMEktzeHaoGOGOoI8oEZD"; - private static final String HARDCODED_PAGE_ID = "918835061309575"; + @Value("${facebook.app.id:}") + private String appId; - private final WebClient webClient; - private final ObjectMapper objectMapper; + @Value("${facebook.app.secret:}") + private String appSecret; + + @Value("${facebook.page.token:EAAaocqgT3JoBQhROtnPQ3echpBNU7pnrRxmj7XOoyFzDzOeJIZCjjck4ilraxYEBNXgDZCWfvUZBjDWPLGKrvF7ZCc1ZAmnDj1RHnW06I69ZBvCVbpoiKzsMqPihLZAwZB7WJykDAlaCVbhSBFmOY2YdMlf1fwi3cUq0tvKAaiJesKJbfOuXgMV0c7lSFKNz2yxsWiRfTzpMjhjk8Y9EsYpM8FW0HMEktzeHaoGOGOoI8oEZD}") + private String pageToken; + + @Value("${facebook.page.id:918835061309575}") + private String pageId; @Value("${facebook.api.timeout:30000}") private int timeoutMs; + private final WebClient webClient; + private final ObjectMapper objectMapper; + public FacebookPostingService() { + this.objectMapper = new ObjectMapper(); + HttpClient httpClient = HttpClient.create() .responseTimeout(Duration.ofMillis(30000)); @@ -47,89 +60,141 @@ public class FacebookPostingService { .baseUrl(FACEBOOK_GRAPH_API_BASE) .clientConnector(new ReactorClientHttpConnector(httpClient)) .build(); - - this.objectMapper = new ObjectMapper(); } - public PageCredentials getPageCredentials(String ignoredAccessToken) { - return new PageCredentials(HARDCODED_PAGE_ID, HARDCODED_PAGE_TOKEN); + // ── Credentials management ──────────────────────────────────────────────── + + public PageCredentials getPageCredentials() { + return new PageCredentials(pageId, pageToken); } - public void sendPrivateMessage(String ignoredToken, String recipientId, String messageText) { + /** + * Обмен короткого токена пользователя на долгосрочный токен страницы. + */ + public String exchangeForLongLivedPageToken(String shortLivedUserToken, String targetPageId) { + log.info("[Facebook] Запуск обмена токена для страницы {}", targetPageId); + + if (appId == null || appId.contains("dummy") || appSecret == null || appSecret.contains("dummy")) { + throw new IllegalStateException("Hеобходимo настроить facebook.app.id и facebook.app.secret"); + } + try { - Map recipient = new HashMap<>(); - recipient.put("id", recipientId); + // 1. Короткий токен -> Долгосрочный User Token (60 дней) + JsonNode userTokenNode = webClient.get() + .uri(u -> u.path("/oauth/access_token") + .queryParam("grant_type", "fb_exchange_token") + .queryParam("client_id", appId) + .queryParam("client_secret", appSecret) + .queryParam("fb_exchange_token", shortLivedUserToken) + .build()) + .retrieve() + .bodyToMono(JsonNode.class) + .block(Duration.ofMillis(timeoutMs)); - Map messageContent = new HashMap<>(); - messageContent.put("text", messageText); + String longLivedUserToken = userTokenNode.path("access_token").asText(); + log.info("[Facebook] Получен долгосрочный User Token"); - Map body = new HashMap<>(); - body.put("recipient", recipient); - body.put("message", messageContent); + // 2. Список страниц этого пользователя + JsonNode accounts = webClient.get() + .uri(u -> u.path("/me/accounts") + .queryParam("access_token", longLivedUserToken) + .build()) + .retrieve() + .bodyToMono(JsonNode.class) + .block(Duration.ofMillis(timeoutMs)); + + if (accounts != null && accounts.has("data")) { + for (JsonNode page : accounts.get("data")) { + if (page.path("id").asText().equals(targetPageId)) { + String permToken = page.path("access_token").asText(); + log.info("[Facebook] УСПЕХ! Получен надежный Page Token для {}", targetPageId); + log.info("[Facebook] ПЕРЕЗАПИШИТЕ ЕГО В application.properties: {}", permToken); + return permToken; + } + } + } + throw new RuntimeException("Страница " + targetPageId + " не найдена в аккаунте"); + } catch (Exception e) { + log.error("[Facebook] Ошибка обмена токена: {}", e.getMessage()); + throw new RuntimeException("Ошибка Meta API: " + e.getMessage(), e); + } + } + + // ── Posting methods ─────────────────────────────────────────────────────── + + public String postToPage(String postText, List hashtags) { + return internalPost(getPageCredentials(), postText, hashtags, null); + } + + public String postToPageWithImage(String postText, List hashtags, byte[] imageData) { + return internalPost(getPageCredentials(), postText, hashtags, imageData); + } + + // ── Messaging ──────────────────────────────────────────────────────────── + + public void sendPrivateMessage(String recipientId, String messageText) { + PageCredentials creds = getPageCredentials(); + try { + java.util.Map body = new java.util.HashMap<>(); + body.put("recipient", java.util.Map.of("id", recipientId)); + body.put("message", java.util.Map.of("text", messageText)); body.put("messaging_type", "RESPONSE"); webClient.post() - .uri(uriBuilder -> uriBuilder.path("/me/messages") - .queryParam("access_token", HARDCODED_PAGE_TOKEN).build()) + .uri(u -> u.path("/me/messages") + .queryParam("access_token", creds.pageToken).build()) .contentType(MediaType.APPLICATION_JSON) .bodyValue(body) .retrieve() .bodyToMono(String.class) .block(Duration.ofMillis(timeoutMs)); - - logger.info("Message sent to user {}", recipientId); + log.info("[Facebook] Сообщение отправлено пользователю {}", recipientId); } catch (Exception e) { - logger.error("Failed to send text message: ", e); + log.error("[Facebook] Ошибка отправки сообщения: {}", e.getMessage()); } } - public void sendPrivateImageMessage(String ignoredToken, String recipientId, byte[] imageData) { + public void sendPrivateImageMessage(String recipientId, byte[] imageData) { + PageCredentials creds = getPageCredentials(); try { ByteArrayResource imageResource = new ByteArrayResource(imageData) { @Override public String getFilename() { return "image.jpg"; } }; - String recipientJson = "{\"id\":\"" + recipientId + "\"}"; - String messageJson = "{\"attachment\":{\"type\":\"image\", \"payload\":{\"is_reusable\":true}}}"; - MultiValueMap formData = new LinkedMultiValueMap<>(); - formData.add("recipient", recipientJson); - formData.add("message", messageJson); + formData.add("recipient", "{\"id\":\"" + recipientId + "\"}"); + formData.add("message", "{\"attachment\":{\"type\":\"image\", \"payload\":{\"is_reusable\":true}}}"); formData.add("filedata", imageResource); - formData.add("access_token", HARDCODED_PAGE_TOKEN); + formData.add("access_token", creds.pageToken); webClient.post() - .uri(uriBuilder -> uriBuilder.path("/me/messages").build()) + .uri(u -> u.path("/me/messages").build()) .contentType(MediaType.MULTIPART_FORM_DATA) .body(BodyInserters.fromMultipartData(formData)) .retrieve() .bodyToMono(String.class) .block(Duration.ofMillis(timeoutMs * 2)); - logger.info("Image message sent to user {}", recipientId); + log.info("[Facebook] Изображение отправлено пользователю {}", recipientId); } catch (Exception e) { - logger.error("Failed to send image message: ", e); + log.error("[Facebook] Ошибка отправки изображения: {}", e.getMessage()); } } - public String postToPage(String ignoredToken, String postText, List hashtags) throws JsonProcessingException { - return internalPost(postText, hashtags, null); - } + // ── Internal ────────────────────────────────────────────────────────────── - public String postToPageWithImage(String ignoredToken, String postText, List hashtags, byte[] imageData) { - return internalPost(postText, hashtags, imageData); - } + private String internalPost(PageCredentials creds, String postText, List hashtags, byte[] imageData) { + if (creds.pageToken == null || creds.pageToken.length() < 10) { + throw new IllegalStateException("Facebook Access Token не настроен! Проверьте application.properties"); + } - private String internalPost(String postText, List hashtags, byte[] imageData) { try { - String fullPostText = buildPostText(postText, hashtags); - + String fullText = buildPostText(postText, hashtags); MultiValueMap formData = new LinkedMultiValueMap<>(); - formData.add("access_token", HARDCODED_PAGE_TOKEN); - formData.add("message", fullPostText); + formData.add("access_token", creds.pageToken); + formData.add("message", fullText); String endpoint; - if (imageData != null) { endpoint = "/{pageId}/photos"; formData.add("source", new ByteArrayResource(imageData) { @@ -140,54 +205,52 @@ public class FacebookPostingService { } String response = webClient.post() - .uri(uriBuilder -> uriBuilder.path(endpoint).build(HARDCODED_PAGE_ID)) + .uri(u -> u.path(endpoint).build(creds.pageId)) .contentType(imageData != null ? MediaType.MULTIPART_FORM_DATA : MediaType.APPLICATION_FORM_URLENCODED) .body(BodyInserters.fromMultipartData(formData)) .retrieve() .bodyToMono(String.class) .block(Duration.ofMillis(timeoutMs * 2)); - JsonNode jsonNode = objectMapper.readTree(response); - String postId = jsonNode.get("id").asText(); - logger.info("Published to Facebook. ID: {}", postId); + JsonNode json = objectMapper.readTree(response); + String postId = json.path("id").asText(null); + log.info("[Facebook] Пост опубликован. id={}, pageId={}", postId, creds.pageId); return postId; - } catch (WebClientResponseException e) { - handleFacebookError(e, "internalPost"); + handleFacebookError(e); return null; } catch (Exception e) { - throw new RuntimeException("Error posting: " + e.getMessage(), e); + throw new RuntimeException("[Facebook] Ошибка: " + e.getMessage(), e); } } private String buildPostText(String postText, List hashtags) { - StringBuilder fullText = new StringBuilder(postText != null ? postText : ""); + StringBuilder sb = new StringBuilder(postText != null ? postText : ""); if (hashtags != null && !hashtags.isEmpty()) { - if (fullText.length() > 0) fullText.append("\n\n"); - String hashtagsText = hashtags.stream() - .map(tag -> tag.startsWith("#") ? tag : "#" + tag) - .collect(Collectors.joining(" ")); - fullText.append(hashtagsText); + if (sb.length() > 0) sb.append("\n\n"); + sb.append(hashtags.stream() + .map(t -> t.startsWith("#") ? t : "#" + t) + .collect(Collectors.joining(" "))); } - return fullText.toString(); + return sb.toString(); + } + + private void handleFacebookError(WebClientResponseException e) { + log.error("[Facebook] API Error: status={}, body={}", e.getStatusCode().value(), e.getResponseBodyAsString()); + String body = e.getResponseBodyAsString(); + if (body.contains("access token") || body.contains("expired")) { + throw new FacebookTokenExpiredException("Токен Facebook истёк. Получите новый через утилиту обмена.", + "Token Expired", 190, 463, e); + } + throw new RuntimeException("[Facebook] API Error: " + body, e); } public static class PageCredentials { - public String pageId; public String pageToken; - public PageCredentials(String pageId, String pageToken) { this.pageId = pageId; this.pageToken = pageToken; } + public final String pageId; + public final String pageToken; + public PageCredentials(String pageId, String pageToken) { + this.pageId = pageId; + this.pageToken = pageToken; + } } - - private void handleFacebookError(WebClientResponseException e, String context) { - logger.error("FB API Error [{}]: Body={}", context, e.getResponseBodyAsString()); - if (isTokenExpiredError(e)) throw parseFacebookError(e); - throw new RuntimeException("Facebook API Error: " + e.getResponseBodyAsString(), e); - } - - private boolean isTokenExpiredError(WebClientResponseException e) { - return e.getStatusCode().value() == 400 || e.getStatusCode().value() == 401; - } - - private FacebookTokenExpiredException parseFacebookError(WebClientResponseException e) { - return new FacebookTokenExpiredException("Token Expired", "Token Expired", 190, 463, e); - } -} \ No newline at end of file +} diff --git a/src/main/java/kz/konturai/parser/service/PostingTaskService.java b/src/main/java/kz/konturai/parser/service/PostingTaskService.java index a7c5a85..c2931e3 100644 --- a/src/main/java/kz/konturai/parser/service/PostingTaskService.java +++ b/src/main/java/kz/konturai/parser/service/PostingTaskService.java @@ -146,9 +146,9 @@ public class PostingTaskService { if ("facebook".equalsIgnoreCase(task.getPlatform())) { if (imageData != null && imageData.length > 0) { - postId = facebookPostingService.postToPageWithImage(credentials, task.getPostText(), task.getHashtags(), imageData); + postId = facebookPostingService.postToPageWithImage(task.getPostText(), task.getHashtags(), imageData); } else { - postId = facebookPostingService.postToPage(credentials, task.getPostText(), task.getHashtags()); + postId = facebookPostingService.postToPage(task.getPostText(), task.getHashtags()); } if (postId != null) { @@ -158,10 +158,10 @@ public class PostingTaskService { "🔗 Ссылка: https://facebook.com/" + postId + "\n" + "🕒 Время: " + LocalDateTime.now(); - facebookPostingService.sendPrivateMessage(credentials, TEST_RECIPIENT_ID, reportText); + facebookPostingService.sendPrivateMessage(TEST_RECIPIENT_ID, reportText); if (imageData != null && imageData.length > 0) { - facebookPostingService.sendPrivateImageMessage(credentials, TEST_RECIPIENT_ID, imageData); + facebookPostingService.sendPrivateImageMessage(TEST_RECIPIENT_ID, imageData); } } catch (Exception msgEx) { logger.warn("Post published but failed to send private notification: {}", msgEx.getMessage()); diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 2f63ffa..27fff82 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -134,6 +134,12 @@ telegram.api.timeout=30000 # targeting.tiktok.oauth-redirect-uri=${TIKTOK_REDIRECT_URI:http://localhost:8080/api/v1/targeting/tiktok/callback} # targeting.tiktok.base-url=https://business-api.tiktok.com/open_api/v1.3 +# Facebook Posting Configuration +facebook.app.id=1874060259875994 +facebook.app.secret=1e096c34d375641b7c4d05fff64299ea +facebook.page.id=918835061309575 +facebook.page.token=EAAaocqgT3JoBRIdErZC0vZAleU3lrszUXzX8aSylq7sr9aFDGnxUt4cZAJGPec3uUONzEdlgwNclXyDhp77vJMTie5PcSb9CLAZAQZBMX3KR3RkmLOZChZCjgjoN58RJ6DwQqZA8N0PbW27Hdr7xGfdlobYopEL2nNbpdX1DHvMdXbGa4M3C92crZAjqyMkggCeiSjTQ8WU5FuZB8T8bQXrAYjGuZAImJF82mx9gcjqtDkxcarLxZA1M41nJpf5Q2Dx6F3tpcsuc1hcHbnhdZCn96nq8aHwGa + targeting.ai.audience-model=gpt-4o targeting.ai.max-audience-tokens=2500 targeting.ai.budget-model=gpt-4o-mini diff --git a/src/test/java/kz/konturai/parser/service/PostingTaskServiceTest.java b/src/test/java/kz/konturai/parser/service/PostingTaskServiceTest.java index f554951..66b4c15 100644 --- a/src/test/java/kz/konturai/parser/service/PostingTaskServiceTest.java +++ b/src/test/java/kz/konturai/parser/service/PostingTaskServiceTest.java @@ -240,8 +240,8 @@ class PostingTaskServiceTest { 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"))) + // FacebookPostingService now handles credentials internally + when(facebookPostingService.postToPage("Product launch", List.of("#launch"))) .thenReturn("fb-post-1"); postingTaskService.executeTask("task-1"); @@ -250,12 +250,11 @@ class PostingTaskServiceTest { assertEquals("completed", task.getStatus()); assertNull(task.getErrorMessage()); assertNotNull(task.getExecutedAt()); - verify(facebookPostingService).postToPage("fb-token", "Product launch", List.of("#launch")); + verify(facebookPostingService).postToPage("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(facebookPostingService, never()).sendPrivateImageMessage(anyString(), any()); verify(minIOService, never()).downloadFile(anyString()); } @@ -269,28 +268,25 @@ class PostingTaskServiceTest { 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()); + .sendPrivateMessage(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()); + verify(facebookPostingService, never()).sendPrivateImageMessage(anyString(), any()); } @Test @@ -393,8 +389,8 @@ class PostingTaskServiceTest { 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())) + // FacebookPostingService now handles credentials internally + when(facebookPostingService.postToPage(task.getPostText(), task.getHashtags())) .thenThrow(new FacebookTokenExpiredException("Expired", "code 190", 190, 463)); postingTaskService.executeTask("task-1");