feat(customerprofile): band customer-profile end to end (PR 2 of 5) - #32
Open
kshahbw wants to merge 17 commits into
Open
feat(customerprofile): band customer-profile end to end (PR 2 of 5)#32kshahbw wants to merge 17 commits into
kshahbw wants to merge 17 commits into
Conversation
Nested maps like contact were aliased between the returned update body and the caller's current resource. Deep-copy them so mutating one cannot silently corrupt the other, and add coverage for the aliasing case and for partial contact updates.
…harness Replaces test-only PersistentFlags on Cmd and a bespoke stdout-capture closure with the shared internal/testutil helpers already used by cmd/tendlc and cmd/sip. runCmd now wraps Cmd in a single package-level testRoot built once via testutil.NewTestRoot, reused (not rebuilt) on every call to avoid cobra's ancestor-flag cache going stale across repeated executions of the same package-level command.
TestRunCmdRootFlagsSurviveAcrossCalls makes two sequential runCmd calls and asserts the second one's --format table flag actually took effect. cobra caches a command's merged ancestor flags on first parse and never refreshes that cache for a different root object, so building a fresh testutil.NewTestRoot(Cmd) per call would silently drop the root's flags from the second call onward with no other test in this file able to notice, since create/list/get's payloads are already flat and render identically whether --plain is honored or not.
…n update
The API rejects an empty string on fields like website ("size must be
between 1 and 500") but accepts and applies JSON null, measured against
production. The documented way to clear a field is passing the flag with
an empty value, so that value must reach the server as null.
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
BLUF
band customer-profileend to end — create, list, get, update, delete, restore, history list, history get. Second of five PRs adding 10DLC Registration Center support.Stacked on #27. Review that first; this branch contains its commits.
A customer profile is a hard prerequisite for 10DLC brand registration, and a profile backs exactly one brand — so every brand needs a fresh one. The prerequisite chain is customer profile → brand → campaign.
Why this one is first
PR 1 was a horizontal slice — all plumbing, one visible command. That produced primitives designed against imagined consumers. The remaining PRs are sliced vertically instead: one resource, end to end, per PR.
Customer profiles go first because they're the smallest resource that exercises every shared pattern PRs 3–5 will reuse, against something real:
api.ForEachPage— one paging loop instead of four hand-rolled onesValidate/Build*Requestseparated from flag parsing--confirmgating for destructive operations--allpagination anddeepObjectfilter flagsThe decision this PR settles — closes #26
The API's
PUTis a full replacement: any field absent from the body is nulled server-side. The published spec documents no request body at all for the update operation, so this was measured.Building the payload from a typed struct would therefore silently delete every production field the CLI doesn't model. That's the same data loss the lossless-response decision exists to prevent, moved to write time — and worse, because it destroys customer data rather than hiding it.
Resolution: validate typed, emit from the read map.
updateGETs the profile, deep-copies the returned map, strips read-only fields, overlays only explicitly-changed flags, and PUTs that. Typed validation going in, losslessness coming out — a field nobody has heard of survives because it was never removed.Verified live, not just in tests: setting
website, then renaming with--namealone, leaveswebsiteintact. Same for a phone-only contact update preserving the contact's name and email.Measured API behavior this is built on
Every item below contradicts or exceeds the published spec. All probed against account 9901287.
PUTsemanticsversiononPUTreadOnlyin the schema. Omitting it returns the same 409 as a stale versionsize must be between 1 and 500){"softDeleted": false}. The documented{"deleted": false}returns 404 "Customer profile not found" whileGETreturns the recordsoftDeletedon every read; nodeletedfield exists{data, metadata}envelope, newest-first; version is atmetadata.versionoperation: CREATED, so an audited profile shows two creationscannot be assigned to another brandReported upstream as MV-23429.
Verification
go build,go vet, full suite, and the doc-contract test all pass.knownDriftuntouched.Full live end-to-end on 9901287, nine steps:
--website ""sends null, name preserved → 6. delete without--confirmexits 6 with zero stdout; with it,deleted:trueand the record stays retrievable atsoftDeleted:true→ 7. soft-deleted excluded fromlist, still returned byget→ 8. restore →softDeleted:false→ 9. history returns 8 entries newest-first;history get 0returns the originalCREATEDAll throwaway profiles cleaned up.
Agent-facing contracts
list/history listalways return arrays;get/history getalways return objects. Shape never depends on argument count.--confirmis a flag, never a prompt — no TTY check, so agents and humans get one contract. Missing it fails client-side: exit 6, zero HTTP requests, nothing on stdout.createis non-idempotent and has no--if-not-exists: a profile has no safe natural key, and reusing one would link an old brand's profile to new customer data. AGENTS.md's blanket--if-not-existsprinciple was amended to match, since this is the first shipped create without a key.Known gaps, deliberately deferred
pager.goinfinite-loop guard message says "N total elements remaining" where N is the grand total, overstating when some pages were already read. Cosmetic, on a rare inconsistency path — fix opportunistically in PR 3 when--allgains rate-limit pacing.restore, so a profile whose contact lacked anamecouldn't be restored. Unreachable in practice: the API schema markscontact.namerequired, so such a profile cannot exist.Related
context.Contextthroughapi.Client