gogcli/internal/cmd/contacts_export_test.go
Peter Steinberger e30d870b3f
feat(contacts): export vCards
Co-authored-by: Dinakar Sarbada <dinakars777@users.noreply.github.com>
2026-04-28 02:01:07 +01:00

195 lines
6.5 KiB
Go

package cmd
import (
"context"
"encoding/json"
"net/http"
"strings"
"testing"
"google.golang.org/api/people/v1"
)
func TestContactsExport_AllVCF_PaginatesAndIncludesCategories(t *testing.T) {
svc, closeSrv := newPeopleService(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
switch {
case strings.Contains(r.URL.Path, "people/me/connections") && r.Method == http.MethodGet:
if got := r.URL.Query().Get("personFields"); !strings.Contains(got, "memberships") || !strings.Contains(got, "birthdays") {
t.Fatalf("missing export fields: %q", got)
}
if r.URL.Query().Get("pageToken") == "" {
_ = json.NewEncoder(w).Encode(map[string]any{
"connections": []map[string]any{{
"resourceName": "people/c1",
"names": []map[string]any{{"givenName": "Ada", "familyName": "Lovelace", "displayName": "Ada Lovelace"}},
"emailAddresses": []map[string]any{
{"value": "ada@example.com", "type": "work"},
},
"memberships": []map[string]any{{
"contactGroupMembership": map[string]any{"contactGroupResourceName": "contactGroups/friends"},
}},
}},
"nextPageToken": "next",
})
return
}
_ = json.NewEncoder(w).Encode(map[string]any{
"connections": []map[string]any{{
"resourceName": "people/c2",
"names": []map[string]any{{"givenName": "Grace", "familyName": "Hopper", "displayName": "Grace Hopper"}},
"birthdays": []map[string]any{{"date": map[string]any{"month": 12, "day": 9}}},
}},
})
case strings.Contains(r.URL.Path, "contactGroups") && r.Method == http.MethodGet:
_ = json.NewEncoder(w).Encode(map[string]any{
"contactGroups": []map[string]any{
{"resourceName": "contactGroups/friends", "name": "Friends, Work", "groupType": "USER_CONTACT_GROUP"},
{"resourceName": "contactGroups/myContacts", "name": "Contacts", "groupType": "SYSTEM_CONTACT_GROUP"},
},
})
default:
http.NotFound(w, r)
}
}))
t.Cleanup(closeSrv)
stubPeopleServices(t, svc)
out := captureStdout(t, func() {
_ = captureStderr(t, func() {
err := Execute([]string{"--account", "a@b.com", "contacts", "export", "--all", "--page-size", "1", "--out", "-"})
if err != nil {
t.Fatalf("Execute: %v", err)
}
})
})
for _, want := range []string{
"BEGIN:VCARD\r\nVERSION:4.0\r\n",
"FN:Ada Lovelace\r\n",
"N:Lovelace;Ada;;;\r\n",
"EMAIL;TYPE=work:ada@example.com\r\n",
"CATEGORIES:Friends\\, Work\r\n",
"FN:Grace Hopper\r\n",
"BDAY:--1209\r\n",
} {
if !strings.Contains(out, want) {
t.Fatalf("missing %q in VCF:\n%s", want, out)
}
}
if got := strings.Count(out, "BEGIN:VCARD"); got != 2 {
t.Fatalf("expected 2 cards, got %d:\n%s", got, out)
}
}
func TestContactsExport_SelectorEmailExactMatch(t *testing.T) {
svc, closeSrv := newPeopleService(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !strings.Contains(r.URL.Path, "people:searchContacts") || r.Method != http.MethodGet {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "application/json")
if r.URL.Query().Get("query") == "" {
_ = json.NewEncoder(w).Encode(map[string]any{"results": []map[string]any{}})
return
}
_ = json.NewEncoder(w).Encode(map[string]any{
"results": []map[string]any{
{"person": map[string]any{
"resourceName": "people/c1",
"names": []map[string]any{{"displayName": "Wrong Ada"}},
"emailAddresses": []map[string]any{{"value": "wrong@example.com"}},
"phoneNumbers": []map[string]any{{"value": "+1"}},
}},
{"person": map[string]any{
"resourceName": "people/c2",
"names": []map[string]any{{"displayName": "Right Ada"}},
"emailAddresses": []map[string]any{
{"value": "other@example.com"},
{"value": "ada@example.com"},
},
}},
},
})
}))
t.Cleanup(closeSrv)
stubPeopleServices(t, svc)
out := captureStdout(t, func() {
_ = captureStderr(t, func() {
err := Execute([]string{"--account", "a@b.com", "contacts", "export", "ada@example.com", "--out", "-"})
if err != nil {
t.Fatalf("Execute: %v", err)
}
})
})
if !strings.Contains(out, "FN:Right Ada\r\n") || strings.Contains(out, "Wrong Ada") {
t.Fatalf("unexpected VCF:\n%s", out)
}
}
func TestContactsExport_AmbiguousSelectorErrors(t *testing.T) {
svc, closeSrv := newPeopleService(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !strings.Contains(r.URL.Path, "people:searchContacts") || r.Method != http.MethodGet {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "application/json")
if r.URL.Query().Get("query") == "" {
_ = json.NewEncoder(w).Encode(map[string]any{"results": []map[string]any{}})
return
}
_ = json.NewEncoder(w).Encode(map[string]any{
"results": []map[string]any{
{"person": map[string]any{"resourceName": "people/c1", "names": []map[string]any{{"displayName": "Ada One"}}}},
{"person": map[string]any{"resourceName": "people/c2", "names": []map[string]any{{"displayName": "Ada Two"}}}},
},
})
}))
t.Cleanup(closeSrv)
stubPeopleServices(t, svc)
err := runKong(t, &ContactsExportCmd{}, []string{"Ada"}, context.Background(), &RootFlags{Account: "a@b.com"})
if err == nil || !strings.Contains(err.Error(), "ambiguous contact selector") {
t.Fatalf("expected ambiguous selector error, got %v", err)
}
}
func TestWriteContactVCard_EscapesStructuredFieldsAndFolds(t *testing.T) {
var b strings.Builder
p := &people.Person{
ResourceName: "people/c1",
Names: []*people.Name{{GivenName: "Ada", FamilyName: "Love;lace", DisplayName: "Ada Love;lace"}},
Nicknames: []*people.Nickname{{Value: "Countess, friend"}},
PhoneNumbers: []*people.PhoneNumber{{Value: "+1 555", Type: "mobile"}},
Addresses: []*people.Address{{
Type: "home",
PoBox: "Box 1",
ExtendedAddress: "Suite 2",
StreetAddress: "1 Main; Street",
City: "London",
Region: "London",
PostalCode: "SW1A",
Country: "UK",
}},
Biographies: []*people.Biography{{Value: strings.Repeat("long note ", 12)}},
}
if err := writeContactVCard(&b, p, nil); err != nil {
t.Fatalf("writeContactVCard: %v", err)
}
out := b.String()
for _, want := range []string{
"VERSION:4.0\r\n",
"FN:Ada Love\\;lace\r\n",
"N:Love\\;lace;Ada;;;\r\n",
"NICKNAME:Countess\\, friend\r\n",
"TEL;TYPE=cell:+1 555\r\n",
"ADR;TYPE=home:Box 1;Suite 2;1 Main\\; Street;London;London;SW1A;UK\r\n",
"\r\n ",
} {
if !strings.Contains(out, want) {
t.Fatalf("missing %q in VCF:\n%s", want, out)
}
}
}