Skip to content

Validate timesheet uuid flags - #175

Open
cbento-gusto wants to merge 3 commits into
mainfrom
timesheet-uuid-validation
Open

Validate timesheet uuid flags#175
cbento-gusto wants to merge 3 commits into
mainfrom
timesheet-uuid-validation

Conversation

@cbento-gusto

@cbento-gusto cbento-gusto commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Summary

  • timesheet create, sync, show, and list now reject a malformed UUID before sending a request. Six arguments gain validation, none of which was checked before: --employee-uuid, --contractor-uuid, --job-uuid, --pay-schedule-uuid, --company-uuid, and show's positional time_sheet_uuid.
  • Adds validateUuid alongside the existing validateEnum in src/lib/parse.ts. It returns a blocked_on entry rather than an envelope, so the flag checks live inside the three existing validators and collect into one response instead of failing on the first bad argument. A caller who typos three flags sees all three, not one per retry.
  • show's positional stays on the fail-fast invalidUuid path, matching the other show commands. invalidUuid's hint becomes optional and show passes none: timesheet list only returns time sheet uuids for companies on third-party time tracking, so there is no single command to name, and validationFailure already documents omitting a hint over offering a generic one.
  • invalidUuid now shares its reason string with validateUuid instead of duplicating the echo-and-truncate logic, so the two surfaces cannot word the same defect differently.

Test fixtures move from short placeholders (emp-1, ps-1, ts-1, co-1) to real-shaped UUID constants, which is most of the diff in timesheet.test.ts.

Test plan

  • bun run test:all passes locally
  • Manual run of touched commands works against sandbox
  • --agent and --human output verified where touched

Every guard was mutation-tested individually: each was neutered in turn to confirm specific tests fail, then restored. The sandbox box is unchecked because all six paths short-circuit before any request is made, so there is nothing for a sandbox run to exercise beyond what the local run already covers.

Two pre-existing tests were checked for silent degradation. show's 404 test passed a non-uuid, which would now be rejected before the request and pass for the wrong reason; it takes a well-formed uuid and asserts the request was made. A smoke test asserting the get alias reaches the show handler pinned exit 3 (auth) and now needs a real uuid to get there, matching the rows for the other commands that validate.

DCO

  • Every commit is signed off (git commit -s) per the DCO

🤖 Generated with Claude Code

Signed-off-by: Cody Bentosino <cody.bentosino@gusto.com>
Signed-off-by: Cody Bentosino <cody.bentosino@gusto.com>
Signed-off-by: Cody Bentosino <cody.bentosino@gusto.com>
@cbento-gusto
cbento-gusto marked this pull request as ready for review September 2, 2026 18:01
@cbento-gusto
cbento-gusto requested review from a team and ashieh as code owners September 2, 2026 18:01

@tmfahey tmfahey left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Collecting the format errors into the existing validators instead of failing on the first bad flag is the right call - a caller who typos three uuids shouldn't need three round trips to find out.

Two things worth a look: --company-uuid is guarded on the flag but not on the GUSTO_COMPANY_UUID/session path that actually resolves it, and the older flag-level uuid check in contractor-payment.ts now words the same failure differently than validateUuid does. Two smaller notes inline.

Comment thread src/commands/timesheet.ts

export function validateTimesheetList(opts: TimesheetListInput): TimesheetListValidation {
const blocked: BlockedOn[] = [];
pushUuidBlockedOn("company-uuid", opts.companyUuid, blocked);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The flag is guarded but the env var isn't, so --company-uuid co-1 fails fast while GUSTO_COMPANY_UUID=co-1 still gets spliced into the request and comes back as whatever the API says about co-1. timesheetListHandler picks up the fallback at src/commands/timesheet.ts:492, and create/sync do it at src/lib/api-context.ts:268 (which then builds /v1/companies/${companyUuid}/...), both after this validator has already run.

Those two lines are the only non-test callers of getCompanyUuid, so checking there instead covers the flag, the env var and the session fallback in one place - and every other command's --company-uuid with it. Given the env var is the documented way to set the company, I'd rather have the check at the resolution point than on the flag.

Comment thread src/lib/parse.ts

/** blocked_on entry for a UUID-typed flag, or null when absent or valid. Absence is the caller's
* required-check to make, so a validator can collect format and presence errors in one envelope. */
export function validateUuid(field: string, value: string | undefined): BlockedOn | null {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's one older flag-level uuid check this doesn't absorb: src/commands/contractor-payment.ts:196 pushes a bare must be a valid UUID with no value echoed, so the same typo reports two different ways depending on which command you hit. Worth folding it into validateUuid, which probably means pushUuidBlockedOn moves out of timesheet.ts and lands next to this so other commands can reach it.

Comment thread src/commands/timesheet.ts
export type TimesheetCreateValidation = ValidationResult<TimesheetCreateBody>;

interface TimesheetCreateInput {
companyUuid?: string;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that the *Input interfaces carry companyUuid, the three *Opts interfaces that extend them redeclare it for no reason (lines 255, 263, 281). Three lines to drop.

const JOB_UUID = "1f2e3d4c-0000-1111-2222-333344445555";
const PAY_SCHEDULE_UUID = "1a2b3c4d-0000-1111-2222-333344445555";
const TIME_SHEET_UUID = "7a6b5c4d-0000-1111-2222-333344445555";
const COMPANY_UUID = "3c2b1a09-0000-1111-2222-333344445555";

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This exists because the shared TEST_AUTH fixture (src/lib/test-support.ts:36) hardcodes co-1, which the new check rejects. Fine to keep a local constant here, but the next command to validate --company-uuid hits the same wall, so I'd flag the fixture as a follow-up even though fixing it means touching every co-1 assertion in the nine other files that import it.

@jeff-gusto

Copy link
Copy Markdown
Contributor

Looks good overall and I think it's a good change to use real UUIDs in our test fixtures. Happy to re-review once Taylor's comments are addressed — especially the env variable bypass route!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants