fix(email): retire legacy SMTP follow-up
Keep follow-up draft generation but remove the direct application SMTP delivery boundary. Route users to the provider-aware Job email flow and return 410 for legacy API callers.
This commit is contained in:
@@ -125,9 +125,7 @@ public sealed class AiWorkspaceNotePersistenceTests
|
|||||||
var controller = new JobApplicationsController(
|
var controller = new JobApplicationsController(
|
||||||
db,
|
db,
|
||||||
summarizer,
|
summarizer,
|
||||||
Mock.Of<IAppEmailSender>(),
|
|
||||||
TestHostFactory.CreateUserManager(user).Object,
|
TestHostFactory.CreateUserManager(user).Object,
|
||||||
NullLogger<JobApplicationsController>.Instance,
|
|
||||||
Mock.Of<ICvTemplateRenderer>(),
|
Mock.Of<ICvTemplateRenderer>(),
|
||||||
Mock.Of<ICvPdfExporter>());
|
Mock.Of<ICvPdfExporter>());
|
||||||
controller.ControllerContext = new ControllerContext
|
controller.ControllerContext = new ControllerContext
|
||||||
|
|||||||
@@ -114,9 +114,7 @@ public sealed class InterviewPrepPersistenceTests
|
|||||||
var controller = new JobApplicationsController(
|
var controller = new JobApplicationsController(
|
||||||
db,
|
db,
|
||||||
summarizer,
|
summarizer,
|
||||||
Mock.Of<IAppEmailSender>(),
|
|
||||||
TestHostFactory.CreateUserManager(null).Object,
|
TestHostFactory.CreateUserManager(null).Object,
|
||||||
NullLogger<JobApplicationsController>.Instance,
|
|
||||||
Mock.Of<ICvTemplateRenderer>(),
|
Mock.Of<ICvTemplateRenderer>(),
|
||||||
Mock.Of<ICvPdfExporter>());
|
Mock.Of<ICvPdfExporter>());
|
||||||
controller.ControllerContext = new ControllerContext
|
controller.ControllerContext = new ControllerContext
|
||||||
|
|||||||
@@ -455,9 +455,7 @@ public sealed class JobApplicationsApplicationPackageTests
|
|||||||
var controller = new JobApplicationsController(
|
var controller = new JobApplicationsController(
|
||||||
db,
|
db,
|
||||||
summarizer,
|
summarizer,
|
||||||
Mock.Of<IAppEmailSender>(),
|
|
||||||
CreateUserManager(user).Object,
|
CreateUserManager(user).Object,
|
||||||
NullLogger<JobApplicationsController>.Instance,
|
|
||||||
renderer ?? new TestCvTemplateRenderer(),
|
renderer ?? new TestCvTemplateRenderer(),
|
||||||
exporter ?? new TestCvPdfExporter());
|
exporter ?? new TestCvPdfExporter());
|
||||||
controller.ControllerContext = new ControllerContext
|
controller.ControllerContext = new ControllerContext
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ public sealed class JobApplicationsAppliedDateHistoryTests
|
|||||||
|
|
||||||
private static JobApplicationsController CreateController(JobTrackerContext db, string userId)
|
private static JobApplicationsController CreateController(JobTrackerContext db, string userId)
|
||||||
{
|
{
|
||||||
var controller = new JobApplicationsController(db, Mock.Of<ISummarizerService>(), Mock.Of<IAppEmailSender>(), TestHostFactory.CreateUserManager().Object, NullLogger<JobApplicationsController>.Instance);
|
var controller = new JobApplicationsController(db, Mock.Of<ISummarizerService>(), TestHostFactory.CreateUserManager().Object);
|
||||||
controller.ControllerContext = new ControllerContext
|
controller.ControllerContext = new ControllerContext
|
||||||
{
|
{
|
||||||
HttpContext = new DefaultHttpContext
|
HttpContext = new DefaultHttpContext
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ public sealed class JobApplicationsAuthorizationTests
|
|||||||
{
|
{
|
||||||
var summarizer = new Mock<ISummarizerService>();
|
var summarizer = new Mock<ISummarizerService>();
|
||||||
var users = TestHostFactory.CreateUserManager();
|
var users = TestHostFactory.CreateUserManager();
|
||||||
return new JobApplicationsController(db, summarizer.Object, Mock.Of<IAppEmailSender>(), users.Object, NullLogger<JobApplicationsController>.Instance)
|
return new JobApplicationsController(db, summarizer.Object, users.Object)
|
||||||
{
|
{
|
||||||
ControllerContext = new ControllerContext
|
ControllerContext = new ControllerContext
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -295,7 +295,7 @@ public sealed class JobApplicationsEndpointBehaviorTests
|
|||||||
summarizer.Setup(x => x.SummarizeSectionAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<int>(), It.IsAny<int>())).ReturnsAsync("generated text");
|
summarizer.Setup(x => x.SummarizeSectionAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<int>(), It.IsAny<int>())).ReturnsAsync("generated text");
|
||||||
var users = CreateUserManager();
|
var users = CreateUserManager();
|
||||||
|
|
||||||
var controller = new JobApplicationsController(db, summarizer.Object, Mock.Of<IAppEmailSender>(), users.Object, NullLogger<JobApplicationsController>.Instance);
|
var controller = new JobApplicationsController(db, summarizer.Object, users.Object);
|
||||||
controller.ControllerContext = new ControllerContext
|
controller.ControllerContext = new ControllerContext
|
||||||
{
|
{
|
||||||
HttpContext = new DefaultHttpContext
|
HttpContext = new DefaultHttpContext
|
||||||
|
|||||||
@@ -103,9 +103,29 @@ public sealed class JobApplicationsFollowUpDraftTests
|
|||||||
Assert.Contains("Maria Recruiter", payload.LastCorrespondenceFrom ?? string.Empty);
|
Assert.Contains("Maria Recruiter", payload.LastCorrespondenceFrom ?? string.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Legacy_follow_up_delivery_is_gone_and_has_no_smtp_dependency()
|
||||||
|
{
|
||||||
|
using var db = CreateDb();
|
||||||
|
var controller = CreateController(db, Mock.Of<ISummarizerService>(), "user-1");
|
||||||
|
|
||||||
|
var result = controller.SendFollowUp(42, new SendFollowUpRequest(
|
||||||
|
"recruiter@example.test",
|
||||||
|
"Synthetic subject",
|
||||||
|
"Synthetic body",
|
||||||
|
DateTime.UtcNow.AddDays(3)), default);
|
||||||
|
|
||||||
|
var gone = Assert.IsType<ObjectResult>(result);
|
||||||
|
Assert.Equal(StatusCodes.Status410Gone, gone.StatusCode);
|
||||||
|
Assert.Equal("Legacy email delivery retired", Assert.IsType<ProblemDetails>(gone.Value).Title);
|
||||||
|
Assert.Empty(db.Correspondences);
|
||||||
|
Assert.DoesNotContain(typeof(JobApplicationsController).GetConstructors().Single().GetParameters(),
|
||||||
|
parameter => parameter.ParameterType == typeof(IAppEmailSender));
|
||||||
|
}
|
||||||
|
|
||||||
private static JobApplicationsController CreateController(JobTrackerContext db, ISummarizerService summarizer, string userId)
|
private static JobApplicationsController CreateController(JobTrackerContext db, ISummarizerService summarizer, string userId)
|
||||||
{
|
{
|
||||||
var controller = new JobApplicationsController(db, summarizer, Mock.Of<IAppEmailSender>(), CreateUserManager().Object, NullLogger<JobApplicationsController>.Instance);
|
var controller = new JobApplicationsController(db, summarizer, CreateUserManager().Object);
|
||||||
controller.ControllerContext = new ControllerContext
|
controller.ControllerContext = new ControllerContext
|
||||||
{
|
{
|
||||||
HttpContext = new DefaultHttpContext
|
HttpContext = new DefaultHttpContext
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ public sealed class JobApplicationsMariaDraftTests
|
|||||||
summarizer.Setup(x => x.SummarizeSectionAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<int>(), It.IsAny<int>())).ReturnsAsync("generated text");
|
summarizer.Setup(x => x.SummarizeSectionAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<int>(), It.IsAny<int>())).ReturnsAsync("generated text");
|
||||||
var users = CreateUserManager();
|
var users = CreateUserManager();
|
||||||
|
|
||||||
var controller = new JobApplicationsController(db, summarizer.Object, Mock.Of<IAppEmailSender>(), users.Object, NullLogger<JobApplicationsController>.Instance);
|
var controller = new JobApplicationsController(db, summarizer.Object, users.Object);
|
||||||
controller.ControllerContext = new ControllerContext
|
controller.ControllerContext = new ControllerContext
|
||||||
{
|
{
|
||||||
HttpContext = new DefaultHttpContext
|
HttpContext = new DefaultHttpContext
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ public sealed class JobApplicationsWorkflowSignalsTests
|
|||||||
|
|
||||||
private static JobApplicationsController CreateController(JobTrackerContext db, string userId)
|
private static JobApplicationsController CreateController(JobTrackerContext db, string userId)
|
||||||
{
|
{
|
||||||
var controller = new JobApplicationsController(db, Mock.Of<ISummarizerService>(), Mock.Of<IAppEmailSender>(), CreateUserManager().Object, NullLogger<JobApplicationsController>.Instance);
|
var controller = new JobApplicationsController(db, Mock.Of<ISummarizerService>(), CreateUserManager().Object);
|
||||||
controller.ControllerContext = new ControllerContext
|
controller.ControllerContext = new ControllerContext
|
||||||
{
|
{
|
||||||
HttpContext = new DefaultHttpContext
|
HttpContext = new DefaultHttpContext
|
||||||
|
|||||||
@@ -25,9 +25,7 @@ namespace JobTrackerApi.Controllers
|
|||||||
{
|
{
|
||||||
private readonly JobTrackerContext _db;
|
private readonly JobTrackerContext _db;
|
||||||
private readonly ISummarizerService _summarizer;
|
private readonly ISummarizerService _summarizer;
|
||||||
private readonly IAppEmailSender _email;
|
|
||||||
private readonly UserManager<ApplicationUser> _users;
|
private readonly UserManager<ApplicationUser> _users;
|
||||||
private readonly ILogger<JobApplicationsController> _logger;
|
|
||||||
private readonly ICvTemplateRenderer _cvTemplateRenderer;
|
private readonly ICvTemplateRenderer _cvTemplateRenderer;
|
||||||
private readonly ICvPdfExporter _cvPdfExporter;
|
private readonly ICvPdfExporter _cvPdfExporter;
|
||||||
private readonly AnalyticsService _analytics;
|
private readonly AnalyticsService _analytics;
|
||||||
@@ -35,14 +33,12 @@ namespace JobTrackerApi.Controllers
|
|||||||
private readonly IMemoryCache _cache;
|
private readonly IMemoryCache _cache;
|
||||||
private readonly IApplicationChecklistService _checklist;
|
private readonly IApplicationChecklistService _checklist;
|
||||||
|
|
||||||
public JobApplicationsController(JobTrackerContext db, ISummarizerService summarizer, IAppEmailSender email, UserManager<ApplicationUser> users, ILogger<JobApplicationsController> logger, ICvTemplateRenderer? cvTemplateRenderer = null, ICvPdfExporter? cvPdfExporter = null, AnalyticsService? analytics = null, IJobCvMatchService? matchService = null, IMemoryCache? cache = null, IApplicationChecklistService? checklist = null)
|
public JobApplicationsController(JobTrackerContext db, ISummarizerService summarizer, UserManager<ApplicationUser> users, ICvTemplateRenderer? cvTemplateRenderer = null, ICvPdfExporter? cvPdfExporter = null, AnalyticsService? analytics = null, IJobCvMatchService? matchService = null, IMemoryCache? cache = null, IApplicationChecklistService? checklist = null)
|
||||||
{
|
{
|
||||||
_checklist = checklist ?? new ApplicationChecklistService(db);
|
_checklist = checklist ?? new ApplicationChecklistService(db);
|
||||||
_db = db;
|
_db = db;
|
||||||
_summarizer = summarizer;
|
_summarizer = summarizer;
|
||||||
_email = email;
|
|
||||||
_users = users;
|
_users = users;
|
||||||
_logger = logger;
|
|
||||||
_cvTemplateRenderer = cvTemplateRenderer ?? new CvTemplateRenderer();
|
_cvTemplateRenderer = cvTemplateRenderer ?? new CvTemplateRenderer();
|
||||||
_cvPdfExporter = cvPdfExporter ?? new ThrowingCvPdfExporter();
|
_cvPdfExporter = cvPdfExporter ?? new ThrowingCvPdfExporter();
|
||||||
_analytics = analytics ?? new AnalyticsService(db);
|
_analytics = analytics ?? new AnalyticsService(db);
|
||||||
@@ -2252,55 +2248,13 @@ Job description:
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("{id:int}/send-followup")]
|
[HttpPost("{id:int}/send-followup")]
|
||||||
public async Task<IActionResult> SendFollowUp([FromRoute] int id, [FromBody] SendFollowUpRequest request, CancellationToken cancellationToken)
|
public IActionResult SendFollowUp([FromRoute] int id, [FromBody] SendFollowUpRequest request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var job = await _db.JobApplications
|
return StatusCode(StatusCodes.Status410Gone, new ProblemDetails
|
||||||
.Include(j => j.Company)
|
|
||||||
.FirstOrDefaultAsync(j => j.Id == id, cancellationToken);
|
|
||||||
|
|
||||||
if (job is null) return NotFound();
|
|
||||||
if (string.IsNullOrWhiteSpace(request.Subject)) return BadRequest("Subject is required.");
|
|
||||||
if (string.IsNullOrWhiteSpace(request.Body)) return BadRequest("Body is required.");
|
|
||||||
|
|
||||||
var toEmail = (request.ToEmail ?? job.Company?.RecruiterEmail ?? string.Empty).Trim();
|
|
||||||
if (string.IsNullOrWhiteSpace(toEmail)) return BadRequest("Recipient email is required.");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
await _email.SendAsync(toEmail, request.Subject.Trim(), request.Body.Trim(), cancellationToken);
|
Title = "Legacy email delivery retired",
|
||||||
}
|
Detail = "Use Job email to review the connected provider and explicitly confirm delivery.",
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Failed to send follow-up email for job {JobId} to {Email}", id, toEmail);
|
|
||||||
return Problem(statusCode: StatusCodes.Status503ServiceUnavailable, title: "Email delivery unavailable", detail: "Follow-up email could not be sent right now. Please try again later.");
|
|
||||||
}
|
|
||||||
|
|
||||||
_db.Correspondences.Add(new Correspondence
|
|
||||||
{
|
|
||||||
JobApplicationId = id,
|
|
||||||
From = "Me",
|
|
||||||
Subject = request.Subject.Trim(),
|
|
||||||
Channel = "Email",
|
|
||||||
Content = request.Body.Trim(),
|
|
||||||
Date = DateTime.Now,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (job.Company is not null)
|
|
||||||
{
|
|
||||||
job.Company.LastContactedAt = DateTime.Now;
|
|
||||||
if (request.NextFollowUpAt is not null)
|
|
||||||
{
|
|
||||||
job.Company.NextContactAt = request.NextFollowUpAt.Value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request.NextFollowUpAt is not null)
|
|
||||||
{
|
|
||||||
job.FollowUpAt = request.NextFollowUpAt.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
await _db.SaveChangesAsync(cancellationToken);
|
|
||||||
return NoContent();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("ai-metrics")]
|
[HttpGet("ai-metrics")]
|
||||||
|
|||||||
@@ -178,7 +178,6 @@ export default function JobDetailsDialog({ open, jobId, onClose, initialTab = 0,
|
|||||||
|
|
||||||
const [followUpDraft, setFollowUpDraft] = useState<FollowUpDraft | null>(null);
|
const [followUpDraft, setFollowUpDraft] = useState<FollowUpDraft | null>(null);
|
||||||
const [loadingDraft, setLoadingDraft] = useState(false);
|
const [loadingDraft, setLoadingDraft] = useState(false);
|
||||||
const [sendingDraft, setSendingDraft] = useState(false);
|
|
||||||
const [refreshingAi, setRefreshingAi] = useState(false);
|
const [refreshingAi, setRefreshingAi] = useState(false);
|
||||||
const [candidateFit, setCandidateFit] = useState<CandidateFit | null>(null);
|
const [candidateFit, setCandidateFit] = useState<CandidateFit | null>(null);
|
||||||
const [matchScore, setMatchScore] = useState<MatchScore | null>(null);
|
const [matchScore, setMatchScore] = useState<MatchScore | null>(null);
|
||||||
@@ -1265,29 +1264,15 @@ export default function JobDetailsDialog({ open, jobId, onClose, initialTab = 0,
|
|||||||
<Box sx={{ p: 1.5, borderRadius: 3, border: "1px solid", borderColor: "warning.main", backgroundColor: "background.default" }}>
|
<Box sx={{ p: 1.5, borderRadius: 3, border: "1px solid", borderColor: "warning.main", backgroundColor: "background.default" }}>
|
||||||
<Typography variant="overline">Manual send boundary</Typography>
|
<Typography variant="overline">Manual send boundary</Typography>
|
||||||
<Typography variant="body2" sx={{ color: "text.secondary" }}>
|
<Typography variant="body2" sx={{ color: "text.secondary" }}>
|
||||||
Generating or regenerating this grounded draft never sends recruiter email. The only outbound step is the explicit “Send and log email” action below.
|
Generating or regenerating this grounded draft never sends recruiter email. Copy it or open Job email, where the connected provider and final confirmation are shown before delivery.
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<TextField label={t("jobDetailsRecipient")} value={draftRecipient} onChange={(e) => setDraftRecipient(e.target.value)} helperText={`${t("jobDetailsRecipientHelp")} Manual send only — nothing is dispatched until you press send.`} />
|
<TextField label={t("jobDetailsRecipient")} value={draftRecipient} onChange={(e) => setDraftRecipient(e.target.value)} helperText={`${t("jobDetailsRecipientHelp")} This draft is not sent from the Follow up tab.`} />
|
||||||
<TextField label={t("jobDetailsSubject")} value={draftSubject} onChange={(e) => setDraftSubject(e.target.value)} />
|
<TextField label={t("jobDetailsSubject")} value={draftSubject} onChange={(e) => setDraftSubject(e.target.value)} />
|
||||||
<TextField label={t("jobDetailsDraft")} multiline minRows={8} value={draftBody} onChange={(e) => setDraftBody(e.target.value)} helperText="You can edit this before sending. Sending stays manual and logs the sent note back to correspondence." />
|
<TextField label={t("jobDetailsDraft")} multiline minRows={8} value={draftBody} onChange={(e) => setDraftBody(e.target.value)} helperText="You can edit and copy this draft. Provider delivery is available only from Job email." />
|
||||||
<Box sx={{ display: "flex", justifyContent: "space-between", gap: 1, flexWrap: "wrap" }}>
|
<Box sx={{ display: "flex", justifyContent: "space-between", gap: 1, flexWrap: "wrap" }}>
|
||||||
<Button variant="outlined" onClick={() => navigator.clipboard.writeText(`${draftSubject}\n\n${draftBody}`)}>{t("jobDetailsCopyDraft")}</Button>
|
<Button variant="outlined" onClick={() => navigator.clipboard.writeText(`${draftSubject}\n\n${draftBody}`)}>{t("jobDetailsCopyDraft")}</Button>
|
||||||
<Button variant="contained" disabled={sendingDraft || !draftSubject.trim() || !draftBody.trim()} onClick={async () => {
|
<Button variant="contained" href="/correspondence">{t("jobDetailsOpenJobEmail")}</Button>
|
||||||
if (!jobId) return;
|
|
||||||
setSendingDraft(true);
|
|
||||||
try {
|
|
||||||
await api.post(`/jobapplications/${jobId}/send-followup`, { toEmail: draftRecipient || null, subject: draftSubject, body: draftBody, nextFollowUpAt: followUpDraft.suggestedSendOn || null });
|
|
||||||
setJob((prev) => prev ? { ...prev, followUpAt: followUpDraft.suggestedSendOn } : prev);
|
|
||||||
readinessCache.clearCached();
|
|
||||||
setReadiness(null);
|
|
||||||
toast(t("jobDetailsFollowUpSent"), "success");
|
|
||||||
} catch (error: any) {
|
|
||||||
toast(getApiErrorMessage(error, t("jobDetailsFollowUpSendFailed")), "error");
|
|
||||||
} finally {
|
|
||||||
setSendingDraft(false);
|
|
||||||
}
|
|
||||||
}}>{sendingDraft ? t("jobDetailsSending") : t("jobDetailsSendAndLogEmail")}</Button>
|
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
) : <Typography sx={{ color: "text.secondary" }}>{t("jobDetailsNoDraftAvailable")}</Typography>}
|
) : <Typography sx={{ color: "text.secondary" }}>{t("jobDetailsNoDraftAvailable")}</Typography>}
|
||||||
|
|||||||
@@ -217,7 +217,7 @@ describe('end-to-end trust loop', () => {
|
|||||||
expect(await screen.findByText(/follow-up context/i)).toBeInTheDocument();
|
expect(await screen.findByText(/follow-up context/i)).toBeInTheDocument();
|
||||||
expect(await screen.findByText(/saved application package material is available for reuse/i)).toBeInTheDocument();
|
expect(await screen.findByText(/saved application package material is available for reuse/i)).toBeInTheDocument();
|
||||||
expect(await screen.findByText(/manual send boundary/i)).toBeInTheDocument();
|
expect(await screen.findByText(/manual send boundary/i)).toBeInTheDocument();
|
||||||
expect(await screen.findByText(/the only outbound step is the explicit “send and log email” action below/i)).toBeInTheDocument();
|
expect(await screen.findByText(/open job email.*connected provider.*final confirmation/i)).toBeInTheDocument();
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('tab', { name: /tailored cv/i }));
|
fireEvent.click(screen.getByRole('tab', { name: /tailored cv/i }));
|
||||||
|
|
||||||
@@ -243,6 +243,7 @@ describe('end-to-end trust loop', () => {
|
|||||||
fireEvent.click(screen.getByRole('tab', { name: /follow up/i }));
|
fireEvent.click(screen.getByRole('tab', { name: /follow up/i }));
|
||||||
|
|
||||||
expect(await screen.findByDisplayValue(/i wanted to follow up on the backend developer thread/i)).toBeInTheDocument();
|
expect(await screen.findByDisplayValue(/i wanted to follow up on the backend developer thread/i)).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole('link', { name: /open job email/i })).toHaveAttribute('href', '/correspondence');
|
||||||
expect(mockedApi.post.mock.calls.some(([url]) => url === '/jobapplications/42/send-followup')).toBe(false);
|
expect(mockedApi.post.mock.calls.some(([url]) => url === '/jobapplications/42/send-followup')).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1053,6 +1053,7 @@ export const translations = {
|
|||||||
jobDetailsSubject: "Subject",
|
jobDetailsSubject: "Subject",
|
||||||
jobDetailsDraft: "Draft",
|
jobDetailsDraft: "Draft",
|
||||||
jobDetailsCopyDraft: "Copy draft",
|
jobDetailsCopyDraft: "Copy draft",
|
||||||
|
jobDetailsOpenJobEmail: "Open Job email",
|
||||||
jobDetailsSendAndLogEmail: "Send and log email",
|
jobDetailsSendAndLogEmail: "Send and log email",
|
||||||
jobDetailsSending: "Sending...",
|
jobDetailsSending: "Sending...",
|
||||||
jobDetailsFollowUpSent: "Follow-up sent and logged.",
|
jobDetailsFollowUpSent: "Follow-up sent and logged.",
|
||||||
@@ -2183,6 +2184,7 @@ export const translations = {
|
|||||||
jobDetailsSubject: "Emne",
|
jobDetailsSubject: "Emne",
|
||||||
jobDetailsDraft: "Utkast",
|
jobDetailsDraft: "Utkast",
|
||||||
jobDetailsCopyDraft: "Kopier utkast",
|
jobDetailsCopyDraft: "Kopier utkast",
|
||||||
|
jobDetailsOpenJobEmail: "Åpne Job email",
|
||||||
jobDetailsSendAndLogEmail: "Send og loggfør e-post",
|
jobDetailsSendAndLogEmail: "Send og loggfør e-post",
|
||||||
jobDetailsSending: "Sender...",
|
jobDetailsSending: "Sender...",
|
||||||
jobDetailsFollowUpSent: "Oppfølging sendt og loggført.",
|
jobDetailsFollowUpSent: "Oppfølging sendt og loggført.",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import '@testing-library/jest-dom';
|
import '@testing-library/jest-dom';
|
||||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
import { fireEvent, render, screen } from '@testing-library/react';
|
||||||
import { ConfirmProvider } from './confirm';
|
import { ConfirmProvider } from './confirm';
|
||||||
import { PromptProvider } from './prompt';
|
import { PromptProvider } from './prompt';
|
||||||
import { ToastProvider } from './toast';
|
import { ToastProvider } from './toast';
|
||||||
@@ -92,7 +92,7 @@ afterEach(() => {
|
|||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('follow-up workspace shows thread grounding and keeps sending manual', async () => {
|
test('follow-up workspace keeps the draft editable and routes delivery to Job email', async () => {
|
||||||
renderDialog();
|
renderDialog();
|
||||||
|
|
||||||
fireEvent.click(await screen.findByRole('tab', { name: /follow up/i }));
|
fireEvent.click(await screen.findByRole('tab', { name: /follow up/i }));
|
||||||
@@ -100,20 +100,14 @@ test('follow-up workspace shows thread grounding and keeps sending manual', asyn
|
|||||||
expect(await screen.findByText(/follow-up context/i)).toBeInTheDocument();
|
expect(await screen.findByText(/follow-up context/i)).toBeInTheDocument();
|
||||||
expect(await screen.findByText(/saved application package material is available for reuse/i)).toBeInTheDocument();
|
expect(await screen.findByText(/saved application package material is available for reuse/i)).toBeInTheDocument();
|
||||||
expect(await screen.findByText(/saved cover letter available/i)).toBeInTheDocument();
|
expect(await screen.findByText(/saved cover letter available/i)).toBeInTheDocument();
|
||||||
expect(await screen.findByText(/manual send only/i)).toBeInTheDocument();
|
expect(await screen.findByText(/manual send boundary/i)).toBeInTheDocument();
|
||||||
|
expect(await screen.findByText(/open job email.*connected provider.*final confirmation/i)).toBeInTheDocument();
|
||||||
expect(await screen.findByDisplayValue(/i wanted to follow up on the backend developer thread/i)).toBeInTheDocument();
|
expect(await screen.findByDisplayValue(/i wanted to follow up on the backend developer thread/i)).toBeInTheDocument();
|
||||||
|
|
||||||
const draft = screen.getByDisplayValue(/i wanted to follow up on the backend developer thread/i);
|
const draft = screen.getByDisplayValue(/i wanted to follow up on the backend developer thread/i);
|
||||||
fireEvent.change(draft, { target: { value: 'Hi Maria,\n\nEdited follow-up.\n\nThanks,\nCasey' } });
|
fireEvent.change(draft, { target: { value: 'Hi Maria,\n\nEdited follow-up.\n\nThanks,\nCasey' } });
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: /send and log email/i }));
|
expect(draft).toHaveValue('Hi Maria,\n\nEdited follow-up.\n\nThanks,\nCasey');
|
||||||
|
expect(screen.getByRole('link', { name: /open job email/i })).toHaveAttribute('href', '/correspondence');
|
||||||
await waitFor(() => {
|
expect(mockedApi.post).not.toHaveBeenCalledWith('/jobapplications/42/send-followup', expect.anything());
|
||||||
expect(mockedApi.post).toHaveBeenCalledWith('/jobapplications/42/send-followup', {
|
|
||||||
toEmail: 'recruiter@acme.test',
|
|
||||||
subject: 'Re: Backend Developer application update',
|
|
||||||
body: 'Hi Maria,\n\nEdited follow-up.\n\nThanks,\nCasey',
|
|
||||||
nextFollowUpAt: expect.any(String),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user