diff --git a/.env.production.example b/.env.production.example index cee47da..9ec164c 100644 --- a/.env.production.example +++ b/.env.production.example @@ -1,7 +1,7 @@ # Production deployments intentionally have no working secret defaults. # Copy this file to .env.production, replace every CHANGE-ME value, keep the # result out of version control, and pin a released OSApplyTrack version. -OSAPPLYTRACK_VERSION=1.67.0 +OSAPPLYTRACK_VERSION=1.68.0 POSTGRES_USER=applytrack POSTGRES_PASSWORD=CHANGE-ME-use-a-long-random-password POSTGRES_DB=applytrack diff --git a/BACKLOG.md b/BACKLOG.md index 5cc9eb0..5751351 100644 --- a/BACKLOG.md +++ b/BACKLOG.md @@ -175,7 +175,7 @@ review items and unsupported forms still need human attention. - [x] [#438 — Agent: phone numbers rejected — an international widget's dial code, and a résumé parse overwriting the box (Motion Recruitment, Robert Half) — fixed in 1.67.0](https://github.com/CryptoJones/OSApplyTrack/issues/438) - [x] [#439 — Agent: a generic placeholder ("Enter") is taken for a field's label — fixed in 1.67.0](https://github.com/CryptoJones/OSApplyTrack/issues/439) - [ ] [#440 — Agent: Manatal (careers-page.com) Radix selects and checkbox are not driven](https://github.com/CryptoJones/OSApplyTrack/issues/440) (Yo AI Labs: after #436 the résumé attaches and the hidden questions are found, but the Nationalities / years / salary Radix selects and the terms checkbox are custom widgets neither discovered nor driven) -- [ ] [#443 — Agent: drive Workday's application wizard end to end](https://github.com/CryptoJones/OSApplyTrack/issues/443) (`BrowserCanSubmit` still refuses Workday, so #277's account recipe never gets a run to fire in; NVIDIA JR2023533 was walked by hand on 2026-10-12 and the seven steps — create account under the `click_filter` overlay with the `beecatcher` honeypot empty, résumé autofill, My Information, My Experience, Application Questions, Voluntary Disclosures, Review → Submit — are mapped on the issue, with the résumé parser's junk Work Experience rows and the Enter-committed typeaheads as the two things a driver must handle) +- [x] [#443 — Agent: drive Workday's application wizard end to end](https://github.com/CryptoJones/OSApplyTrack/issues/443) (`BrowserCanSubmit` refused Workday, so #277's account recipe never got a run to fire in; NVIDIA JR2023533 was walked by hand on 2026-10-12. **1.68.0:** Workday is driven. The application starts at the manual flow (`…/apply/applyManually`, never the résumé autofill, whose parse explodes an "earlier roles" list into rows with no company); the way in presses Apply Manually and "Sign in with email" — the password box sits behind that choice — and the sign-in types the saved account key by key (a fill Workday sometimes never hears), presses the click_filter overlay beside the aria-hidden Sign In, waits for a complaint rather than a spinner, and loads the address again afterwards, because a sign-in from inside the flow draws "Something went wrong — please refresh". A step is read only once its controls have held still (the address and phone arrive a second after the names). Discovery and the sweep now see Workday's drop-down buttons (`aria-haspopup=listbox`, named by their label, their group, or — for a named button — the text above them), its search boxes (chips beside the input are the answer, committed with Enter), and radio groups required on their wrapper; the page header's own menus are never questions. The walk reads each step's menus so a question handed back carries its choices, ticks the terms box, attaches the résumé to the file input the packet never saw, declines a demographic drop-down Workday requires an answer to (never a guess), leaves a question the draft already answers alone, waits out the Save and Continue Workday disables while it autosaves, and hands back only starred fields when Next stays disabled. My Experience's Work Experience and Education rows are filled from the structured résumé (three jobs, every school: title, company, month/year spinbuttons, "I currently work here", description; school, Degree menu, Field of Study, years) and rows a draft already holds are completed, not duplicated. A long menu (Country) is typed at so the listbox jumps to the entry. The drafter's phone rule no longer answers "Phone Device Type" with the number. Verified live on nvidia.wd5 as a dry run: three rounds — page 2's rows and page 3's authorization pair handed back and answered — then every step to Review.) - [x] [#417 — Agent: a résumé dropzone whose wrapping label is only "Click or drag & drop PDF" is never recognised as the résumé (Ethos) — fixed in 1.67.0: dropzone text never names a file field; its row's title does](https://github.com/CryptoJones/OSApplyTrack/issues/417) - [x] [#418 — Agent: an application form folded behind "Easy Apply" is never revealed (N-iX) — fixed in 1.67.0: "Easy/Quick/One-click Apply" open the form (N-iX itself then ends at a reCAPTCHA v2 handoff)](https://github.com/CryptoJones/OSApplyTrack/issues/418) - [x] [#419 — Agent: a footer newsletter signup (mc4wp) is discovered as an application question (Tecbrains) — fixed in 1.67.0: newsletter forms are page widgets](https://github.com/CryptoJones/OSApplyTrack/issues/419) diff --git a/SPRINTS.md b/SPRINTS.md index cf4b0be..63512d8 100644 --- a/SPRINTS.md +++ b/SPRINTS.md @@ -176,8 +176,9 @@ All four shipped; **v1 is feature-complete** (every `plan.md` build step is done `/application`) get real questions this way; the same `PacketQuestion` shape Greenhouse's API gives, so the drafter and the submitter need no per-ATS code. The generic adapter (fill by label, refuse to click on an unmapped required - field) is behind the per-tenant `long_tail` opt-in (0023). Workday stays - manual, permanently: detected, packet prepared, routed to copy-and-open. + field) is behind the per-tenant `long_tail` opt-in (0023). Workday was routed + to copy-and-open until #443 (v1.68.0): its wizard is now walked signed in as + the candidate, with the account made on the way under #277's opt-in. - ✅ **Step 6 — Encryption at rest, operator allowlist & captcha code fallback** (#117, #156, #165, #166; v1.26–v1.27): Sensitive columns sealed with AES-256-GCM at rest (résumés, cover letters, packet answers, evidence screenshots, tenant API keys, bot tokens). Production audit closed diff --git a/api/ApplyTrack.Api.Tests/AtsProviderTests.cs b/api/ApplyTrack.Api.Tests/AtsProviderTests.cs index 285cf39..18069c9 100644 --- a/api/ApplyTrack.Api.Tests/AtsProviderTests.cs +++ b/api/ApplyTrack.Api.Tests/AtsProviderTests.cs @@ -58,11 +58,12 @@ public void The_apply_url_is_the_forms_page(string link, string expected) => Assert.Equal(expected, AtsProvider.ApplyUrl(link, AtsProvider.Detect(link, ""))); [Fact] - public void The_browser_drives_the_discovered_boards_and_never_an_aggregator_or_workday() + public void The_browser_drives_the_discovered_boards_and_workday_and_never_an_aggregator() { foreach (var p in new[] { AtsProvider.Greenhouse, AtsProvider.Lever, AtsProvider.Ashby, AtsProvider.Workable, AtsProvider.Breezy, AtsProvider.SmartRecruiters, AtsProvider.Join }) Assert.True(AtsProvider.BrowserCanSubmit(p, longTailOptIn: false), p); - Assert.False(AtsProvider.BrowserCanSubmit(AtsProvider.Workday, longTailOptIn: true)); + // Workday's wizard is walked signed in as the candidate (#443). + Assert.True(AtsProvider.BrowserCanSubmit(AtsProvider.Workday, longTailOptIn: false)); Assert.False(AtsProvider.BrowserCanSubmit(AtsProvider.SuccessFactors, longTailOptIn: true)); Assert.False(AtsProvider.BrowserCanSubmit(AtsProvider.Aggregator, longTailOptIn: true)); Assert.False(AtsProvider.BrowserCanSubmit(AtsProvider.Unknown, longTailOptIn: false)); @@ -174,7 +175,7 @@ public void Easy_apply_is_driven_only_by_its_own_switch() Assert.True(AtsProvider.BrowserCanSubmit(AtsProvider.LinkedInEasy, longTailOptIn: false, linkedInEasyOptIn: true)); // And it switches nothing else on. Assert.False(AtsProvider.BrowserCanSubmit(AtsProvider.Aggregator, longTailOptIn: true, linkedInEasyOptIn: true)); - Assert.False(AtsProvider.BrowserCanSubmit(AtsProvider.Workday, longTailOptIn: true, linkedInEasyOptIn: true)); + Assert.False(AtsProvider.BrowserCanSubmit(AtsProvider.SuccessFactors, longTailOptIn: true, linkedInEasyOptIn: true)); } [Fact] @@ -205,4 +206,26 @@ public void A_kept_linkedin_session_becomes_cookies_on_the_whole_site() [InlineData("https://jobs.lever.co/acme/1", null)] public void A_workday_posting_has_a_json_address_that_says_whether_it_still_exists(string link, string? expected) => Assert.Equal(expected, AtsProvider.WorkdayJobApi(link)); + + [Theory] + [InlineData("https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite/job/US-CA-Santa-Clara/Senior-Solutions-Architect--Inference-Service-Providers_JR2023533/apply?source=Eightfold", + "https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite/job/US-CA-Santa-Clara/Senior-Solutions-Architect--Inference-Service-Providers_JR2023533/apply/applyManually")] + [InlineData("https://allstate.wd5.myworkdayjobs.com/en-US/allstate_careers/job/USA---IL-Remote/Lead-Net-Software-Engineer_R34186/?src=x", + "https://allstate.wd5.myworkdayjobs.com/en-US/allstate_careers/job/USA---IL-Remote/Lead-Net-Software-Engineer_R34186/apply/applyManually")] + [InlineData("https://strongtie.wd1.myworkdayjobs.com/en-US/External/job/Plano-TX/Senior-Software-Engineer---Cloud-Services_R1", + "https://strongtie.wd1.myworkdayjobs.com/en-US/External/job/Plano-TX/Senior-Software-Engineer---Cloud-Services_R1/apply/applyManually")] + public void A_workday_application_starts_at_the_manual_flow_with_the_query_dropped(string link, string expected) + { + // The manual flow (#443): the sign-in choices, then step one — not the résumé autofill. + Assert.Equal(expected, AtsProvider.WorkdayApplyUrl(link)); + Assert.Equal(expected, AtsProvider.ApplyUrl(link, AtsProvider.Workday)); + } + + [Fact] + public void A_link_that_is_not_a_workday_posting_has_no_manual_flow() + { + Assert.Null(AtsProvider.WorkdayApplyUrl("https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite")); + Assert.Null(AtsProvider.WorkdayApplyUrl("https://boards.greenhouse.io/acme/jobs/1")); + Assert.Equal("https://boards.greenhouse.io/acme/jobs/1", AtsProvider.ApplyUrl("https://boards.greenhouse.io/acme/jobs/1", AtsProvider.Workday)); + } } diff --git a/api/ApplyTrack.Api.Tests/BrowserSubmitterTests.cs b/api/ApplyTrack.Api.Tests/BrowserSubmitterTests.cs index 5b90d2c..327cce4 100644 --- a/api/ApplyTrack.Api.Tests/BrowserSubmitterTests.cs +++ b/api/ApplyTrack.Api.Tests/BrowserSubmitterTests.cs @@ -40,6 +40,9 @@ public sealed partial class BrowserSubmitterTests : IAsyncLifetime private WebApplication? _fixture; private string _fixtureUrl = ""; private readonly Dictionary _wdAccounts = []; + /// What the Workday wizard fixture reported as the run moved through it (#443): each step + /// reached with the answers held so far, and the submission. + private readonly List _wdSteps = []; private string _wdHoneypot = ""; private readonly List> _posts = []; private readonly List> _laterPosts = []; @@ -570,6 +573,18 @@ function suggest(box) { ? Results.Content("

