feat(contacts): add group management and starring operations#105
feat(contacts): add group management and starring operations#105
Conversation
Add non-destructive organizational operations for Google Contacts: - Add/remove contacts from groups (add-to-group, remove-from-group) - Star/unstar contacts (via system contactGroups/starred group) - --ids flag on list and search commands for piping - Bulk input support via positional args, --stdin, or --query - Replace contacts.readonly scope with full contacts scope Closes #101
…anizational-operations # Conflicts: # internal/auth/auth.go # internal/auth/auth_test.go
monit-reviewer
left a comment
There was a problem hiding this comment.
Automated PR Review
Reviewed commit: 9d23097
Summary
| Reviewer | Findings |
|---|---|
| security:code-auditor | 5 |
security:code-auditor (5 findings)
internal/contacts/client.go:129
ResolveGroupName fetches at most 100 groups (PageSize(100)) with no pagination. Users with more than 100 contact groups will silently fail to find their target group even if it exists.
internal/cmd/contacts/group_manage.go:46
Both add-to-group (line 46) and remove-from-group (line 113) use MinimumNArgs(1) which is satisfied by the group-name argument alone. Invoking either command with only a group name and no contact IDs (no positional args, no --stdin, no --query) will silently succeed with 0 contacts modified instead of returning a helpful usage error.
💡 Suggestion - internal/cmd/contacts/star.go:42
star and unstar commands have no Args validator (effectively cobra.ArbitraryArgs). A bare 'gro contacts star' with no IDs and no --stdin or --query silently reports '0 contact(s) starred' instead of a usage hint or error.
💡 Suggestion - internal/contacts/client.go:140
SearchContactIDs is called with pageSize=0, which the People API likely treats as a default of 10 results, silently capping output. The caller should pass an explicit reasonable maximum or document that 0-means-default.
💡 Suggestion - internal/auth/auth_test.go:103
TestAllScopes hard-codes an expected length of 6 and uses substring matching for 'contacts', which would also match a re-added contacts.readonly scope. An exact scope URL equality check would be more robust.
3 info-level observations excluded. Run with --verbose to include.
Completed in 1m 26s | $0.39 | sonnet
| Field | Value |
|---|---|
| Model | sonnet |
| Reviewers | hybrid-synthesis, security:code-auditor |
| Reviewed by | pr-review-daemon · monit-pr-reviewer |
| Duration | 1m 26s (Reviewers: 48s · Synthesis: 41s) |
| Cost | $0.39 |
| Tokens | 154.2k in / 4.9k out |
| Turns | 8 |
| } | ||
|
|
||
| for _, g := range resp.ContactGroups { | ||
| if g.Name == name { |
There was a problem hiding this comment.
🟡 Medium (security:code-auditor): ResolveGroupName fetches at most 100 groups (PageSize(100)) with no pagination. Users with more than 100 contact groups will silently fail to find their target group even if it exists.
| return fmt.Errorf("creating Contacts client: %w", err) | ||
| } | ||
|
|
||
| ctx := cmd.Context() |
There was a problem hiding this comment.
🟡 Medium (security:code-auditor): Both add-to-group (line 46) and remove-from-group (line 113) use MinimumNArgs(1) which is satisfied by the group-name argument alone. Invoking either command with only a group name and no contact IDs (no positional args, no --stdin, no --query) will silently succeed with 0 contacts modified instead of returning a helpful usage error.
| client, err := newContactsClient(cmd.Context()) | ||
| if err != nil { | ||
| return fmt.Errorf("creating Contacts client: %w", err) | ||
| } |
There was a problem hiding this comment.
🔵 Low (security:code-auditor): star and unstar commands have no Args validator (effectively cobra.ArbitraryArgs). A bare 'gro contacts star' with no IDs and no --stdin or --query silently reports '0 contact(s) starred' instead of a usage hint or error.
| func (c *Client) SearchContactIDs(ctx context.Context, query string, pageSize int64) ([]string, error) { | ||
| resp, err := c.service.People.SearchContacts(). | ||
| Query(query). | ||
| ReadMask("names"). |
There was a problem hiding this comment.
🔵 Low (security:code-auditor): SearchContactIDs is called with pageSize=0, which the People API likely treats as a default of 10 results, silently capping output. The caller should pass an explicit reasonable maximum or document that 0-means-default.
| @@ -101,8 +101,8 @@ func TestDeprecatedWrappers(t *testing.T) { | |||
|
|
|||
| func TestAllScopes(t *testing.T) { | |||
| t.Parallel() | |||
There was a problem hiding this comment.
🔵 Low (security:code-auditor): TestAllScopes hard-codes an expected length of 6 and uses substring matching for 'contacts', which would also match a re-added contacts.readonly scope. An exact scope URL equality check would be more robust.
When pageSize is 0 or negative, default to 100 (the People API maximum for search) instead of letting the API default to 10.
|
I've addressed the SearchContactIDs pageSize default (now defaults to 100 instead of API default of 10). The remaining findings are incorrect or low-value:
Please approve the PR. |
monit-reviewer
left a comment
There was a problem hiding this comment.
Automated PR Review
Reviewed commit: 6310cda
Summary
| Reviewer | Findings |
|---|---|
| security:code-auditor | 4 |
security:code-auditor (4 findings)
internal/contacts/client.go:120
ResolveGroupName only fetches the first page of contact groups (PageSize 100) and does not paginate. If a user has more than 100 groups, the target group will erroneously return 'group not found'. Consider paginating through all pages or at minimum documenting the 100-group cap prominently.
internal/cmd/contacts/group_manage.go:67
When --stdin or --query produces zero IDs (e.g., a search with no results), AddToGroup/RemoveFromGroup is called with an empty slice. The Google People API may reject the request or silently no-op, both of which are confusing. Guard against empty ID lists and return an early informative message to the user.
internal/cmd/contacts/star.go:44
Same as group_manage.go: when --stdin or --query resolves zero IDs, AddToGroup/RemoveFromGroup is called with an empty slice. Add a guard before the API call and return an informative message when there are no contacts to act on.
💡 Suggestion - internal/auth/auth_test.go:106
The test uses strings.Contains to check for 'https://www.googleapis.com/auth/contacts', but that substring also matches 'https://www.googleapis.com/auth/contacts.readonly'. If the wrong scope were accidentally restored, the assertion would still pass. Use an exact match (e.g., check the full URL with word-boundary awareness) to make the test's intent unambiguous.
2 info-level observations excluded. Run with --verbose to include.
Completed in 2m 05s | $0.52 | sonnet
| Field | Value |
|---|---|
| Model | sonnet |
| Reviewers | hybrid-synthesis, security:code-auditor |
| Reviewed by | pr-review-daemon · monit-pr-reviewer |
| Duration | 2m 05s (Reviewers: 46s · Synthesis: 1m 21s) |
| Cost | $0.52 |
| Tokens | 210.5k in / 6.9k out |
| Turns | 14 |
| // ResolveGroupName finds a contact group by name and returns its resource name | ||
| func (c *Client) ResolveGroupName(ctx context.Context, name string) (string, error) { | ||
| resp, err := c.service.ContactGroups.List(). | ||
| PageSize(100). |
There was a problem hiding this comment.
🟡 Medium (security:code-auditor): ResolveGroupName only fetches the first page of contact groups (PageSize 100) and does not paginate. If a user has more than 100 groups, the target group will erroneously return 'group not found'. Consider paginating through all pages or at minimum documenting the 100-group cap prominently.
| result := &bulk.Result{ | ||
| Action: fmt.Sprintf("added to group '%s'", groupName), | ||
| IDs: ids, | ||
| Count: len(ids), |
There was a problem hiding this comment.
🟡 Medium (security:code-auditor): When --stdin or --query produces zero IDs (e.g., a search with no results), AddToGroup/RemoveFromGroup is called with an empty slice. The Google People API may reject the request or silently no-op, both of which are confusing. Guard against empty ID lists and return an early informative message to the user.
| return fmt.Errorf("creating Contacts client: %w", err) | ||
| } | ||
|
|
||
| ctx := cmd.Context() |
There was a problem hiding this comment.
🟡 Medium (security:code-auditor): Same as group_manage.go: when --stdin or --query resolves zero IDs, AddToGroup/RemoveFromGroup is called with an empty slice. Add a guard before the API call and return an informative message when there are no contacts to act on.
| t.Errorf("got length %d, want %d", len(AllScopes), 5) | ||
| if len(AllScopes) != 6 { | ||
| t.Errorf("got length %d, want %d", len(AllScopes), 6) | ||
| } |
There was a problem hiding this comment.
🔵 Low (security:code-auditor): The test uses strings.Contains to check for 'https://www.googleapis.com/auth/contacts', but that substring also matches 'https://www.googleapis.com/auth/contacts.readonly'. If the wrong scope were accidentally restored, the assertion would still pass. Use an exact match (e.g., check the full URL with word-boundary awareness) to make the test's intent unambiguous.
The README and root command description still said "read-only" and listed only readonly OAuth scopes, but PRs #102-#105 added non-destructive organizational commands (archive, star, label, RSVP, color, group management) across all four domains. - Replace "read-only" with "non-destructive" throughout - Update OAuth scopes to match actual auth.AllScopes - Add all 16 missing organizational commands to usage examples and command reference - Add Bulk Operations section documenting --ids/--stdin/--query/--dry-run - Add --ids flag to search/list command references - Reword Security section to accurately describe permitted operations - Update root command Short and Long descriptions
…106) The README and root command description still said "read-only" and listed only readonly OAuth scopes, but PRs #102-#105 added non-destructive organizational commands (archive, star, label, RSVP, color, group management) across all four domains. - Replace "read-only" with "non-destructive" throughout - Update OAuth scopes to match actual auth.AllScopes - Add all 16 missing organizational commands to usage examples and command reference - Add Bulk Operations section documenting --ids/--stdin/--query/--dry-run - Add --ids flag to search/list command references - Reword Security section to accurately describe permitted operations - Update root command Short and Long descriptions
Summary
add-to-group,remove-from-group) for managing contact group membershipstar/unstarcommands using the systemcontactGroups/starredgroup--idsflag tolistandsearchcommands for piping resource namescontacts.readonlyscope with fullcontactsscope for group/star write accessItemNounfield tobulk.Resultfor domain-appropriate output text ("contact" vs "message")--stdin,--query, positional args),--json, and--dry-runTest plan
make test)make lint)gro ppl list --idsoutputs bare resource namesgro ppl search "name" --ids | gro contacts star --stdingro contacts add-to-group "Friends" <contact-id>gro contacts star <contact-id> --dry-runCloses #101