target fix

This commit is contained in:
arys
2026-04-03 10:57:54 +05:00
parent 630ddb9561
commit 52156c824b
8 changed files with 242 additions and 100 deletions
@@ -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<String, String> 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));
}
}
}
@@ -286,8 +286,8 @@ public class TargetingCampaignController {
// ── Инсайты ─────────────────────────────────────────────────────────────── // ── Инсайты ───────────────────────────────────────────────────────────────
@GetMapping("/campaigns/{id}/insights") @GetMapping("/campaigns/{id}/insights/raw")
public ResponseEntity<?> getInsights( public ResponseEntity<?> getRawInsights(
@RequestHeader(value = "X-User-Id", required = false) String userIdHeader, @RequestHeader(value = "X-User-Id", required = false) String userIdHeader,
@RequestHeader(value = "Authorization", required = false) String authHeader, @RequestHeader(value = "Authorization", required = false) String authHeader,
@PathVariable String id) { @PathVariable String id) {
@@ -24,6 +24,9 @@ public class SocialMediaCredentials {
@Field("ad_account_id") @Field("ad_account_id")
private String adAccountId; private String adAccountId;
@Field("page_id")
private String pageId;
@Field("created_at") @Field("created_at")
private LocalDateTime createdAt; private LocalDateTime createdAt;
@@ -84,6 +87,15 @@ public class SocialMediaCredentials {
this.updatedAt = LocalDateTime.now(); this.updatedAt = LocalDateTime.now();
} }
public String getPageId() {
return pageId;
}
public void setPageId(String pageId) {
this.pageId = pageId;
this.updatedAt = LocalDateTime.now();
}
public LocalDateTime getCreatedAt() { public LocalDateTime getCreatedAt() {
return createdAt; return createdAt;
} }
@@ -485,13 +485,13 @@ public class AiTargetingSystemService {
if (imageBytes != null) { if (imageBytes != null) {
facebookPostId = facebookPostingService.postToPageWithImage( facebookPostId = facebookPostingService.postToPageWithImage(
null, post.getPostText(), post.getHashtags(), imageBytes); post.getPostText(), post.getHashtags(), imageBytes);
imagePosted = true; imagePosted = true;
facebookStatus = "PUBLISHED"; facebookStatus = "PUBLISHED";
logger.info("Facebook post published WITH image. postId={}", facebookPostId); logger.info("Facebook post published WITH image. postId={}", facebookPostId);
} else { } else {
facebookPostId = facebookPostingService.postToPage( facebookPostId = facebookPostingService.postToPage(
null, post.getPostText(), post.getHashtags()); post.getPostText(), post.getHashtags());
facebookStatus = "TEXT_ONLY"; facebookStatus = "TEXT_ONLY";
logger.info("Facebook post published TEXT ONLY. postId={}", facebookPostId); logger.info("Facebook post published TEXT ONLY. postId={}", facebookPostId);
} }
@@ -1,11 +1,9 @@
package kz.konturai.parser.service; package kz.konturai.parser.service;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import kz.konturai.parser.exception.FacebookTokenExpiredException; import kz.konturai.parser.exception.FacebookTokenExpiredException;
import org.slf4j.Logger; import lombok.extern.slf4j.Slf4j;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ByteArrayResource; import org.springframework.core.io.ByteArrayResource;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
@@ -19,27 +17,42 @@ import org.springframework.web.reactive.function.client.WebClientResponseExcepti
import reactor.netty.http.client.HttpClient; import reactor.netty.http.client.HttpClient;
import java.time.Duration; import java.time.Duration;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/**
* Сервис публикации постов в Facebook.
*
* Использует надежную схему с Long-Lived Page Access Token.
* Настройки приоритетно берутся из application.properties.
*/
@Service @Service
@Slf4j
public class FacebookPostingService { 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 FACEBOOK_GRAPH_API_BASE = "https://graph.facebook.com/v19.0";
private static final String HARDCODED_PAGE_TOKEN = "EAAaocqgT3JoBQhROtnPQ3echpBNU7pnrRxmj7XOoyFzDzOeJIZCjjck4ilraxYEBNXgDZCWfvUZBjDWPLGKrvF7ZCc1ZAmnDj1RHnW06I69ZBvCVbpoiKzsMqPihLZAwZB7WJykDAlaCVbhSBFmOY2YdMlf1fwi3cUq0tvKAaiJesKJbfOuXgMV0c7lSFKNz2yxsWiRfTzpMjhjk8Y9EsYpM8FW0HMEktzeHaoGOGOoI8oEZD"; @Value("${facebook.app.id:}")
private static final String HARDCODED_PAGE_ID = "918835061309575"; private String appId;
private final WebClient webClient; @Value("${facebook.app.secret:}")
private final ObjectMapper objectMapper; private String appSecret;
@Value("${facebook.page.token:EAAaocqgT3JoBQhROtnPQ3echpBNU7pnrRxmj7XOoyFzDzOeJIZCjjck4ilraxYEBNXgDZCWfvUZBjDWPLGKrvF7ZCc1ZAmnDj1RHnW06I69ZBvCVbpoiKzsMqPihLZAwZB7WJykDAlaCVbhSBFmOY2YdMlf1fwi3cUq0tvKAaiJesKJbfOuXgMV0c7lSFKNz2yxsWiRfTzpMjhjk8Y9EsYpM8FW0HMEktzeHaoGOGOoI8oEZD}")
private String pageToken;
@Value("${facebook.page.id:918835061309575}")
private String pageId;
@Value("${facebook.api.timeout:30000}") @Value("${facebook.api.timeout:30000}")
private int timeoutMs; private int timeoutMs;
private final WebClient webClient;
private final ObjectMapper objectMapper;
public FacebookPostingService() { public FacebookPostingService() {
this.objectMapper = new ObjectMapper();
HttpClient httpClient = HttpClient.create() HttpClient httpClient = HttpClient.create()
.responseTimeout(Duration.ofMillis(30000)); .responseTimeout(Duration.ofMillis(30000));
@@ -47,89 +60,141 @@ public class FacebookPostingService {
.baseUrl(FACEBOOK_GRAPH_API_BASE) .baseUrl(FACEBOOK_GRAPH_API_BASE)
.clientConnector(new ReactorClientHttpConnector(httpClient)) .clientConnector(new ReactorClientHttpConnector(httpClient))
.build(); .build();
this.objectMapper = new ObjectMapper();
} }
public PageCredentials getPageCredentials(String ignoredAccessToken) { // ── Credentials management ────────────────────────────────────────────────
return new PageCredentials(HARDCODED_PAGE_ID, HARDCODED_PAGE_TOKEN);
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 { try {
Map<String, Object> recipient = new HashMap<>(); // 1. Короткий токен -> Долгосрочный User Token (60 дней)
recipient.put("id", recipientId); 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<String, String> messageContent = new HashMap<>(); String longLivedUserToken = userTokenNode.path("access_token").asText();
messageContent.put("text", messageText); log.info("[Facebook] Получен долгосрочный User Token");
Map<String, Object> body = new HashMap<>(); // 2. Список страниц этого пользователя
body.put("recipient", recipient); JsonNode accounts = webClient.get()
body.put("message", messageContent); .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<String> hashtags) {
return internalPost(getPageCredentials(), postText, hashtags, null);
}
public String postToPageWithImage(String postText, List<String> hashtags, byte[] imageData) {
return internalPost(getPageCredentials(), postText, hashtags, imageData);
}
// ── Messaging ────────────────────────────────────────────────────────────
public void sendPrivateMessage(String recipientId, String messageText) {
PageCredentials creds = getPageCredentials();
try {
java.util.Map<String, Object> 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"); body.put("messaging_type", "RESPONSE");
webClient.post() webClient.post()
.uri(uriBuilder -> uriBuilder.path("/me/messages") .uri(u -> u.path("/me/messages")
.queryParam("access_token", HARDCODED_PAGE_TOKEN).build()) .queryParam("access_token", creds.pageToken).build())
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.bodyValue(body) .bodyValue(body)
.retrieve() .retrieve()
.bodyToMono(String.class) .bodyToMono(String.class)
.block(Duration.ofMillis(timeoutMs)); .block(Duration.ofMillis(timeoutMs));
log.info("[Facebook] Сообщение отправлено пользователю {}", recipientId);
logger.info("Message sent to user {}", recipientId);
} catch (Exception e) { } 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 { try {
ByteArrayResource imageResource = new ByteArrayResource(imageData) { ByteArrayResource imageResource = new ByteArrayResource(imageData) {
@Override public String getFilename() { return "image.jpg"; } @Override public String getFilename() { return "image.jpg"; }
}; };
String recipientJson = "{\"id\":\"" + recipientId + "\"}";
String messageJson = "{\"attachment\":{\"type\":\"image\", \"payload\":{\"is_reusable\":true}}}";
MultiValueMap<String, Object> formData = new LinkedMultiValueMap<>(); MultiValueMap<String, Object> formData = new LinkedMultiValueMap<>();
formData.add("recipient", recipientJson); formData.add("recipient", "{\"id\":\"" + recipientId + "\"}");
formData.add("message", messageJson); formData.add("message", "{\"attachment\":{\"type\":\"image\", \"payload\":{\"is_reusable\":true}}}");
formData.add("filedata", imageResource); formData.add("filedata", imageResource);
formData.add("access_token", HARDCODED_PAGE_TOKEN); formData.add("access_token", creds.pageToken);
webClient.post() webClient.post()
.uri(uriBuilder -> uriBuilder.path("/me/messages").build()) .uri(u -> u.path("/me/messages").build())
.contentType(MediaType.MULTIPART_FORM_DATA) .contentType(MediaType.MULTIPART_FORM_DATA)
.body(BodyInserters.fromMultipartData(formData)) .body(BodyInserters.fromMultipartData(formData))
.retrieve() .retrieve()
.bodyToMono(String.class) .bodyToMono(String.class)
.block(Duration.ofMillis(timeoutMs * 2)); .block(Duration.ofMillis(timeoutMs * 2));
logger.info("Image message sent to user {}", recipientId); log.info("[Facebook] Изображение отправлено пользователю {}", recipientId);
} catch (Exception e) { } catch (Exception e) {
logger.error("Failed to send image message: ", e); log.error("[Facebook] Ошибка отправки изображения: {}", e.getMessage());
} }
} }
public String postToPage(String ignoredToken, String postText, List<String> hashtags) throws JsonProcessingException { // ── Internal ──────────────────────────────────────────────────────────────
return internalPost(postText, hashtags, null);
}
public String postToPageWithImage(String ignoredToken, String postText, List<String> hashtags, byte[] imageData) { private String internalPost(PageCredentials creds, String postText, List<String> hashtags, byte[] imageData) {
return internalPost(postText, hashtags, imageData); if (creds.pageToken == null || creds.pageToken.length() < 10) {
} throw new IllegalStateException("Facebook Access Token не настроен! Проверьте application.properties");
}
private String internalPost(String postText, List<String> hashtags, byte[] imageData) {
try { try {
String fullPostText = buildPostText(postText, hashtags); String fullText = buildPostText(postText, hashtags);
MultiValueMap<String, Object> formData = new LinkedMultiValueMap<>(); MultiValueMap<String, Object> formData = new LinkedMultiValueMap<>();
formData.add("access_token", HARDCODED_PAGE_TOKEN); formData.add("access_token", creds.pageToken);
formData.add("message", fullPostText); formData.add("message", fullText);
String endpoint; String endpoint;
if (imageData != null) { if (imageData != null) {
endpoint = "/{pageId}/photos"; endpoint = "/{pageId}/photos";
formData.add("source", new ByteArrayResource(imageData) { formData.add("source", new ByteArrayResource(imageData) {
@@ -140,54 +205,52 @@ public class FacebookPostingService {
} }
String response = webClient.post() 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) .contentType(imageData != null ? MediaType.MULTIPART_FORM_DATA : MediaType.APPLICATION_FORM_URLENCODED)
.body(BodyInserters.fromMultipartData(formData)) .body(BodyInserters.fromMultipartData(formData))
.retrieve() .retrieve()
.bodyToMono(String.class) .bodyToMono(String.class)
.block(Duration.ofMillis(timeoutMs * 2)); .block(Duration.ofMillis(timeoutMs * 2));
JsonNode jsonNode = objectMapper.readTree(response); JsonNode json = objectMapper.readTree(response);
String postId = jsonNode.get("id").asText(); String postId = json.path("id").asText(null);
logger.info("Published to Facebook. ID: {}", postId); log.info("[Facebook] Пост опубликован. id={}, pageId={}", postId, creds.pageId);
return postId; return postId;
} catch (WebClientResponseException e) { } catch (WebClientResponseException e) {
handleFacebookError(e, "internalPost"); handleFacebookError(e);
return null; return null;
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException("Error posting: " + e.getMessage(), e); throw new RuntimeException("[Facebook] Ошибка: " + e.getMessage(), e);
} }
} }
private String buildPostText(String postText, List<String> hashtags) { private String buildPostText(String postText, List<String> hashtags) {
StringBuilder fullText = new StringBuilder(postText != null ? postText : ""); StringBuilder sb = new StringBuilder(postText != null ? postText : "");
if (hashtags != null && !hashtags.isEmpty()) { if (hashtags != null && !hashtags.isEmpty()) {
if (fullText.length() > 0) fullText.append("\n\n"); if (sb.length() > 0) sb.append("\n\n");
String hashtagsText = hashtags.stream() sb.append(hashtags.stream()
.map(tag -> tag.startsWith("#") ? tag : "#" + tag) .map(t -> t.startsWith("#") ? t : "#" + t)
.collect(Collectors.joining(" ")); .collect(Collectors.joining(" ")));
fullText.append(hashtagsText);
} }
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 static class PageCredentials {
public String pageId; public String pageToken; public final String pageId;
public PageCredentials(String pageId, String pageToken) { this.pageId = pageId; this.pageToken = pageToken; } public final String pageToken;
} public PageCredentials(String pageId, String pageToken) {
this.pageId = pageId;
private void handleFacebookError(WebClientResponseException e, String context) { this.pageToken = pageToken;
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);
} }
} }
@@ -146,9 +146,9 @@ public class PostingTaskService {
if ("facebook".equalsIgnoreCase(task.getPlatform())) { if ("facebook".equalsIgnoreCase(task.getPlatform())) {
if (imageData != null && imageData.length > 0) { if (imageData != null && imageData.length > 0) {
postId = facebookPostingService.postToPageWithImage(credentials, task.getPostText(), task.getHashtags(), imageData); postId = facebookPostingService.postToPageWithImage(task.getPostText(), task.getHashtags(), imageData);
} else { } else {
postId = facebookPostingService.postToPage(credentials, task.getPostText(), task.getHashtags()); postId = facebookPostingService.postToPage(task.getPostText(), task.getHashtags());
} }
if (postId != null) { if (postId != null) {
@@ -158,10 +158,10 @@ public class PostingTaskService {
"🔗 Ссылка: https://facebook.com/" + postId + "\n" + "🔗 Ссылка: https://facebook.com/" + postId + "\n" +
"🕒 Время: " + LocalDateTime.now(); "🕒 Время: " + LocalDateTime.now();
facebookPostingService.sendPrivateMessage(credentials, TEST_RECIPIENT_ID, reportText); facebookPostingService.sendPrivateMessage(TEST_RECIPIENT_ID, reportText);
if (imageData != null && imageData.length > 0) { if (imageData != null && imageData.length > 0) {
facebookPostingService.sendPrivateImageMessage(credentials, TEST_RECIPIENT_ID, imageData); facebookPostingService.sendPrivateImageMessage(TEST_RECIPIENT_ID, imageData);
} }
} catch (Exception msgEx) { } catch (Exception msgEx) {
logger.warn("Post published but failed to send private notification: {}", msgEx.getMessage()); logger.warn("Post published but failed to send private notification: {}", msgEx.getMessage());
@@ -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.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 # 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.audience-model=gpt-4o
targeting.ai.max-audience-tokens=2500 targeting.ai.max-audience-tokens=2500
targeting.ai.budget-model=gpt-4o-mini targeting.ai.budget-model=gpt-4o-mini
@@ -240,8 +240,8 @@ class PostingTaskServiceTest {
savedStatuses.add(savedTask.getStatus()); savedStatuses.add(savedTask.getStatus());
return savedTask; return savedTask;
}); });
when(credentialsService.getCredentials("user-1", "facebook")).thenReturn("fb-token"); // FacebookPostingService now handles credentials internally
when(facebookPostingService.postToPage("fb-token", "Product launch", List.of("#launch"))) when(facebookPostingService.postToPage("Product launch", List.of("#launch")))
.thenReturn("fb-post-1"); .thenReturn("fb-post-1");
postingTaskService.executeTask("task-1"); postingTaskService.executeTask("task-1");
@@ -250,12 +250,11 @@ class PostingTaskServiceTest {
assertEquals("completed", task.getStatus()); assertEquals("completed", task.getStatus());
assertNull(task.getErrorMessage()); assertNull(task.getErrorMessage());
assertNotNull(task.getExecutedAt()); assertNotNull(task.getExecutedAt());
verify(facebookPostingService).postToPage("fb-token", "Product launch", List.of("#launch")); verify(facebookPostingService).postToPage("Product launch", List.of("#launch"));
verify(facebookPostingService).sendPrivateMessage( verify(facebookPostingService).sendPrivateMessage(
eq("fb-token"),
eq(TEST_RECIPIENT_ID), eq(TEST_RECIPIENT_ID),
argThat(message -> message.contains("Product launch") && message.contains("https://facebook.com/fb-post-1"))); 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()); verify(minIOService, never()).downloadFile(anyString());
} }
@@ -269,28 +268,25 @@ class PostingTaskServiceTest {
when(taskRepository.findById("task-1")).thenReturn(Optional.of(task)); when(taskRepository.findById("task-1")).thenReturn(Optional.of(task));
when(taskRepository.save(any(PostingTask.class))).thenAnswer(invocation -> invocation.getArgument(0)); 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(minIOService.downloadFile("banner.png")).thenReturn(streamOf(imageBytes));
when(facebookPostingService.postToPageWithImage( when(facebookPostingService.postToPageWithImage(
eq("fb-token"),
eq("Image post"), eq("Image post"),
eq(List.of("#image")), eq(List.of("#image")),
aryEq(imageBytes))) aryEq(imageBytes)))
.thenReturn("fb-post-2"); .thenReturn("fb-post-2");
doThrow(new RuntimeException("Messenger unavailable")) doThrow(new RuntimeException("Messenger unavailable"))
.when(facebookPostingService) .when(facebookPostingService)
.sendPrivateMessage(eq("fb-token"), eq(TEST_RECIPIENT_ID), anyString()); .sendPrivateMessage(eq(TEST_RECIPIENT_ID), anyString());
postingTaskService.executeTask("task-1"); postingTaskService.executeTask("task-1");
assertEquals("completed", task.getStatus()); assertEquals("completed", task.getStatus());
assertNull(task.getErrorMessage()); assertNull(task.getErrorMessage());
verify(facebookPostingService).postToPageWithImage( verify(facebookPostingService).postToPageWithImage(
eq("fb-token"),
eq("Image post"), eq("Image post"),
eq(List.of("#image")), eq(List.of("#image")),
aryEq(imageBytes)); aryEq(imageBytes));
verify(facebookPostingService, never()).sendPrivateImageMessage(anyString(), anyString(), any()); verify(facebookPostingService, never()).sendPrivateImageMessage(anyString(), any());
} }
@Test @Test
@@ -393,8 +389,8 @@ class PostingTaskServiceTest {
when(taskRepository.findById("task-1")).thenReturn(Optional.of(task)); when(taskRepository.findById("task-1")).thenReturn(Optional.of(task));
when(taskRepository.save(any(PostingTask.class))).thenAnswer(invocation -> invocation.getArgument(0)); when(taskRepository.save(any(PostingTask.class))).thenAnswer(invocation -> invocation.getArgument(0));
when(credentialsService.getCredentials("user-1", "facebook")).thenReturn("fb-token"); // FacebookPostingService now handles credentials internally
when(facebookPostingService.postToPage("fb-token", task.getPostText(), task.getHashtags())) when(facebookPostingService.postToPage(task.getPostText(), task.getHashtags()))
.thenThrow(new FacebookTokenExpiredException("Expired", "code 190", 190, 463)); .thenThrow(new FacebookTokenExpiredException("Expired", "code 190", 190, 463));
postingTaskService.executeTask("task-1"); postingTaskService.executeTask("task-1");