Candidate Home

My Applications

", "text/html") : Results.Content(WorkdaySignInHtml.Replace("", "
Wrong email or password
"), "text/html"); }); + // Workday's application wizard as nvidia.wd5 serves it (#443): the manual flow's sign-in + // choices, the email sign-in under a click_filter overlay, then My Information (a search + // box, a drop-down button, a required radio group), Application Questions (a drop-down), + // Voluntary Disclosures (a demographic drop-down, the terms box) and Review with Submit. + _fixture.MapGet("/wd/en-US/Careers/job/Omaha/Engineer_R1/apply/applyManually", () => Results.Content(WorkdayWizardHtml, "text/html")); + _fixture.MapPost("/wd/step", async (HttpRequest req) => + { + using var reader = new StreamReader(req.Body); + var body = await reader.ReadToEndAsync(); + lock (_wdSteps) _wdSteps.Add(body); + return Results.Ok(); + }); // Oracle Taleo's IAM pages as upmcjobs.taleo.net serves them (#277): Candidate Login with // New User, then New User Registration; the user name is the email. _fixture.MapGet("/careersection/iam/accessmanagement/login.jsf", () => Results.Content(TaleoLoginHtml, "text/html")); @@ -1493,6 +1508,93 @@ Click the object that does not fit the column pattern """; + private const string WorkdayWizardHtml = """ + +
+

Sign In

+ + +
+ + + + + + + + + + """; + private const string WorkdaySignInHtml = """

Sign In

@@ -2847,6 +2949,176 @@ public async Task An_email_step_with_a_terms_box_is_a_sign_in_and_the_box_is_tic private AccountCreator Creator() => new(new BrowserOptions { Endpoint = _ws, AllowPrivateTargets = true, TimeoutSeconds = 60 }, NullLogger.Instance); + private static AgentPacket WorkdayPacket(bool authorized) => new() + { + ApplicationName = "acme-engineer-wd.md", Provider = "workday", + Questions = + [ + new("legalName--firstName", "First Name", true, PacketQuestion.Text, [], PacketQuestion.Standard), + new("legalName--lastName", "Last Name", true, PacketQuestion.Text, [], PacketQuestion.Standard), + new("source--source", "How Did You Hear About Us?", true, PacketQuestion.Text, [], PacketQuestion.Custom), + new("country", "Country", true, PacketQuestion.Select, ["Canada", "United States of America"], PacketQuestion.Custom), + new("candidateIsPreviousWorker", "Have you previously worked for Acme?", true, PacketQuestion.Select, ["Yes", "No"], PacketQuestion.Custom), + .. authorized ? new PacketQuestion[] { new("legallyAuthorized", "Are you legally authorized to work in the country where this position is located?", true, PacketQuestion.Select, ["Yes", "No"], PacketQuestion.Custom) } : [], + ], + Answers = new() + { + ["legalName--firstName"] = "Ada", ["legalName--lastName"] = "Byte", ["source--source"] = "LinkedIn", + ["country"] = "United States", ["candidateIsPreviousWorker"] = "No", ["legallyAuthorized"] = "Yes", + }, + }; + + /// + /// Workday's wizard (#443), walked signed in with the saved account: the email sign-in behind + /// "Sign in with email" and its click_filter overlay; a search box committed with Enter; a + /// drop-down button chosen from its menu; a radio group required on its wrapper. A step + /// whose question the packet never saw is handed back with its choices, so the drafter can + /// answer it; the next run answers it and goes on — the demographic drop-down declined, the + /// terms box ticked — to Review, where a real run presses Submit. + /// + [SkippableFact] + public async Task A_workday_wizard_is_walked_signed_in_and_hands_back_what_it_meets() + { + Skip.IfNot(Available, "Node Playwright is not installed (npm ci)"); + var url = $"{_fixtureUrl}/wd/en-US/Careers/job/Omaha/Engineer_R1/apply/applyManually"; + var account = new BoardAccount(new Uri(_fixtureUrl).Host, "ada@example.com", "pw"); + lock (_wdSteps) _wdSteps.Clear(); + + // Run 1: Application Questions asks something the packet has never seen. + var first = await Submitter().RunAsync(url, WorkdayPacket(authorized: false), (Pdf, "resume.pdf"), dryRun: true, accounts: [account]); + Assert.True(first.Filled, first.Error); + Assert.False(first.Submitted); + Assert.Contains("legallyAuthorized", first.Unmapped); + var met = Assert.Single(first.Discovered ?? []); + Assert.Equal("legallyAuthorized", met.Id); + Assert.Equal(PacketQuestion.Select, met.Type); + Assert.Equal(["Yes", "No"], met.Options); + string[] steps; + lock (_wdSteps) steps = [.. _wdSteps]; + var step2 = Assert.Single(steps, s => s.StartsWith("step2 ")); + Assert.Contains("\"source\":[\"Linkedin Jobs\"]", step2); + Assert.Contains("\"previous\":\"false\"", step2); + Assert.Contains("\"country\":\"United States of America\"", step2); + Assert.Contains("\"first\":\"Ada\"", step2); + Assert.DoesNotContain(steps, s => s.StartsWith("step3 ")); + + // Run 2: answered, the walk reaches Review — ethnicity declined, the terms ticked — and a dry run stops there. + lock (_wdSteps) _wdSteps.Clear(); + var second = await Submitter().RunAsync(url, WorkdayPacket(authorized: true), (Pdf, "resume.pdf"), dryRun: true, accounts: [account]); + Assert.True(second.Filled, second.Error); + Assert.False(second.Submitted); + Assert.Empty(second.Unmapped); + Assert.Null(second.Discovered); + lock (_wdSteps) steps = [.. _wdSteps]; + var review = Assert.Single(steps, s => s.StartsWith("step4 ")); + Assert.Contains("\"authorized\":\"Yes\"", review); + Assert.Contains("\"ethnicity\":\"Decline to State (United States of America)\"", review); + Assert.Contains("\"terms\":true", review); + Assert.DoesNotContain(steps, s => s.StartsWith("submitted ")); + + // Run 3: for real — Submit is pressed on Review and Workday's confirmation read. + lock (_wdSteps) _wdSteps.Clear(); + var real = await Submitter().RunAsync(url, WorkdayPacket(authorized: true), (Pdf, "resume.pdf"), dryRun: false, accounts: [account]); + Assert.True(real.Submitted, real.Error); + Assert.Contains("successfully submitted", real.Confirmation, StringComparison.OrdinalIgnoreCase); + lock (_wdSteps) steps = [.. _wdSteps]; + Assert.Contains(steps, s => s.StartsWith("submitted ")); + } + + /// + /// The live check for Workday (#443): set APPLYTRACK_LIVE_WORKDAY_URL to a posting and + /// APPLYTRACK_LIVE_WORKDAY_USER / _PASSWORD to a candidate account on that tenant, + /// and the wizard is discovered and dry-run exactly as the worker does it — signed in, step + /// by step, never past Submit. Workday saves each step as a draft under the account, which + /// is why this cannot run with POSTs blocked and why it only ever dry-runs. What it stopped + /// on, and what it handed back, is printed. Never runs in CI. + /// + [SkippableFact] + public async Task Live_workday_wizard_is_walked_to_review_as_a_dry_run() + { + Skip.IfNot(Available, "Node Playwright is not installed (npm ci)"); + var url = Environment.GetEnvironmentVariable("APPLYTRACK_LIVE_WORKDAY_URL") ?? ""; + var user = Environment.GetEnvironmentVariable("APPLYTRACK_LIVE_WORKDAY_USER") ?? ""; + var password = Environment.GetEnvironmentVariable("APPLYTRACK_LIVE_WORKDAY_PASSWORD") ?? ""; + Skip.If(url.Length == 0 || user.Length == 0 || password.Length == 0, "set APPLYTRACK_LIVE_WORKDAY_URL/USER/PASSWORD to run this"); + var options = new BrowserOptions { Endpoint = _ws, Proxy = LiveProxy, TimeoutSeconds = 180 }; + var accounts = new[] { new BoardAccount(new Uri(url).Host, user, password) }; + var apply = ApplyTrack.Api.Agent.AtsProvider.ApplyUrl(url, ApplyTrack.Api.Agent.AtsProvider.Workday); + var questions = await new FormDiscoverer(options, new LiveLogger()).DiscoverAsync(apply, accounts: accounts); + Assert.NotNull(questions); + Console.WriteLine($"LIVE QUESTIONS: {string.Join(" | ", questions!.Select(q => $"{q.Id}={q.Label}[{q.Type}{(q.Required ? "*" : "")}{(q.Options.Count > 0 ? ":" + string.Join("/", q.Options) : "")}]"))}"); + var resume = new Resume + { + FullName = "Aaron K. Clark", Location = "Minden, Nebraska", + Experience = [new ResumeExperience("Ronin 48", "Principal Software Architect & ML Engineer", "Mar 2026 - Present", ["Designing overall system architecture for an AI startup."])], + Education = [new ResumeEducation("Eastern University", "MS", "Applied Artificial Intelligence", "2026 - Present")], + }; + var packet = new AgentPacket { ApplicationName = "live-wd.md", Provider = ApplyTrack.Api.Agent.AtsProvider.Workday, Questions = questions, Resume = resume }; + var ctx = new ApplyTrack.Api.Agent.AnswerContext(resume, + new AgentSettings { Phone = "5712975406", WorkAuthorization = "US citizen", Country = "United States", SalaryExpectation = "125000" }, + user, "", ""); + foreach (var q in packet.Questions.Where(q => q.Kind != PacketQuestion.Eeo && q.Type != PacketQuestion.File)) + { + var (answer, _) = ApplyTrack.Api.Agent.AnswerDrafter.Deterministic(q, ctx); + packet.Answers[q.Id] = answer ?? q.Id switch + { + "source--source" => "LinkedIn", + "legalName--firstName" => "Aaron", "legalName--lastName" => "Clark", + "addressLine1" => "PO Box 28", "city" => "Minden", "countryRegion" => "Nebraska", "postalCode" => "68959", + "phoneType" => "Home Cellular", "phoneNumber" => "5712975406", "candidateIsPreviousWorker" => "No", + _ => q.Options.Count > 0 ? q.Options[0] : q.Type == PacketQuestion.Textarea ? "Stand-in text." : "n/a", + }; + } + // The questions later steps hand back get the standing answers a drafter would give. + var submitter = new BrowserSubmitter(options, new LiveLogger()); + for (var round = 1; round <= 3; round++) + { + var dry = await submitter.RunAsync(url, packet, (Pdf, "resume.pdf"), dryRun: true, resumeText: "Aaron K. Clark.", accounts: accounts); + Console.WriteLine($"LIVE DRY {round}: filled={dry.Filled} url={dry.Url} mapped=[{string.Join(", ", dry.Mapped)}] unmapped=[{string.Join(", ", dry.Unmapped)}] error={dry.Error}"); + if (dry.Screenshot is { } png) await File.WriteAllBytesAsync(Path.Combine(Path.GetTempPath(), $"live-workday-dry{round}.png"), png); + if (dry.Discovered is not { Count: > 0 } met) break; + Console.WriteLine($"LIVE MET {round}: {string.Join(" | ", met.Select(q => $"{q.Id}={q.Label}[{q.Type}{(q.Required ? "*" : "")}{(q.Options.Count > 0 ? ":" + string.Join("/", q.Options) : "")}]"))}"); + foreach (var q in met) + { + if (packet.Questions.Any(p => p.Id == q.Id)) continue; + packet.Questions.Add(q); + var (answer, _) = ApplyTrack.Api.Agent.AnswerDrafter.Deterministic(q, ctx); + if (q.Kind == PacketQuestion.Eeo || q.Type == PacketQuestion.File) continue; + var standIn = q.Id switch + { + "linkedInAccount" => "https://www.linkedin.com/in/aaronkclark", + "jobTitle" => "Principal Software Architect & ML Engineer", "companyName" => "Ronin 48", "location" => "Remote", + "currentlyWorkHere" => "Yes", "schoolName" => "Eastern University", "degree" => "Masters", + _ when q.Options.Count > 0 => q.Options[0], + _ when System.Text.RegularExpressions.Regex.IsMatch(q.Id + " " + q.Label, @"date|year|month|gpa|grade", System.Text.RegularExpressions.RegexOptions.IgnoreCase) => "", + _ => "n/a", + }; + var chosen = answer ?? standIn; + if (chosen.Length > 0) packet.Answers[q.Id] = chosen; + } + } + } + + /// What a live run says as it goes, on the console, prefixed so it can be grepped. + private sealed class LiveLogger : ILogger + { + public IDisposable? BeginScope(TState state) where TState : notnull => null; + public bool IsEnabled(LogLevel logLevel) => true; + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) => + Console.WriteLine($"LIVE LOG [{logLevel}] {formatter(state, exception)}"); + } + + [SkippableFact] + public async Task A_workday_posting_with_no_saved_account_names_the_host_that_needs_one() + { + Skip.IfNot(Available, "Node Playwright is not installed (npm ci)"); + var url = $"{_fixtureUrl}/wd/en-US/Careers/job/Omaha/Engineer_R1/apply/applyManually"; + var outcome = await Submitter().RunAsync(url, WorkdayPacket(authorized: true), (Pdf, "resume.pdf"), dryRun: true); + Assert.False(outcome.Filled); + Assert.Equal(new Uri(_fixtureUrl).Host, outcome.NeedsAccountAt); + Assert.Contains("no account is saved", outcome.Error); + } + [SkippableFact] public async Task A_workday_account_is_created_verified_from_the_mailbox_and_proved_by_signing_in_before_it_is_kept() { diff --git a/api/ApplyTrack.Api.Tests/ErrorsEndpointTests.cs b/api/ApplyTrack.Api.Tests/ErrorsEndpointTests.cs index 251370d..0a8260a 100644 --- a/api/ApplyTrack.Api.Tests/ErrorsEndpointTests.cs +++ b/api/ApplyTrack.Api.Tests/ErrorsEndpointTests.cs @@ -121,13 +121,14 @@ await conn.ExecuteAsync( Assert.True(rows[captcha].GetProperty("captcha").GetBoolean()); Assert.Equal("you", rows[clicked].GetProperty("next").GetString()); Assert.Contains("may have gone through", rows[clicked].GetProperty("why").GetString()); - Assert.Equal("you", rows[workday].GetProperty("next").GetString()); - Assert.Contains("Workday", rows[workday].GetProperty("why").GetString()); + // Workday is driven since #443: a transient failure there is retried like any other board's. + Assert.Equal("retry", rows[workday].GetProperty("next").GetString()); + Assert.Contains("attempt", rows[workday].GetProperty("why").GetString()); Assert.Equal("you", rows[givenUp].GetProperty("next").GetString()); Assert.Contains("gave up after 3 failed runs", rows[givenUp].GetProperty("why").GetString()); - Assert.Equal(1, body.GetProperty("retrying").GetInt32()); - Assert.Equal(4, body.GetProperty("needs_you").GetInt32()); + Assert.Equal(2, body.GetProperty("retrying").GetInt32()); + Assert.Equal(3, body.GetProperty("needs_you").GetInt32()); } /// The view with the client holding . diff --git a/api/ApplyTrack.Api.Tests/FormDiscovererTests.cs b/api/ApplyTrack.Api.Tests/FormDiscovererTests.cs index c4b1dd1..19e88ea 100644 --- a/api/ApplyTrack.Api.Tests/FormDiscovererTests.cs +++ b/api/ApplyTrack.Api.Tests/FormDiscovererTests.cs @@ -283,6 +283,21 @@ public async Task A_form_collapsed_behind_apply_now_is_expanded_so_every_field_i Assert.Equal(0, _posts); } + [Fact] + public void A_workday_drop_down_button_is_a_select_keyed_by_its_name() + { + //