From 6add2d9465cc8205053555066a7e2d238a66b011 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 27 May 2026 14:46:48 +0000 Subject: [PATCH 1/9] Initial plan From f096d6b3ab76caaebe735d22eca623bec78100f5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 27 May 2026 14:52:30 +0000 Subject: [PATCH 2/9] Add integration coverage for CV template placeholder flow --- .../integration/GoogleDriveControllerIT.java | 90 ++++++++++++++++++- 1 file changed, 89 insertions(+), 1 deletion(-) diff --git a/src/test/java/com/jobtracker/integration/GoogleDriveControllerIT.java b/src/test/java/com/jobtracker/integration/GoogleDriveControllerIT.java index 0f04d94..834ba7a 100644 --- a/src/test/java/com/jobtracker/integration/GoogleDriveControllerIT.java +++ b/src/test/java/com/jobtracker/integration/GoogleDriveControllerIT.java @@ -36,6 +36,7 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -73,6 +74,7 @@ class GoogleDriveControllerIT extends AbstractIntegrationTest { @BeforeEach void setUp() throws Exception { + googleDriveApiClient.reset(); googleDriveOAuthStateRepository.deleteAll(); googleDriveBaseResumeRepository.deleteAll(); googleDriveConnectionRepository.deleteAll(); @@ -331,6 +333,71 @@ void copyResume_shouldReturn400WhenNoRootFolderConfigured() throws Exception { .andExpect(status().isBadRequest()); } + @Test + void detectResumePlaceholders_shouldReturnTemplatePlaceholders() throws Exception { + GoogleDriveConnection connection = googleDriveConnectionRepository.save(buildConnection()); + GoogleDriveBaseResume resume = buildBaseResume(connection); + googleDriveBaseResumeRepository.save(resume); + + mockMvc.perform(post("/api/v1/google-drive/resume-placeholders") + .header("Authorization", "Bearer " + betaAccessToken) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"baseResumeId\":\"" + resume.getId() + "\"}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.baseResumeId").value(resume.getId().toString())) + .andExpect(jsonPath("$.applicationId").isEmpty()) + .andExpect(jsonPath("$.placeholders[0]").value("SUMMARY")) + .andExpect(jsonPath("$.placeholders[1]").value("SKILLS")); + } + + @Test + void generateResume_shouldReplaceTemplatePlaceholdersAndReturnGeneratedResume() throws Exception { + GoogleDriveConnection connection = googleDriveConnectionRepository.save(buildConnectionWithRootFolder()); + GoogleDriveBaseResume resume = buildBaseResume(connection); + googleDriveBaseResumeRepository.save(resume); + + JobApplication application = new JobApplication(); + application.setUser(userRepository.findByEmail("driveuser@example.com").orElseThrow()); + application.setVacancyName("Backend Engineer"); + application.setOrganization("Acme"); + application.setApplicationDate(LocalDate.now()); + application = applicationRepository.save(application); + + googleDriveApiClient.fileMetadataById.put("root-folder-id", + new GoogleDriveApiClient.DriveFileMetadata( + "root-folder-id", + "Job Tracker Root", + GoogleDriveApiClient.GOOGLE_FOLDER_MIME_TYPE, + "https://drive.google.com/drive/folders/root-folder-id" + )); + + mockMvc.perform(post("/api/v1/google-drive/applications/" + application.getId() + "/generated-resumes") + .header("Authorization", "Bearer " + betaAccessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "baseResumeId": "%s", + "values": { + "SUMMARY": "Senior Java Engineer", + "SKILLS": "Spring Boot, PostgreSQL" + } + } + """.formatted(resume.getId()))) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.applicationId").value(application.getId().toString())) + .andExpect(jsonPath("$.baseResumeId").value(resume.getId().toString())) + .andExpect(jsonPath("$.values.SUMMARY").value("Senior Java Engineer")) + .andExpect(jsonPath("$.values.SKILLS").value("Spring Boot, PostgreSQL")) + .andExpect(jsonPath("$.placeholders").isEmpty()) + .andExpect(jsonPath("$.copiedFileId").value("copied-file")) + .andExpect(jsonPath("$.pdfFileId").value("pdf-file")) + .andExpect(jsonPath("$.generatedAt").isNotEmpty()); + + JobApplication savedApplication = applicationRepository.findById(application.getId()).orElseThrow(); + assertThat(savedApplication.getDriveResumeFileId()).isEqualTo("copied-file"); + assertThat(savedApplication.getDriveResumeDocumentUrl()).contains("copied-file"); + } + private GoogleDriveConnection buildConnectionWithRootFolder() { GoogleDriveConnection connection = buildConnection(); connection.setRootFolderId("root-folder-id"); @@ -380,6 +447,14 @@ static class FakeGoogleDriveApiClient implements GoogleDriveApiClient { private GoogleDriveAccountProfile accountProfile = new GoogleDriveAccountProfile("perm-123", "connected@example.com", "Drive User"); private final Map fileMetadataById = new HashMap<>(); + private final Map documentTextById = new HashMap<>(); + + void reset() { + fileMetadataById.clear(); + documentTextById.clear(); + documentTextById.put("resume-file-id", "{{SUMMARY}}\n{{SKILLS}}"); + documentTextById.put("copied-file", "{{SUMMARY}}\n{{SKILLS}}"); + } @Override public String buildAuthorizationUrl(String state) { @@ -418,16 +493,29 @@ public DriveFileMetadata createFolder(String accessToken, String parentFolderId, @Override public DriveFileMetadata copyGoogleDoc(String accessToken, String sourceFileId, String targetFolderId, String newName) { + String sourceText = documentTextById.getOrDefault(sourceFileId, "{{SUMMARY}}\n{{SKILLS}}"); + documentTextById.put("copied-file", sourceText); return new DriveFileMetadata("copied-file", newName, GOOGLE_DOC_MIME_TYPE, null); } @Override public String readGoogleDocText(String accessToken, String documentId) { - return "{{SUMMARY}}\n{{SKILLS}}"; + return documentTextById.getOrDefault(documentId, "{{SUMMARY}}\n{{SKILLS}}"); } @Override public void replaceGoogleDocPlaceholders(String accessToken, String documentId, Map values) { + if (values == null || values.isEmpty()) { + return; + } + String currentText = documentTextById.getOrDefault(documentId, "{{SUMMARY}}\n{{SKILLS}}"); + String updatedText = currentText; + Map orderedValues = new LinkedHashMap<>(values); + for (Map.Entry entry : orderedValues.entrySet()) { + String replacement = entry.getValue() == null ? "" : entry.getValue(); + updatedText = updatedText.replace("{{" + entry.getKey() + "}}", replacement); + } + documentTextById.put(documentId, updatedText); } @Override From d5a5d6b1f69e048ab80eaadeafc84f0ab4817a5c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 27 May 2026 14:54:19 +0000 Subject: [PATCH 3/9] Refine template integration test fake client --- .../integration/GoogleDriveControllerIT.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/test/java/com/jobtracker/integration/GoogleDriveControllerIT.java b/src/test/java/com/jobtracker/integration/GoogleDriveControllerIT.java index 834ba7a..40512ac 100644 --- a/src/test/java/com/jobtracker/integration/GoogleDriveControllerIT.java +++ b/src/test/java/com/jobtracker/integration/GoogleDriveControllerIT.java @@ -36,7 +36,6 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.util.HashMap; -import java.util.LinkedHashMap; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -438,6 +437,8 @@ FakeGoogleDriveApiClient googleDriveApiClient() { } static class FakeGoogleDriveApiClient implements GoogleDriveApiClient { + private static final String DEFAULT_TEMPLATE_TEXT = "{{SUMMARY}}\n{{SKILLS}}"; + private OAuthTokens tokens = new OAuthTokens( "drive-access", "drive-refresh", @@ -452,8 +453,8 @@ static class FakeGoogleDriveApiClient implements GoogleDriveApiClient { void reset() { fileMetadataById.clear(); documentTextById.clear(); - documentTextById.put("resume-file-id", "{{SUMMARY}}\n{{SKILLS}}"); - documentTextById.put("copied-file", "{{SUMMARY}}\n{{SKILLS}}"); + documentTextById.put("resume-file-id", DEFAULT_TEMPLATE_TEXT); + documentTextById.put("copied-file", DEFAULT_TEMPLATE_TEXT); } @Override @@ -493,14 +494,14 @@ public DriveFileMetadata createFolder(String accessToken, String parentFolderId, @Override public DriveFileMetadata copyGoogleDoc(String accessToken, String sourceFileId, String targetFolderId, String newName) { - String sourceText = documentTextById.getOrDefault(sourceFileId, "{{SUMMARY}}\n{{SKILLS}}"); + String sourceText = documentTextById.getOrDefault(sourceFileId, DEFAULT_TEMPLATE_TEXT); documentTextById.put("copied-file", sourceText); return new DriveFileMetadata("copied-file", newName, GOOGLE_DOC_MIME_TYPE, null); } @Override public String readGoogleDocText(String accessToken, String documentId) { - return documentTextById.getOrDefault(documentId, "{{SUMMARY}}\n{{SKILLS}}"); + return documentTextById.getOrDefault(documentId, DEFAULT_TEMPLATE_TEXT); } @Override @@ -508,10 +509,9 @@ public void replaceGoogleDocPlaceholders(String accessToken, String documentId, if (values == null || values.isEmpty()) { return; } - String currentText = documentTextById.getOrDefault(documentId, "{{SUMMARY}}\n{{SKILLS}}"); + String currentText = documentTextById.getOrDefault(documentId, DEFAULT_TEMPLATE_TEXT); String updatedText = currentText; - Map orderedValues = new LinkedHashMap<>(values); - for (Map.Entry entry : orderedValues.entrySet()) { + for (Map.Entry entry : values.entrySet()) { String replacement = entry.getValue() == null ? "" : entry.getValue(); updatedText = updatedText.replace("{{" + entry.getKey() + "}}", replacement); } From 357d3b0f313bc10efb7dd7fa990e8c4881b77233 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 27 May 2026 15:38:33 +0000 Subject: [PATCH 4/9] Implement template placeholder replacement from document tokens --- .../service/ResumeGenerationService.java | 22 ++- .../service/SdkGoogleDriveApiClient.java | 14 +- .../integration/GoogleDriveControllerIT.java | 53 ++++++- .../ResumeGenerationTemplateFlowTest.java | 131 ++++++++++++++++++ 4 files changed, 217 insertions(+), 3 deletions(-) create mode 100644 src/test/java/com/jobtracker/unit/ResumeGenerationTemplateFlowTest.java diff --git a/src/main/java/com/jobtracker/service/ResumeGenerationService.java b/src/main/java/com/jobtracker/service/ResumeGenerationService.java index 14e4b5b..b93ff57 100644 --- a/src/main/java/com/jobtracker/service/ResumeGenerationService.java +++ b/src/main/java/com/jobtracker/service/ResumeGenerationService.java @@ -17,6 +17,7 @@ import org.springframework.util.StringUtils; import java.time.LocalDateTime; +import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -106,7 +107,9 @@ public ResumePlaceholderResponse generateResume(UUID applicationId, ResumePlaceh ); Map values = request.values() == null ? Map.of() : request.values(); - googleDriveApiClient.replaceGoogleDocPlaceholders(connection.getAccessToken(), copiedFile.id(), values); + String copiedTextBeforeReplacement = googleDriveApiClient.readGoogleDocText(connection.getAccessToken(), copiedFile.id()); + Map replacementValues = buildReplacementValues(copiedTextBeforeReplacement, values); + googleDriveApiClient.replaceGoogleDocPlaceholders(connection.getAccessToken(), copiedFile.id(), replacementValues); String copiedDocumentUrl = resolveDocumentLink(copiedFile); String copiedText = googleDriveApiClient.readGoogleDocText(connection.getAccessToken(), copiedFile.id()); @@ -162,6 +165,23 @@ public List detectPlaceholders(String text) { return List.copyOf(placeholders); } + private Map buildReplacementValues(String templateText, Map providedValues) { + if (!StringUtils.hasText(templateText) || providedValues == null || providedValues.isEmpty()) { + return Map.of(); + } + + Matcher matcher = PLACEHOLDER_PATTERN.matcher(templateText); + Map replacementValues = new LinkedHashMap<>(); + while (matcher.find()) { + String placeholderName = matcher.group(1) == null ? null : matcher.group(1).trim(); + if (!StringUtils.hasText(placeholderName) || !providedValues.containsKey(placeholderName)) { + continue; + } + replacementValues.putIfAbsent(matcher.group(0), providedValues.get(placeholderName)); + } + return replacementValues; + } + private GoogleDriveBaseResume getBaseResume(UUID baseResumeId, UUID userId) { return baseResumeRepository.findByIdAndConnectionUserId(baseResumeId, userId) .orElseThrow(() -> new ResourceNotFoundException("Base resume not found with id: " + baseResumeId)); diff --git a/src/main/java/com/jobtracker/service/SdkGoogleDriveApiClient.java b/src/main/java/com/jobtracker/service/SdkGoogleDriveApiClient.java index 78fef1e..d0cf9eb 100644 --- a/src/main/java/com/jobtracker/service/SdkGoogleDriveApiClient.java +++ b/src/main/java/com/jobtracker/service/SdkGoogleDriveApiClient.java @@ -249,12 +249,16 @@ public void replaceGoogleDocPlaceholders(String accessToken, String documentId, } List requests = values.entrySet().stream() + .filter(entry -> entry.getKey() != null && !entry.getKey().isBlank()) .map(entry -> new Request().setReplaceAllText(new ReplaceAllTextRequest() .setContainsText(new SubstringMatchCriteria() - .setText("{{" + entry.getKey() + "}}") + .setText(resolvePlaceholderToken(entry.getKey())) .setMatchCase(true)) .setReplaceText(entry.getValue() == null ? "" : entry.getValue()))) .toList(); + if (requests.isEmpty()) { + return; + } executeDocsOp(accessToken, "replace placeholders", docs -> { docs.documents().batchUpdate(documentId, new BatchUpdateDocumentRequest().setRequests(requests)).execute(); @@ -355,4 +359,12 @@ private DriveFileMetadata toDriveFileMetadata(File file) { file.getMimeType(), file.getWebViewLink()); } + + private String resolvePlaceholderToken(String value) { + String trimmed = value.trim(); + if (trimmed.startsWith("{{") && trimmed.endsWith("}}")) { + return trimmed; + } + return "{{" + trimmed + "}}"; + } } diff --git a/src/test/java/com/jobtracker/integration/GoogleDriveControllerIT.java b/src/test/java/com/jobtracker/integration/GoogleDriveControllerIT.java index 40512ac..f8cd13c 100644 --- a/src/test/java/com/jobtracker/integration/GoogleDriveControllerIT.java +++ b/src/test/java/com/jobtracker/integration/GoogleDriveControllerIT.java @@ -397,6 +397,46 @@ void generateResume_shouldReplaceTemplatePlaceholdersAndReturnGeneratedResume() assertThat(savedApplication.getDriveResumeDocumentUrl()).contains("copied-file"); } + @Test + void generateResume_shouldReplacePlaceholdersWhenTemplateContainsWhitespaceInBraces() throws Exception { + GoogleDriveConnection connection = googleDriveConnectionRepository.save(buildConnectionWithRootFolder()); + GoogleDriveBaseResume resume = buildBaseResume(connection); + googleDriveBaseResumeRepository.save(resume); + googleDriveApiClient.setDocumentText("resume-file-id", "{{ SUMMARY }}\n{{SKILLS}}\n{{UNKNOWN}}"); + + JobApplication application = new JobApplication(); + application.setUser(userRepository.findByEmail("driveuser@example.com").orElseThrow()); + application.setVacancyName("Backend Engineer"); + application.setOrganization("Acme"); + application.setApplicationDate(LocalDate.now()); + application = applicationRepository.save(application); + + googleDriveApiClient.fileMetadataById.put("root-folder-id", + new GoogleDriveApiClient.DriveFileMetadata( + "root-folder-id", + "Job Tracker Root", + GoogleDriveApiClient.GOOGLE_FOLDER_MIME_TYPE, + "https://drive.google.com/drive/folders/root-folder-id" + )); + + mockMvc.perform(post("/api/v1/google-drive/applications/" + application.getId() + "/generated-resumes") + .header("Authorization", "Bearer " + betaAccessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "baseResumeId": "%s", + "values": { + "SUMMARY": "Senior Java Engineer", + "SKILLS": "Spring Boot, PostgreSQL" + } + } + """.formatted(resume.getId()))) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.values.SUMMARY").value("Senior Java Engineer")) + .andExpect(jsonPath("$.values.SKILLS").value("Spring Boot, PostgreSQL")) + .andExpect(jsonPath("$.placeholders[0]").value("UNKNOWN")); + } + private GoogleDriveConnection buildConnectionWithRootFolder() { GoogleDriveConnection connection = buildConnection(); connection.setRootFolderId("root-folder-id"); @@ -457,6 +497,10 @@ void reset() { documentTextById.put("copied-file", DEFAULT_TEMPLATE_TEXT); } + void setDocumentText(String documentId, String text) { + documentTextById.put(documentId, text); + } + @Override public String buildAuthorizationUrl(String state) { return "https://accounts.google.com/o/oauth2/v2/auth?state=" + state; @@ -513,7 +557,14 @@ public void replaceGoogleDocPlaceholders(String accessToken, String documentId, String updatedText = currentText; for (Map.Entry entry : values.entrySet()) { String replacement = entry.getValue() == null ? "" : entry.getValue(); - updatedText = updatedText.replace("{{" + entry.getKey() + "}}", replacement); + String token = entry.getKey(); + if (token == null || token.isBlank()) { + continue; + } + String placeholderToken = token.trim().startsWith("{{") && token.trim().endsWith("}}") + ? token.trim() + : "{{" + token + "}}"; + updatedText = updatedText.replace(placeholderToken, replacement); } documentTextById.put(documentId, updatedText); } diff --git a/src/test/java/com/jobtracker/unit/ResumeGenerationTemplateFlowTest.java b/src/test/java/com/jobtracker/unit/ResumeGenerationTemplateFlowTest.java new file mode 100644 index 0000000..d8b1a7d --- /dev/null +++ b/src/test/java/com/jobtracker/unit/ResumeGenerationTemplateFlowTest.java @@ -0,0 +1,131 @@ +package com.jobtracker.unit; + +import com.jobtracker.config.GoogleDriveProperties; +import com.jobtracker.dto.gdrive.ResumePlaceholderRequest; +import com.jobtracker.entity.GoogleDriveBaseResume; +import com.jobtracker.entity.GoogleDriveConnection; +import com.jobtracker.entity.JobApplication; +import com.jobtracker.entity.User; +import com.jobtracker.repository.ApplicationRepository; +import com.jobtracker.repository.GoogleDriveBaseResumeRepository; +import com.jobtracker.repository.GoogleDriveConnectionRepository; +import com.jobtracker.service.GoogleDriveApiClient; +import com.jobtracker.service.ResumeGenerationService; +import com.jobtracker.util.SecurityUtils; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class ResumeGenerationTemplateFlowTest { + + private static final UUID USER_ID = UUID.fromString("00000000-0000-0000-0000-000000000001"); + private static final UUID BASE_RESUME_ID = UUID.fromString("00000000-0000-0000-0000-000000000002"); + private static final UUID APPLICATION_ID = UUID.fromString("00000000-0000-0000-0000-000000000003"); + + @Mock private GoogleDriveApiClient googleDriveApiClient; + @Mock private GoogleDriveConnectionRepository connectionRepository; + @Mock private GoogleDriveBaseResumeRepository baseResumeRepository; + @Mock private ApplicationRepository applicationRepository; + @Mock private SecurityUtils securityUtils; + + @Test + void generateResume_shouldReplaceSpacedAndUnspacedPlaceholdersFromTemplateContent() { + ResumeGenerationService service = new ResumeGenerationService( + googleDriveApiClient, + new GoogleDriveProperties("client", "secret", "cb", "frontend", "auth", "token"), + connectionRepository, + baseResumeRepository, + applicationRepository, + securityUtils + ); + + User user = new User(); + user.setId(USER_ID); + + GoogleDriveConnection connection = new GoogleDriveConnection(); + connection.setUser(user); + connection.setAccessToken("access-token"); + connection.setRefreshToken("refresh-token"); + connection.setAccessTokenExpiresAt(LocalDateTime.now().plusHours(1)); + connection.setRootFolderId("root-folder-id"); + + GoogleDriveBaseResume baseResume = new GoogleDriveBaseResume(); + baseResume.setId(BASE_RESUME_ID); + baseResume.setConnection(connection); + baseResume.setGoogleFileId("base-doc-id"); + baseResume.setDocumentName("Base Resume"); + + JobApplication application = new JobApplication(); + application.setId(APPLICATION_ID); + application.setUser(user); + application.setVacancyName("Backend Engineer"); + application.setOrganization("Acme"); + application.setApplicationDate(LocalDate.now()); + application.setDriveVacancyFolderId("vacancy-folder-id"); + + when(securityUtils.getCurrentUserId()).thenReturn(USER_ID); + when(connectionRepository.findByUserId(USER_ID)).thenReturn(Optional.of(connection)); + when(applicationRepository.findByIdAndUserId(APPLICATION_ID, USER_ID)).thenReturn(Optional.of(application)); + when(baseResumeRepository.findByIdAndConnectionUserId(BASE_RESUME_ID, USER_ID)).thenReturn(Optional.of(baseResume)); + when(googleDriveApiClient.getFileMetadata("access-token", "root-folder-id")) + .thenReturn(new GoogleDriveApiClient.DriveFileMetadata( + "root-folder-id", + "Root", + GoogleDriveApiClient.GOOGLE_FOLDER_MIME_TYPE, + "https://drive.google.com/drive/folders/root-folder-id" + )); + when(googleDriveApiClient.getFileMetadata("access-token", "vacancy-folder-id")) + .thenReturn(new GoogleDriveApiClient.DriveFileMetadata( + "vacancy-folder-id", + "Vacancy", + GoogleDriveApiClient.GOOGLE_FOLDER_MIME_TYPE, + "https://drive.google.com/drive/folders/vacancy-folder-id" + )); + when(googleDriveApiClient.copyGoogleDoc(eq("access-token"), eq("base-doc-id"), eq("vacancy-folder-id"), any())) + .thenReturn(new GoogleDriveApiClient.DriveFileMetadata( + "copied-doc-id", + "Copied Resume", + GoogleDriveApiClient.GOOGLE_DOC_MIME_TYPE, + "https://docs.google.com/document/d/copied-doc-id/edit" + )); + when(googleDriveApiClient.readGoogleDocText("access-token", "copied-doc-id")) + .thenReturn("{{ SUMMARY }}\n{{SKILLS}}\n{{UNMAPPED}}") + .thenReturn("Senior Java Engineer\nSpring Boot, PostgreSQL\n{{UNMAPPED}}"); + when(googleDriveApiClient.exportGoogleDocAsPdf(eq("access-token"), eq("copied-doc-id"), eq("vacancy-folder-id"), any())) + .thenReturn(new GoogleDriveApiClient.DriveFileMetadata( + "pdf-id", + "Resume.pdf", + "application/pdf", + "https://drive.google.com/file/d/pdf-id/view" + )); + + service.generateResume(APPLICATION_ID, new ResumePlaceholderRequest( + BASE_RESUME_ID, + Map.of( + "SUMMARY", "Senior Java Engineer", + "SKILLS", "Spring Boot, PostgreSQL" + ) + )); + + ArgumentCaptor> valuesCaptor = ArgumentCaptor.forClass(Map.class); + verify(googleDriveApiClient).replaceGoogleDocPlaceholders(eq("access-token"), eq("copied-doc-id"), valuesCaptor.capture()); + assertThat(valuesCaptor.getValue()).containsEntry("{{ SUMMARY }}", "Senior Java Engineer"); + assertThat(valuesCaptor.getValue()).containsEntry("{{SKILLS}}", "Spring Boot, PostgreSQL"); + assertThat(valuesCaptor.getValue()).doesNotContainKeys("SUMMARY", "SKILLS"); + } +} From e15b9dbd5dc32c4c680a1d9ae02463896bdefe53 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 27 May 2026 15:41:52 +0000 Subject: [PATCH 5/9] Refine template replacement compatibility and tests --- .../service/ResumeGenerationService.java | 3 ++- .../service/SdkGoogleDriveApiClient.java | 5 +++++ .../unit/ResumeGenerationTemplateFlowTest.java | 15 +++++++++++++-- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/jobtracker/service/ResumeGenerationService.java b/src/main/java/com/jobtracker/service/ResumeGenerationService.java index b93ff57..95eb2b4 100644 --- a/src/main/java/com/jobtracker/service/ResumeGenerationService.java +++ b/src/main/java/com/jobtracker/service/ResumeGenerationService.java @@ -173,7 +173,8 @@ private Map buildReplacementValues(String templateText, Map replacementValues = new LinkedHashMap<>(); while (matcher.find()) { - String placeholderName = matcher.group(1) == null ? null : matcher.group(1).trim(); + String placeholderContent = matcher.group(1); + String placeholderName = placeholderContent == null ? null : placeholderContent.trim(); if (!StringUtils.hasText(placeholderName) || !providedValues.containsKey(placeholderName)) { continue; } diff --git a/src/main/java/com/jobtracker/service/SdkGoogleDriveApiClient.java b/src/main/java/com/jobtracker/service/SdkGoogleDriveApiClient.java index d0cf9eb..2dab9b9 100644 --- a/src/main/java/com/jobtracker/service/SdkGoogleDriveApiClient.java +++ b/src/main/java/com/jobtracker/service/SdkGoogleDriveApiClient.java @@ -360,6 +360,11 @@ private DriveFileMetadata toDriveFileMetadata(File file) { file.getWebViewLink()); } + /** + * Accepts both legacy placeholder keys (e.g. {@code SUMMARY}) and already wrapped tokens + * (e.g. {@code {{ SUMMARY }}}) so callers can replace placeholders exactly as detected from + * template content while remaining backward compatible with existing callers. + */ private String resolvePlaceholderToken(String value) { String trimmed = value.trim(); if (trimmed.startsWith("{{") && trimmed.endsWith("}}")) { diff --git a/src/test/java/com/jobtracker/unit/ResumeGenerationTemplateFlowTest.java b/src/test/java/com/jobtracker/unit/ResumeGenerationTemplateFlowTest.java index d8b1a7d..af92fbe 100644 --- a/src/test/java/com/jobtracker/unit/ResumeGenerationTemplateFlowTest.java +++ b/src/test/java/com/jobtracker/unit/ResumeGenerationTemplateFlowTest.java @@ -23,10 +23,12 @@ import java.util.Map; import java.util.Optional; import java.util.UUID; +import java.util.concurrent.atomic.AtomicReference; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -103,9 +105,18 @@ void generateResume_shouldReplaceSpacedAndUnspacedPlaceholdersFromTemplateConten GoogleDriveApiClient.GOOGLE_DOC_MIME_TYPE, "https://docs.google.com/document/d/copied-doc-id/edit" )); + AtomicReference copiedText = new AtomicReference<>("{{ SUMMARY }}\n{{SKILLS}}\n{{UNMAPPED}}"); when(googleDriveApiClient.readGoogleDocText("access-token", "copied-doc-id")) - .thenReturn("{{ SUMMARY }}\n{{SKILLS}}\n{{UNMAPPED}}") - .thenReturn("Senior Java Engineer\nSpring Boot, PostgreSQL\n{{UNMAPPED}}"); + .thenAnswer(invocation -> copiedText.get()); + doAnswer(invocation -> { + Map replacements = invocation.getArgument(2); + String updated = copiedText.get(); + for (Map.Entry entry : replacements.entrySet()) { + updated = updated.replace(entry.getKey(), entry.getValue()); + } + copiedText.set(updated); + return null; + }).when(googleDriveApiClient).replaceGoogleDocPlaceholders(eq("access-token"), eq("copied-doc-id"), any()); when(googleDriveApiClient.exportGoogleDocAsPdf(eq("access-token"), eq("copied-doc-id"), eq("vacancy-folder-id"), any())) .thenReturn(new GoogleDriveApiClient.DriveFileMetadata( "pdf-id", From c7171539f0a903b166cea2083f0ae55c04753c6e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 27 May 2026 15:43:54 +0000 Subject: [PATCH 6/9] Polish template placeholder replacement and tests --- .../com/jobtracker/service/ResumeGenerationService.java | 9 ++++++--- .../com/jobtracker/service/SdkGoogleDriveApiClient.java | 1 + .../jobtracker/integration/GoogleDriveControllerIT.java | 5 +++-- .../unit/ResumeGenerationTemplateFlowTest.java | 8 ++++---- 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/jobtracker/service/ResumeGenerationService.java b/src/main/java/com/jobtracker/service/ResumeGenerationService.java index 95eb2b4..9d6e946 100644 --- a/src/main/java/com/jobtracker/service/ResumeGenerationService.java +++ b/src/main/java/com/jobtracker/service/ResumeGenerationService.java @@ -173,9 +173,12 @@ private Map buildReplacementValues(String templateText, Map replacementValues = new LinkedHashMap<>(); while (matcher.find()) { - String placeholderContent = matcher.group(1); - String placeholderName = placeholderContent == null ? null : placeholderContent.trim(); - if (!StringUtils.hasText(placeholderName) || !providedValues.containsKey(placeholderName)) { + String placeholderValue = matcher.group(1); + if (!StringUtils.hasText(placeholderValue)) { + continue; + } + String placeholderName = placeholderValue.trim(); + if (!providedValues.containsKey(placeholderName)) { continue; } replacementValues.putIfAbsent(matcher.group(0), providedValues.get(placeholderName)); diff --git a/src/main/java/com/jobtracker/service/SdkGoogleDriveApiClient.java b/src/main/java/com/jobtracker/service/SdkGoogleDriveApiClient.java index 2dab9b9..059577f 100644 --- a/src/main/java/com/jobtracker/service/SdkGoogleDriveApiClient.java +++ b/src/main/java/com/jobtracker/service/SdkGoogleDriveApiClient.java @@ -252,6 +252,7 @@ public void replaceGoogleDocPlaceholders(String accessToken, String documentId, .filter(entry -> entry.getKey() != null && !entry.getKey().isBlank()) .map(entry -> new Request().setReplaceAllText(new ReplaceAllTextRequest() .setContainsText(new SubstringMatchCriteria() + // Supports both legacy keys (SUMMARY) and detected tokens ({{ SUMMARY }}). .setText(resolvePlaceholderToken(entry.getKey())) .setMatchCase(true)) .setReplaceText(entry.getValue() == null ? "" : entry.getValue()))) diff --git a/src/test/java/com/jobtracker/integration/GoogleDriveControllerIT.java b/src/test/java/com/jobtracker/integration/GoogleDriveControllerIT.java index f8cd13c..f230c86 100644 --- a/src/test/java/com/jobtracker/integration/GoogleDriveControllerIT.java +++ b/src/test/java/com/jobtracker/integration/GoogleDriveControllerIT.java @@ -561,8 +561,9 @@ public void replaceGoogleDocPlaceholders(String accessToken, String documentId, if (token == null || token.isBlank()) { continue; } - String placeholderToken = token.trim().startsWith("{{") && token.trim().endsWith("}}") - ? token.trim() + String trimmedToken = token.trim(); + String placeholderToken = trimmedToken.startsWith("{{") && trimmedToken.endsWith("}}") + ? trimmedToken : "{{" + token + "}}"; updatedText = updatedText.replace(placeholderToken, replacement); } diff --git a/src/test/java/com/jobtracker/unit/ResumeGenerationTemplateFlowTest.java b/src/test/java/com/jobtracker/unit/ResumeGenerationTemplateFlowTest.java index af92fbe..0848ba3 100644 --- a/src/test/java/com/jobtracker/unit/ResumeGenerationTemplateFlowTest.java +++ b/src/test/java/com/jobtracker/unit/ResumeGenerationTemplateFlowTest.java @@ -105,16 +105,16 @@ void generateResume_shouldReplaceSpacedAndUnspacedPlaceholdersFromTemplateConten GoogleDriveApiClient.GOOGLE_DOC_MIME_TYPE, "https://docs.google.com/document/d/copied-doc-id/edit" )); - AtomicReference copiedText = new AtomicReference<>("{{ SUMMARY }}\n{{SKILLS}}\n{{UNMAPPED}}"); + AtomicReference mockDocumentContent = new AtomicReference<>("{{ SUMMARY }}\n{{SKILLS}}\n{{UNMAPPED}}"); when(googleDriveApiClient.readGoogleDocText("access-token", "copied-doc-id")) - .thenAnswer(invocation -> copiedText.get()); + .thenAnswer(invocation -> mockDocumentContent.get()); doAnswer(invocation -> { Map replacements = invocation.getArgument(2); - String updated = copiedText.get(); + String updated = mockDocumentContent.get(); for (Map.Entry entry : replacements.entrySet()) { updated = updated.replace(entry.getKey(), entry.getValue()); } - copiedText.set(updated); + mockDocumentContent.set(updated); return null; }).when(googleDriveApiClient).replaceGoogleDocPlaceholders(eq("access-token"), eq("copied-doc-id"), any()); when(googleDriveApiClient.exportGoogleDocAsPdf(eq("access-token"), eq("copied-doc-id"), eq("vacancy-folder-id"), any())) From d24d515ee01261d1d5d3c3d462349b4bc201bb58 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 27 May 2026 18:26:55 +0000 Subject: [PATCH 7/9] Simplify strict CV placeholder replacement flow --- .../service/ResumeGenerationService.java | 26 +------------------ .../service/SdkGoogleDriveApiClient.java | 16 +++--------- .../integration/GoogleDriveControllerIT.java | 11 +++----- .../ResumeGenerationTemplateFlowTest.java | 11 ++++---- 4 files changed, 13 insertions(+), 51 deletions(-) diff --git a/src/main/java/com/jobtracker/service/ResumeGenerationService.java b/src/main/java/com/jobtracker/service/ResumeGenerationService.java index 9d6e946..14e4b5b 100644 --- a/src/main/java/com/jobtracker/service/ResumeGenerationService.java +++ b/src/main/java/com/jobtracker/service/ResumeGenerationService.java @@ -17,7 +17,6 @@ import org.springframework.util.StringUtils; import java.time.LocalDateTime; -import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -107,9 +106,7 @@ public ResumePlaceholderResponse generateResume(UUID applicationId, ResumePlaceh ); Map values = request.values() == null ? Map.of() : request.values(); - String copiedTextBeforeReplacement = googleDriveApiClient.readGoogleDocText(connection.getAccessToken(), copiedFile.id()); - Map replacementValues = buildReplacementValues(copiedTextBeforeReplacement, values); - googleDriveApiClient.replaceGoogleDocPlaceholders(connection.getAccessToken(), copiedFile.id(), replacementValues); + googleDriveApiClient.replaceGoogleDocPlaceholders(connection.getAccessToken(), copiedFile.id(), values); String copiedDocumentUrl = resolveDocumentLink(copiedFile); String copiedText = googleDriveApiClient.readGoogleDocText(connection.getAccessToken(), copiedFile.id()); @@ -165,27 +162,6 @@ public List detectPlaceholders(String text) { return List.copyOf(placeholders); } - private Map buildReplacementValues(String templateText, Map providedValues) { - if (!StringUtils.hasText(templateText) || providedValues == null || providedValues.isEmpty()) { - return Map.of(); - } - - Matcher matcher = PLACEHOLDER_PATTERN.matcher(templateText); - Map replacementValues = new LinkedHashMap<>(); - while (matcher.find()) { - String placeholderValue = matcher.group(1); - if (!StringUtils.hasText(placeholderValue)) { - continue; - } - String placeholderName = placeholderValue.trim(); - if (!providedValues.containsKey(placeholderName)) { - continue; - } - replacementValues.putIfAbsent(matcher.group(0), providedValues.get(placeholderName)); - } - return replacementValues; - } - private GoogleDriveBaseResume getBaseResume(UUID baseResumeId, UUID userId) { return baseResumeRepository.findByIdAndConnectionUserId(baseResumeId, userId) .orElseThrow(() -> new ResourceNotFoundException("Base resume not found with id: " + baseResumeId)); diff --git a/src/main/java/com/jobtracker/service/SdkGoogleDriveApiClient.java b/src/main/java/com/jobtracker/service/SdkGoogleDriveApiClient.java index 059577f..ccec425 100644 --- a/src/main/java/com/jobtracker/service/SdkGoogleDriveApiClient.java +++ b/src/main/java/com/jobtracker/service/SdkGoogleDriveApiClient.java @@ -252,8 +252,7 @@ public void replaceGoogleDocPlaceholders(String accessToken, String documentId, .filter(entry -> entry.getKey() != null && !entry.getKey().isBlank()) .map(entry -> new Request().setReplaceAllText(new ReplaceAllTextRequest() .setContainsText(new SubstringMatchCriteria() - // Supports both legacy keys (SUMMARY) and detected tokens ({{ SUMMARY }}). - .setText(resolvePlaceholderToken(entry.getKey())) + .setText(toPlaceholderToken(entry.getKey())) .setMatchCase(true)) .setReplaceText(entry.getValue() == null ? "" : entry.getValue()))) .toList(); @@ -361,16 +360,7 @@ private DriveFileMetadata toDriveFileMetadata(File file) { file.getWebViewLink()); } - /** - * Accepts both legacy placeholder keys (e.g. {@code SUMMARY}) and already wrapped tokens - * (e.g. {@code {{ SUMMARY }}}) so callers can replace placeholders exactly as detected from - * template content while remaining backward compatible with existing callers. - */ - private String resolvePlaceholderToken(String value) { - String trimmed = value.trim(); - if (trimmed.startsWith("{{") && trimmed.endsWith("}}")) { - return trimmed; - } - return "{{" + trimmed + "}}"; + private String toPlaceholderToken(String key) { + return "{{" + key.trim() + "}}"; } } diff --git a/src/test/java/com/jobtracker/integration/GoogleDriveControllerIT.java b/src/test/java/com/jobtracker/integration/GoogleDriveControllerIT.java index f230c86..41be3b3 100644 --- a/src/test/java/com/jobtracker/integration/GoogleDriveControllerIT.java +++ b/src/test/java/com/jobtracker/integration/GoogleDriveControllerIT.java @@ -344,7 +344,7 @@ void detectResumePlaceholders_shouldReturnTemplatePlaceholders() throws Exceptio .content("{\"baseResumeId\":\"" + resume.getId() + "\"}")) .andExpect(status().isOk()) .andExpect(jsonPath("$.baseResumeId").value(resume.getId().toString())) - .andExpect(jsonPath("$.applicationId").isEmpty()) + .andExpect(jsonPath("$.applicationId").doesNotExist()) .andExpect(jsonPath("$.placeholders[0]").value("SUMMARY")) .andExpect(jsonPath("$.placeholders[1]").value("SKILLS")); } @@ -398,11 +398,11 @@ void generateResume_shouldReplaceTemplatePlaceholdersAndReturnGeneratedResume() } @Test - void generateResume_shouldReplacePlaceholdersWhenTemplateContainsWhitespaceInBraces() throws Exception { + void generateResume_shouldKeepUnresolvedPlaceholdersInResponse() throws Exception { GoogleDriveConnection connection = googleDriveConnectionRepository.save(buildConnectionWithRootFolder()); GoogleDriveBaseResume resume = buildBaseResume(connection); googleDriveBaseResumeRepository.save(resume); - googleDriveApiClient.setDocumentText("resume-file-id", "{{ SUMMARY }}\n{{SKILLS}}\n{{UNKNOWN}}"); + googleDriveApiClient.setDocumentText("resume-file-id", "{{SUMMARY}}\n{{SKILLS}}\n{{UNKNOWN}}"); JobApplication application = new JobApplication(); application.setUser(userRepository.findByEmail("driveuser@example.com").orElseThrow()); @@ -561,10 +561,7 @@ public void replaceGoogleDocPlaceholders(String accessToken, String documentId, if (token == null || token.isBlank()) { continue; } - String trimmedToken = token.trim(); - String placeholderToken = trimmedToken.startsWith("{{") && trimmedToken.endsWith("}}") - ? trimmedToken - : "{{" + token + "}}"; + String placeholderToken = "{{" + token.trim() + "}}"; updatedText = updatedText.replace(placeholderToken, replacement); } documentTextById.put(documentId, updatedText); diff --git a/src/test/java/com/jobtracker/unit/ResumeGenerationTemplateFlowTest.java b/src/test/java/com/jobtracker/unit/ResumeGenerationTemplateFlowTest.java index 0848ba3..8a83d43 100644 --- a/src/test/java/com/jobtracker/unit/ResumeGenerationTemplateFlowTest.java +++ b/src/test/java/com/jobtracker/unit/ResumeGenerationTemplateFlowTest.java @@ -46,7 +46,7 @@ class ResumeGenerationTemplateFlowTest { @Mock private SecurityUtils securityUtils; @Test - void generateResume_shouldReplaceSpacedAndUnspacedPlaceholdersFromTemplateContent() { + void generateResume_shouldReplaceStrictPlaceholdersFromProvidedValues() { ResumeGenerationService service = new ResumeGenerationService( googleDriveApiClient, new GoogleDriveProperties("client", "secret", "cb", "frontend", "auth", "token"), @@ -105,14 +105,14 @@ void generateResume_shouldReplaceSpacedAndUnspacedPlaceholdersFromTemplateConten GoogleDriveApiClient.GOOGLE_DOC_MIME_TYPE, "https://docs.google.com/document/d/copied-doc-id/edit" )); - AtomicReference mockDocumentContent = new AtomicReference<>("{{ SUMMARY }}\n{{SKILLS}}\n{{UNMAPPED}}"); + AtomicReference mockDocumentContent = new AtomicReference<>("{{SUMMARY}}\n{{SKILLS}}\n{{UNMAPPED}}"); when(googleDriveApiClient.readGoogleDocText("access-token", "copied-doc-id")) .thenAnswer(invocation -> mockDocumentContent.get()); doAnswer(invocation -> { Map replacements = invocation.getArgument(2); String updated = mockDocumentContent.get(); for (Map.Entry entry : replacements.entrySet()) { - updated = updated.replace(entry.getKey(), entry.getValue()); + updated = updated.replace("{{" + entry.getKey().trim() + "}}", entry.getValue()); } mockDocumentContent.set(updated); return null; @@ -135,8 +135,7 @@ void generateResume_shouldReplaceSpacedAndUnspacedPlaceholdersFromTemplateConten ArgumentCaptor> valuesCaptor = ArgumentCaptor.forClass(Map.class); verify(googleDriveApiClient).replaceGoogleDocPlaceholders(eq("access-token"), eq("copied-doc-id"), valuesCaptor.capture()); - assertThat(valuesCaptor.getValue()).containsEntry("{{ SUMMARY }}", "Senior Java Engineer"); - assertThat(valuesCaptor.getValue()).containsEntry("{{SKILLS}}", "Spring Boot, PostgreSQL"); - assertThat(valuesCaptor.getValue()).doesNotContainKeys("SUMMARY", "SKILLS"); + assertThat(valuesCaptor.getValue()).containsEntry("SUMMARY", "Senior Java Engineer"); + assertThat(valuesCaptor.getValue()).containsEntry("SKILLS", "Spring Boot, PostgreSQL"); } } From 2d3a6b781dbda226c82b9e854c3b0706a67dde18 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 27 May 2026 18:33:28 +0000 Subject: [PATCH 8/9] Align placeholder flow with strict backend contract --- .../com/jobtracker/integration/GoogleDriveControllerIT.java | 3 +++ .../com/jobtracker/unit/ResumeGenerationTemplateFlowTest.java | 1 + 2 files changed, 4 insertions(+) diff --git a/src/test/java/com/jobtracker/integration/GoogleDriveControllerIT.java b/src/test/java/com/jobtracker/integration/GoogleDriveControllerIT.java index 41be3b3..4ada54c 100644 --- a/src/test/java/com/jobtracker/integration/GoogleDriveControllerIT.java +++ b/src/test/java/com/jobtracker/integration/GoogleDriveControllerIT.java @@ -435,6 +435,9 @@ void generateResume_shouldKeepUnresolvedPlaceholdersInResponse() throws Exceptio .andExpect(jsonPath("$.values.SUMMARY").value("Senior Java Engineer")) .andExpect(jsonPath("$.values.SKILLS").value("Spring Boot, PostgreSQL")) .andExpect(jsonPath("$.placeholders[0]").value("UNKNOWN")); + + assertThat(googleDriveApiClient.readGoogleDocText(connection.getAccessToken(), "copied-file")) + .contains("{{UNKNOWN}}"); } private GoogleDriveConnection buildConnectionWithRootFolder() { diff --git a/src/test/java/com/jobtracker/unit/ResumeGenerationTemplateFlowTest.java b/src/test/java/com/jobtracker/unit/ResumeGenerationTemplateFlowTest.java index 8a83d43..25a2e35 100644 --- a/src/test/java/com/jobtracker/unit/ResumeGenerationTemplateFlowTest.java +++ b/src/test/java/com/jobtracker/unit/ResumeGenerationTemplateFlowTest.java @@ -137,5 +137,6 @@ void generateResume_shouldReplaceStrictPlaceholdersFromProvidedValues() { verify(googleDriveApiClient).replaceGoogleDocPlaceholders(eq("access-token"), eq("copied-doc-id"), valuesCaptor.capture()); assertThat(valuesCaptor.getValue()).containsEntry("SUMMARY", "Senior Java Engineer"); assertThat(valuesCaptor.getValue()).containsEntry("SKILLS", "Spring Boot, PostgreSQL"); + assertThat(mockDocumentContent.get()).contains("{{UNMAPPED}}"); } } From b12932e0588f521bcefe5a7a7ec874023a75a444 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 27 May 2026 18:58:16 +0000 Subject: [PATCH 9/9] Add explicit CV template placeholder endpoints contracts --- .../controller/GoogleDriveController.java | 6 +-- .../ResumePlaceholderDetectionRequest.java | 13 +++++++ .../ResumePlaceholderDetectionResponse.java | 12 ++++++ .../dto/gdrive/ResumePlaceholderResponse.java | 15 +++----- .../service/ResumeGenerationService.java | 38 +++++-------------- .../integration/GoogleDriveControllerIT.java | 2 + .../ResumeGenerationTemplateFlowTest.java | 2 +- 7 files changed, 45 insertions(+), 43 deletions(-) create mode 100644 src/main/java/com/jobtracker/dto/gdrive/ResumePlaceholderDetectionRequest.java create mode 100644 src/main/java/com/jobtracker/dto/gdrive/ResumePlaceholderDetectionResponse.java diff --git a/src/main/java/com/jobtracker/controller/GoogleDriveController.java b/src/main/java/com/jobtracker/controller/GoogleDriveController.java index 9550e77..92e051b 100644 --- a/src/main/java/com/jobtracker/controller/GoogleDriveController.java +++ b/src/main/java/com/jobtracker/controller/GoogleDriveController.java @@ -112,8 +112,8 @@ public ResponseEntity copyBaseResume( @Operation(summary = "Detect placeholders in a configured base resume") @PreAuthorize("hasRole('BETA')") @PostMapping("/resume-placeholders") - public ResponseEntity detectResumePlaceholders( - @Valid @RequestBody ResumePlaceholderRequest request) { + public ResponseEntity detectResumePlaceholders( + @Valid @RequestBody ResumePlaceholderDetectionRequest request) { return ResponseEntity.ok(resumeGenerationService.detectPlaceholders(request)); } @@ -124,6 +124,6 @@ public ResponseEntity generateResume( @PathVariable UUID applicationId, @Valid @RequestBody ResumePlaceholderRequest request) { return ResponseEntity.status(HttpStatus.CREATED) - .body(resumeGenerationService.generateResume(applicationId, request)); + .body(resumeGenerationService.generateTemplateResume(applicationId, request)); } } diff --git a/src/main/java/com/jobtracker/dto/gdrive/ResumePlaceholderDetectionRequest.java b/src/main/java/com/jobtracker/dto/gdrive/ResumePlaceholderDetectionRequest.java new file mode 100644 index 0000000..96995cb --- /dev/null +++ b/src/main/java/com/jobtracker/dto/gdrive/ResumePlaceholderDetectionRequest.java @@ -0,0 +1,13 @@ +package com.jobtracker.dto.gdrive; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; + +import java.util.UUID; + +@Schema(description = "Request for resume placeholder detection") +public record ResumePlaceholderDetectionRequest( + @Schema(description = "Configured base resume identifier") + @NotNull(message = "baseResumeId is required") + UUID baseResumeId +) {} diff --git a/src/main/java/com/jobtracker/dto/gdrive/ResumePlaceholderDetectionResponse.java b/src/main/java/com/jobtracker/dto/gdrive/ResumePlaceholderDetectionResponse.java new file mode 100644 index 0000000..fb2f528 --- /dev/null +++ b/src/main/java/com/jobtracker/dto/gdrive/ResumePlaceholderDetectionResponse.java @@ -0,0 +1,12 @@ +package com.jobtracker.dto.gdrive; + +import io.swagger.v3.oas.annotations.media.Schema; + +import java.util.List; +import java.util.UUID; + +@Schema(description = "Resume template placeholder detection result") +public record ResumePlaceholderDetectionResponse( + UUID baseResumeId, + List placeholders +) {} diff --git a/src/main/java/com/jobtracker/dto/gdrive/ResumePlaceholderResponse.java b/src/main/java/com/jobtracker/dto/gdrive/ResumePlaceholderResponse.java index 49b19e9..b504cc6 100644 --- a/src/main/java/com/jobtracker/dto/gdrive/ResumePlaceholderResponse.java +++ b/src/main/java/com/jobtracker/dto/gdrive/ResumePlaceholderResponse.java @@ -7,20 +7,15 @@ import java.util.Map; import java.util.UUID; -@Schema(description = "Resume placeholder detection or generation result") +@Schema(description = "Generated resume from template placeholders") public record ResumePlaceholderResponse( UUID applicationId, UUID baseResumeId, - List placeholders, - Map values, String copiedFileId, - String copiedFileName, - String documentWebViewLink, String pdfFileId, - String pdfFileName, - String pdfWebViewLink, - String vacancyFolderId, - String vacancyFolderName, - String vacancyFolderWebViewLink, + String documentUrl, + String pdfUrl, + Map values, + List placeholders, LocalDateTime generatedAt ) {} diff --git a/src/main/java/com/jobtracker/service/ResumeGenerationService.java b/src/main/java/com/jobtracker/service/ResumeGenerationService.java index 14e4b5b..07bc777 100644 --- a/src/main/java/com/jobtracker/service/ResumeGenerationService.java +++ b/src/main/java/com/jobtracker/service/ResumeGenerationService.java @@ -1,6 +1,8 @@ package com.jobtracker.service; import com.jobtracker.config.GoogleDriveProperties; +import com.jobtracker.dto.gdrive.ResumePlaceholderDetectionRequest; +import com.jobtracker.dto.gdrive.ResumePlaceholderDetectionResponse; import com.jobtracker.dto.gdrive.ResumePlaceholderRequest; import com.jobtracker.dto.gdrive.ResumePlaceholderResponse; import com.jobtracker.entity.GoogleDriveBaseResume; @@ -51,32 +53,20 @@ public ResumeGenerationService(GoogleDriveApiClient googleDriveApiClient, } @Transactional - public ResumePlaceholderResponse detectPlaceholders(ResumePlaceholderRequest request) { + public ResumePlaceholderDetectionResponse detectPlaceholders(ResumePlaceholderDetectionRequest request) { UUID userId = securityUtils.getCurrentUserId(); GoogleDriveConnection connection = getConnectionWithFreshAccessToken(); GoogleDriveBaseResume baseResume = getBaseResume(request.baseResumeId(), userId); String documentText = googleDriveApiClient.readGoogleDocText(connection.getAccessToken(), baseResume.getGoogleFileId()); - return new ResumePlaceholderResponse( - null, + return new ResumePlaceholderDetectionResponse( baseResume.getId(), - detectPlaceholders(documentText), - Map.of(), - null, - null, - null, - null, - null, - null, - null, - null, - null, - null + detectPlaceholders(documentText) ); } @Transactional - public ResumePlaceholderResponse generateResume(UUID applicationId, ResumePlaceholderRequest request) { + public ResumePlaceholderResponse generateTemplateResume(UUID applicationId, ResumePlaceholderRequest request) { UUID userId = securityUtils.getCurrentUserId(); GoogleDriveConnection connection = getConnectionWithFreshAccessToken(); JobApplication application = applicationRepository.findByIdAndUserId(applicationId, userId) @@ -131,17 +121,12 @@ public ResumePlaceholderResponse generateResume(UUID applicationId, ResumePlaceh return new ResumePlaceholderResponse( application.getId(), baseResume.getId(), - remainingPlaceholders, - values, copiedFile.id(), - copiedFile.name(), - copiedDocumentUrl, pdfFile.id(), - pdfFile.name(), + copiedDocumentUrl, resolveDocumentLink(pdfFile), - vacancyFolder.id(), - vacancyFolder.name(), - resolveFolderLink(vacancyFolder.id(), vacancyFolder.webViewLink()), + values, + remainingPlaceholders, generatedAt ); } @@ -260,9 +245,4 @@ private String resolveDocumentLink(GoogleDriveApiClient.DriveFileMetadata file) : "https://docs.google.com/document/d/" + file.id() + "/edit"; } - private String resolveFolderLink(String folderId, String webViewLink) { - return StringUtils.hasText(webViewLink) - ? webViewLink - : "https://drive.google.com/drive/folders/" + folderId; - } } diff --git a/src/test/java/com/jobtracker/integration/GoogleDriveControllerIT.java b/src/test/java/com/jobtracker/integration/GoogleDriveControllerIT.java index 4ada54c..9ba59b5 100644 --- a/src/test/java/com/jobtracker/integration/GoogleDriveControllerIT.java +++ b/src/test/java/com/jobtracker/integration/GoogleDriveControllerIT.java @@ -390,6 +390,8 @@ void generateResume_shouldReplaceTemplatePlaceholdersAndReturnGeneratedResume() .andExpect(jsonPath("$.placeholders").isEmpty()) .andExpect(jsonPath("$.copiedFileId").value("copied-file")) .andExpect(jsonPath("$.pdfFileId").value("pdf-file")) + .andExpect(jsonPath("$.documentUrl").value("https://docs.google.com/document/d/copied-file/edit")) + .andExpect(jsonPath("$.pdfUrl").value("https://docs.google.com/document/d/pdf-file/edit")) .andExpect(jsonPath("$.generatedAt").isNotEmpty()); JobApplication savedApplication = applicationRepository.findById(application.getId()).orElseThrow(); diff --git a/src/test/java/com/jobtracker/unit/ResumeGenerationTemplateFlowTest.java b/src/test/java/com/jobtracker/unit/ResumeGenerationTemplateFlowTest.java index 25a2e35..d0f144c 100644 --- a/src/test/java/com/jobtracker/unit/ResumeGenerationTemplateFlowTest.java +++ b/src/test/java/com/jobtracker/unit/ResumeGenerationTemplateFlowTest.java @@ -125,7 +125,7 @@ void generateResume_shouldReplaceStrictPlaceholdersFromProvidedValues() { "https://drive.google.com/file/d/pdf-id/view" )); - service.generateResume(APPLICATION_ID, new ResumePlaceholderRequest( + service.generateTemplateResume(APPLICATION_ID, new ResumePlaceholderRequest( BASE_RESUME_ID, Map.of( "SUMMARY", "Senior Java Engineer",