mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-04-13 20:07:41 -04:00
groupware: improve tests
* jmap: add UpdateContactCard and UpdateCalendarEvent funcs * use JSON marshalling and unmarshalling into a map for toPatch() implementations * add updating ContactCards and CalendarEvents to tests * add deleting ContactCards and CalendarEvents to tests * make query response totals work when the value is 0 * tests: use CreateContactCard and CreateCalendarEvent funcs to create objects in tests instead of using a different JMAP stack that works with untyped maps
This commit is contained in:
@@ -76,7 +76,7 @@ func (j *Client) QueryContactCards(accountIds []string,
|
||||
Results: get.List,
|
||||
CanCalculateChanges: query.CanCalculateChanges,
|
||||
Position: query.Position,
|
||||
Total: uintPtrIf(query.Total, calculateTotal),
|
||||
Total: uintPtrIfPtr(query.Total, calculateTotal),
|
||||
Limit: query.Limit,
|
||||
}
|
||||
},
|
||||
@@ -114,3 +114,18 @@ func (j *Client) DeleteContactCard(accountId string, destroyIds []string, ctx Co
|
||||
ctx,
|
||||
)
|
||||
}
|
||||
|
||||
func (j *Client) UpdateContactCard(accountId string, id string, changes ContactCardChange, ctx Context) (ContactCard, SessionState, State, Language, Error) {
|
||||
return update(j, "UpdateContactCard", ContactCardType,
|
||||
func(update map[string]PatchObject) ContactCardSetCommand {
|
||||
return ContactCardSetCommand{AccountId: accountId, Update: update}
|
||||
},
|
||||
func(id string) ContactCardGetCommand {
|
||||
return ContactCardGetCommand{AccountId: accountId, Ids: []string{id}}
|
||||
},
|
||||
func(resp ContactCardSetResponse) map[string]SetError { return resp.NotUpdated },
|
||||
func(resp ContactCardGetResponse) ContactCard { return resp.List[0] },
|
||||
id, changes,
|
||||
ctx,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ func (j *Client) QueryCalendarEvents(accountIds []string, //NOSONAR
|
||||
Results: get.List,
|
||||
CanCalculateChanges: query.CanCalculateChanges,
|
||||
Position: query.Position,
|
||||
Total: uintPtrIf(query.Total, calculateTotal),
|
||||
Total: uintPtrIfPtr(query.Total, calculateTotal),
|
||||
Limit: query.Limit,
|
||||
}
|
||||
},
|
||||
@@ -103,3 +103,18 @@ func (j *Client) DeleteCalendarEvent(accountId string, destroyIds []string, ctx
|
||||
ctx,
|
||||
)
|
||||
}
|
||||
|
||||
func (j *Client) UpdateCalendarEvent(accountId string, id string, changes CalendarEventChange, ctx Context) (CalendarEvent, SessionState, State, Language, Error) {
|
||||
return update(j, "UpdateCalendarEvent", CalendarEventType,
|
||||
func(update map[string]PatchObject) CalendarEventSetCommand {
|
||||
return CalendarEventSetCommand{AccountId: accountId, Update: update}
|
||||
},
|
||||
func(id string) CalendarEventGetCommand {
|
||||
return CalendarEventGetCommand{AccountId: accountId, Ids: []string{id}}
|
||||
},
|
||||
func(resp CalendarEventSetResponse) map[string]SetError { return resp.NotUpdated },
|
||||
func(resp CalendarEventGetResponse) CalendarEvent { return resp.List[0] },
|
||||
id, changes,
|
||||
ctx,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ const (
|
||||
JmapErrorSocketPushUnsupported
|
||||
JmapErrorMissingCreatedObject
|
||||
JmapInvalidObjectState
|
||||
JmapPatchObjectSerialization
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
@@ -2,11 +2,12 @@ package jmap
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
golog "log"
|
||||
"maps"
|
||||
"math"
|
||||
"math/rand"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -77,7 +78,7 @@ func TestEvents(t *testing.T) {
|
||||
session := s.Session(user.name)
|
||||
ctx := s.Context(session)
|
||||
|
||||
accountId, calendarId, expectedEventsById, boxes, err := s.fillEvents(t, count, session, user)
|
||||
accountId, calendarId, expectedEventsById, boxes, err := s.fillEvents(t, count, ctx, user)
|
||||
require.NoError(err)
|
||||
require.NotEmpty(accountId)
|
||||
require.NotEmpty(calendarId)
|
||||
@@ -89,8 +90,10 @@ func TestEvents(t *testing.T) {
|
||||
{Property: CalendarEventPropertyStart, IsAscending: true},
|
||||
}
|
||||
|
||||
ss := EmptySessionState
|
||||
os := EmptyState
|
||||
{
|
||||
resultsByAccount, _, _, _, err := s.client.QueryCalendarEvents([]string{accountId}, filter, sortBy, 0, 0, true, ctx)
|
||||
resultsByAccount, sessionState, state, _, err := s.client.QueryCalendarEvents([]string{accountId}, filter, sortBy, 0, 0, true, ctx)
|
||||
require.NoError(err)
|
||||
|
||||
require.Len(resultsByAccount, 1)
|
||||
@@ -108,6 +111,9 @@ func TestEvents(t *testing.T) {
|
||||
require.True(ok, "failed to find created contact by its id")
|
||||
matchEvent(t, actual, expected)
|
||||
}
|
||||
|
||||
ss = sessionState
|
||||
os = state
|
||||
}
|
||||
|
||||
{
|
||||
@@ -118,8 +124,7 @@ func TestEvents(t *testing.T) {
|
||||
for i := range slices {
|
||||
position := int(i * limit)
|
||||
page := min(remainder, limit)
|
||||
m, _, _, _, err := s.client.QueryCalendarEvents([]string{accountId}, filter, sortBy, position, limit, true, ctx)
|
||||
fmt.Printf("=== i=%d | limit=%d | remainder=%d | position=%d | limit=%d | results=%d\n", i, limit, remainder, position, limit, len(m[accountId].Results))
|
||||
m, sessionState, _, _, err := s.client.QueryCalendarEvents([]string{accountId}, filter, sortBy, position, limit, true, ctx)
|
||||
require.NoError(err)
|
||||
require.Len(m, 1)
|
||||
require.Contains(m, accountId)
|
||||
@@ -131,9 +136,54 @@ func TestEvents(t *testing.T) {
|
||||
require.NotNil(results.Total)
|
||||
require.Equal(count, *results.Total)
|
||||
remainder -= uint(len(results.Results))
|
||||
|
||||
require.Equal(ss, sessionState)
|
||||
}
|
||||
}
|
||||
|
||||
for _, event := range expectedEventsById {
|
||||
change := CalendarEventChange{
|
||||
EventChange: jscalendar.EventChange{
|
||||
Status: ptr(jscalendar.StatusCancelled),
|
||||
ObjectChange: jscalendar.ObjectChange{
|
||||
Sequence: uintPtr(99),
|
||||
ShowWithoutTime: boolPtr(true),
|
||||
},
|
||||
},
|
||||
}
|
||||
changed, sessionState, state, _, err := s.client.UpdateCalendarEvent(accountId, event.Id, change, ctx)
|
||||
require.NoError(err)
|
||||
require.Equal(jscalendar.StatusCancelled, changed.Status)
|
||||
require.Equal(uint(99), changed.Sequence)
|
||||
require.Equal(true, changed.ShowWithoutTime)
|
||||
require.Equal(ss, sessionState)
|
||||
require.NotEqual(os, state)
|
||||
os = state
|
||||
}
|
||||
|
||||
{
|
||||
ids := structs.Map(slices.Collect(maps.Values(expectedEventsById)), func(e CalendarEvent) string { return e.Id })
|
||||
errMap, sessionState, state, _, err := s.client.DeleteCalendarEvent(accountId, ids, ctx)
|
||||
require.NoError(err)
|
||||
require.Empty(errMap)
|
||||
|
||||
require.Equal(ss, sessionState)
|
||||
require.NotEqual(os, state)
|
||||
os = state
|
||||
}
|
||||
|
||||
{
|
||||
shouldBeEmpty, sessionState, state, _, err := s.client.QueryCalendarEvents([]string{accountId}, filter, sortBy, 0, 0, true, ctx)
|
||||
require.NoError(err)
|
||||
require.Contains(shouldBeEmpty, accountId)
|
||||
resp := shouldBeEmpty[accountId]
|
||||
require.Empty(resp.Results)
|
||||
require.NotNil(resp.Total)
|
||||
require.Equal(uint(0), *resp.Total)
|
||||
require.Equal(ss, sessionState)
|
||||
require.Equal(os, state)
|
||||
}
|
||||
|
||||
exceptions := []string{}
|
||||
if !EnableEventMayInviteFields {
|
||||
exceptions = append(exceptions, "mayInvite")
|
||||
@@ -165,7 +215,7 @@ func (s *StalwartTest) fillCalendar( //NOSONAR
|
||||
|
||||
boxes := CalendarBoxes{}
|
||||
created := []Calendar{}
|
||||
ss := SessionState("")
|
||||
ss := EmptySessionState
|
||||
as := EmptyState
|
||||
|
||||
printer := func(s string) { golog.Println(s) }
|
||||
@@ -269,7 +319,7 @@ func (s *StalwartTest) fillCalendar( //NOSONAR
|
||||
}
|
||||
require.NotEmpty(sessionState)
|
||||
require.NotEmpty(state)
|
||||
if ss != SessionState("") {
|
||||
if ss != EmptySessionState {
|
||||
require.Equal(ss, sessionState)
|
||||
}
|
||||
if as != EmptyState {
|
||||
@@ -294,11 +344,11 @@ type EventsBoxes struct {
|
||||
func (s *StalwartTest) fillEvents( //NOSONAR
|
||||
t *testing.T,
|
||||
count uint,
|
||||
session *Session,
|
||||
ctx Context,
|
||||
user User,
|
||||
) (string, string, map[string]CalendarEvent, EventsBoxes, error) {
|
||||
require := require.New(t)
|
||||
c, err := NewTestJmapClient(session, user.name, user.password, true, true)
|
||||
c, err := NewTestJmapClient(ctx.Session, user.name, user.password, true, true)
|
||||
require.NoError(err)
|
||||
defer c.Close()
|
||||
|
||||
@@ -332,7 +382,6 @@ func (s *StalwartTest) fillEvents( //NOSONAR
|
||||
isDraft := false
|
||||
mainLocationId := ""
|
||||
locationIds := []string{}
|
||||
locationMaps := map[string]map[string]any{}
|
||||
locationObjs := map[string]jscalendar.Location{}
|
||||
{
|
||||
n := 1
|
||||
@@ -340,8 +389,7 @@ func (s *StalwartTest) fillEvents( //NOSONAR
|
||||
n++
|
||||
}
|
||||
for range n {
|
||||
locationId, locationMap, locationObj := pickLocation()
|
||||
locationMaps[locationId] = locationMap
|
||||
locationId, locationObj := pickLocation()
|
||||
locationObjs[locationId] = locationObj
|
||||
locationIds = append(locationIds, locationId)
|
||||
if n > 0 && mainLocationId == "" {
|
||||
@@ -349,8 +397,8 @@ func (s *StalwartTest) fillEvents( //NOSONAR
|
||||
}
|
||||
}
|
||||
}
|
||||
virtualLocationId, virtualLocationMap, virtualLocationObj := pickVirtualLocation()
|
||||
participantMaps, participantObjs, organizerEmail := createParticipants(uid, locationIds, []string{virtualLocationId})
|
||||
virtualLocationId, virtualLocationObj := pickVirtualLocation()
|
||||
participantObjs, organizerEmail := createParticipants(uid, locationIds, []string{virtualLocationId})
|
||||
duration := pickRandom("PT30M", "PT45M", "PT1H", "PT90M")
|
||||
tz := pickRandom(timezones...)
|
||||
daysDiff := rand.Intn(31) - 15
|
||||
@@ -379,52 +427,10 @@ func (s *StalwartTest) fillEvents( //NOSONAR
|
||||
alertId := id()
|
||||
alertOffset := pickRandom("-PT5M", "-PT10M", "-PT15M")
|
||||
|
||||
event := map[string]any{
|
||||
"@type": "Event",
|
||||
"calendarIds": toBoolMapS(calendarId),
|
||||
"isDraft": isDraft,
|
||||
"start": start,
|
||||
"duration": duration,
|
||||
"status": string(status),
|
||||
"uid": uid,
|
||||
"prodId": productName,
|
||||
"title": title,
|
||||
"description": description,
|
||||
"descriptionContentType": descriptionFormat,
|
||||
"locale": locale,
|
||||
"color": color,
|
||||
"sequence": sequence,
|
||||
"showWithoutTime": false,
|
||||
"freeBusyStatus": string(freeBusy),
|
||||
"privacy": string(privacy),
|
||||
"sentBy": organizerEmail,
|
||||
"participants": participantMaps,
|
||||
"timeZone": tz,
|
||||
"hideAttendees": false,
|
||||
"replyTo": map[string]string{
|
||||
"imip": "mailto:" + organizerEmail, //NOSONAR
|
||||
},
|
||||
"locations": locationMaps,
|
||||
"virtualLocations": map[string]any{
|
||||
virtualLocationId: virtualLocationMap,
|
||||
},
|
||||
"alerts": map[string]map[string]any{
|
||||
alertId: {
|
||||
"@type": "Alert",
|
||||
"trigger": map[string]any{
|
||||
"@type": "OffsetTrigger",
|
||||
"offset": alertOffset,
|
||||
"relativeTo": "start",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
obj := CalendarEvent{
|
||||
Id: "",
|
||||
CalendarIds: toBoolMapS(calendarId),
|
||||
IsDraft: isDraft,
|
||||
IsOrigin: true,
|
||||
Event: jscalendar.Event{
|
||||
Type: jscalendar.EventType,
|
||||
Start: jscalendar.LocalDateTime(start),
|
||||
@@ -449,7 +455,7 @@ func (s *StalwartTest) fillEvents( //NOSONAR
|
||||
TimeZone: tz,
|
||||
HideAttendees: false,
|
||||
ReplyTo: map[jscalendar.ReplyMethod]string{
|
||||
jscalendar.ReplyMethodImip: "mailto:" + organizerEmail,
|
||||
jscalendar.ReplyMethodImip: "mailto:" + organizerEmail, //NOSONAR
|
||||
},
|
||||
Locations: locationObjs,
|
||||
VirtualLocations: map[string]jscalendar.VirtualLocation{
|
||||
@@ -470,31 +476,26 @@ func (s *StalwartTest) fillEvents( //NOSONAR
|
||||
}
|
||||
|
||||
if EnableEventMayInviteFields {
|
||||
event["mayInviteSelf"] = true
|
||||
event["mayInviteOthers"] = true
|
||||
obj.MayInviteSelf = true
|
||||
obj.MayInviteOthers = true
|
||||
boxes.mayInvite = true
|
||||
}
|
||||
|
||||
if len(keywords) > 0 {
|
||||
event["keywords"] = keywords
|
||||
obj.Keywords = keywords
|
||||
boxes.keywords = true
|
||||
}
|
||||
|
||||
if len(categories) > 0 {
|
||||
event["categories"] = categories
|
||||
obj.Categories = categories
|
||||
boxes.categories = true
|
||||
}
|
||||
|
||||
if mainLocationId != "" {
|
||||
event["mainLocationId"] = mainLocationId
|
||||
obj.MainLocationId = mainLocationId
|
||||
}
|
||||
|
||||
err = propmap(i%2 == 0, 1, 1, event, "links", &obj.Links, func(int, string) (map[string]any, jscalendar.Link, error) {
|
||||
err = propmap(i%2 == 0, 1, 1, &obj.Links, func(int, string) (jscalendar.Link, error) {
|
||||
mime := ""
|
||||
uri := ""
|
||||
rel := jscalendar.RelAbout
|
||||
@@ -508,17 +509,12 @@ func (s *StalwartTest) fillEvents( //NOSONAR
|
||||
mime = "image/jpeg" //NOSONAR
|
||||
uri = externalImageUri()
|
||||
}
|
||||
return map[string]any{
|
||||
"@type": "Link",
|
||||
"href": uri,
|
||||
"contentType": mime,
|
||||
"rel": string(rel),
|
||||
}, jscalendar.Link{
|
||||
Type: jscalendar.LinkType,
|
||||
Href: uri,
|
||||
ContentType: mime,
|
||||
Rel: rel,
|
||||
}, nil
|
||||
return jscalendar.Link{
|
||||
Type: jscalendar.LinkType,
|
||||
Href: uri,
|
||||
ContentType: mime,
|
||||
Rel: rel,
|
||||
}, nil
|
||||
})
|
||||
|
||||
if rand.Intn(10) > 7 {
|
||||
@@ -530,15 +526,6 @@ func (s *StalwartTest) fillEvents( //NOSONAR
|
||||
} else {
|
||||
count = 1 + rand.Intn(4)
|
||||
}
|
||||
event["recurrenceRule"] = map[string]any{
|
||||
"@type": "RecurrenceRule",
|
||||
"frequency": string(frequency),
|
||||
"interval": interval,
|
||||
"rscale": string(jscalendar.RscaleIso8601),
|
||||
"skip": string(jscalendar.SkipOmit),
|
||||
"firstDayOfWeek": string(jscalendar.DayOfWeekMonday),
|
||||
"count": count,
|
||||
}
|
||||
rr := jscalendar.RecurrenceRule{
|
||||
Type: jscalendar.RecurrenceRuleType,
|
||||
Frequency: frequency,
|
||||
@@ -551,23 +538,18 @@ func (s *StalwartTest) fillEvents( //NOSONAR
|
||||
obj.RecurrenceRule = &rr
|
||||
}
|
||||
|
||||
id, err := s.CreateEvent(c, accountId, event)
|
||||
created, _, _, _, err := s.client.CreateCalendarEvent(accountId, obj, ctx)
|
||||
if err != nil {
|
||||
return accountId, calendarId, nil, boxes, err
|
||||
}
|
||||
|
||||
obj.Id = id
|
||||
filled[id] = obj
|
||||
filled[created.Id] = *created
|
||||
|
||||
printer(fmt.Sprintf("📅 created %*s/%v id=%v", int(math.Log10(float64(count))+1), strconv.Itoa(int(i+1)), count, uid))
|
||||
}
|
||||
return accountId, calendarId, filled, boxes, nil
|
||||
}
|
||||
|
||||
func (s *StalwartTest) CreateEvent(j *TestJmapClient, accountId string, event map[string]any) (string, error) {
|
||||
return j.create1(accountId, CalendarEventType, event)
|
||||
}
|
||||
|
||||
var rooms = []jscalendar.Location{
|
||||
{
|
||||
Type: jscalendar.LocationType,
|
||||
@@ -630,56 +612,35 @@ var virtualRooms = []jscalendar.VirtualLocation{
|
||||
},
|
||||
}
|
||||
|
||||
func pickLocation() (string, map[string]any, jscalendar.Location) {
|
||||
func pickLocation() (string, jscalendar.Location) {
|
||||
locationId := id()
|
||||
room := rooms[rand.Intn(len(rooms))]
|
||||
b, err := json.Marshal(room)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
var m map[string]any
|
||||
err = json.Unmarshal(b, &m)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return locationId, m, room
|
||||
return locationId, room
|
||||
}
|
||||
|
||||
func pickVirtualLocation() (string, map[string]any, jscalendar.VirtualLocation) {
|
||||
func pickVirtualLocation() (string, jscalendar.VirtualLocation) {
|
||||
locationId := id()
|
||||
vroom := virtualRooms[rand.Intn(len(virtualRooms))]
|
||||
b, err := json.Marshal(vroom)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
var m map[string]any
|
||||
err = json.Unmarshal(b, &m)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return locationId, m, vroom
|
||||
return locationId, vroom
|
||||
}
|
||||
|
||||
var ChairRoles = toBoolMapS(jscalendar.RoleChair, jscalendar.RoleOwner)
|
||||
var RegularRoles = toBoolMapS(jscalendar.RoleOptional)
|
||||
|
||||
func createParticipants(uid string, locationIds []string, virtualLocationIds []string) (map[string]map[string]any, map[string]jscalendar.Participant, string) {
|
||||
func createParticipants(uid string, locationIds []string, virtualLocationIds []string) (map[string]jscalendar.Participant, string) {
|
||||
options := structs.Concat(locationIds, virtualLocationIds)
|
||||
n := 1 + rand.Intn(4)
|
||||
maps := map[string]map[string]any{}
|
||||
objs := map[string]jscalendar.Participant{}
|
||||
organizerId, organizerEmail, organizerMap, organizerObj := createParticipant(0, uid, pickRandom(options...), "", "")
|
||||
maps[organizerId] = organizerMap
|
||||
organizerId, organizerEmail, organizerObj := createParticipant(0, uid, pickRandom(options...), "", "")
|
||||
objs[organizerId] = organizerObj
|
||||
for i := 1; i < n; i++ {
|
||||
id, _, participantMap, participantObj := createParticipant(i, uid, pickRandom(options...), organizerId, organizerEmail)
|
||||
maps[id] = participantMap
|
||||
id, _, participantObj := createParticipant(i, uid, pickRandom(options...), organizerId, organizerEmail)
|
||||
objs[id] = participantObj
|
||||
}
|
||||
return maps, objs, organizerEmail
|
||||
return objs, organizerEmail
|
||||
}
|
||||
|
||||
func createParticipant(i int, uid string, locationId string, organizerEmail string, organizerId string) (string, string, map[string]any, jscalendar.Participant) {
|
||||
func createParticipant(i int, uid string, locationId string, organizerEmail string, organizerId string) (string, string, jscalendar.Participant) {
|
||||
participantId := id()
|
||||
person := gofakeit.Person()
|
||||
roles := RegularRoles
|
||||
@@ -731,26 +692,6 @@ func createParticipant(i int, uid string, locationId string, organizerEmail stri
|
||||
}
|
||||
}
|
||||
|
||||
m := map[string]any{
|
||||
"@type": "Participant",
|
||||
"name": name,
|
||||
"email": email,
|
||||
"calendarAddress": calendarAddress,
|
||||
"kind": "individual",
|
||||
"roles": structs.MapKeys(roles, func(r jscalendar.Role) string { return string(r) }),
|
||||
"locationId": locationId,
|
||||
"language": language,
|
||||
"participationStatus": string(status),
|
||||
"participationComment": statusComment,
|
||||
"expectReply": true,
|
||||
"scheduleAgent": "server",
|
||||
"scheduleSequence": 1,
|
||||
"scheduleStatus": []string{"1.0"},
|
||||
"scheduleUpdated": updated,
|
||||
"sentBy": organizerEmail,
|
||||
"invitedBy": organizerId,
|
||||
"scheduleId": "mailto:" + email,
|
||||
}
|
||||
o := jscalendar.Participant{
|
||||
Type: jscalendar.ParticipantType,
|
||||
Name: name,
|
||||
@@ -773,36 +714,27 @@ func createParticipant(i int, uid string, locationId string, organizerEmail stri
|
||||
}
|
||||
|
||||
if EnableEventParticipantDescriptionFields {
|
||||
m["description"] = description
|
||||
m["descriptionContentType"] = descriptionContentType
|
||||
o.Description = description
|
||||
o.DescriptionContentType = descriptionContentType
|
||||
}
|
||||
|
||||
err = propmap(i%2 == 0, 1, 2, m, "links", &o.Links, func(int, string) (map[string]any, jscalendar.Link, error) {
|
||||
err = propmap(i%2 == 0, 1, 2, &o.Links, func(int, string) (jscalendar.Link, error) {
|
||||
href := externalImageUri()
|
||||
title := person.FirstName + "'s Cake Day pick"
|
||||
return map[string]any{
|
||||
"@type": "Link",
|
||||
"href": href,
|
||||
"contentType": "image/jpeg",
|
||||
"rel": "icon",
|
||||
"display": "badge",
|
||||
"title": title,
|
||||
}, jscalendar.Link{
|
||||
Type: jscalendar.LinkType,
|
||||
Href: href,
|
||||
ContentType: "image/jpeg",
|
||||
Rel: jscalendar.RelIcon,
|
||||
Display: jscalendar.DisplayBadge,
|
||||
Title: title,
|
||||
}, nil
|
||||
return jscalendar.Link{
|
||||
Type: jscalendar.LinkType,
|
||||
Href: href,
|
||||
ContentType: "image/jpeg",
|
||||
Rel: jscalendar.RelIcon,
|
||||
Display: jscalendar.DisplayBadge,
|
||||
Title: title,
|
||||
}, nil
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return participantId, person.Contact.Email, m, o
|
||||
return participantId, person.Contact.Email, o
|
||||
}
|
||||
|
||||
var Keywords = []string{
|
||||
@@ -825,3 +757,7 @@ var Categories = []string{
|
||||
func pickCategories() map[string]bool {
|
||||
return toBoolMap(pickRandoms(Categories...))
|
||||
}
|
||||
|
||||
func ptr[T any](t T) *T {
|
||||
return &t
|
||||
}
|
||||
|
||||
@@ -2,10 +2,12 @@ package jmap
|
||||
|
||||
import (
|
||||
golog "log"
|
||||
"maps"
|
||||
"math/rand"
|
||||
"regexp"
|
||||
"slices"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
@@ -86,7 +88,7 @@ func TestContacts(t *testing.T) {
|
||||
session := s.Session(user.name)
|
||||
ctx := s.Context(session)
|
||||
|
||||
accountId, addressbookId, expectedContactCardsById, boxes, err := s.fillContacts(t, count, session, user)
|
||||
accountId, addressbookId, expectedContactCardsById, boxes, err := s.fillContacts(t, count, session, ctx, user)
|
||||
require.NoError(err)
|
||||
require.NotEmpty(accountId)
|
||||
require.NotEmpty(addressbookId)
|
||||
@@ -98,7 +100,7 @@ func TestContacts(t *testing.T) {
|
||||
{Property: ContactCardPropertyCreated, IsAscending: true},
|
||||
}
|
||||
|
||||
contactsByAccount, _, _, _, err := s.client.QueryContactCards([]string{accountId}, filter, sortBy, 0, 0, true, ctx)
|
||||
contactsByAccount, ss, os, _, err := s.client.QueryContactCards([]string{accountId}, filter, sortBy, 0, 0, true, ctx)
|
||||
require.NoError(err)
|
||||
|
||||
require.Len(contactsByAccount, 1)
|
||||
@@ -140,6 +142,44 @@ func TestContacts(t *testing.T) {
|
||||
matchContact(t, fetched.List[0], actual)
|
||||
}
|
||||
|
||||
{
|
||||
now := time.Now().Truncate(time.Duration(1) * time.Second).UTC()
|
||||
for _, event := range expectedContactCardsById {
|
||||
change := ContactCardChange{
|
||||
Language: strPtr("xyz"),
|
||||
Updated: ptr(now),
|
||||
}
|
||||
changed, sessionState, state, _, err := s.client.UpdateContactCard(accountId, event.Id, change, ctx)
|
||||
require.NoError(err)
|
||||
require.Equal("xyz", changed.Language)
|
||||
require.Equal(now, changed.Updated)
|
||||
require.Equal(ss, sessionState)
|
||||
require.NotEqual(os, state)
|
||||
os = state
|
||||
}
|
||||
}
|
||||
{
|
||||
ids := structs.Map(slices.Collect(maps.Values(expectedContactCardsById)), func(e ContactCard) string { return e.Id })
|
||||
errMap, sessionState, state, _, err := s.client.DeleteContactCard(accountId, ids, ctx)
|
||||
require.NoError(err)
|
||||
require.Empty(errMap)
|
||||
|
||||
require.Equal(ss, sessionState)
|
||||
require.NotEqual(os, state)
|
||||
os = state
|
||||
}
|
||||
{
|
||||
shouldBeEmpty, sessionState, state, _, err := s.client.QueryContactCards([]string{accountId}, filter, sortBy, 0, 0, true, ctx)
|
||||
require.NoError(err)
|
||||
require.Contains(shouldBeEmpty, accountId)
|
||||
resp := shouldBeEmpty[accountId]
|
||||
require.Empty(resp.Results)
|
||||
require.NotNil(resp.Total)
|
||||
require.Equal(uint(0), *resp.Total)
|
||||
require.Equal(ss, sessionState)
|
||||
require.Equal(os, state)
|
||||
}
|
||||
|
||||
exceptions := []string{}
|
||||
if !EnableMediaWithBlobId {
|
||||
exceptions = append(exceptions, "mediaWithBlobId")
|
||||
@@ -181,7 +221,7 @@ func (s *StalwartTest) fillAddressBook( //NOSONAR
|
||||
|
||||
boxes := AddressBookBoxes{}
|
||||
created := []AddressBook{}
|
||||
ss := SessionState("")
|
||||
ss := EmptySessionState
|
||||
as := EmptyState
|
||||
|
||||
printer := func(s string) { golog.Println(s) }
|
||||
@@ -228,7 +268,7 @@ func (s *StalwartTest) fillAddressBook( //NOSONAR
|
||||
}
|
||||
require.NotEmpty(sessionState)
|
||||
require.NotEmpty(state)
|
||||
if ss != SessionState("") {
|
||||
if ss != EmptySessionState {
|
||||
require.Equal(ss, sessionState)
|
||||
}
|
||||
if as != EmptyState {
|
||||
@@ -248,6 +288,7 @@ func (s *StalwartTest) fillContacts( //NOSONAR
|
||||
t *testing.T,
|
||||
count uint,
|
||||
session *Session,
|
||||
ctx Context,
|
||||
user User,
|
||||
) (string, string, map[string]ContactCard, ContactsBoxes, error) {
|
||||
require := require.New(t)
|
||||
@@ -288,17 +329,9 @@ func (s *StalwartTest) fillContacts( //NOSONAR
|
||||
filled := map[string]ContactCard{}
|
||||
for i := range count {
|
||||
person := gofakeit.Person()
|
||||
nameMap, nameObj := createName(person)
|
||||
nameObj := createName(person)
|
||||
language := pickLanguage()
|
||||
contact := map[string]any{
|
||||
"@type": "Card",
|
||||
"version": "1.0",
|
||||
"addressBookIds": toBoolMap([]string{addressbookId}),
|
||||
"prodId": productName,
|
||||
"language": language,
|
||||
"kind": "individual",
|
||||
"name": nameMap,
|
||||
}
|
||||
|
||||
card := ContactCard{
|
||||
Type: jscontact.ContactCardType,
|
||||
Version: "1.0",
|
||||
@@ -310,34 +343,29 @@ func (s *StalwartTest) fillContacts( //NOSONAR
|
||||
}
|
||||
|
||||
if i%3 == 0 {
|
||||
nicknameMap, nicknameObj := createNickName(person)
|
||||
nicknameObj := createNickName(person)
|
||||
id := id()
|
||||
contact["nicknames"] = map[string]map[string]any{id: nicknameMap}
|
||||
card.Nicknames = map[string]jscontact.Nickname{id: nicknameObj}
|
||||
boxes.nicknames = true
|
||||
}
|
||||
|
||||
{
|
||||
emailMaps := map[string]map[string]any{}
|
||||
emailObjs := map[string]jscontact.EmailAddress{}
|
||||
emailId := id()
|
||||
emailMap, emailObj := createEmail(person, 10)
|
||||
emailMaps[emailId] = emailMap
|
||||
emailObj := createEmail(person, 10)
|
||||
emailObjs[emailId] = emailObj
|
||||
|
||||
for i := range rand.Intn(3) {
|
||||
id := id()
|
||||
m, o := createSecondaryEmail(gofakeit.Email(), i*100)
|
||||
emailMaps[id] = m
|
||||
o := createSecondaryEmail(gofakeit.Email(), i*100)
|
||||
emailObjs[id] = o
|
||||
boxes.secondaryEmails = true
|
||||
}
|
||||
if len(emailMaps) > 0 {
|
||||
contact["emails"] = emailMaps
|
||||
if len(emailObjs) > 0 {
|
||||
card.Emails = emailObjs
|
||||
}
|
||||
}
|
||||
if err := propmap(i%2 == 0, 1, 2, contact, "phones", &card.Phones, func(i int, id string) (map[string]any, jscontact.Phone, error) {
|
||||
if err := propmap(i%2 == 0, 1, 2, &card.Phones, func(i int, id string) (jscontact.Phone, error) {
|
||||
boxes.phones = true
|
||||
num := person.Contact.Phone
|
||||
if i > 0 {
|
||||
@@ -355,21 +383,16 @@ func (s *StalwartTest) fillContacts( //NOSONAR
|
||||
contexts[jscontact.PhoneContextPrivate] = true
|
||||
}
|
||||
tel := "tel:" + "+1" + num
|
||||
return map[string]any{
|
||||
"@type": "Phone",
|
||||
"number": tel,
|
||||
"features": structs.MapKeys(features, func(f jscontact.PhoneFeature) string { return string(f) }),
|
||||
"contexts": structs.MapKeys(contexts, func(c jscontact.PhoneContext) string { return string(c) }),
|
||||
}, jscontact.Phone{
|
||||
Type: jscontact.PhoneType,
|
||||
Number: tel,
|
||||
Features: features,
|
||||
Contexts: contexts,
|
||||
}, nil
|
||||
return jscontact.Phone{
|
||||
Type: jscontact.PhoneType,
|
||||
Number: tel,
|
||||
Features: features,
|
||||
Contexts: contexts,
|
||||
}, nil
|
||||
}); err != nil {
|
||||
return "", "", nil, boxes, err
|
||||
}
|
||||
if err := propmap(i%5 < 4, 1, 2, contact, "addresses", &card.Addresses, func(i int, id string) (map[string]any, jscontact.Address, error) {
|
||||
if err := propmap(i%5 < 4, 1, 2, &card.Addresses, func(i int, id string) (jscontact.Address, error) {
|
||||
var source *gofakeit.AddressInfo
|
||||
if i == 0 {
|
||||
source = person.Address
|
||||
@@ -392,108 +415,70 @@ func (s *StalwartTest) fillContacts( //NOSONAR
|
||||
jscontact.AddressComponent{Type: jscontact.AddressComponentType, Kind: jscontact.AddressComponentKindPostcode, Value: source.Zip},
|
||||
)
|
||||
tz := pickRandom(timezones...)
|
||||
return map[string]any{
|
||||
"@type": "Address",
|
||||
"components": structs.Map(components, func(c jscontact.AddressComponent) map[string]string {
|
||||
return map[string]string{"kind": string(c.Kind), "value": c.Value}
|
||||
}),
|
||||
"defaultSeparator": ", ",
|
||||
"isOrdered": true,
|
||||
"timeZone": tz,
|
||||
}, jscontact.Address{
|
||||
Type: jscontact.AddressType,
|
||||
Components: components,
|
||||
DefaultSeparator: ", ",
|
||||
IsOrdered: true,
|
||||
TimeZone: tz,
|
||||
}, nil
|
||||
return jscontact.Address{
|
||||
Type: jscontact.AddressType,
|
||||
Components: components,
|
||||
DefaultSeparator: ", ",
|
||||
IsOrdered: true,
|
||||
TimeZone: tz,
|
||||
}, nil
|
||||
}); err != nil {
|
||||
return "", "", nil, boxes, err
|
||||
}
|
||||
if err := propmap(i%2 == 0, 1, 2, contact, "onlineServices", &card.OnlineServices, func(i int, id string) (map[string]any, jscontact.OnlineService, error) {
|
||||
if err := propmap(i%2 == 0, 1, 2, &card.OnlineServices, func(i int, id string) (jscontact.OnlineService, error) {
|
||||
boxes.onlineService = true
|
||||
switch rand.Intn(3) {
|
||||
case 0:
|
||||
return map[string]any{
|
||||
"@type": "OnlineService",
|
||||
"service": "Mastodon",
|
||||
"user": "@" + person.Contact.Email,
|
||||
"uri": "https://mastodon.example.com/@" + strings.ToLower(person.FirstName),
|
||||
}, jscontact.OnlineService{
|
||||
Type: jscontact.OnlineServiceType,
|
||||
Service: "Mastodon",
|
||||
User: "@" + person.Contact.Email,
|
||||
Uri: "https://mastodon.example.com/@" + strings.ToLower(person.FirstName),
|
||||
}, nil
|
||||
return jscontact.OnlineService{
|
||||
Type: jscontact.OnlineServiceType,
|
||||
Service: "Mastodon",
|
||||
User: "@" + person.Contact.Email,
|
||||
Uri: "https://mastodon.example.com/@" + strings.ToLower(person.FirstName),
|
||||
}, nil
|
||||
case 1:
|
||||
return map[string]any{
|
||||
"@type": "OnlineService",
|
||||
"uri": "xmpp:" + person.Contact.Email,
|
||||
}, jscontact.OnlineService{
|
||||
Type: jscontact.OnlineServiceType,
|
||||
Uri: "xmpp:" + person.Contact.Email,
|
||||
}, nil
|
||||
return jscontact.OnlineService{
|
||||
Type: jscontact.OnlineServiceType,
|
||||
Uri: "xmpp:" + person.Contact.Email,
|
||||
}, nil
|
||||
default:
|
||||
return map[string]any{
|
||||
"@type": "OnlineService",
|
||||
"service": "Discord",
|
||||
"user": person.Contact.Email,
|
||||
"uri": "https://discord.example.com/user/" + person.Contact.Email,
|
||||
}, jscontact.OnlineService{
|
||||
Type: jscontact.OnlineServiceType,
|
||||
Service: "Discord",
|
||||
User: person.Contact.Email,
|
||||
Uri: "https://discord.example.com/user/" + person.Contact.Email,
|
||||
}, nil
|
||||
return jscontact.OnlineService{
|
||||
Type: jscontact.OnlineServiceType,
|
||||
Service: "Discord",
|
||||
User: person.Contact.Email,
|
||||
Uri: "https://discord.example.com/user/" + person.Contact.Email,
|
||||
}, nil
|
||||
}
|
||||
}); err != nil {
|
||||
return "", "", nil, boxes, err
|
||||
}
|
||||
|
||||
if err := propmap(i%3 == 0, 1, 2, contact, "preferredLanguages", &card.PreferredLanguages, func(i int, id string) (map[string]any, jscontact.LanguagePref, error) {
|
||||
if err := propmap(i%3 == 0, 1, 2, &card.PreferredLanguages, func(i int, id string) (jscontact.LanguagePref, error) {
|
||||
boxes.preferredLanguage = true
|
||||
lang := pickRandom("en", "fr", "de", "es", "it")
|
||||
contexts := pickRandoms1("work", "private")
|
||||
return map[string]any{
|
||||
"@type": "LanguagePref",
|
||||
"language": lang,
|
||||
"contexts": toBoolMap(contexts),
|
||||
"pref": i + 1,
|
||||
}, jscontact.LanguagePref{
|
||||
Type: jscontact.LanguagePrefType,
|
||||
Language: lang,
|
||||
Contexts: toBoolMap(structs.Map(contexts, func(s string) jscontact.LanguagePrefContext { return jscontact.LanguagePrefContext(s) })),
|
||||
Pref: uint(i + 1),
|
||||
}, nil
|
||||
return jscontact.LanguagePref{
|
||||
Type: jscontact.LanguagePrefType,
|
||||
Language: lang,
|
||||
Contexts: toBoolMap(structs.Map(contexts, func(s string) jscontact.LanguagePrefContext { return jscontact.LanguagePrefContext(s) })),
|
||||
Pref: uint(i + 1),
|
||||
}, nil
|
||||
}); err != nil {
|
||||
return "", "", nil, boxes, err
|
||||
}
|
||||
|
||||
if i%2 == 0 {
|
||||
organizationMaps := map[string]map[string]any{}
|
||||
organizationObjs := map[string]jscontact.Organization{}
|
||||
titleMaps := map[string]map[string]any{}
|
||||
titleObjs := map[string]jscontact.Title{}
|
||||
for range 1 + rand.Intn(2) {
|
||||
boxes.organization = true
|
||||
orgId := id()
|
||||
titleId := id()
|
||||
organizationMaps[orgId] = map[string]any{
|
||||
"@type": "Organization",
|
||||
"name": person.Job.Company,
|
||||
"contexts": toBoolMapS("work"),
|
||||
}
|
||||
organizationObjs[orgId] = jscontact.Organization{
|
||||
Type: jscontact.OrganizationType,
|
||||
Name: person.Job.Company,
|
||||
Contexts: toBoolMapS(jscontact.OrganizationContextWork),
|
||||
}
|
||||
titleMaps[titleId] = map[string]any{
|
||||
"@type": "Title",
|
||||
"kind": "title",
|
||||
"name": person.Job.Title,
|
||||
"organizationId": orgId,
|
||||
}
|
||||
|
||||
titleObjs[titleId] = jscontact.Title{
|
||||
Type: jscontact.TitleType,
|
||||
Kind: jscontact.TitleKindTitle,
|
||||
@@ -501,38 +486,32 @@ func (s *StalwartTest) fillContacts( //NOSONAR
|
||||
OrganizationId: orgId,
|
||||
}
|
||||
}
|
||||
contact["organizations"] = organizationMaps
|
||||
contact["titles"] = titleMaps
|
||||
card.Organizations = organizationObjs
|
||||
card.Titles = titleObjs
|
||||
}
|
||||
|
||||
if err := propmap(i%2 == 0, 1, 1, contact, "cryptoKeys", &card.CryptoKeys, func(i int, id string) (map[string]any, jscontact.CryptoKey, error) {
|
||||
if err := propmap(i%2 == 0, 1, 1, &card.CryptoKeys, func(i int, id string) (jscontact.CryptoKey, error) {
|
||||
boxes.cryptoKey = true
|
||||
entity, err := openpgp.NewEntity(person.FirstName+" "+person.LastName, "test", person.Contact.Email, nil)
|
||||
if err != nil {
|
||||
return nil, jscontact.CryptoKey{}, err
|
||||
return jscontact.CryptoKey{}, err
|
||||
}
|
||||
var b bytes.Buffer
|
||||
err = entity.PrimaryKey.Serialize(&b)
|
||||
if err != nil {
|
||||
return nil, jscontact.CryptoKey{}, err
|
||||
return jscontact.CryptoKey{}, err
|
||||
}
|
||||
encoded := base64.RawStdEncoding.EncodeToString(b.Bytes())
|
||||
return map[string]any{
|
||||
"@type": "CryptoKey",
|
||||
"uri": "data:application/pgp-keys;base64," + encoded,
|
||||
"mediaType": "application/pgp-keys",
|
||||
}, jscontact.CryptoKey{
|
||||
Type: jscontact.CryptoKeyType,
|
||||
Uri: "data:application/pgp-keys;base64," + encoded,
|
||||
MediaType: "application/pgp-keys",
|
||||
}, nil
|
||||
return jscontact.CryptoKey{
|
||||
Type: jscontact.CryptoKeyType,
|
||||
Uri: "data:application/pgp-keys;base64," + encoded,
|
||||
MediaType: "application/pgp-keys",
|
||||
}, nil
|
||||
}); err != nil {
|
||||
return "", "", nil, boxes, err
|
||||
}
|
||||
|
||||
if err := propmap(i%2 == 0, 1, 2, contact, "media", &card.Media, func(i int, id string) (map[string]any, jscontact.Media, error) {
|
||||
if err := propmap(i%2 == 0, 1, 2, &card.Media, func(i int, id string) (jscontact.Media, error) {
|
||||
label := fmt.Sprintf("photo-%d", 1000+rand.Intn(9000))
|
||||
|
||||
r := 0
|
||||
@@ -552,40 +531,27 @@ func (s *StalwartTest) fillContacts( //NOSONAR
|
||||
mime := "image/png"
|
||||
uri := "data:" + mime + ";base64," + base64.StdEncoding.EncodeToString(img)
|
||||
contexts := toBoolMapS(jscontact.MediaContextPrivate)
|
||||
return map[string]any{
|
||||
"@type": "Media",
|
||||
"kind": string(jscontact.MediaKindPhoto),
|
||||
"uri": uri,
|
||||
"mediaType": mime,
|
||||
"contexts": structs.MapKeys(contexts, func(c jscontact.MediaContext) string { return string(c) }),
|
||||
"label": label,
|
||||
}, jscontact.Media{
|
||||
Type: jscontact.MediaType,
|
||||
Kind: jscontact.MediaKindPhoto,
|
||||
Uri: uri,
|
||||
MediaType: mime,
|
||||
Contexts: contexts,
|
||||
Label: label,
|
||||
}, nil
|
||||
return jscontact.Media{
|
||||
Type: jscontact.MediaType,
|
||||
Kind: jscontact.MediaKindPhoto,
|
||||
Uri: uri,
|
||||
MediaType: mime,
|
||||
Contexts: contexts,
|
||||
Label: label,
|
||||
}, nil
|
||||
|
||||
case 1:
|
||||
boxes.mediaWithExternalUri = true
|
||||
// use external uri
|
||||
uri := externalImageUri()
|
||||
contexts := toBoolMapS(jscontact.MediaContextWork)
|
||||
return map[string]any{
|
||||
"@type": "Media",
|
||||
"kind": string(jscontact.MediaKindPhoto),
|
||||
"uri": uri,
|
||||
"contexts": structs.MapKeys(contexts, func(c jscontact.MediaContext) string { return string(c) }),
|
||||
"label": label,
|
||||
}, jscontact.Media{
|
||||
Type: jscontact.MediaType,
|
||||
Kind: jscontact.MediaKindPhoto,
|
||||
Uri: uri,
|
||||
Contexts: contexts,
|
||||
Label: label,
|
||||
}, nil
|
||||
return jscontact.Media{
|
||||
Type: jscontact.MediaType,
|
||||
Kind: jscontact.MediaKindPhoto,
|
||||
Uri: uri,
|
||||
Contexts: contexts,
|
||||
Label: label,
|
||||
}, nil
|
||||
|
||||
default:
|
||||
boxes.mediaWithBlobId = true
|
||||
@@ -593,56 +559,41 @@ func (s *StalwartTest) fillContacts( //NOSONAR
|
||||
img := gofakeit.ImageJpeg(size, size)
|
||||
blob, err := c.uploadBlob(accountId, img, "image/jpeg")
|
||||
if err != nil {
|
||||
return nil, jscontact.Media{}, err
|
||||
return jscontact.Media{}, err
|
||||
}
|
||||
contexts := toBoolMapS(jscontact.MediaContextPrivate)
|
||||
return map[string]any{
|
||||
"@type": "Media",
|
||||
"kind": string(jscontact.MediaKindPhoto),
|
||||
"blobId": blob.BlobId,
|
||||
"contexts": structs.MapKeys(contexts, func(c jscontact.MediaContext) string { return string(c) }),
|
||||
"label": label,
|
||||
}, jscontact.Media{
|
||||
Type: jscontact.MediaType,
|
||||
Kind: jscontact.MediaKindPhoto,
|
||||
BlobId: blob.BlobId,
|
||||
MediaType: blob.Type,
|
||||
Contexts: contexts,
|
||||
Label: label,
|
||||
}, nil
|
||||
return jscontact.Media{
|
||||
Type: jscontact.MediaType,
|
||||
Kind: jscontact.MediaKindPhoto,
|
||||
BlobId: blob.BlobId,
|
||||
MediaType: blob.Type,
|
||||
Contexts: contexts,
|
||||
Label: label,
|
||||
}, nil
|
||||
|
||||
}
|
||||
}); err != nil {
|
||||
return "", "", nil, boxes, err
|
||||
}
|
||||
if err := propmap(i%2 == 0, 1, 1, contact, "links", &card.Links, func(i int, id string) (map[string]any, jscontact.Link, error) {
|
||||
if err := propmap(i%2 == 0, 1, 1, &card.Links, func(i int, id string) (jscontact.Link, error) {
|
||||
boxes.link = true
|
||||
return map[string]any{
|
||||
"@type": "Link",
|
||||
"kind": "contact",
|
||||
"uri": "mailto:" + person.Contact.Email,
|
||||
"pref": (i + 1) * 10,
|
||||
}, jscontact.Link{
|
||||
Type: jscontact.LinkType,
|
||||
Kind: jscontact.LinkKindContact,
|
||||
Uri: "mailto:" + person.Contact.Email,
|
||||
Pref: uint((i + 1) * 10),
|
||||
}, nil
|
||||
return jscontact.Link{
|
||||
Type: jscontact.LinkType,
|
||||
Kind: jscontact.LinkKindContact,
|
||||
Uri: "mailto:" + person.Contact.Email,
|
||||
Pref: uint((i + 1) * 10),
|
||||
}, nil
|
||||
}); err != nil {
|
||||
return "", "", nil, boxes, err
|
||||
}
|
||||
|
||||
id, err := s.CreateContact(c, accountId, contact)
|
||||
created, _, _, _, err := s.client.CreateContactCard(accountId, card, ctx)
|
||||
if err != nil {
|
||||
return "", "", nil, boxes, err
|
||||
return accountId, addressbookId, filled, boxes, err
|
||||
}
|
||||
card.Id = id
|
||||
filled[id] = card
|
||||
printer(fmt.Sprintf("🧑🏻 created %*s/%v id=%v", int(math.Log10(float64(count))+1), strconv.Itoa(int(i+1)), count, id))
|
||||
require.NotNil(created)
|
||||
filled[created.Id] = *created
|
||||
printer(fmt.Sprintf("🧑🏻 created %*s/%v id=%v", int(math.Log10(float64(count))+1), strconv.Itoa(int(i+1)), count, created.Id))
|
||||
}
|
||||
return accountId, addressbookId, filled, boxes, nil
|
||||
}
|
||||
|
||||
func (s *StalwartTest) CreateContact(j *TestJmapClient, accountId string, contact map[string]any) (string, error) {
|
||||
return j.create1(accountId, ContactCardType, contact)
|
||||
}
|
||||
|
||||
@@ -707,48 +707,6 @@ func (c Commander[T]) command(body map[string]any) (T, error) {
|
||||
return c.closure(methodResponses)
|
||||
}
|
||||
|
||||
func (j *TestJmapClient) create(id string, objectType ObjectType, body map[string]any) (string, error) {
|
||||
return newCommander(j, func(methodResponses []any) (string, error) {
|
||||
z := methodResponses[0].([]any)
|
||||
f := z[1].(map[string]any)
|
||||
if x, ok := f["created"]; ok {
|
||||
created := x.(map[string]any)
|
||||
if c, ok := created[id].(map[string]any); ok {
|
||||
return c["id"].(string), nil
|
||||
} else {
|
||||
return "", fmt.Errorf("failed to create %v", objectType)
|
||||
}
|
||||
} else {
|
||||
if ncx, ok := f["notCreated"]; ok {
|
||||
nc := ncx.(map[string]any)
|
||||
c := nc[id].(map[string]any)
|
||||
return "", fmt.Errorf("failed to create %v: %v", objectType, c["description"])
|
||||
} else {
|
||||
return "", fmt.Errorf("failed to create %v", objectType)
|
||||
}
|
||||
}
|
||||
}).command(body)
|
||||
}
|
||||
|
||||
func (j *TestJmapClient) create1(accountId string, objectType ObjectType, obj map[string]any) (string, error) {
|
||||
body := map[string]any{
|
||||
"using": structs.Map(objectType.Namespaces, func(n JmapNamespace) string { return string(n) }),
|
||||
"methodCalls": []any{
|
||||
[]any{
|
||||
objectType.Name + "/set",
|
||||
map[string]any{
|
||||
"accountId": accountId,
|
||||
"create": map[string]any{
|
||||
"c": obj,
|
||||
},
|
||||
},
|
||||
"0",
|
||||
},
|
||||
},
|
||||
}
|
||||
return j.create("c", objectType, body)
|
||||
}
|
||||
|
||||
func (j *TestJmapClient) objectsById(accountId string, objectType ObjectType) (map[string]map[string]any, error) {
|
||||
m := map[string]map[string]any{}
|
||||
{
|
||||
@@ -785,91 +743,60 @@ func (j *TestJmapClient) objectsById(accountId string, objectType ObjectType) (m
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func createName(person *gofakeit.PersonInfo) (map[string]any, jscontact.Name) {
|
||||
o := jscontact.Name{
|
||||
func createName(person *gofakeit.PersonInfo) jscontact.Name {
|
||||
name := jscontact.Name{
|
||||
Type: jscontact.NameType,
|
||||
}
|
||||
m := map[string]any{
|
||||
"@type": "Name",
|
||||
}
|
||||
mComps := make([]map[string]string, 2)
|
||||
oComps := make([]jscontact.NameComponent, 2)
|
||||
mComps[0] = map[string]string{
|
||||
"kind": "given",
|
||||
"value": person.FirstName,
|
||||
}
|
||||
oComps[0] = jscontact.NameComponent{
|
||||
comps := make([]jscontact.NameComponent, 2)
|
||||
comps[0] = jscontact.NameComponent{
|
||||
Type: jscontact.NameComponentType,
|
||||
Kind: jscontact.NameComponentKindGiven,
|
||||
Value: person.FirstName,
|
||||
}
|
||||
mComps[1] = map[string]string{
|
||||
"kind": "surname",
|
||||
"value": person.LastName,
|
||||
}
|
||||
oComps[1] = jscontact.NameComponent{
|
||||
comps[1] = jscontact.NameComponent{
|
||||
Type: jscontact.NameComponentType,
|
||||
Kind: jscontact.NameComponentKindSurname,
|
||||
Value: person.LastName,
|
||||
}
|
||||
m["components"] = mComps
|
||||
o.Components = oComps
|
||||
m["isOrdered"] = true
|
||||
o.IsOrdered = true
|
||||
m["defaultSeparator"] = " "
|
||||
o.DefaultSeparator = " "
|
||||
name.Components = comps
|
||||
name.IsOrdered = true
|
||||
name.DefaultSeparator = " "
|
||||
full := fmt.Sprintf("%s %s", person.FirstName, person.LastName)
|
||||
m["full"] = full
|
||||
o.Full = full
|
||||
return m, o
|
||||
name.Full = full
|
||||
return name
|
||||
}
|
||||
|
||||
func createNickName(_ *gofakeit.PersonInfo) (map[string]any, jscontact.Nickname) {
|
||||
func createNickName(_ *gofakeit.PersonInfo) jscontact.Nickname {
|
||||
name := gofakeit.PetName()
|
||||
contexts := pickRandoms(jscontact.NicknameContextPrivate, jscontact.NicknameContextWork)
|
||||
return map[string]any{
|
||||
"@type": "Nickname",
|
||||
"name": name,
|
||||
"contexts": toBoolMap(structs.Map(contexts, func(s jscontact.NicknameContext) string { return string(s) })),
|
||||
}, jscontact.Nickname{
|
||||
Type: jscontact.NicknameType,
|
||||
Name: name,
|
||||
Contexts: orNilMap(toBoolMap(contexts)),
|
||||
}
|
||||
return jscontact.Nickname{
|
||||
Type: jscontact.NicknameType,
|
||||
Name: name,
|
||||
Contexts: orNilMap(toBoolMap(contexts)),
|
||||
}
|
||||
}
|
||||
|
||||
func createEmail(person *gofakeit.PersonInfo, pref int) (map[string]any, jscontact.EmailAddress) {
|
||||
func createEmail(person *gofakeit.PersonInfo, pref int) jscontact.EmailAddress {
|
||||
email := person.Contact.Email
|
||||
contexts := pickRandoms1(jscontact.EmailAddressContextWork, jscontact.EmailAddressContextPrivate)
|
||||
label := strings.ToLower(person.FirstName)
|
||||
return map[string]any{
|
||||
"@type": "EmailAddress",
|
||||
"address": email,
|
||||
"contexts": toBoolMap(structs.Map(contexts, func(s jscontact.EmailAddressContext) string { return string(s) })),
|
||||
"label": label,
|
||||
"pref": pref,
|
||||
}, jscontact.EmailAddress{
|
||||
Type: jscontact.EmailAddressType,
|
||||
Address: email,
|
||||
Contexts: orNilMap(toBoolMap(contexts)),
|
||||
Label: label,
|
||||
Pref: uint(pref),
|
||||
}
|
||||
return jscontact.EmailAddress{
|
||||
Type: jscontact.EmailAddressType,
|
||||
Address: email,
|
||||
Contexts: orNilMap(toBoolMap(contexts)),
|
||||
Label: label,
|
||||
Pref: uint(pref),
|
||||
}
|
||||
}
|
||||
|
||||
func createSecondaryEmail(email string, pref int) (map[string]any, jscontact.EmailAddress) {
|
||||
func createSecondaryEmail(email string, pref int) jscontact.EmailAddress {
|
||||
contexts := pickRandoms(jscontact.EmailAddressContextWork, jscontact.EmailAddressContextPrivate)
|
||||
return map[string]any{
|
||||
"@type": "EmailAddress",
|
||||
"address": email,
|
||||
"contexts": toBoolMap(structs.Map(contexts, func(s jscontact.EmailAddressContext) string { return string(s) })),
|
||||
"pref": pref,
|
||||
}, jscontact.EmailAddress{
|
||||
Type: jscontact.EmailAddressType,
|
||||
Address: email,
|
||||
Contexts: orNilMap(toBoolMap(contexts)),
|
||||
Pref: uint(pref),
|
||||
}
|
||||
return jscontact.EmailAddress{
|
||||
Type: jscontact.EmailAddressType,
|
||||
Address: email,
|
||||
Contexts: orNilMap(toBoolMap(contexts)),
|
||||
Pref: uint(pref),
|
||||
}
|
||||
}
|
||||
|
||||
var idFirstLetters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||
@@ -1087,27 +1014,22 @@ var extendedColors = []string{
|
||||
}
|
||||
*/
|
||||
|
||||
func propmap[T any](enabled bool, min int, max int, container map[string]any, name string, cardProperty *map[string]T, generator func(int, string) (map[string]any, T, error)) error {
|
||||
func propmap[T any](enabled bool, min int, max int, cardProperty *map[string]T, generator func(int, string) (T, error)) error {
|
||||
if !enabled {
|
||||
return nil
|
||||
}
|
||||
n := min + rand.Intn(max-min+1)
|
||||
|
||||
m := make(map[string]map[string]any, n)
|
||||
o := make(map[string]T, n)
|
||||
for i := range n {
|
||||
id := id()
|
||||
itemForMap, itemForCard, err := generator(i, id)
|
||||
itemForCard, err := generator(i, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if itemForMap != nil {
|
||||
m[id] = itemForMap
|
||||
o[id] = itemForCard
|
||||
}
|
||||
o[id] = itemForCard
|
||||
}
|
||||
if len(m) > 0 {
|
||||
container[name] = m
|
||||
if len(o) > 0 {
|
||||
*cardProperty = o
|
||||
}
|
||||
return nil
|
||||
@@ -1255,19 +1177,18 @@ func containerTest[OBJ Idable, RESP GetResponse[OBJ], BOXES any, CHANGE Change](
|
||||
principalIds = structs.Map(principals.List, func(p Principal) string { return p.Id })
|
||||
}
|
||||
|
||||
ss := SessionState("")
|
||||
ss := EmptySessionState
|
||||
as := EmptyState
|
||||
|
||||
// we need to fetch the ID of the default object that automatically exists for each user, in order to exclude it
|
||||
// from the tests below
|
||||
defaultContainerId := ""
|
||||
preExistingIds := []string{}
|
||||
{
|
||||
resp, sessionState, state, _, err := get(s, accountId, []string{}, ctx)
|
||||
require.NoError(err)
|
||||
require.Empty(resp.GetNotFound())
|
||||
objs := obj(resp)
|
||||
require.Len(objs, 1) // the personal calendar that exists by default
|
||||
defaultContainerId = id(objs[0])
|
||||
preExistingIds = structs.Map(objs, id)
|
||||
ss = sessionState
|
||||
as = state
|
||||
}
|
||||
@@ -1287,8 +1208,8 @@ func containerTest[OBJ Idable, RESP GetResponse[OBJ], BOXES any, CHANGE Change](
|
||||
require.NoError(err)
|
||||
require.Empty(resp.GetNotFound())
|
||||
objs := obj(resp)
|
||||
// lets skip the default object since we did not create that one
|
||||
found := structs.Filter(objs, func(a OBJ) bool { return id(a) != defaultContainerId })
|
||||
// lets skip the objects that already exist since we did not create those
|
||||
found := structs.Filter(objs, func(a OBJ) bool { return !slices.Contains(preExistingIds, id(a)) })
|
||||
require.Len(found, int(num))
|
||||
m := structs.Index(found, id)
|
||||
require.Len(m, int(num))
|
||||
@@ -1320,7 +1241,25 @@ func containerTest[OBJ Idable, RESP GetResponse[OBJ], BOXES any, CHANGE Change](
|
||||
require.Equal(objs[0], a)
|
||||
}
|
||||
|
||||
// lets modify each AddressBook
|
||||
// let's retrieve them all by their IDs, but this time all at once
|
||||
{
|
||||
ids := structs.Map(all, id)
|
||||
resp, sessionState, state, _, err := get(s, accountId, ids, ctx)
|
||||
require.NoError(err)
|
||||
require.Empty(resp.GetNotFound())
|
||||
objs := obj(resp)
|
||||
require.Len(objs, len(all))
|
||||
require.Equal(sessionState, ss)
|
||||
require.Equal(state, as)
|
||||
allById := structs.Index(all, id)
|
||||
for _, r := range resp.GetList() {
|
||||
a, ok := allById[r.GetId()]
|
||||
require.True(ok, "failed to find object that was retrieved in mass ID request in the list of objects that were created")
|
||||
require.Equal(a, r)
|
||||
}
|
||||
}
|
||||
|
||||
// lets modify each object
|
||||
for _, a := range all {
|
||||
i := id(a)
|
||||
ch := change(a)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package jmap
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
@@ -930,6 +931,8 @@ type SessionPrimaryAccounts struct {
|
||||
|
||||
type SessionState string
|
||||
|
||||
const EmptySessionState = SessionState("")
|
||||
|
||||
type State string
|
||||
|
||||
const EmptyState = State("")
|
||||
@@ -1258,6 +1261,19 @@ type Idable interface {
|
||||
// ```
|
||||
type PatchObject map[string]any
|
||||
|
||||
func toPatchObject[T any](value T) (PatchObject, error) {
|
||||
b, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return PatchObject{}, err
|
||||
}
|
||||
var target PatchObject
|
||||
err = json.Unmarshal(b, &target)
|
||||
if err != nil {
|
||||
return PatchObject{}, err
|
||||
}
|
||||
return target, nil
|
||||
}
|
||||
|
||||
// Reference to Previous Method Results
|
||||
//
|
||||
// To allow clients to make more efficient use of the network and avoid round trips, an argument to one method
|
||||
@@ -1336,7 +1352,7 @@ type SetResponse[T Foo] interface {
|
||||
}
|
||||
|
||||
type Change interface {
|
||||
AsPatch() PatchObject
|
||||
AsPatch() (PatchObject, error)
|
||||
}
|
||||
|
||||
type ChangesCommand[T Foo] interface {
|
||||
@@ -1659,24 +1675,8 @@ type MailboxChange struct {
|
||||
|
||||
var _ Change = MailboxChange{}
|
||||
|
||||
func (m MailboxChange) AsPatch() PatchObject {
|
||||
p := PatchObject{}
|
||||
if m.Name != "" {
|
||||
p["name"] = m.Name
|
||||
}
|
||||
if m.ParentId != "" {
|
||||
p["parentId"] = m.ParentId
|
||||
}
|
||||
if m.Role != "" {
|
||||
p["role"] = m.Role
|
||||
}
|
||||
if m.SortOrder != nil {
|
||||
p["sortOrder"] = m.SortOrder
|
||||
}
|
||||
if m.IsSubscribed != nil {
|
||||
p["isSubscribed"] = m.IsSubscribed
|
||||
}
|
||||
return p
|
||||
func (m MailboxChange) AsPatch() (PatchObject, error) {
|
||||
return toPatchObject(m)
|
||||
}
|
||||
|
||||
type MailboxGetCommand struct {
|
||||
@@ -3953,27 +3953,8 @@ type IdentityChange struct {
|
||||
|
||||
var _ Change = IdentityChange{}
|
||||
|
||||
func (i IdentityChange) AsPatch() PatchObject {
|
||||
p := PatchObject{}
|
||||
if i.Name != "" {
|
||||
p["name"] = i.Name
|
||||
}
|
||||
if i.Email != "" {
|
||||
p["email"] = i.Email
|
||||
}
|
||||
if i.ReplyTo != "" {
|
||||
p["replyTo"] = i.ReplyTo
|
||||
}
|
||||
if i.Bcc != nil {
|
||||
p["bcc"] = i.Bcc
|
||||
}
|
||||
if i.TextSignature != nil {
|
||||
p["textSignature"] = i.TextSignature
|
||||
}
|
||||
if i.HtmlSignature != nil {
|
||||
p["htmlSignature"] = i.HtmlSignature
|
||||
}
|
||||
return p
|
||||
func (i IdentityChange) AsPatch() (PatchObject, error) {
|
||||
return toPatchObject(i)
|
||||
}
|
||||
|
||||
type IdentityGetResponse struct {
|
||||
@@ -4794,6 +4775,222 @@ var ContactCardProperties = []string{
|
||||
ContactCardPropertyPersonalInfo,
|
||||
}
|
||||
|
||||
type ContactCardChange struct {
|
||||
// The set of AddressBook ids this Card belongs to.
|
||||
//
|
||||
// A card MUST belong to at least one AddressBook at all times (until it is destroyed).
|
||||
//
|
||||
// The set is represented as an object, with each key being an AddressBook id.
|
||||
//
|
||||
// The value for each key in the object MUST be true.
|
||||
//
|
||||
// This is a JMAP extension and not part of [RFC9553].
|
||||
AddressBookIds map[string]bool `json:"addressBookIds,omitempty"`
|
||||
|
||||
// The JSContact type of the Card object: the value MUST be "Card".
|
||||
Type jscontact.TypeOfContactCard `json:"@type,omitempty"`
|
||||
|
||||
// The JSContact version of this Card.
|
||||
//
|
||||
// The value MUST be one of the IANA-registered JSContact Version values for the version property.
|
||||
Version *jscontact.JSContactVersion `json:"version,omitzero"`
|
||||
|
||||
// The kind of the entity the Card represents (default: `individual`).
|
||||
//
|
||||
// Values are:
|
||||
// * `individual`: a single person
|
||||
// * `group`: a group of people or entities
|
||||
// * `org`: an organization
|
||||
// * `location`: a named location
|
||||
// * `device`: a device such as an appliance, a computer, or a network element
|
||||
// * `application`: a software application
|
||||
Kind *jscontact.ContactCardKind `json:"kind,omitempty"`
|
||||
|
||||
// The language tag, as defined in [RFC5646].
|
||||
//
|
||||
// The language tag that best describes the language used for text in the Card, optionally including
|
||||
// additional information such as the script.
|
||||
//
|
||||
// Note that values MAY be localized in the `localizations` property.
|
||||
Language *string `json:"language,omitempty"`
|
||||
|
||||
// The set of Cards that are members of this group Card.
|
||||
//
|
||||
// Each key in the set is the uid property value of the member, and each boolean value MUST be `true`.
|
||||
// If this property is set, then the value of the kind property MUST be `group`.
|
||||
//
|
||||
// The opposite is not true. A group Card will usually contain the members property to specify the members
|
||||
// of the group, but it is not required to.
|
||||
//
|
||||
// A group Card without the members property can be considered an abstract grouping or one whose members
|
||||
// are known empirically (e.g., `IETF Participants`).
|
||||
Members map[string]bool `json:"members,omitempty"`
|
||||
|
||||
// The identifier for the product that created the Card.
|
||||
//
|
||||
// If set, the value MUST be at least one character long.
|
||||
ProdId *string `json:"prodId,omitempty"`
|
||||
|
||||
// The set of Card objects that relate to the Card.
|
||||
//
|
||||
// The value is a map, where each key is the uid property value of the related Card, and the value
|
||||
// defines the relation
|
||||
//
|
||||
// ```json
|
||||
// {
|
||||
// "relatedTo": {
|
||||
// "urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6": {
|
||||
// "relation": {"friend": true}
|
||||
// },
|
||||
// "8cacdfb7d1ffdb59@example.com": {
|
||||
// "relation": {}
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// ```
|
||||
RelatedTo map[string]jscontact.Relation `json:"relatedTo,omitempty"`
|
||||
|
||||
// The date and time when the data in the Card was last modified (UTCDateTime).
|
||||
Updated *time.Time `json:"updated,omitzero"`
|
||||
|
||||
// The name of the entity represented by the Card.
|
||||
//
|
||||
// This can be any type of name, e.g., it can, but need not, be the legal name of a person.
|
||||
Name *jscontact.Name `json:"name,omitempty"`
|
||||
|
||||
// The nicknames of the entity represented by the Card.
|
||||
Nicknames map[string]jscontact.Nickname `json:"nicknames,omitempty"`
|
||||
|
||||
// The company or organization names and units associated with the Card.
|
||||
Organizations map[string]jscontact.Organization `json:"organizations,omitempty"`
|
||||
|
||||
// The information that directs how to address, speak to, or refer to the entity that is represented by the Card.
|
||||
SpeakToAs *jscontact.SpeakToAs `json:"speakToAs,omitempty"`
|
||||
|
||||
// The job titles or functional positions of the entity represented by the Card.
|
||||
Titles map[string]jscontact.Title `json:"titles,omitempty"`
|
||||
|
||||
// The email addresses in which to contact the entity represented by the Card.
|
||||
Emails map[string]jscontact.EmailAddress `json:"emails,omitempty"`
|
||||
|
||||
// The online services that are associated with the entity represented by the Card.
|
||||
//
|
||||
// This can be messaging services, social media profiles, and other.
|
||||
OnlineServices map[string]jscontact.OnlineService `json:"onlineServices,omitempty"`
|
||||
|
||||
// The phone numbers by which to contact the entity represented by the Card.
|
||||
Phones map[string]jscontact.Phone `json:"phones,omitempty"`
|
||||
|
||||
// The preferred languages for contacting the entity associated with the Card.
|
||||
PreferredLanguages map[string]jscontact.LanguagePref `json:"preferredLanguages,omitempty"`
|
||||
|
||||
// The calendaring resources of the entity represented by the Card, such as to look up free-busy information.
|
||||
//
|
||||
// A Calendar object has all properties of the Resource data type, with the following additional definitions:
|
||||
// * The `@type` property value MUST be `Calendar`, if set
|
||||
// * The `kind` property is mandatory. Its enumerated values are:
|
||||
// * `calendar`: The resource is a calendar that contains entries such as calendar events or tasks
|
||||
// * `freeBusy`: The resource allows for free-busy lookups, for example, to schedule group events
|
||||
Calendars map[string]jscontact.Calendar `json:"calendars,omitempty"`
|
||||
|
||||
// The scheduling addresses by which the entity may receive calendar scheduling invitations.
|
||||
SchedulingAddresses map[string]jscontact.SchedulingAddress `json:"schedulingAddresses,omitempty"`
|
||||
|
||||
// The addresses of the entity represented by the Card, such as postal addresses or geographic locations.
|
||||
Addresses map[string]jscontact.Address `json:"addresses,omitempty"`
|
||||
|
||||
// The cryptographic resources such as public keys and certificates associated with the entity represented by the Card.
|
||||
//
|
||||
// A CryptoKey object has all properties of the `Resource` data type, with the following additional definition:
|
||||
// the `@type` property value MUST be `CryptoKey`, if set.
|
||||
//
|
||||
// The following example shows how to refer to an external cryptographic resource:
|
||||
// ```json
|
||||
// "cryptoKeys": {
|
||||
// "mykey1": {
|
||||
// "uri": "https://www.example.com/keys/jdoe.cer"
|
||||
// }
|
||||
// }
|
||||
// ```
|
||||
CryptoKeys map[string]jscontact.CryptoKey `json:"cryptoKeys,omitempty"`
|
||||
|
||||
// The directories containing information about the entity represented by the Card.
|
||||
//
|
||||
// A Directory object has all properties of the `Resource` data type, with the following additional definitions:
|
||||
// * The `@type` property value MUST be `Directory`, if set
|
||||
// * The `kind` property is mandatory; tts enumerated values are:
|
||||
// * `directory`: the resource is a directory service that the entity represented by the Card is a part of; this
|
||||
// typically is an organizational directory that also contains associated entities, e.g., co-workers and management
|
||||
// in a company directory
|
||||
// * `entry`: the resource is a directory entry of the entity represented by the Card; in contrast to the `directory`
|
||||
// type, this is the specific URI for the entity within a directory
|
||||
Directories map[string]jscontact.Directory `json:"directories,omitempty"`
|
||||
|
||||
// The links to resources that do not fit any of the other use-case-specific resource properties.
|
||||
//
|
||||
// A Link object has all properties of the `Resource` data type, with the following additional definitions:
|
||||
// * The `@type` property value MUST be `Link`, if set
|
||||
// * The `kind` property is optional; tts enumerated values are:
|
||||
// * `contact`: the resource is a URI by which the entity represented by the Card may be contacted;
|
||||
// this includes web forms or other media that require user interaction
|
||||
Links map[string]jscontact.Link `json:"links,omitempty"`
|
||||
|
||||
// The media resources such as photographs, avatars, or sounds that are associated with the entity represented by the Card.
|
||||
//
|
||||
// A Media object has all properties of the Resource data type, with the following additional definitions:
|
||||
// * the `@type` property value MUST be `Media`, if set
|
||||
// * the `kind` property is mandatory; its enumerated values are:
|
||||
// * `photo`: the resource is a photograph or avatar
|
||||
// * `sound`: the resource is audio media, e.g., to specify the proper pronunciation of the name property contents
|
||||
// * `logo`: the resource is a graphic image or logo associated with the entity represented by the Card
|
||||
Media map[string]jscontact.Media `json:"media,omitempty"`
|
||||
|
||||
// The property values localized to languages other than the main `language` of the Card.
|
||||
//
|
||||
// Localizations provide language-specific alternatives for existing property values and SHOULD NOT add new properties.
|
||||
//
|
||||
// The keys in the localizations property value are language tags [RFC5646]; the values are of type `PatchObject` and
|
||||
// localize the Card in that language tag.
|
||||
//
|
||||
// The paths in the `PatchObject` are relative to the Card that includes the localizations property.
|
||||
//
|
||||
// A patch MUST NOT target the localizations property.
|
||||
//
|
||||
// Conceptually, a Card is localized as follows:
|
||||
// * Determine the language tag in which the Card should be localized.
|
||||
// * If the localizations property includes a key for that language, obtain the PatchObject value;
|
||||
// if there is no such key, stop.
|
||||
// * Create a copy of the Card, but do not copy the localizations property.
|
||||
// * Apply all patches in the PatchObject to the copy of the Card.
|
||||
// * Optionally, set the language property in the copy of the Card.
|
||||
// * Use the patched copy of the Card as the localized variant of the original Card.
|
||||
//
|
||||
// A patch in the `PatchObject` may contain any value type.
|
||||
//
|
||||
// Its value MUST be a valid value according to the definition of the patched property.
|
||||
Localizations map[string]jscontact.PatchObject `json:"localizations,omitempty"`
|
||||
|
||||
// The memorable dates and events for the entity represented by the Card.
|
||||
Anniversaries map[string]jscontact.Anniversary `json:"anniversaries,omitempty"`
|
||||
|
||||
// The set of free-text keywords, also known as tags.
|
||||
//
|
||||
// Each key in the set is a keyword, and each boolean value MUST be `true`.
|
||||
Keywords map[string]bool `json:"keywords,omitempty"`
|
||||
|
||||
// The free-text notes that are associated with the Card.
|
||||
Notes map[string]jscontact.Note `json:"notes,omitempty"`
|
||||
|
||||
// The personal information of the entity represented by the Card.
|
||||
PersonalInfo map[string]jscontact.PersonalInfo `json:"personalInfo,omitempty"`
|
||||
}
|
||||
|
||||
var _ Change = ContactCardChange{}
|
||||
|
||||
func (e ContactCardChange) AsPatch() (PatchObject, error) {
|
||||
return toPatchObject(e)
|
||||
}
|
||||
|
||||
type CalendarRights struct {
|
||||
// The user may read the free-busy information for this calendar.
|
||||
MayReadFreeBusy bool `json:"mayReadFreeBusy"`
|
||||
@@ -5016,11 +5213,11 @@ type CalendarChange struct {
|
||||
// The user-visible name of the calendar.
|
||||
//
|
||||
// This may be any UTF-8 string of at least 1 character in length and maximum 255 octets in size.
|
||||
Name *string `json:"name"`
|
||||
Name *string `json:"name,omitzero"`
|
||||
|
||||
// An optional longer-form description of the calendar, to provide context in shared environments
|
||||
// where users need more than just the name.
|
||||
Description *string `json:"description,omitempty"`
|
||||
Description *string `json:"description,omitzero"`
|
||||
|
||||
// A color to be used when displaying events associated with the calendar.
|
||||
//
|
||||
@@ -5029,7 +5226,7 @@ type CalendarChange struct {
|
||||
// notation, as defined in Section 4.2.1 of CSS Color Module Level 3.
|
||||
//
|
||||
// The color SHOULD have sufficient contrast to be used as text on a white background.
|
||||
Color *string `json:"color,omitempty"`
|
||||
Color *string `json:"color,omitzero"`
|
||||
|
||||
// Defines the sort order of calendars when presented in the client’s UI, so it is consistent
|
||||
// between devices.
|
||||
@@ -5055,7 +5252,7 @@ type CalendarChange struct {
|
||||
// For example, a company may have a large number of shared calendars which all employees have
|
||||
// permission to access, but you would only subscribe to the ones you care about and want to be able
|
||||
// to have normally accessible.
|
||||
IsSubscribed *bool `json:"isSubscribed"`
|
||||
IsSubscribed *bool `json:"isSubscribed,omitempty"`
|
||||
|
||||
// Should the calendar’s events be displayed to the user at the moment?
|
||||
//
|
||||
@@ -5063,7 +5260,9 @@ type CalendarChange struct {
|
||||
//
|
||||
// If an event is in multiple calendars, it should be displayed if `isVisible` is `true`
|
||||
// for any of those calendars.
|
||||
IsVisible *bool `json:"isVisible" default:"true" doc:"opt"`
|
||||
//
|
||||
// Currently unsupported in Stalwart when modifying existing objects.
|
||||
IsVisible *bool `json:"isVisible,omitempty" default:"true" doc:"opt"`
|
||||
|
||||
// Should the calendar’s events be used as part of availability calculation?
|
||||
//
|
||||
@@ -5138,35 +5337,8 @@ type CalendarChange struct {
|
||||
|
||||
var _ Change = CalendarChange{}
|
||||
|
||||
func (a CalendarChange) AsPatch() PatchObject {
|
||||
p := PatchObject{}
|
||||
if a.Name != nil {
|
||||
p["name"] = *a.Name
|
||||
}
|
||||
if a.Description != nil {
|
||||
p["description"] = *a.Description
|
||||
}
|
||||
if a.Color != nil {
|
||||
p["color"] = *a.Color
|
||||
}
|
||||
if a.SortOrder != nil {
|
||||
p["sortOrder"] = *a.SortOrder
|
||||
}
|
||||
if a.IsSubscribed != nil {
|
||||
p["isSubscribed"] = *a.IsSubscribed
|
||||
}
|
||||
if a.IsVisible != nil {
|
||||
p["isVisible"] = *a.IsVisible
|
||||
}
|
||||
if a.IncludeInAvailability != nil {
|
||||
p["includeInAvailability"] = *a.IncludeInAvailability
|
||||
}
|
||||
// TODO DefaultAlertsWithTime
|
||||
// TODO DefaultAlertsWithoutTime
|
||||
// TODO TimeZone
|
||||
// TODO ShareWith
|
||||
// TODO MyRights
|
||||
return p
|
||||
func (c CalendarChange) AsPatch() (PatchObject, error) {
|
||||
return toPatchObject(c)
|
||||
}
|
||||
|
||||
// A CalendarEvent object contains information about an event, or recurring series of events,
|
||||
@@ -5252,6 +5424,79 @@ type CalendarEvent struct {
|
||||
jscalendar.Event
|
||||
}
|
||||
|
||||
type CalendarEventChange struct {
|
||||
// The set of Calendar ids this event belongs to.
|
||||
//
|
||||
// An event MUST belong to one or more Calendars at all times (until it is destroyed).
|
||||
//
|
||||
// The set is represented as an object, with each key being a Calendar id.
|
||||
//
|
||||
// The value for each key in the object MUST be `true`.
|
||||
CalendarIds map[string]bool `json:"calendarIds,omitempty"`
|
||||
|
||||
// If true, this event is to be considered a draft.
|
||||
//
|
||||
// The server will not send any scheduling messages to participants or send push notifications
|
||||
// for alerts.
|
||||
//
|
||||
// This may only be set to `true` upon creation.
|
||||
//
|
||||
// Once set to `false`, the value cannot be updated to `true`.
|
||||
//
|
||||
// This property MUST NOT appear in `recurrenceOverrides`.
|
||||
IsDraft *bool `json:"isDraft,omitzero"`
|
||||
|
||||
// Is this the authoritative source for this event (i.e., does it control scheduling for
|
||||
// this event; the event has not been added as a result of an invitation from another calendar system)?
|
||||
//
|
||||
// This is true if, and only if:
|
||||
// * the event’s `replyTo` property is null; or
|
||||
// * the account will receive messages sent to at least one of the methods specified in the `replyTo` property of the event.
|
||||
IsOrigin *bool `json:"isOrigin,omitzero"`
|
||||
|
||||
// For simple clients that do not implement time zone support.
|
||||
//
|
||||
// Clients should only use this if also asking the server to expand recurrences, as you cannot accurately
|
||||
// expand a recurrence without the original time zone.
|
||||
//
|
||||
// This property is calculated at fetch time by the server.
|
||||
//
|
||||
// Time zones are political and they can and do change at any time.
|
||||
//
|
||||
// Fetching exactly the same property again may return a different results if the time zone data has been updated on the server.
|
||||
//
|
||||
// Time zone data changes are not considered `updates` to the event.
|
||||
//
|
||||
// If set, the server will convert the UTC date to the event's current time zone and store the local time.
|
||||
//
|
||||
// This property is not included in `CalendarEvent/get` responses by default and must be requested explicitly.
|
||||
//
|
||||
// Floating events (events without a time zone) will be interpreted as per the time zone given as a `CalendarEvent/get` argument.
|
||||
//
|
||||
// Note that it is not possible to accurately calculate the expansion of recurrence rules or recurrence overrides with the
|
||||
// `utcStart` property rather than the local start time. Even simple recurrences such as "repeat weekly" may cross a
|
||||
// daylight-savings boundary and end up at a different UTC time. Clients that wish to use "utcStart" are RECOMMENDED to
|
||||
// request the server expand recurrences.
|
||||
UtcStart UTCDate `json:"utcStart,omitzero"`
|
||||
|
||||
// The server calculates the end time in UTC from the start/timeZone/duration properties of the event.
|
||||
//
|
||||
// This property is not included by default and must be requested explicitly.
|
||||
//
|
||||
// Like `utcStart`, it is calculated at fetch time if requested and may change due to time zone data changes.
|
||||
//
|
||||
// Floating events will be interpreted as per the time zone given as a `CalendarEvent/get` argument.
|
||||
UtcEnd UTCDate `json:"utcEnd,omitzero"`
|
||||
|
||||
jscalendar.EventChange
|
||||
}
|
||||
|
||||
var _ Change = CalendarEventChange{}
|
||||
|
||||
func (e CalendarEventChange) AsPatch() (PatchObject, error) {
|
||||
return toPatchObject(e)
|
||||
}
|
||||
|
||||
var _ Idable = &CalendarEvent{}
|
||||
|
||||
func (f CalendarEvent) GetObjectType() ObjectType { return CalendarEventType }
|
||||
@@ -6356,7 +6601,7 @@ type AddressBookChange struct {
|
||||
// The user-visible name of the AddressBook.
|
||||
//
|
||||
// This may be any UTF-8 string of at least 1 character in length and maximum 255 octets in size.
|
||||
Name *string `json:"name"`
|
||||
Name *string `json:"name,omitempty"`
|
||||
|
||||
// An optional longer-form description of the AddressBook, to provide context in shared environments
|
||||
// where users need more than just the name.
|
||||
@@ -6381,7 +6626,7 @@ type AddressBookChange struct {
|
||||
//
|
||||
// If false, the AddressBook and its contents SHOULD only be displayed when the user explicitly requests it
|
||||
// or to offer it for the user to subscribe to.
|
||||
IsSubscribed *bool `json:"isSubscribed"`
|
||||
IsSubscribed *bool `json:"isSubscribed,omitzero"`
|
||||
|
||||
// A map of Principal id to rights for principals this AddressBook is shared with.
|
||||
//
|
||||
@@ -6398,21 +6643,8 @@ type AddressBookChange struct {
|
||||
|
||||
var _ Change = AddressBookChange{}
|
||||
|
||||
func (a AddressBookChange) AsPatch() PatchObject {
|
||||
p := PatchObject{}
|
||||
if a.Name != nil {
|
||||
p["name"] = *a.Name
|
||||
}
|
||||
if a.Description != nil {
|
||||
p["description"] = *a.Description
|
||||
}
|
||||
if a.IsSubscribed != nil {
|
||||
p["isSubscribed"] = *a.IsSubscribed
|
||||
}
|
||||
if a.ShareWith != nil {
|
||||
p["shareWith"] = a.ShareWith
|
||||
}
|
||||
return p
|
||||
func (a AddressBookChange) AsPatch() (PatchObject, error) {
|
||||
return toPatchObject(a)
|
||||
}
|
||||
|
||||
type AddressBookSetCommand struct {
|
||||
@@ -6842,12 +7074,12 @@ type ContactCardQueryResponse struct {
|
||||
// Only if requested.
|
||||
//
|
||||
// This argument MUST be omitted if the calculateTotal request argument is not true.
|
||||
Total uint `json:"total,omitempty,omitzero"`
|
||||
Total *uint `json:"total,omitempty"`
|
||||
|
||||
// The limit enforced by the server on the maximum number of results to return (if set by the server).
|
||||
//
|
||||
// This is only returned if the server set a limit or used a different limit than that given in the request.
|
||||
Limit uint `json:"limit,omitempty,omitzero"`
|
||||
Limit uint `json:"limit,omitzero"`
|
||||
}
|
||||
|
||||
var _ QueryResponse[ContactCard] = &ContactCardQueryResponse{}
|
||||
@@ -6999,8 +7231,6 @@ func (r ContactCardChangesResponse) GetUpdated() []string { return r.Updated
|
||||
func (r ContactCardChangesResponse) GetDestroyed() []string { return r.Destroyed }
|
||||
func (r ContactCardChangesResponse) GetMarker() ContactCard { return ContactCard{} }
|
||||
|
||||
type ContactCardUpdate map[string]any
|
||||
|
||||
type ContactCardSetCommand struct {
|
||||
// The id of the account to use.
|
||||
AccountId string `json:"accountId"`
|
||||
@@ -7048,7 +7278,7 @@ type ContactCardSetCommand struct {
|
||||
//
|
||||
// The client may choose to optimise network usage by just sending the diff or may send the whole object; the server
|
||||
// processes it the same either way.
|
||||
Update map[string]ContactCardUpdate `json:"update,omitempty"`
|
||||
Update map[string]PatchObject `json:"update,omitempty"`
|
||||
|
||||
// A list of ids for ContactCard objects to permanently delete, or null if no objects are to be destroyed.
|
||||
Destroy []string `json:"destroy,omitempty"`
|
||||
@@ -7572,12 +7802,12 @@ type CalendarEventQueryResponse struct {
|
||||
// Only if requested.
|
||||
//
|
||||
// This argument MUST be omitted if the calculateTotal request argument is not true.
|
||||
Total uint `json:"total,omitempty,omitzero"`
|
||||
Total *uint `json:"total,omitempty"`
|
||||
|
||||
// The limit enforced by the server on the maximum number of results to return (if set by the server).
|
||||
//
|
||||
// This is only returned if the server set a limit or used a different limit than that given in the request.
|
||||
Limit uint `json:"limit,omitempty,omitzero"`
|
||||
Limit uint `json:"limit,omitzero"`
|
||||
}
|
||||
|
||||
var _ QueryResponse[CalendarEvent] = &CalendarEventQueryResponse{}
|
||||
@@ -7735,8 +7965,6 @@ func (r CalendarEventChangesResponse) GetUpdated() []string { return r.Updat
|
||||
func (r CalendarEventChangesResponse) GetDestroyed() []string { return r.Destroyed }
|
||||
func (r CalendarEventChangesResponse) GetMarker() CalendarEvent { return CalendarEvent{} }
|
||||
|
||||
type CalendarEventUpdate map[string]any
|
||||
|
||||
type CalendarEventSetCommand struct {
|
||||
// The id of the account to use.
|
||||
AccountId string `json:"accountId"`
|
||||
@@ -7784,7 +8012,7 @@ type CalendarEventSetCommand struct {
|
||||
//
|
||||
// The client may choose to optimise network usage by just sending the diff or may send the whole object; the server
|
||||
// processes it the same either way.
|
||||
Update map[string]CalendarEventUpdate `json:"update,omitempty"`
|
||||
Update map[string]PatchObject `json:"update,omitempty"`
|
||||
|
||||
// A list of ids for CalendarEvent objects to permanently delete, or null if no objects are to be destroyed.
|
||||
Destroy []string `json:"destroy,omitempty"`
|
||||
|
||||
@@ -398,10 +398,18 @@ func update[T Foo, CHANGES Change, SET SetCommand[T], GET GetCommand[T], RESP an
|
||||
logger := client.logger(name, ctx)
|
||||
ctx = ctx.WithLogger(logger)
|
||||
|
||||
update := setCommandFactory(map[string]PatchObject{id: changes.AsPatch()})
|
||||
var zero RESP
|
||||
|
||||
var update SET
|
||||
{
|
||||
patch, err := changes.AsPatch()
|
||||
if err != nil {
|
||||
return zero, "", "", "", jmapError(err, JmapPatchObjectSerialization)
|
||||
}
|
||||
update = setCommandFactory(map[string]PatchObject{id: patch})
|
||||
}
|
||||
get := getCommandFactory(id)
|
||||
cmd, err := client.request(ctx, objType.Namespaces, invocation(update, "0"), invocation(get, "1"))
|
||||
var zero RESP
|
||||
if err != nil {
|
||||
return zero, "", "", "", err
|
||||
}
|
||||
|
||||
@@ -423,6 +423,14 @@ func uintPtrIf(i uint, condition bool) *uint {
|
||||
}
|
||||
}
|
||||
|
||||
func uintPtrIfPtr(i *uint, condition bool) *uint {
|
||||
if condition {
|
||||
return i
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func ns(namespaces ...JmapNamespace) []JmapNamespace {
|
||||
result := make([]JmapNamespace, len(namespaces)+1)
|
||||
result[0] = JmapCore
|
||||
|
||||
@@ -790,6 +790,19 @@ func (t *LocalDateTime) UnmarshalJSON(b []byte) error {
|
||||
// automatically still inherit this.
|
||||
type PatchObject map[string]any
|
||||
|
||||
func toPatchObject[T any](value T) (PatchObject, error) {
|
||||
b, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return PatchObject{}, err
|
||||
}
|
||||
var target PatchObject
|
||||
err = json.Unmarshal(b, &target)
|
||||
if err != nil {
|
||||
return PatchObject{}, err
|
||||
}
|
||||
return target, nil
|
||||
}
|
||||
|
||||
// A Relation object defines the relation to other objects, using a possibly empty set of relation types.
|
||||
//
|
||||
// The object that defines this relation is the linking object, while the other object is the linked
|
||||
@@ -1740,6 +1753,25 @@ type CommonObject struct {
|
||||
Color string `json:"color,omitempty"`
|
||||
}
|
||||
|
||||
type CommonObjectChange struct {
|
||||
Uid *string `json:"uid,omitempty"`
|
||||
ProdId *string `json:"prodId,omitempty"`
|
||||
Created UTCDateTime `json:"created,omitzero"`
|
||||
Updated UTCDateTime `json:"updated,omitzero"`
|
||||
Title *string `json:"title,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
DescriptionContentType *string `json:"descriptionContentType,omitempty" doc:"opt" default:"text/plain"`
|
||||
Links map[string]Link `json:"links,omitempty"`
|
||||
Locale *string `json:"locale,omitempty"`
|
||||
Keywords map[string]bool `json:"keywords,omitempty"`
|
||||
Categories map[string]bool `json:"categories,omitempty"`
|
||||
Color *string `json:"color,omitempty"`
|
||||
}
|
||||
|
||||
func (m CommonObjectChange) AsPatch() (PatchObject, error) {
|
||||
return toPatchObject(m)
|
||||
}
|
||||
|
||||
// ### Recurrence Properties
|
||||
//
|
||||
// Some events and tasks occur at regular or irregular intervals. Rather than having to copy the data for every occurrence,
|
||||
@@ -2100,6 +2132,40 @@ type Object struct {
|
||||
HideAttendees bool `json:"hideAttendees,omitzero" doc:"opt" default:"false"`
|
||||
}
|
||||
|
||||
type ObjectChange struct {
|
||||
CommonObjectChange
|
||||
RelatedTo map[string]Relation `json:"relatedTo,omitempty"`
|
||||
Sequence *uint `json:"sequence,omitzero"`
|
||||
ShowWithoutTime *bool `json:"showWithoutTime,omitzero" doc:"opt" default:"false"`
|
||||
Locations map[string]Location `json:"locations,omitempty"`
|
||||
MainLocationId *string `json:"mainLocationId,omitempty"`
|
||||
VirtualLocations map[string]VirtualLocation `json:"virtualLocations,omitempty"`
|
||||
RecurrenceId *LocalDateTime `json:"recurrenceId,omitempty"`
|
||||
RecurrenceIdTimeZone string `json:"recurrenceIdTimeZone,omitempty"`
|
||||
RecurrenceRule *RecurrenceRule `json:"recurrenceRule,omitempty"`
|
||||
ExcludedRecurrenceRules []RecurrenceRule `json:"excludedRecurrenceRules,omitempty"`
|
||||
RecurrenceOverrides map[LocalDateTime]PatchObject `json:"recurrenceOverrides,omitempty"`
|
||||
Excluded *bool `json:"excluded,omitzero"`
|
||||
Priority *int `json:"priority,omitzero"`
|
||||
FreeBusyStatus *FreeBusyStatus `json:"freeBusyStatus,omitempty" doc:"opt" default:"busy"`
|
||||
Privacy *Privacy `json:"privacy,omitempty"`
|
||||
ReplyTo map[ReplyMethod]string `json:"replyTo,omitempty"`
|
||||
SentBy string `json:"sentBy,omitempty"`
|
||||
Participants map[string]Participant `json:"participants,omitempty"`
|
||||
RequestStatus string `json:"requestStatus,omitempty"`
|
||||
UseDefaultAlerts *bool `json:"useDefaultAlerts,omitzero" doc:"opt" default:"false"`
|
||||
Alerts map[string]Alert `json:"alerts,omitempty"`
|
||||
Localizations map[string]PatchObject `json:"localizations,omitempty"`
|
||||
TimeZone *string `json:"timeZone,omitempty"`
|
||||
MayInviteSelf *bool `json:"mayInviteSelf,omitzero" doc:"opt" default:"false"`
|
||||
MayInviteOthers *bool `json:"mayInviteOthers,omitzero" doc:"opt" default:"false"`
|
||||
HideAttendees *bool `json:"hideAttendees,omitzero" doc:"opt" default:"false"`
|
||||
}
|
||||
|
||||
func (m ObjectChange) AsPatch() (PatchObject, error) {
|
||||
return toPatchObject(m)
|
||||
}
|
||||
|
||||
type Event struct {
|
||||
Type TypeOfEvent `json:"@type,omitempty"`
|
||||
|
||||
@@ -2135,6 +2201,45 @@ type Event struct {
|
||||
Status Status `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
type EventChange struct {
|
||||
Type TypeOfEvent `json:"@type,omitempty"`
|
||||
|
||||
ObjectChange
|
||||
|
||||
// This is the date/time the event starts in the event's time zone (as specified in the timeZone property, see Section 4.7.1).
|
||||
Start LocalDateTime `json:"start,omitempty"`
|
||||
|
||||
// This is the zero or positive duration of the event in the event's start time zone.
|
||||
//
|
||||
// The end time of an event can be found by adding the duration to the event's start time.
|
||||
//
|
||||
// An Event MAY involve start and end locations that are in different time zones
|
||||
// (e.g., a transcontinental flight). This can be expressed using the `relativeTo` and `timeZone` properties of
|
||||
// the `Event`'s Location objects (see Section 4.2.5).
|
||||
Duration *Duration `json:"duration,omitempty"`
|
||||
|
||||
// This identifies the time zone in which this event ends, for cases where the start and time zones of the event differ
|
||||
// (e.g., a transcontinental flight).
|
||||
//
|
||||
// If this property is not set, then the event starts and ends in the same time zone.
|
||||
//
|
||||
// This property MUST NOT be set if the timeZone property value is null or not set.
|
||||
EndTimeZone string `json:"endTimeZone,omitempty"`
|
||||
|
||||
// This is the scheduling status (Section 4.4) of an Event.
|
||||
//
|
||||
// If set, it MUST be one of the following values, another value registered in the IANA
|
||||
// "JSCalendar Enum Values" registry, or a vendor-specific value (see Section 3.3):
|
||||
// * `confirmed`: indicates the event is definitely happening
|
||||
// * `cancelled`: indicates the event has been cancelled
|
||||
// * `tentative`: indicates the event may happen
|
||||
Status *Status `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
func (e EventChange) AsPatch() (PatchObject, error) {
|
||||
return toPatchObject(e)
|
||||
}
|
||||
|
||||
type Task struct {
|
||||
Type TypeOfTask `json:"@type,omitempty"`
|
||||
|
||||
|
||||
@@ -714,3 +714,33 @@ func TestEvent(t *testing.T) {
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestPatch(t *testing.T) {
|
||||
require := require.New(t)
|
||||
for _, tt := range []struct {
|
||||
change ObjectChange
|
||||
expected PatchObject
|
||||
}{
|
||||
{ObjectChange{}, PatchObject{}},
|
||||
{ObjectChange{
|
||||
CommonObjectChange: CommonObjectChange{
|
||||
Uid: strPtr("e9787e0b-e824-4284-964e-6b5d77af4bc9"),
|
||||
},
|
||||
}, PatchObject{
|
||||
"uid": "e9787e0b-e824-4284-964e-6b5d77af4bc9",
|
||||
}},
|
||||
} {
|
||||
b, err := json.Marshal(tt.expected)
|
||||
require.NoError(err)
|
||||
title := string(b)
|
||||
t.Run(title, func(t *testing.T) {
|
||||
patch, err := tt.change.AsPatch()
|
||||
require.NoError(err)
|
||||
require.Equal(tt.expected, patch)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func strPtr(s string) *string {
|
||||
return &s
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user