mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-02-14 08:11:21 -05:00
groupware: feature test improvements and upgrade to Stalwart 0.14.1
* upgrade Stalwart image for devtools/full to 0.14.1 * re-assert which features are implemented or not in 0.14.1 * refactor the integration tests yet again to make it clearer and easier to see those "features-or-not" * get rid of old tests that are now better covered by integration tests * rewrite how we compare expected and actual objects in integration tests, finally having found a way to ignore the @type attribute properly instead of having to mutate all objects to remove it
This commit is contained in:
@@ -21,6 +21,11 @@ import (
|
||||
"github.com/opencloud-eu/opencloud/pkg/structs"
|
||||
)
|
||||
|
||||
const (
|
||||
// currently not supported, reported as https://github.com/stalwartlabs/stalwart/issues/2431
|
||||
EnableMediaWithBlobId = false
|
||||
)
|
||||
|
||||
func TestContacts(t *testing.T) {
|
||||
if skip(t) {
|
||||
return
|
||||
@@ -39,8 +44,6 @@ func TestContacts(t *testing.T) {
|
||||
require.NotEmpty(accountId)
|
||||
require.NotEmpty(addressbookId)
|
||||
|
||||
allTrue(t, boxes, "mediaWithBlobId")
|
||||
|
||||
filter := ContactCardFilterCondition{
|
||||
InAddressBook: addressbookId,
|
||||
}
|
||||
@@ -61,10 +64,17 @@ func TestContacts(t *testing.T) {
|
||||
require.True(ok, "failed to find created contact by its id")
|
||||
matchContact(t, actual, expected)
|
||||
}
|
||||
|
||||
exceptions := []string{}
|
||||
if !EnableMediaWithBlobId {
|
||||
exceptions = append(exceptions, "mediaWithBlobId")
|
||||
}
|
||||
allBoxesAreTicked(t, boxes, exceptions...)
|
||||
}
|
||||
|
||||
func matchContact(t *testing.T, actual jscontact.ContactCard, expected jscontact.ContactCard) {
|
||||
require.Equal(t, expected, actual)
|
||||
// require.Equal(t, expected, actual)
|
||||
deepEqual(t, expected, actual)
|
||||
}
|
||||
|
||||
type ContactsBoxes struct {
|
||||
@@ -116,33 +126,32 @@ func (s *StalwartTest) fillContacts(
|
||||
}
|
||||
require.NotEmpty(addressbookId)
|
||||
|
||||
u := true
|
||||
|
||||
filled := map[string]jscontact.ContactCard{}
|
||||
for i := range count {
|
||||
person := gofakeit.Person()
|
||||
nameMap, nameObj := createName(person, u)
|
||||
nameMap, nameObj := createName(person)
|
||||
language := pickLanguage()
|
||||
contact := map[string]any{
|
||||
"@type": "Card",
|
||||
"version": "1.0",
|
||||
"addressBookIds": toBoolMap([]string{addressbookId}),
|
||||
"prodId": productName,
|
||||
"language": pickLanguage(),
|
||||
"language": language,
|
||||
"kind": "individual",
|
||||
"name": nameMap,
|
||||
}
|
||||
card := jscontact.ContactCard{
|
||||
//Type: jscontact.ContactCardType,
|
||||
Type: jscontact.ContactCardType,
|
||||
Version: "1.0",
|
||||
AddressBookIds: toBoolMap([]string{addressbookId}),
|
||||
ProdId: productName,
|
||||
Language: contact["language"].(string),
|
||||
Language: language,
|
||||
Kind: jscontact.ContactCardKindIndividual,
|
||||
Name: &nameObj,
|
||||
}
|
||||
|
||||
if i%3 == 0 {
|
||||
nicknameMap, nicknameObj := createNickName(person, u)
|
||||
nicknameMap, nicknameObj := createNickName(person)
|
||||
id := id()
|
||||
contact["nicknames"] = map[string]map[string]any{id: nicknameMap}
|
||||
card.Nicknames = map[string]jscontact.Nickname{id: nicknameObj}
|
||||
@@ -153,13 +162,13 @@ func (s *StalwartTest) fillContacts(
|
||||
emailMaps := map[string]map[string]any{}
|
||||
emailObjs := map[string]jscontact.EmailAddress{}
|
||||
emailId := id()
|
||||
emailMap, emailObj := createEmail(person, 10, u)
|
||||
emailMap, emailObj := createEmail(person, 10)
|
||||
emailMaps[emailId] = emailMap
|
||||
emailObjs[emailId] = emailObj
|
||||
|
||||
for i := range rand.Intn(3) {
|
||||
id := id()
|
||||
m, o := createSecondaryEmail(gofakeit.Email(), i*100, u)
|
||||
m, o := createSecondaryEmail(gofakeit.Email(), i*100)
|
||||
emailMaps[id] = m
|
||||
emailObjs[id] = o
|
||||
boxes.secondaryEmails = true
|
||||
@@ -192,12 +201,12 @@ func (s *StalwartTest) fillContacts(
|
||||
"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) }),
|
||||
}, untype(jscontact.Phone{
|
||||
}, jscontact.Phone{
|
||||
Type: jscontact.PhoneType,
|
||||
Number: tel,
|
||||
Features: features,
|
||||
Contexts: contexts,
|
||||
}, u), nil
|
||||
}, nil
|
||||
}); err != nil {
|
||||
return "", "", nil, boxes, err
|
||||
}
|
||||
@@ -212,16 +221,16 @@ func (s *StalwartTest) fillContacts(
|
||||
components := []jscontact.AddressComponent{}
|
||||
m := streetNumberRegex.FindAllStringSubmatch(source.Street, -1)
|
||||
if m != nil {
|
||||
components = append(components, jscontact.AddressComponent{ /*Type: jscontact.AddressComponentType,*/ Kind: jscontact.AddressComponentKindName, Value: m[0][2]})
|
||||
components = append(components, jscontact.AddressComponent{ /*Type: jscontact.AddressComponentType,*/ Kind: jscontact.AddressComponentKindNumber, Value: m[0][1]})
|
||||
components = append(components, jscontact.AddressComponent{Type: jscontact.AddressComponentType, Kind: jscontact.AddressComponentKindName, Value: m[0][2]})
|
||||
components = append(components, jscontact.AddressComponent{Type: jscontact.AddressComponentType, Kind: jscontact.AddressComponentKindNumber, Value: m[0][1]})
|
||||
} else {
|
||||
components = append(components, jscontact.AddressComponent{ /*Type: jscontact.AddressComponentType,*/ Kind: jscontact.AddressComponentKindName, Value: source.Street})
|
||||
components = append(components, jscontact.AddressComponent{Type: jscontact.AddressComponentType, Kind: jscontact.AddressComponentKindName, Value: source.Street})
|
||||
}
|
||||
components = append(components,
|
||||
jscontact.AddressComponent{ /*Type: jscontact.AddressComponentType,*/ Kind: jscontact.AddressComponentKindLocality, Value: source.City},
|
||||
jscontact.AddressComponent{ /*Type: jscontact.AddressComponentType,*/ Kind: jscontact.AddressComponentKindCountry, Value: source.Country},
|
||||
jscontact.AddressComponent{ /*Type: jscontact.AddressComponentType,*/ Kind: jscontact.AddressComponentKindRegion, Value: source.State},
|
||||
jscontact.AddressComponent{ /*Type: jscontact.AddressComponentType,*/ Kind: jscontact.AddressComponentKindPostcode, Value: source.Zip},
|
||||
jscontact.AddressComponent{Type: jscontact.AddressComponentType, Kind: jscontact.AddressComponentKindLocality, Value: source.City},
|
||||
jscontact.AddressComponent{Type: jscontact.AddressComponentType, Kind: jscontact.AddressComponentKindCountry, Value: source.Country},
|
||||
jscontact.AddressComponent{Type: jscontact.AddressComponentType, Kind: jscontact.AddressComponentKindRegion, Value: source.State},
|
||||
jscontact.AddressComponent{Type: jscontact.AddressComponentType, Kind: jscontact.AddressComponentKindPostcode, Value: source.Zip},
|
||||
)
|
||||
tz := pickRandom(timezones...)
|
||||
return map[string]any{
|
||||
@@ -232,13 +241,13 @@ func (s *StalwartTest) fillContacts(
|
||||
"defaultSeparator": ", ",
|
||||
"isOrdered": true,
|
||||
"timeZone": tz,
|
||||
}, untype(jscontact.Address{
|
||||
}, jscontact.Address{
|
||||
Type: jscontact.AddressType,
|
||||
Components: components,
|
||||
DefaultSeparator: ", ",
|
||||
IsOrdered: true,
|
||||
TimeZone: tz,
|
||||
}, u), nil
|
||||
}, nil
|
||||
}); err != nil {
|
||||
return "", "", nil, boxes, err
|
||||
}
|
||||
@@ -251,32 +260,32 @@ func (s *StalwartTest) fillContacts(
|
||||
"service": "Mastodon",
|
||||
"user": "@" + person.Contact.Email,
|
||||
"uri": "https://mastodon.example.com/@" + strings.ToLower(person.FirstName),
|
||||
}, untype(jscontact.OnlineService{
|
||||
}, jscontact.OnlineService{
|
||||
Type: jscontact.OnlineServiceType,
|
||||
Service: "Mastodon",
|
||||
User: "@" + person.Contact.Email,
|
||||
Uri: "https://mastodon.example.com/@" + strings.ToLower(person.FirstName),
|
||||
}, u), nil
|
||||
}, nil
|
||||
case 1:
|
||||
return map[string]any{
|
||||
"@type": "OnlineService",
|
||||
"uri": "xmpp:" + person.Contact.Email,
|
||||
}, untype(jscontact.OnlineService{
|
||||
}, jscontact.OnlineService{
|
||||
Type: jscontact.OnlineServiceType,
|
||||
Uri: "xmpp:" + person.Contact.Email,
|
||||
}, u), nil
|
||||
}, nil
|
||||
default:
|
||||
return map[string]any{
|
||||
"@type": "OnlineService",
|
||||
"service": "Discord",
|
||||
"user": person.Contact.Email,
|
||||
"uri": "https://discord.example.com/user/" + person.Contact.Email,
|
||||
}, untype(jscontact.OnlineService{
|
||||
}, jscontact.OnlineService{
|
||||
Type: jscontact.OnlineServiceType,
|
||||
Service: "Discord",
|
||||
User: person.Contact.Email,
|
||||
Uri: "https://discord.example.com/user/" + person.Contact.Email,
|
||||
}, u), nil
|
||||
}, nil
|
||||
}
|
||||
}); err != nil {
|
||||
return "", "", nil, boxes, err
|
||||
@@ -291,12 +300,12 @@ func (s *StalwartTest) fillContacts(
|
||||
"language": lang,
|
||||
"contexts": toBoolMap(contexts),
|
||||
"pref": i + 1,
|
||||
}, untype(jscontact.LanguagePref{
|
||||
}, 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),
|
||||
}, u), nil
|
||||
}, nil
|
||||
}); err != nil {
|
||||
return "", "", nil, boxes, err
|
||||
}
|
||||
@@ -315,23 +324,23 @@ func (s *StalwartTest) fillContacts(
|
||||
"name": person.Job.Company,
|
||||
"contexts": toBoolMapS("work"),
|
||||
}
|
||||
organizationObjs[orgId] = untype(jscontact.Organization{
|
||||
organizationObjs[orgId] = jscontact.Organization{
|
||||
Type: jscontact.OrganizationType,
|
||||
Name: person.Job.Company,
|
||||
Contexts: toBoolMapS(jscontact.OrganizationContextWork),
|
||||
}, u)
|
||||
}
|
||||
titleMaps[titleId] = map[string]any{
|
||||
"@type": "Title",
|
||||
"kind": "title",
|
||||
"name": person.Job.Title,
|
||||
"organizationId": orgId,
|
||||
}
|
||||
titleObjs[titleId] = untype(jscontact.Title{
|
||||
titleObjs[titleId] = jscontact.Title{
|
||||
Type: jscontact.TitleType,
|
||||
Kind: jscontact.TitleKindTitle,
|
||||
Name: person.Job.Title,
|
||||
OrganizationId: orgId,
|
||||
}, u)
|
||||
}
|
||||
}
|
||||
contact["organizations"] = organizationMaps
|
||||
contact["titles"] = titleMaps
|
||||
@@ -352,19 +361,29 @@ func (s *StalwartTest) fillContacts(
|
||||
}
|
||||
encoded := base64.RawStdEncoding.EncodeToString(b.Bytes())
|
||||
return map[string]any{
|
||||
"@type": "CryptoKey",
|
||||
"uri": "data:application/pgp-keys;base64," + encoded,
|
||||
}, untype(jscontact.CryptoKey{
|
||||
Type: jscontact.CryptoKeyType,
|
||||
Uri: "data:application/pgp-keys;base64," + encoded,
|
||||
}, u), nil
|
||||
"@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
|
||||
}); 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) {
|
||||
label := fmt.Sprintf("photo-%d", 1000+rand.Intn(9000))
|
||||
switch rand.Intn(3) {
|
||||
|
||||
r := 0
|
||||
if EnableMediaWithBlobId {
|
||||
r = rand.Intn(3)
|
||||
} else {
|
||||
r = rand.Intn(2)
|
||||
}
|
||||
|
||||
switch r {
|
||||
case 0:
|
||||
boxes.mediaWithDataUri = true
|
||||
// use data uri
|
||||
@@ -381,16 +400,35 @@ func (s *StalwartTest) fillContacts(
|
||||
"mediaType": mime,
|
||||
"contexts": structs.MapKeys(contexts, func(c jscontact.MediaContext) string { return string(c) }),
|
||||
"label": label,
|
||||
}, untype(jscontact.Media{
|
||||
}, jscontact.Media{
|
||||
Type: jscontact.MediaType,
|
||||
Kind: jscontact.MediaKindPhoto,
|
||||
Uri: uri,
|
||||
MediaType: mime,
|
||||
Contexts: contexts,
|
||||
Label: label,
|
||||
}, u), nil
|
||||
// currently not supported, reported as https://github.com/stalwartlabs/stalwart/issues/2431
|
||||
case -1: // change this to 1 to enable it again
|
||||
}, 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
|
||||
|
||||
default:
|
||||
boxes.mediaWithBlobId = true
|
||||
size := pickRandom(16, 24, 32, 48, 64)
|
||||
img := gofakeit.ImageJpeg(size, size)
|
||||
@@ -405,33 +443,15 @@ func (s *StalwartTest) fillContacts(
|
||||
"blobId": blob.BlobId,
|
||||
"contexts": structs.MapKeys(contexts, func(c jscontact.MediaContext) string { return string(c) }),
|
||||
"label": label,
|
||||
}, untype(jscontact.Media{
|
||||
}, jscontact.Media{
|
||||
Type: jscontact.MediaType,
|
||||
Kind: jscontact.MediaKindPhoto,
|
||||
BlobId: blob.BlobId,
|
||||
MediaType: blob.Type,
|
||||
Contexts: contexts,
|
||||
Label: label,
|
||||
}, u), nil
|
||||
}, nil
|
||||
|
||||
default:
|
||||
boxes.mediaWithExternalUri = true
|
||||
// use external uri
|
||||
uri := picsum(128, 128)
|
||||
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,
|
||||
}, untype(jscontact.Media{
|
||||
Type: jscontact.MediaType,
|
||||
Kind: jscontact.MediaKindPhoto,
|
||||
Uri: uri,
|
||||
Contexts: contexts,
|
||||
Label: label,
|
||||
}, u), nil
|
||||
}
|
||||
}); err != nil {
|
||||
return "", "", nil, boxes, err
|
||||
@@ -443,12 +463,12 @@ func (s *StalwartTest) fillContacts(
|
||||
"kind": "contact",
|
||||
"uri": "mailto:" + person.Contact.Email,
|
||||
"pref": (i + 1) * 10,
|
||||
}, untype(jscontact.Link{
|
||||
}, jscontact.Link{
|
||||
Type: jscontact.LinkType,
|
||||
Kind: jscontact.LinkKindContact,
|
||||
Uri: "mailto:" + person.Contact.Email,
|
||||
Pref: uint((i + 1) * 10),
|
||||
}, u), nil
|
||||
}, nil
|
||||
}); err != nil {
|
||||
return "", "", nil, boxes, err
|
||||
}
|
||||
|
||||
@@ -19,6 +19,12 @@ import (
|
||||
"github.com/opencloud-eu/opencloud/pkg/structs"
|
||||
)
|
||||
|
||||
// fields that are currently unsupported in Stalwart
|
||||
const (
|
||||
EnableEventMayInviteFields = false
|
||||
EnableEventParticipantDescriptionFields = false
|
||||
)
|
||||
|
||||
func TestEvents(t *testing.T) {
|
||||
if skip(t) {
|
||||
return
|
||||
@@ -37,8 +43,6 @@ func TestEvents(t *testing.T) {
|
||||
require.NotEmpty(accountId)
|
||||
require.NotEmpty(calendarId)
|
||||
|
||||
allTrue(t, boxes)
|
||||
|
||||
filter := CalendarEventFilterCondition{
|
||||
InCalendar: calendarId,
|
||||
}
|
||||
@@ -59,13 +63,23 @@ func TestEvents(t *testing.T) {
|
||||
require.True(ok, "failed to find created contact by its id")
|
||||
matchEvent(t, actual, expected)
|
||||
}
|
||||
|
||||
exceptions := []string{}
|
||||
if !EnableEventMayInviteFields {
|
||||
exceptions = append(exceptions, "mayInvite")
|
||||
}
|
||||
allBoxesAreTicked(t, boxes, exceptions...)
|
||||
}
|
||||
|
||||
func matchEvent(t *testing.T, actual CalendarEvent, expected CalendarEvent) {
|
||||
require.Equal(t, expected, actual)
|
||||
//require.Equal(t, expected, actual)
|
||||
deepEqual(t, expected, actual)
|
||||
}
|
||||
|
||||
type EventsBoxes struct {
|
||||
categories bool
|
||||
keywords bool
|
||||
mayInvite bool
|
||||
}
|
||||
|
||||
func (s *StalwartTest) fillEvents(
|
||||
@@ -100,8 +114,6 @@ func (s *StalwartTest) fillEvents(
|
||||
}
|
||||
require.NotEmpty(calendarId)
|
||||
|
||||
u := true
|
||||
|
||||
filled := map[string]CalendarEvent{}
|
||||
for i := range count {
|
||||
uid := gofakeit.UUID()
|
||||
@@ -119,7 +131,7 @@ func (s *StalwartTest) fillEvents(
|
||||
for range n {
|
||||
locationId, locationMap, locationObj := pickLocation()
|
||||
locationMaps[locationId] = locationMap
|
||||
locationObjs[locationId] = untype(locationObj, u)
|
||||
locationObjs[locationId] = locationObj
|
||||
locationIds = append(locationIds, locationId)
|
||||
if n > 0 && mainLocationId == "" {
|
||||
mainLocationId = locationId
|
||||
@@ -127,7 +139,7 @@ func (s *StalwartTest) fillEvents(
|
||||
}
|
||||
}
|
||||
virtualLocationId, virtualLocationMap, virtualLocationObj := pickVirtualLocation()
|
||||
participantMaps, participantObjs, organizerEmail := createParticipants(uid, locationIds, []string{virtualLocationId}, u)
|
||||
participantMaps, participantObjs, organizerEmail := createParticipants(uid, locationIds, []string{virtualLocationId})
|
||||
duration := pickRandom("PT30M", "PT45M", "PT1H", "PT90M")
|
||||
tz := pickRandom(timezones...)
|
||||
daysDiff := rand.Intn(31) - 15
|
||||
@@ -148,9 +160,9 @@ func (s *StalwartTest) fillEvents(
|
||||
privacy := pickRandom(jscalendar.Privacies...)
|
||||
color := pickRandom(basicColors...)
|
||||
locale := pickLocale()
|
||||
keywords := keywords()
|
||||
categories := categories()
|
||||
var _ = categories // currently not used because it's unsupported in Stalwart
|
||||
keywords := pickKeywords()
|
||||
categories := pickCategories()
|
||||
|
||||
sequence := 0
|
||||
|
||||
alertId := id()
|
||||
@@ -169,18 +181,15 @@ func (s *StalwartTest) fillEvents(
|
||||
"description": description,
|
||||
"descriptionContentType": descriptionFormat,
|
||||
"locale": locale,
|
||||
// "categories": categories, // currently unsupported in Stalwart
|
||||
"color": color,
|
||||
"sequence": sequence,
|
||||
"showWithoutTime": false,
|
||||
"freeBusyStatus": string(freeBusy),
|
||||
"privacy": string(privacy),
|
||||
"sentBy": organizerEmail,
|
||||
"participants": participantMaps,
|
||||
"timeZone": tz,
|
||||
// "mayInviteSelf": true, // currently unsupported in Stalwart
|
||||
// "mayInviteOthers": true, // currently unsupported in Stalwart
|
||||
"hideAttendees": false,
|
||||
"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,
|
||||
},
|
||||
@@ -199,12 +208,13 @@ func (s *StalwartTest) fillEvents(
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
obj := CalendarEvent{
|
||||
Id: "",
|
||||
CalendarIds: toBoolMapS(calendarId),
|
||||
IsDraft: isDraft,
|
||||
IsOrigin: true,
|
||||
Event: untype(jscalendar.Event{
|
||||
Event: jscalendar.Event{
|
||||
Type: jscalendar.EventType,
|
||||
Start: jscalendar.LocalDateTime(start),
|
||||
Duration: jscalendar.Duration(duration),
|
||||
@@ -217,8 +227,7 @@ func (s *StalwartTest) fillEvents(
|
||||
Description: description,
|
||||
DescriptionContentType: descriptionFormat,
|
||||
Locale: locale,
|
||||
// Categories: categories, // currently unsupported in Stalwart
|
||||
Color: color,
|
||||
Color: color,
|
||||
},
|
||||
Sequence: uint(sequence),
|
||||
ShowWithoutTime: false,
|
||||
@@ -227,33 +236,46 @@ func (s *StalwartTest) fillEvents(
|
||||
SentBy: organizerEmail,
|
||||
Participants: participantObjs,
|
||||
TimeZone: tz,
|
||||
// MayInviteSelf: true, // currently unsupported in Stalwart
|
||||
// MayInviteOthers: true, // currently unsupported in Stalwart
|
||||
HideAttendees: false,
|
||||
HideAttendees: false,
|
||||
ReplyTo: map[jscalendar.ReplyMethod]string{
|
||||
jscalendar.ReplyMethodImip: "mailto:" + organizerEmail,
|
||||
},
|
||||
Locations: locationObjs,
|
||||
VirtualLocations: map[string]jscalendar.VirtualLocation{
|
||||
virtualLocationId: untype(virtualLocationObj, u),
|
||||
virtualLocationId: virtualLocationObj,
|
||||
},
|
||||
Alerts: map[string]jscalendar.Alert{
|
||||
alertId: untype(jscalendar.Alert{
|
||||
alertId: jscalendar.Alert{
|
||||
Type: jscalendar.AlertType,
|
||||
Trigger: jscalendar.OffsetTrigger{
|
||||
Type: jscalendar.OffsetTriggerType,
|
||||
Offset: jscalendar.SignedDuration(alertOffset),
|
||||
RelativeTo: jscalendar.RelativeToStart,
|
||||
},
|
||||
}, u),
|
||||
},
|
||||
},
|
||||
},
|
||||
}, u),
|
||||
},
|
||||
}
|
||||
|
||||
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 != "" {
|
||||
@@ -273,19 +295,19 @@ func (s *StalwartTest) fillEvents(
|
||||
uri = "data:" + mime + ";base64," + base64.StdEncoding.EncodeToString(img)
|
||||
default:
|
||||
mime = "image/jpeg"
|
||||
uri = "https://picsum.photos/id/" + strconv.Itoa(1+rand.Intn(200)) + "/200/300"
|
||||
uri = externalImageUri()
|
||||
}
|
||||
return map[string]any{
|
||||
"@type": "Link",
|
||||
"href": uri,
|
||||
"contentType": mime,
|
||||
"rel": string(rel),
|
||||
}, untype(jscalendar.Link{
|
||||
}, jscalendar.Link{
|
||||
Type: jscalendar.LinkType,
|
||||
Href: uri,
|
||||
ContentType: mime,
|
||||
Rel: rel,
|
||||
}, u), nil
|
||||
}, nil
|
||||
})
|
||||
|
||||
if rand.Intn(10) > 7 {
|
||||
@@ -306,7 +328,7 @@ func (s *StalwartTest) fillEvents(
|
||||
"firstDayOfWeek": string(jscalendar.DayOfWeekMonday),
|
||||
"count": count,
|
||||
}
|
||||
rr := untype(jscalendar.RecurrenceRule{
|
||||
rr := jscalendar.RecurrenceRule{
|
||||
Type: jscalendar.RecurrenceRuleType,
|
||||
Frequency: frequency,
|
||||
Interval: uint(interval),
|
||||
@@ -314,7 +336,7 @@ func (s *StalwartTest) fillEvents(
|
||||
Skip: jscalendar.SkipOmit,
|
||||
FirstDayOfWeek: jscalendar.DayOfWeekMonday,
|
||||
Count: uint(count),
|
||||
}, u)
|
||||
}
|
||||
obj.RecurrenceRule = &rr
|
||||
}
|
||||
|
||||
@@ -400,7 +422,6 @@ var virtualRooms = []jscalendar.VirtualLocation{
|
||||
func pickLocation() (string, map[string]any, jscalendar.Location) {
|
||||
locationId := id()
|
||||
room := rooms[rand.Intn(len(rooms))]
|
||||
room.Links = nil // currently unsupported in Stalwart
|
||||
b, err := json.Marshal(room)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -431,23 +452,23 @@ func pickVirtualLocation() (string, map[string]any, jscalendar.VirtualLocation)
|
||||
var ChairRoles = toBoolMapS(jscalendar.RoleChair, jscalendar.RoleOwner)
|
||||
var RegularRoles = toBoolMapS(jscalendar.RoleOptional)
|
||||
|
||||
func createParticipants(uid string, locationIds []string, virtualLocationIds []string, u bool) (map[string]map[string]any, map[string]jscalendar.Participant, string) {
|
||||
func createParticipants(uid string, locationIds []string, virtualLocationIds []string) (map[string]map[string]any, 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...), "", "", u)
|
||||
organizerId, organizerEmail, organizerMap, organizerObj := createParticipant(0, uid, pickRandom(options...), "", "")
|
||||
maps[organizerId] = organizerMap
|
||||
objs[organizerId] = organizerObj
|
||||
for i := 1; i < n; i++ {
|
||||
id, _, participantMap, participantObj := createParticipant(i, uid, pickRandom(options...), organizerId, organizerEmail, u)
|
||||
id, _, participantMap, participantObj := createParticipant(i, uid, pickRandom(options...), organizerId, organizerEmail)
|
||||
maps[id] = participantMap
|
||||
objs[id] = participantObj
|
||||
}
|
||||
return maps, objs, organizerEmail
|
||||
}
|
||||
|
||||
func createParticipant(i int, uid string, locationId string, organizerEmail string, organizerId string, u bool) (string, string, map[string]any, jscalendar.Participant) {
|
||||
func createParticipant(i int, uid string, locationId string, organizerEmail string, organizerId string) (string, string, map[string]any, jscalendar.Participant) {
|
||||
participantId := id()
|
||||
person := gofakeit.Person()
|
||||
roles := RegularRoles
|
||||
@@ -500,11 +521,9 @@ func createParticipant(i int, uid string, locationId string, organizerEmail stri
|
||||
}
|
||||
|
||||
m := map[string]any{
|
||||
"@type": "Participant",
|
||||
"name": name,
|
||||
"email": email,
|
||||
// "description": description, // currently not supported in Stalwart
|
||||
// "descriptionContentType": descriptionContentType, // currently not supported in Stalwart
|
||||
"@type": "Participant",
|
||||
"name": name,
|
||||
"email": email,
|
||||
"calendarAddress": calendarAddress,
|
||||
"kind": "individual",
|
||||
"roles": structs.MapKeys(roles, func(r jscalendar.Role) string { return string(r) }),
|
||||
@@ -522,11 +541,9 @@ func createParticipant(i int, uid string, locationId string, organizerEmail stri
|
||||
"scheduleId": "mailto:" + email,
|
||||
}
|
||||
o := jscalendar.Participant{
|
||||
Type: jscalendar.ParticipantType,
|
||||
Name: name,
|
||||
Email: email,
|
||||
// Description: description, // currently not supported in Stalwart
|
||||
// DescriptionContentType: descriptionContentType, // currently not supported in Stalwart
|
||||
Type: jscalendar.ParticipantType,
|
||||
Name: name,
|
||||
Email: email,
|
||||
Kind: jscalendar.ParticipantKindIndividual,
|
||||
CalendarAddress: calendarAddress,
|
||||
Roles: roles,
|
||||
@@ -544,8 +561,15 @@ func createParticipant(i int, uid string, locationId string, organizerEmail stri
|
||||
ScheduleId: "mailto:" + email,
|
||||
}
|
||||
|
||||
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) {
|
||||
href := "https://picsum.photos/id/" + strconv.Itoa(1+rand.Intn(200)) + "/200/300"
|
||||
href := externalImageUri()
|
||||
title := person.FirstName + "'s Cake Day pick"
|
||||
return map[string]any{
|
||||
"@type": "Link",
|
||||
@@ -554,20 +578,20 @@ func createParticipant(i int, uid string, locationId string, organizerEmail stri
|
||||
"rel": "icon",
|
||||
"display": "badge",
|
||||
"title": title,
|
||||
}, untype(jscalendar.Link{
|
||||
}, jscalendar.Link{
|
||||
Type: jscalendar.LinkType,
|
||||
Href: href,
|
||||
ContentType: "image/jpeg",
|
||||
Rel: jscalendar.RelIcon,
|
||||
Display: jscalendar.DisplayBadge,
|
||||
Title: title,
|
||||
}, u), nil
|
||||
}, nil
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return participantId, person.Contact.Email, m, untype(o, u)
|
||||
return participantId, person.Contact.Email, m, o
|
||||
}
|
||||
|
||||
var Keywords = []string{
|
||||
@@ -578,7 +602,7 @@ var Keywords = []string{
|
||||
"decision",
|
||||
}
|
||||
|
||||
func keywords() map[string]bool {
|
||||
func pickKeywords() map[string]bool {
|
||||
return toBoolMap(pickRandoms(Keywords...))
|
||||
}
|
||||
|
||||
@@ -587,6 +611,6 @@ var Categories = []string{
|
||||
"http://opencloud.eu/categories/internal",
|
||||
}
|
||||
|
||||
func categories() map[string]bool {
|
||||
func pickCategories() map[string]bool {
|
||||
return toBoolMap(pickRandoms(Categories...))
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
@@ -42,6 +43,10 @@ import (
|
||||
"github.com/go-crypt/crypt/algorithm/shacrypt"
|
||||
)
|
||||
|
||||
const (
|
||||
EnableTypes = false
|
||||
)
|
||||
|
||||
var (
|
||||
domains = [...]string{"earth.gov", "mars.mil", "opa.org", "acme.com"}
|
||||
people = [...]string{
|
||||
@@ -58,7 +63,7 @@ var (
|
||||
)
|
||||
|
||||
const (
|
||||
stalwartImage = "ghcr.io/stalwartlabs/stalwart:v0.14.0-alpine"
|
||||
stalwartImage = "ghcr.io/stalwartlabs/stalwart:v0.14.1-alpine"
|
||||
httpPort = "8080"
|
||||
imapsPort = "993"
|
||||
configTemplate = `
|
||||
@@ -105,6 +110,7 @@ tracer.log.level = "trace"
|
||||
tracer.log.lossy = false
|
||||
tracer.log.multiline = false
|
||||
tracer.log.type = "stdout"
|
||||
sharing.allow-directory-query = false
|
||||
`
|
||||
)
|
||||
|
||||
@@ -393,13 +399,6 @@ func pickRandomlyFromMap[K comparable, V any](m map[K]V, min int, max int) map[K
|
||||
|
||||
var productName = "jmaptest"
|
||||
|
||||
func untype[S any](s S, t bool) S {
|
||||
if t {
|
||||
reflect.ValueOf(&s).Elem().FieldByName("Type").SetString("")
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
type TestJmapClient struct {
|
||||
h *http.Client
|
||||
username string
|
||||
@@ -645,7 +644,7 @@ func (j *TestJmapClient) objectsById(accountId string, objectType ObjectType, sc
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func createName(person *gofakeit.PersonInfo, u bool) (map[string]any, jscontact.Name) {
|
||||
func createName(person *gofakeit.PersonInfo) (map[string]any, jscontact.Name) {
|
||||
o := jscontact.Name{
|
||||
Type: jscontact.NameType,
|
||||
}
|
||||
@@ -658,20 +657,20 @@ func createName(person *gofakeit.PersonInfo, u bool) (map[string]any, jscontact.
|
||||
"kind": "given",
|
||||
"value": person.FirstName,
|
||||
}
|
||||
oComps[0] = untype(jscontact.NameComponent{
|
||||
oComps[0] = jscontact.NameComponent{
|
||||
Type: jscontact.NameComponentType,
|
||||
Kind: jscontact.NameComponentKindGiven,
|
||||
Value: person.FirstName,
|
||||
}, u)
|
||||
}
|
||||
mComps[1] = map[string]string{
|
||||
"kind": "surname",
|
||||
"value": person.LastName,
|
||||
}
|
||||
oComps[1] = untype(jscontact.NameComponent{
|
||||
oComps[1] = jscontact.NameComponent{
|
||||
Type: jscontact.NameComponentType,
|
||||
Kind: jscontact.NameComponentKindSurname,
|
||||
Value: person.LastName,
|
||||
}, u)
|
||||
}
|
||||
m["components"] = mComps
|
||||
o.Components = oComps
|
||||
m["isOrdered"] = true
|
||||
@@ -681,24 +680,24 @@ func createName(person *gofakeit.PersonInfo, u bool) (map[string]any, jscontact.
|
||||
full := fmt.Sprintf("%s %s", person.FirstName, person.LastName)
|
||||
m["full"] = full
|
||||
o.Full = full
|
||||
return m, untype(o, u)
|
||||
return m, o
|
||||
}
|
||||
|
||||
func createNickName(_ *gofakeit.PersonInfo, u bool) (map[string]any, jscontact.Nickname) {
|
||||
func createNickName(_ *gofakeit.PersonInfo) (map[string]any, 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) })),
|
||||
}, untype(jscontact.Nickname{
|
||||
}, jscontact.Nickname{
|
||||
Type: jscontact.NicknameType,
|
||||
Name: name,
|
||||
Contexts: orNilMap(toBoolMap(contexts)),
|
||||
}, u)
|
||||
}
|
||||
}
|
||||
|
||||
func createEmail(person *gofakeit.PersonInfo, pref int, u bool) (map[string]any, jscontact.EmailAddress) {
|
||||
func createEmail(person *gofakeit.PersonInfo, pref int) (map[string]any, jscontact.EmailAddress) {
|
||||
email := person.Contact.Email
|
||||
contexts := pickRandoms1(jscontact.EmailAddressContextWork, jscontact.EmailAddressContextPrivate)
|
||||
label := strings.ToLower(person.FirstName)
|
||||
@@ -708,28 +707,28 @@ func createEmail(person *gofakeit.PersonInfo, pref int, u bool) (map[string]any,
|
||||
"contexts": toBoolMap(structs.Map(contexts, func(s jscontact.EmailAddressContext) string { return string(s) })),
|
||||
"label": label,
|
||||
"pref": pref,
|
||||
}, untype(jscontact.EmailAddress{
|
||||
}, jscontact.EmailAddress{
|
||||
Type: jscontact.EmailAddressType,
|
||||
Address: email,
|
||||
Contexts: orNilMap(toBoolMap(contexts)),
|
||||
Label: label,
|
||||
Pref: uint(pref),
|
||||
}, u)
|
||||
}
|
||||
}
|
||||
|
||||
func createSecondaryEmail(email string, pref int, u bool) (map[string]any, jscontact.EmailAddress) {
|
||||
func createSecondaryEmail(email string, pref int) (map[string]any, 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,
|
||||
}, untype(jscontact.EmailAddress{
|
||||
}, jscontact.EmailAddress{
|
||||
Type: jscontact.EmailAddressType,
|
||||
Address: email,
|
||||
Contexts: orNilMap(toBoolMap(contexts)),
|
||||
Pref: uint(pref),
|
||||
}, u)
|
||||
}
|
||||
}
|
||||
|
||||
var idFirstLetters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||
@@ -971,8 +970,8 @@ func propmap[T any](enabled bool, min int, max int, container map[string]any, na
|
||||
return nil
|
||||
}
|
||||
|
||||
func picsum(w, h int) string {
|
||||
return fmt.Sprintf("https://picsum.photos/id/%d/%d/%d", 1+rand.Intn(200), h, w)
|
||||
func externalImageUri() string {
|
||||
return fmt.Sprintf("https://picsum.photos/id/%d/%d/%d", 1+rand.Intn(200), 200, 300)
|
||||
}
|
||||
|
||||
func orNilMap[K comparable, V any](m map[K]V) map[K]V {
|
||||
@@ -983,14 +982,6 @@ func orNilMap[K comparable, V any](m map[K]V) map[K]V {
|
||||
}
|
||||
}
|
||||
|
||||
func orNilSlice[E any](s []E) []E {
|
||||
if len(s) < 1 {
|
||||
return nil
|
||||
} else {
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
func toBoolMap[K comparable](s []K) map[K]bool {
|
||||
m := make(map[K]bool, len(s))
|
||||
for _, e := range s {
|
||||
@@ -1046,15 +1037,37 @@ func pickLocale() string {
|
||||
return pickRandom("en", "fr", "de")
|
||||
}
|
||||
|
||||
func allTrue[S any](t *testing.T, s S, exceptions ...string) {
|
||||
func allBoxesAreTicked[S any](t *testing.T, s S, exceptions ...string) {
|
||||
v := reflect.ValueOf(s)
|
||||
typ := v.Type()
|
||||
for i := range v.NumField() {
|
||||
name := typ.Field(i).Name
|
||||
if slices.Contains(exceptions, name) {
|
||||
log.Printf("(/) %s\n", name)
|
||||
continue
|
||||
}
|
||||
value := v.Field(i).Bool()
|
||||
if value {
|
||||
log.Printf("(X) %s\n", name)
|
||||
} else {
|
||||
log.Printf("( ) %s\n", name)
|
||||
}
|
||||
require.True(t, value, "should be true: %v", name)
|
||||
}
|
||||
}
|
||||
|
||||
func deepEqual[T any](t *testing.T, expected, actual T) {
|
||||
diff := ""
|
||||
if EnableTypes {
|
||||
diff = cmp.Diff(expected, actual)
|
||||
} else {
|
||||
diff = cmp.Diff(expected, actual, cmp.FilterPath(func(p cmp.Path) bool {
|
||||
switch sf := p.Last().(type) {
|
||||
case cmp.StructField:
|
||||
return sf.String() == ".Type"
|
||||
}
|
||||
return false
|
||||
}, cmp.Ignore()))
|
||||
}
|
||||
require.Empty(t, diff)
|
||||
}
|
||||
|
||||
@@ -1,772 +0,0 @@
|
||||
package jmap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/opencloud-eu/opencloud/pkg/jscalendar"
|
||||
"github.com/opencloud-eu/opencloud/pkg/jscontact"
|
||||
"github.com/opencloud-eu/opencloud/pkg/log"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func jsoneq[X any](t *testing.T, expected string, object X) {
|
||||
data, err := json.MarshalIndent(object, "", "")
|
||||
require.NoError(t, err)
|
||||
require.JSONEq(t, expected, string(data))
|
||||
|
||||
var rec X
|
||||
err = json.Unmarshal(data, &rec)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, object, rec)
|
||||
}
|
||||
|
||||
func TestEmptySessionCapabilitiesMarshalling(t *testing.T) {
|
||||
jsoneq(t, `{}`, SessionCapabilities{})
|
||||
}
|
||||
|
||||
func TestSessionCapabilitiesMarshalling(t *testing.T) {
|
||||
jsoneq(t, `{
|
||||
"urn:ietf:params:jmap:core": {
|
||||
"maxSizeUpload": 123,
|
||||
"maxConcurrentUpload": 4,
|
||||
"maxSizeRequest": 1000,
|
||||
"maxConcurrentRequests": 8,
|
||||
"maxCallsInRequest": 16,
|
||||
"maxObjectsInGet": 32,
|
||||
"maxObjectsInSet": 8
|
||||
},
|
||||
"urn:ietf:params:jmap:tasks": {
|
||||
}
|
||||
}`, SessionCapabilities{
|
||||
Core: &SessionCoreCapabilities{
|
||||
MaxSizeUpload: 123,
|
||||
MaxConcurrentUpload: 4,
|
||||
MaxSizeRequest: 1000,
|
||||
MaxConcurrentRequests: 8,
|
||||
MaxCallsInRequest: 16,
|
||||
MaxObjectsInGet: 32,
|
||||
MaxObjectsInSet: 8,
|
||||
},
|
||||
Tasks: &SessionTasksCapabilities{},
|
||||
})
|
||||
}
|
||||
|
||||
type TestJmapWellKnownClient struct {
|
||||
t *testing.T
|
||||
}
|
||||
|
||||
func NewTestJmapWellKnownClient(t *testing.T) SessionClient {
|
||||
return &TestJmapWellKnownClient{t: t}
|
||||
}
|
||||
|
||||
func (t *TestJmapWellKnownClient) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *TestJmapWellKnownClient) GetSession(sessionUrl *url.URL, username string, logger *log.Logger) (SessionResponse, Error) {
|
||||
pa := generateRandomString(2 + seededRand.Intn(10))
|
||||
return SessionResponse{
|
||||
Username: generateRandomString(8),
|
||||
ApiUrl: "test://",
|
||||
PrimaryAccounts: SessionPrimaryAccounts{
|
||||
Core: pa,
|
||||
Mail: pa,
|
||||
Submission: pa,
|
||||
VacationResponse: pa,
|
||||
Sieve: pa,
|
||||
Blob: pa,
|
||||
Quota: pa,
|
||||
Websocket: pa,
|
||||
},
|
||||
Capabilities: SessionCapabilities{
|
||||
Core: &SessionCoreCapabilities{
|
||||
MaxCallsInRequest: 64,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
type TestJmapApiClient struct {
|
||||
t *testing.T
|
||||
}
|
||||
|
||||
func NewTestJmapApiClient(t *testing.T) ApiClient {
|
||||
return &TestJmapApiClient{t: t}
|
||||
}
|
||||
|
||||
func (t TestJmapApiClient) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type TestJmapBlobClient struct {
|
||||
t *testing.T
|
||||
}
|
||||
|
||||
func NewTestJmapBlobClient(t *testing.T) BlobClient {
|
||||
return &TestJmapBlobClient{t: t}
|
||||
}
|
||||
|
||||
func (t TestJmapBlobClient) UploadBinary(ctx context.Context, logger *log.Logger, session *Session, uploadUrl string, endpoint string, contentType string, acceptLanguage string, body io.Reader) (UploadedBlob, Language, Error) {
|
||||
bytes, err := io.ReadAll(body)
|
||||
if err != nil {
|
||||
return UploadedBlob{}, "", SimpleError{code: 0, err: err}
|
||||
}
|
||||
return UploadedBlob{
|
||||
BlobId: uuid.NewString(),
|
||||
Size: len(bytes),
|
||||
Type: contentType,
|
||||
}, "", nil
|
||||
}
|
||||
|
||||
func (h *TestJmapBlobClient) DownloadBinary(ctx context.Context, logger *log.Logger, session *Session, downloadUrl string, endpoint string, acceptLanguage string) (*BlobDownload, Language, Error) {
|
||||
return &BlobDownload{
|
||||
Body: io.NopCloser(strings.NewReader("")),
|
||||
Size: -1,
|
||||
Type: "text/plain",
|
||||
ContentDisposition: "attachment; filename=\"file.txt\"",
|
||||
CacheControl: "",
|
||||
}, "", nil
|
||||
}
|
||||
|
||||
func (t TestJmapBlobClient) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type TestWsClientFactory struct {
|
||||
WsClientFactory
|
||||
}
|
||||
|
||||
var _ WsClientFactory = &TestWsClientFactory{}
|
||||
|
||||
func NewTestWsClientFactory(t *testing.T) WsClientFactory {
|
||||
return TestWsClientFactory{}
|
||||
}
|
||||
|
||||
func (t TestWsClientFactory) EnableNotifications(pushState string, sessionProvider func() (*Session, error), listener WsPushListener) (WsClient, Error) {
|
||||
return nil, nil // TODO
|
||||
}
|
||||
|
||||
func (t TestWsClientFactory) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func serveTestFile(t *testing.T, name string) ([]byte, Language, Error) {
|
||||
cwd, _ := os.Getwd()
|
||||
p := filepath.Join(cwd, "testdata", name)
|
||||
bytes, err := os.ReadFile(p)
|
||||
if err != nil {
|
||||
return bytes, "", SimpleError{code: 0, err: err}
|
||||
}
|
||||
// try to parse it first to avoid any deeper issues that are caused by the test tools
|
||||
var target map[string]any
|
||||
err = json.Unmarshal(bytes, &target)
|
||||
if err != nil {
|
||||
t.Errorf("failed to parse JSON test data file '%v': %v", p, err)
|
||||
return nil, "", SimpleError{code: 0, err: err}
|
||||
}
|
||||
return bytes, "", nil
|
||||
}
|
||||
|
||||
func (t *TestJmapApiClient) Command(ctx context.Context, logger *log.Logger, session *Session, request Request, acceptLanguage string) ([]byte, Language, Error) {
|
||||
command := request.MethodCalls[0].Command
|
||||
switch command {
|
||||
case CommandMailboxGet:
|
||||
return serveTestFile(t.t, "mailboxes1.json")
|
||||
case CommandEmailQuery:
|
||||
return serveTestFile(t.t, "mails1.json")
|
||||
default:
|
||||
require.Fail(t.t, "TestJmapApiClient: unsupported jmap command: %v", command)
|
||||
return nil, "", SimpleError{code: 0, err: fmt.Errorf("TestJmapApiClient: unsupported jmap command: %v", command)}
|
||||
}
|
||||
}
|
||||
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
|
||||
var seededRand *rand.Rand = rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
|
||||
func generateRandomString(length int) string {
|
||||
b := make([]byte, length)
|
||||
for i := range b {
|
||||
b[i] = charset[seededRand.Intn(len(charset))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func TestRequests(t *testing.T) {
|
||||
require := require.New(t)
|
||||
apiClient := NewTestJmapApiClient(t)
|
||||
wkClient := NewTestJmapWellKnownClient(t)
|
||||
blobClient := NewTestJmapBlobClient(t)
|
||||
wsClientFactory := NewTestWsClientFactory(t)
|
||||
logger := log.NopLogger()
|
||||
ctx := context.Background()
|
||||
client := NewClient(wkClient, apiClient, blobClient, wsClientFactory)
|
||||
|
||||
jmapUrl, err := url.Parse("http://localhost/jmap")
|
||||
require.NoError(err)
|
||||
|
||||
session := Session{
|
||||
Username: "user123",
|
||||
JmapUrl: *jmapUrl,
|
||||
SessionResponse: SessionResponse{
|
||||
Capabilities: SessionCapabilities{
|
||||
Core: &SessionCoreCapabilities{
|
||||
MaxCallsInRequest: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
foldersByAccountId, sessionState, _, _, err := client.GetAllMailboxes([]string{"a"}, &session, ctx, &logger, "")
|
||||
require.NoError(err)
|
||||
require.Len(foldersByAccountId, 1)
|
||||
require.Contains(foldersByAccountId, "a")
|
||||
folders := foldersByAccountId["a"]
|
||||
require.Len(folders, 5)
|
||||
require.NotEmpty(sessionState)
|
||||
|
||||
emails, sessionState, _, _, err := client.GetAllEmailsInMailbox("a", &session, ctx, &logger, "", "Inbox", 0, 0, false, true, 0, true)
|
||||
require.NoError(err)
|
||||
require.Len(emails.Emails, 3)
|
||||
require.NotEmpty(sessionState)
|
||||
|
||||
{
|
||||
email := emails.Emails[0]
|
||||
require.Equal("Ornare Senectus Ultrices Elit", email.Subject)
|
||||
require.Equal(false, email.HasAttachment)
|
||||
}
|
||||
{
|
||||
email := emails.Emails[1]
|
||||
require.Equal("Lorem Tortor Eros Blandit Adipiscing Scelerisque Fermentum", email.Subject)
|
||||
require.Equal(false, email.HasAttachment)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmailFilterSerialization(t *testing.T) {
|
||||
expectedFilterJson := `
|
||||
{"operator":"AND","conditions":[{"hasKeyword":"seen","text":"sample"},{"hasKeyword":"draft"}]}
|
||||
`
|
||||
|
||||
require := require.New(t)
|
||||
|
||||
text := "sample"
|
||||
mailboxId := ""
|
||||
notInMailboxIds := []string{}
|
||||
from := ""
|
||||
to := ""
|
||||
cc := ""
|
||||
bcc := ""
|
||||
subject := ""
|
||||
body := ""
|
||||
before := time.Time{}
|
||||
after := time.Time{}
|
||||
minSize := 0
|
||||
maxSize := 0
|
||||
keywords := []string{"seen", "draft"}
|
||||
|
||||
var filter EmailFilterElement
|
||||
|
||||
firstFilter := EmailFilterCondition{
|
||||
Text: text,
|
||||
InMailbox: mailboxId,
|
||||
InMailboxOtherThan: notInMailboxIds,
|
||||
From: from,
|
||||
To: to,
|
||||
Cc: cc,
|
||||
Bcc: bcc,
|
||||
Subject: subject,
|
||||
Body: body,
|
||||
Before: before,
|
||||
After: after,
|
||||
MinSize: minSize,
|
||||
MaxSize: maxSize,
|
||||
}
|
||||
filter = &firstFilter
|
||||
|
||||
if len(keywords) > 0 {
|
||||
firstFilter.HasKeyword = keywords[0]
|
||||
if len(keywords) > 1 {
|
||||
firstFilter.HasKeyword = keywords[0]
|
||||
filters := make([]EmailFilterElement, len(keywords))
|
||||
filters[0] = firstFilter
|
||||
for i, keyword := range keywords[1:] {
|
||||
filters[i+1] = EmailFilterCondition{
|
||||
HasKeyword: keyword,
|
||||
}
|
||||
}
|
||||
filter = &EmailFilterOperator{
|
||||
Operator: And,
|
||||
Conditions: filters,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
b, err := json.Marshal(filter)
|
||||
require.NoError(err)
|
||||
json := string(b)
|
||||
require.Equal(strings.TrimSpace(expectedFilterJson), json)
|
||||
}
|
||||
|
||||
func TestUnmarshallingCalendarEvent(t *testing.T) {
|
||||
payload := `
|
||||
{
|
||||
"locale" : "en-US",
|
||||
"description" : "Internal meeting about the grand strategy for the future",
|
||||
"locations" : {
|
||||
"ux1uokie" : {
|
||||
"links" : {
|
||||
"eefe2pax" : {
|
||||
"@type" : "Link",
|
||||
"href" : "https://example.com/office"
|
||||
}
|
||||
},
|
||||
"iCalComponent" : {
|
||||
"name" : "vlocation"
|
||||
},
|
||||
"@type" : "Location",
|
||||
"description" : "Office meeting room upstairs",
|
||||
"name" : "Office",
|
||||
"locationTypes" : {
|
||||
"office" : true
|
||||
},
|
||||
"coordinates" : "geo:52.5334956,13.4079872",
|
||||
"relativeTo" : "start",
|
||||
"timeZone" : "CEST"
|
||||
}
|
||||
},
|
||||
"replyTo" : {
|
||||
"imip" : "mailto:organizer@example.com"
|
||||
},
|
||||
"links" : {
|
||||
"cai0thoh" : {
|
||||
"href" : "https://example.com/9a7ab91a-edca-4988-886f-25e00743430d",
|
||||
"rel" : "about",
|
||||
"contentType" : "text/html",
|
||||
"@type" : "Link"
|
||||
}
|
||||
},
|
||||
"prodId" : "Mock 0.0",
|
||||
"@type" : "Event",
|
||||
"keywords" : {
|
||||
"secret" : true,
|
||||
"meeting" : true
|
||||
},
|
||||
"status" : "confirmed",
|
||||
"freeBusyStatus" : "busy",
|
||||
"categories" : {
|
||||
"internal" : true,
|
||||
"secret" : true
|
||||
},
|
||||
"duration" : "PT30M",
|
||||
"calendarIds" : {
|
||||
"b" : true
|
||||
},
|
||||
"alerts" : {
|
||||
"ahqu4xi0" : {
|
||||
"@type" : "Alert"
|
||||
}
|
||||
},
|
||||
"start" : "2025-09-30T12:00:00",
|
||||
"privacy" : "public",
|
||||
"isDraft" : false,
|
||||
"id" : "c",
|
||||
"isOrigin" : true,
|
||||
"sentBy" : "organizer@example.com",
|
||||
"descriptionContentType" : "text/plain",
|
||||
"updated" : "2025-09-29T16:17:18Z",
|
||||
"created" : "2025-09-29T16:17:18Z",
|
||||
"color" : "purple",
|
||||
"recurrenceRule" : {
|
||||
"skip" : "omit",
|
||||
"count" : 4,
|
||||
"firstDayOfWeek" : "monday",
|
||||
"rscale" : "iso8601",
|
||||
"interval" : 1,
|
||||
"frequency" : "weekly"
|
||||
},
|
||||
"timeZone" : "Etc/UTC",
|
||||
"title" : "Meeting of the Minds",
|
||||
"participants" : {
|
||||
"xeikie9p" : {
|
||||
"name" : "Klaes Ashford",
|
||||
"locationId" : "em4eal0o",
|
||||
"language" : "en-GB",
|
||||
"description" : "As the first officer on the Behemoth",
|
||||
"invitedBy" : "eegh7uph",
|
||||
"@type" : "Participant",
|
||||
"links" : {
|
||||
"oifooj6g" : {
|
||||
"@type" : "Link",
|
||||
"contentType" : "image/png",
|
||||
"display" : "badge",
|
||||
"href" : "https://static.wikia.nocookie.net/expanse/images/0/02/Klaes_Ashford_-_Expanse_season_4_promotional_2.png/revision/latest?cb=20191206012007",
|
||||
"rel" : "icon",
|
||||
"title" : "Ashford on Medina Station"
|
||||
}
|
||||
},
|
||||
"iCalComponent" : {
|
||||
"name" : "participant"
|
||||
},
|
||||
"scheduleAgent" : "server",
|
||||
"scheduleId" : "mailto:ashford@opa.org"
|
||||
},
|
||||
"eegh7uph" : {
|
||||
"description" : "Called the meeting",
|
||||
"language" : "en-GB",
|
||||
"locationId" : "ux1uokie",
|
||||
"name" : "Anderson Dawes",
|
||||
"scheduleAgent" : "server",
|
||||
"scheduleUpdated" : "2025-10-01T11:59:12Z",
|
||||
"links" : {
|
||||
"ieni5eiw" : {
|
||||
"href" : "https://static.wikia.nocookie.net/expanse/images/1/1e/OPA_leader.png/revision/latest?cb=20250121103410",
|
||||
"display" : "badge",
|
||||
"rel" : "icon",
|
||||
"title" : "Anderson Dawes",
|
||||
"contentType" : "image/png",
|
||||
"@type" : "Link"
|
||||
}
|
||||
},
|
||||
"iCalComponent" : {
|
||||
"name" : "participant"
|
||||
},
|
||||
"@type" : "Participant",
|
||||
"invitedBy" : "eegh7uph",
|
||||
"scheduleSequence" : 1,
|
||||
"sendTo" : {
|
||||
"imip" : "mailto:adawes@opa.org"
|
||||
},
|
||||
"scheduleStatus" : [
|
||||
"1.0"
|
||||
],
|
||||
"scheduleId" : "mailto:adawes@opa.org"
|
||||
}
|
||||
},
|
||||
"uid" : "9a7ab91a-edca-4988-886f-25e00743430d",
|
||||
"virtualLocations" : {
|
||||
"em4eal0o" : {
|
||||
"@type" : "VirtualLocation",
|
||||
"description" : "The opentalk Conference Room",
|
||||
"uri" : "https://meet.opentalk.eu",
|
||||
"features" : {
|
||||
"audio" : true,
|
||||
"screen" : true,
|
||||
"chat" : true,
|
||||
"video" : true
|
||||
},
|
||||
"name" : "opentalk"
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
require := require.New(t)
|
||||
var result CalendarEvent
|
||||
err := json.Unmarshal([]byte(payload), &result)
|
||||
require.NoError(err)
|
||||
|
||||
require.Len(result.VirtualLocations, 1)
|
||||
require.Len(result.Locations, 1)
|
||||
require.Equal("9a7ab91a-edca-4988-886f-25e00743430d", result.Uid)
|
||||
require.Equal(jscalendar.PrivacyPublic, result.Privacy)
|
||||
}
|
||||
|
||||
func TestUnmarshallingCalendarEventGetResponse(t *testing.T) {
|
||||
payload := `
|
||||
{
|
||||
"sessionState" : "7d3cae5b",
|
||||
"methodResponses" : [
|
||||
[
|
||||
"CalendarEvent/query",
|
||||
{
|
||||
"position" : 0,
|
||||
"queryState" : "s2yba",
|
||||
"accountId" : "b",
|
||||
"canCalculateChanges" : true,
|
||||
"ids" : [
|
||||
"c"
|
||||
]
|
||||
},
|
||||
"b:0"
|
||||
],
|
||||
[
|
||||
"CalendarEvent/get",
|
||||
{
|
||||
"state" : "s2yba",
|
||||
"list" : [
|
||||
{
|
||||
"links" : {
|
||||
"cai0thoh" : {
|
||||
"contentType" : "text/html",
|
||||
"href" : "https://example.com/9a7ab91a-edca-4988-886f-25e00743430d",
|
||||
"@type" : "Link",
|
||||
"rel" : "about"
|
||||
}
|
||||
},
|
||||
"freeBusyStatus" : "busy",
|
||||
"color" : "purple",
|
||||
"isDraft" : false,
|
||||
"calendarIds" : {
|
||||
"b" : true
|
||||
},
|
||||
"updated" : "2025-09-29T16:17:18Z",
|
||||
"locations" : {
|
||||
"ux1uokie" : {
|
||||
"relativeTo" : "start",
|
||||
"description" : "Office meeting room upstairs",
|
||||
"coordinates" : "geo:52.5334956,13.4079872",
|
||||
"name" : "Office",
|
||||
"locationTypes" : {
|
||||
"office" : true
|
||||
},
|
||||
"links" : {
|
||||
"eefe2pax" : {
|
||||
"href" : "https://example.com/office",
|
||||
"@type" : "Link"
|
||||
}
|
||||
},
|
||||
"iCalComponent" : {
|
||||
"name" : "vlocation"
|
||||
},
|
||||
"@type" : "Location",
|
||||
"timeZone" : "CEST"
|
||||
}
|
||||
},
|
||||
"virtualLocations" : {
|
||||
"em4eal0o" : {
|
||||
"name" : "opentalk",
|
||||
"@type" : "VirtualLocation",
|
||||
"features" : {
|
||||
"screen" : true,
|
||||
"chat" : true,
|
||||
"audio" : true,
|
||||
"video" : true
|
||||
},
|
||||
"uri" : "https://meet.opentalk.eu",
|
||||
"description" : "The opentalk Conference Room"
|
||||
}
|
||||
},
|
||||
"uid" : "9a7ab91a-edca-4988-886f-25e00743430d",
|
||||
"categories" : {
|
||||
"secret" : true,
|
||||
"internal" : true
|
||||
},
|
||||
"keywords" : {
|
||||
"secret" : true,
|
||||
"meeting" : true
|
||||
},
|
||||
"replyTo" : {
|
||||
"imip" : "mailto:organizer@example.com"
|
||||
},
|
||||
"duration" : "PT30M",
|
||||
"created" : "2025-09-29T16:17:18Z",
|
||||
"start" : "2025-09-30T12:00:00",
|
||||
"id" : "c",
|
||||
"sentBy" : "organizer@example.com",
|
||||
"timeZone" : "Etc/UTC",
|
||||
"@type" : "Event",
|
||||
"title" : "Meeting of the Minds",
|
||||
"alerts" : {
|
||||
"ahqu4xi0" : {
|
||||
"@type" : "Alert"
|
||||
}
|
||||
},
|
||||
"participants" : {
|
||||
"xeikie9p" : {
|
||||
"@type" : "Participant",
|
||||
"scheduleId" : "mailto:ashford@opa.org",
|
||||
"invitedBy" : "eegh7uph",
|
||||
"language" : "en-GB",
|
||||
"links" : {
|
||||
"oifooj6g" : {
|
||||
"contentType" : "image/png",
|
||||
"display" : "badge",
|
||||
"title" : "Ashford on Medina Station",
|
||||
"@type" : "Link",
|
||||
"href" : "https://static.wikia.nocookie.net/expanse/images/0/02/Klaes_Ashford_-_Expanse_season_4_promotional_2.png/revision/latest?cb=20191206012007",
|
||||
"rel" : "icon"
|
||||
}
|
||||
},
|
||||
"iCalComponent" : {
|
||||
"name" : "participant"
|
||||
},
|
||||
"scheduleAgent" : "server",
|
||||
"name" : "Klaes Ashford",
|
||||
"locationId" : "em4eal0o",
|
||||
"description" : "As the first officer on the Behemoth"
|
||||
},
|
||||
"eegh7uph" : {
|
||||
"description" : "Called the meeting",
|
||||
"locationId" : "ux1uokie",
|
||||
"scheduleUpdated" : "2025-10-01T11:59:12Z",
|
||||
"sendTo" : {
|
||||
"imip" : "mailto:adawes@opa.org"
|
||||
},
|
||||
"scheduleAgent" : "server",
|
||||
"scheduleStatus" : [
|
||||
"1.0"
|
||||
],
|
||||
"name" : "Anderson Dawes",
|
||||
"invitedBy" : "eegh7uph",
|
||||
"language" : "en-GB",
|
||||
"links" : {
|
||||
"ieni5eiw" : {
|
||||
"rel" : "icon",
|
||||
"display" : "badge",
|
||||
"@type" : "Link",
|
||||
"title" : "Anderson Dawes",
|
||||
"href" : "https://static.wikia.nocookie.net/expanse/images/1/1e/OPA_leader.png/revision/latest?cb=20250121103410",
|
||||
"contentType" : "image/png"
|
||||
}
|
||||
},
|
||||
"iCalComponent" : {
|
||||
"name" : "participant"
|
||||
},
|
||||
"scheduleSequence" : 1,
|
||||
"scheduleId" : "mailto:adawes@opa.org",
|
||||
"@type" : "Participant"
|
||||
}
|
||||
},
|
||||
"status" : "confirmed",
|
||||
"description" : "Internal meeting about the grand strategy for the future",
|
||||
"locale" : "en-US",
|
||||
"recurrenceRule" : {
|
||||
"count" : 4,
|
||||
"rscale" : "iso8601",
|
||||
"frequency" : "weekly",
|
||||
"interval" : 1,
|
||||
"firstDayOfWeek" : "monday",
|
||||
"skip" : "omit"
|
||||
},
|
||||
"descriptionContentType" : "text/plain",
|
||||
"isOrigin" : true,
|
||||
"prodId" : "Mock 0.0",
|
||||
"privacy" : "public"
|
||||
}
|
||||
],
|
||||
"accountId" : "b",
|
||||
"notFound" : []
|
||||
},
|
||||
"b:1"
|
||||
]
|
||||
]
|
||||
}
|
||||
`
|
||||
|
||||
require := require.New(t)
|
||||
var response Response
|
||||
err := json.Unmarshal([]byte(payload), &response)
|
||||
require.NoError(err)
|
||||
r1 := response.MethodResponses[1]
|
||||
require.Equal(CommandCalendarEventGet, r1.Command)
|
||||
get := r1.Parameters.(CalendarEventGetResponse)
|
||||
require.Len(get.List, 1)
|
||||
result := get.List[0]
|
||||
require.Len(result.VirtualLocations, 1)
|
||||
require.Len(result.Locations, 1)
|
||||
require.Equal("9a7ab91a-edca-4988-886f-25e00743430d", result.Uid)
|
||||
require.Equal(jscalendar.PrivacyPublic, result.Privacy)
|
||||
}
|
||||
|
||||
func TestAlertWithOffsetTriggerInResponse(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
text := `{
|
||||
"methodResponses":[
|
||||
["CalendarEvent/get",{
|
||||
"accountId":"b",
|
||||
"state":"ssecq",
|
||||
"list":[{
|
||||
"@type": "Event",
|
||||
"start":"2025-11-01T14:30:00",
|
||||
"alerts": {
|
||||
"M87fT82": {
|
||||
"@type": "Alert",
|
||||
"trigger": {
|
||||
"@type": "OffsetTrigger",
|
||||
"offset": "-PT15M",
|
||||
"relativeTo": "start"
|
||||
}
|
||||
}
|
||||
}
|
||||
}],
|
||||
"notFound":[]
|
||||
},"1"]
|
||||
],"sessionState":"7d3cae5b"
|
||||
}`
|
||||
|
||||
var response Response
|
||||
err := json.Unmarshal([]byte(text), &response)
|
||||
require.NoError(err)
|
||||
|
||||
resp := response.MethodResponses[0]
|
||||
require.Equal(CommandCalendarEventGet, resp.Command)
|
||||
require.IsType(CalendarEventGetResponse{}, resp.Parameters)
|
||||
params := resp.Parameters.(CalendarEventGetResponse)
|
||||
require.Len(params.List, 1)
|
||||
event := params.List[0]
|
||||
require.Contains(event.Alerts, "M87fT82")
|
||||
alert := event.Alerts["M87fT82"]
|
||||
require.NotNil(alert)
|
||||
trigger := alert.Trigger
|
||||
require.NotNil(trigger)
|
||||
require.IsType(jscalendar.OffsetTrigger{}, trigger)
|
||||
offsetTrigger := trigger.(jscalendar.OffsetTrigger)
|
||||
require.Equal(jscalendar.SignedDuration("-PT15M"), offsetTrigger.Offset)
|
||||
require.Equal(jscalendar.RelativeToStart, offsetTrigger.RelativeTo)
|
||||
}
|
||||
|
||||
func TestParseContactCardMedia(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
text := `{
|
||||
"methodResponses":[
|
||||
["ContactCard/get",{
|
||||
"accountId":"b",
|
||||
"state":"ssecq",
|
||||
"list":[{
|
||||
"prodId": "jmaptest",
|
||||
"@type": "Card",
|
||||
"kind": "individual",
|
||||
"media": {
|
||||
"aT8afd59": {
|
||||
"uri": "https://picsum.photos/id/96/128/128",
|
||||
"kind": "photo",
|
||||
"@type": "Media",
|
||||
"contexts": {
|
||||
"work": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}],
|
||||
"notFound":[]
|
||||
},"1"]
|
||||
],"sessionState":"7d3cae5b"
|
||||
}`
|
||||
|
||||
var response Response
|
||||
err := json.Unmarshal([]byte(text), &response)
|
||||
require.NoError(err)
|
||||
|
||||
resp := response.MethodResponses[0]
|
||||
require.Equal(CommandContactCardGet, resp.Command)
|
||||
require.IsType(ContactCardGetResponse{}, resp.Parameters)
|
||||
params := resp.Parameters.(ContactCardGetResponse)
|
||||
require.Len(params.List, 1)
|
||||
contact := params.List[0]
|
||||
require.Contains(contact.Media, "aT8afd59")
|
||||
media := contact.Media["aT8afd59"]
|
||||
require.NotNil(media)
|
||||
require.Equal("https://picsum.photos/id/96/128/128", media.Uri)
|
||||
require.Equal(jscontact.MediaKindPhoto, media.Kind)
|
||||
}
|
||||
@@ -7,87 +7,6 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDeserializeMailboxGetResponse(t *testing.T) {
|
||||
require := require.New(t)
|
||||
jsonBytes, _, jmapErr := serveTestFile(t, "mailboxes1.json")
|
||||
require.NoError(jmapErr)
|
||||
var data Response
|
||||
err := json.Unmarshal(jsonBytes, &data)
|
||||
require.NoError(err)
|
||||
require.Empty(data.CreatedIds)
|
||||
require.Equal("3e25b2a0", data.SessionState)
|
||||
require.Len(data.MethodResponses, 1)
|
||||
resp := data.MethodResponses[0]
|
||||
require.Equal(CommandMailboxGet, resp.Command)
|
||||
require.Equal("0", resp.Tag)
|
||||
require.IsType(MailboxGetResponse{}, resp.Parameters)
|
||||
mgr := resp.Parameters.(MailboxGetResponse)
|
||||
require.Equal("cs", mgr.AccountId)
|
||||
require.Len(mgr.List, 5)
|
||||
require.Equal("n", mgr.State)
|
||||
require.Empty(mgr.NotFound)
|
||||
var folders = []struct {
|
||||
id string
|
||||
name string
|
||||
role string
|
||||
total int
|
||||
unread int
|
||||
}{
|
||||
{"a", "Inbox", "inbox", 10, 8},
|
||||
{"b", "Deleted Items", "trash", 20, 0},
|
||||
{"c", "Junk Mail", "junk", 0, 0},
|
||||
{"d", "Drafts", "drafts", 0, 0},
|
||||
{"e", "Sent Items", "sent", 0, 0},
|
||||
}
|
||||
for i, expected := range folders {
|
||||
folder := mgr.List[i]
|
||||
require.Equal(expected.id, folder.Id)
|
||||
require.Equal(expected.name, folder.Name)
|
||||
require.Equal(expected.role, folder.Role)
|
||||
require.Equal(expected.total, folder.TotalEmails)
|
||||
require.Equal(expected.total, folder.TotalThreads)
|
||||
require.Equal(expected.unread, folder.UnreadEmails)
|
||||
require.Equal(expected.unread, folder.UnreadThreads)
|
||||
require.Empty(folder.ParentId)
|
||||
require.Zero(folder.SortOrder)
|
||||
require.Nil(folder.IsSubscribed)
|
||||
|
||||
require.True(folder.MyRights.MayReadItems)
|
||||
require.True(folder.MyRights.MayAddItems)
|
||||
require.True(folder.MyRights.MayRemoveItems)
|
||||
require.True(folder.MyRights.MaySetSeen)
|
||||
require.True(folder.MyRights.MaySetKeywords)
|
||||
require.True(folder.MyRights.MayCreateChild)
|
||||
require.True(folder.MyRights.MayRename)
|
||||
require.True(folder.MyRights.MayDelete)
|
||||
require.True(folder.MyRights.MaySubmit)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeserializeEmailGetResponse(t *testing.T) {
|
||||
require := require.New(t)
|
||||
jsonBytes, _, jmapErr := serveTestFile(t, "mails1.json")
|
||||
require.NoError(jmapErr)
|
||||
var data Response
|
||||
err := json.Unmarshal(jsonBytes, &data)
|
||||
require.NoError(err)
|
||||
require.Empty(data.CreatedIds)
|
||||
require.Equal("3e25b2a0", data.SessionState)
|
||||
require.Len(data.MethodResponses, 2)
|
||||
resp := data.MethodResponses[1]
|
||||
require.Equal(CommandEmailGet, resp.Command)
|
||||
require.Equal("1", resp.Tag)
|
||||
require.IsType(EmailGetResponse{}, resp.Parameters)
|
||||
egr := resp.Parameters.(EmailGetResponse)
|
||||
require.Equal("d", egr.AccountId)
|
||||
require.Len(egr.List, 3)
|
||||
require.Equal("suqmq", egr.State)
|
||||
require.Empty(egr.NotFound)
|
||||
email := egr.List[0]
|
||||
require.Equal("moyaaaddw", email.Id)
|
||||
require.Equal("cbejozsk1fgcviw7thwzsvtgmf1ep0a3izjoimj02jmtsunpeuwmsaya1yma", email.BlobId)
|
||||
}
|
||||
|
||||
func TestUnmarshallingError(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
|
||||
121
pkg/jmap/testdata/mailboxes1.json
vendored
121
pkg/jmap/testdata/mailboxes1.json
vendored
@@ -1,121 +0,0 @@
|
||||
{"methodResponses": [
|
||||
["Mailbox/get", {
|
||||
"accountId":"cs",
|
||||
"state":"n",
|
||||
"list": [
|
||||
{
|
||||
"id":"a",
|
||||
"name":"Inbox",
|
||||
"parentId":null,
|
||||
"role":"inbox",
|
||||
"sortOrder":0,
|
||||
"isSubscribed":true,
|
||||
"totalEmails":10,
|
||||
"unreadEmails":8,
|
||||
"totalThreads":10,
|
||||
"unreadThreads":8,
|
||||
"myRights":{
|
||||
"mayReadItems":true,
|
||||
"mayAddItems":true,
|
||||
"mayRemoveItems":true,
|
||||
"maySetSeen":true,
|
||||
"maySetKeywords":true,
|
||||
"mayCreateChild":true,
|
||||
"mayRename":true,
|
||||
"mayDelete":true,
|
||||
"maySubmit":true
|
||||
}
|
||||
},{
|
||||
"id":"b",
|
||||
"name":"Deleted Items",
|
||||
"parentId":null,
|
||||
"role":"trash",
|
||||
"sortOrder":0,
|
||||
"isSubscribed":true,
|
||||
"totalEmails":20,
|
||||
"unreadEmails":0,
|
||||
"totalThreads":20,
|
||||
"unreadThreads":0,
|
||||
"myRights":{
|
||||
"mayReadItems":true,
|
||||
"mayAddItems":true,
|
||||
"mayRemoveItems":true,
|
||||
"maySetSeen":true,
|
||||
"maySetKeywords":true,
|
||||
"mayCreateChild":true,
|
||||
"mayRename":true,
|
||||
"mayDelete":true,
|
||||
"maySubmit":true
|
||||
}
|
||||
},{
|
||||
"id":"c",
|
||||
"name":"Junk Mail",
|
||||
"parentId":null,
|
||||
"role":"junk",
|
||||
"sortOrder":0,
|
||||
"isSubscribed":true,
|
||||
"totalEmails":0,
|
||||
"unreadEmails":0,
|
||||
"totalThreads":0,
|
||||
"unreadThreads":0,
|
||||
"myRights":{
|
||||
"mayReadItems":true,
|
||||
"mayAddItems":true,
|
||||
"mayRemoveItems":true,
|
||||
"maySetSeen":true,
|
||||
"maySetKeywords":true,
|
||||
"mayCreateChild":true,
|
||||
"mayRename":true,
|
||||
"mayDelete":true,
|
||||
"maySubmit":true
|
||||
}
|
||||
},{
|
||||
"id":"d",
|
||||
"name":"Drafts",
|
||||
"parentId":null,
|
||||
"role":"drafts",
|
||||
"sortOrder":0,
|
||||
"isSubscribed":true,
|
||||
"totalEmails":0,
|
||||
"unreadEmails":0,
|
||||
"totalThreads":0,
|
||||
"unreadThreads":0,
|
||||
"myRights":{
|
||||
"mayReadItems":true,
|
||||
"mayAddItems":true,
|
||||
"mayRemoveItems":true,
|
||||
"maySetSeen":true,
|
||||
"maySetKeywords":true,
|
||||
"mayCreateChild":true,
|
||||
"mayRename":true,
|
||||
"mayDelete":true,
|
||||
"maySubmit":true
|
||||
}
|
||||
},{
|
||||
"id":"e",
|
||||
"name":"Sent Items",
|
||||
"parentId":null,
|
||||
"role":"sent",
|
||||
"sortOrder":0,
|
||||
"isSubscribed":true,
|
||||
"totalEmails":0,
|
||||
"unreadEmails":0,
|
||||
"totalThreads":0,
|
||||
"unreadThreads":0,
|
||||
"myRights":{
|
||||
"mayReadItems":true,
|
||||
"mayAddItems":true,
|
||||
"mayRemoveItems":true,
|
||||
"maySetSeen":true,
|
||||
"maySetKeywords":true,
|
||||
"mayCreateChild":true,
|
||||
"mayRename":true,
|
||||
"mayDelete":true,
|
||||
"maySubmit":true
|
||||
}
|
||||
}
|
||||
],
|
||||
"notFound":[]
|
||||
},"a:0"]
|
||||
], "sessionState":"3e25b2a0"
|
||||
}
|
||||
277
pkg/jmap/testdata/mails1.json
vendored
277
pkg/jmap/testdata/mails1.json
vendored
File diff suppressed because one or more lines are too long
@@ -1410,8 +1410,8 @@ func MapstructTriggerHook() mapstructure.DecodeHookFunc {
|
||||
return data, nil
|
||||
}
|
||||
m := data.(map[string]any)
|
||||
if typ, ok := m["@type"]; ok {
|
||||
switch typ {
|
||||
if t, ok := m["@type"]; ok {
|
||||
switch t {
|
||||
case string(OffsetTriggerType):
|
||||
return mapOffsetTrigger(m)
|
||||
case string(AbsoluteTriggerType):
|
||||
@@ -1517,25 +1517,37 @@ type Alert struct {
|
||||
}
|
||||
|
||||
func (a *Alert) UnmarshalJSON(b []byte) error {
|
||||
var typ struct {
|
||||
// use a simplified Trigger structure to only deserialize some of
|
||||
// its fields, only in order to detect which Trigger type it actually
|
||||
// is (Offset, Absolute, or Unknown)
|
||||
var peekAlert struct {
|
||||
Trigger struct {
|
||||
Type string `json:"@type"`
|
||||
Type string `json:"@type"`
|
||||
Offset string `json:"offset"`
|
||||
When string `json:"when"`
|
||||
} `json:"trigger"`
|
||||
}
|
||||
if err := json.Unmarshal(b, &typ); err != nil {
|
||||
if err := json.Unmarshal(b, &peekAlert); err != nil {
|
||||
return err
|
||||
}
|
||||
switch typ.Trigger.Type {
|
||||
switch peekAlert.Trigger.Type {
|
||||
case string(OffsetTriggerType):
|
||||
a.Trigger = new(OffsetTrigger)
|
||||
case string(AbsoluteTriggerType):
|
||||
a.Trigger = new(AbsoluteTrigger)
|
||||
default:
|
||||
a.Trigger = new(UnknownTrigger)
|
||||
// it could still be a Trigger without its optional @type field
|
||||
if peekAlert.Trigger.Offset != "" { // if "offset" is set, it's an OffsetTrigger
|
||||
a.Trigger = new(OffsetTrigger)
|
||||
} else if peekAlert.Trigger.When != "" { // if "when" is set, it's an AbsoluteTrigger
|
||||
a.Trigger = new(AbsoluteTrigger)
|
||||
} else { // it's neither -> UnknownTrigger
|
||||
a.Trigger = new(UnknownTrigger)
|
||||
}
|
||||
}
|
||||
|
||||
type tmp Alert
|
||||
return json.Unmarshal(b, (*tmp)(a))
|
||||
type tmpAlert Alert // alias Alert to avoid infinite recursion into this func
|
||||
return json.Unmarshal(b, (*tmpAlert)(a))
|
||||
}
|
||||
|
||||
// A `TimeZoneRule` object maps a `STANDARD` or `DAYLIGHT` sub-component from iCalendar,
|
||||
|
||||
@@ -113,9 +113,8 @@ var T1 = jmap.Task{
|
||||
ShowWithoutTime: false,
|
||||
Locations: map[string]jscalendar.Location{
|
||||
"ruoth5uu": {
|
||||
Type: jscalendar.LocationType,
|
||||
Name: "Sol Gate",
|
||||
Description: "We meet at the Sol gate",
|
||||
Type: jscalendar.LocationType,
|
||||
Name: "Sol Gate",
|
||||
LocationTypes: map[jscalendar.LocationTypeOption]bool{
|
||||
jscalendar.LocationTypeOptionLandmarkAddress: true,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user