groupware: add getting a contact by ID + add integration tests for contacts

This commit is contained in:
Pascal Bleser
2025-11-07 16:13:39 +01:00
parent 97e46f6e9f
commit afd1d37d22
9 changed files with 1185 additions and 13 deletions

View File

@@ -37,6 +37,32 @@ func (j *Client) GetAddressbooks(accountId string, session *Session, ctx context
})
}
func (j *Client) GetContactCardsById(accountId string, session *Session, ctx context.Context, logger *log.Logger,
acceptLanguage string, contactIds []string) (map[string]jscontact.ContactCard, SessionState, State, Language, Error) {
logger = j.logger("GetContactCardsById", session, logger)
cmd, err := j.request(session, logger, invocation(CommandContactCardGet, ContactCardGetCommand{
Ids: contactIds,
AccountId: accountId,
}, "0"))
if err != nil {
return nil, "", "", "", err
}
return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (map[string]jscontact.ContactCard, State, Error) {
var response ContactCardGetResponse
err = retrieveResponseMatchParameters(logger, body, CommandContactCardGet, "0", &response)
if err != nil {
return nil, "", err
}
m := map[string]jscontact.ContactCard{}
for _, contact := range response.List {
m[contact.Id] = contact
}
return m, response.State, nil
})
}
func (j *Client) QueryContactCards(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string,
filter ContactCardFilterElement, sortBy []ContactCardComparator,
position uint, limit uint) (map[string][]jscontact.ContactCard, SessionState, State, Language, Error) {

View File

@@ -0,0 +1,85 @@
package jmap
import (
"math/rand"
"reflect"
"testing"
"github.com/stretchr/testify/require"
"github.com/opencloud-eu/opencloud/pkg/jscontact"
)
func TestContacts(t *testing.T) {
if skip(t) {
return
}
count := uint(15 + rand.Intn(20))
require := require.New(t)
s, err := newStalwartTest(t)
require.NoError(err)
defer s.Close()
accountId, addressbookId, cardsById, err := s.fillContacts(t, count)
require.NoError(err)
require.NotEmpty(accountId)
require.NotEmpty(addressbookId)
filter := ContactCardFilterCondition{
InAddressBook: addressbookId,
}
sortBy := []ContactCardComparator{
{Property: jscontact.ContactCardPropertyCreated, IsAscending: true},
}
contactsByAccount, _, _, _, err := s.client.QueryContactCards([]string{accountId}, s.session, t.Context(), s.logger, "", filter, sortBy, 0, 0)
require.NoError(err)
require.Len(contactsByAccount, 1)
require.Contains(contactsByAccount, accountId)
contacts := contactsByAccount[accountId]
require.Len(contacts, int(count))
for _, actual := range contacts {
expected, ok := cardsById[actual.Id]
require.True(ok, "failed to find created contact by its id")
expected.Id = actual.Id
matchContact(t, actual, expected)
}
}
func matchContact(t *testing.T, actual jscontact.ContactCard, expected jscontact.ContactCard) {
require := require.New(t)
//a := actual
//unType(t, &a)
require.Equal(expected, actual)
}
func unType(t *testing.T, s any) {
ty := reflect.TypeOf(s)
switch ty.Kind() {
case reflect.Map, reflect.Array, reflect.Pointer, reflect.Slice:
ty = ty.Elem()
}
switch ty.Kind() {
case reflect.Struct:
for i := range ty.NumField() {
f := ty.Field(i)
n := f.Name
if n == "Type" {
v := reflect.ValueOf(s).Field(i)
require.True(t, v.Elem().CanSet(), "cannot set the Type field")
v.SetString("")
} else {
//unType(t, f)
}
}
}
}

View File

@@ -58,7 +58,7 @@ func TestEmails(t *testing.T) {
var threads int = 0
var mails []filledMail = nil
{
mails, threads, err = s.fill(inboxFolder, count)
mails, threads, err = s.fillEmailsWithImap(inboxFolder, count)
require.NoError(err)
}
mailsByMessageId := structs.Index(mails, func(mail filledMail) string { return mail.messageId })
@@ -119,11 +119,11 @@ func TestEmails(t *testing.T) {
}
}
func matchEmail(t *testing.T, actual Email, e filledMail, hasBodies bool) {
func matchEmail(t *testing.T, actual Email, expected filledMail, hasBodies bool) {
require := require.New(t)
require.Len(actual.MessageId, 1)
require.Equal(e.messageId, actual.MessageId[0])
require.Equal(e.subject, actual.Subject)
require.Equal(expected.messageId, actual.MessageId[0])
require.Equal(expected.subject, actual.Subject)
require.NotEmpty(actual.Preview)
if hasBodies {
require.Len(actual.TextBody, 1)
@@ -135,7 +135,7 @@ func matchEmail(t *testing.T, actual Email, e filledMail, hasBodies bool) {
} else {
require.Empty(actual.BodyValues)
}
require.ElementsMatch(slices.Collect(maps.Keys(actual.Keywords)), e.keywords)
require.ElementsMatch(slices.Collect(maps.Keys(actual.Keywords)), expected.keywords)
{
list := make([]filledAttachment, len(actual.Attachments))
@@ -150,6 +150,6 @@ func matchEmail(t *testing.T, actual Email, e filledMail, hasBodies bool) {
require.NotEmpty(a.PartId)
}
require.ElementsMatch(list, e.attachments)
require.ElementsMatch(list, expected.attachments)
}
}

View File

File diff suppressed because it is too large Load Diff

View File

@@ -918,8 +918,10 @@ type SessionPrimaryAccounts struct {
Quota string `json:"urn:ietf:params:jmap:quota,omitempty"`
Websocket string `json:"urn:ietf:params:jmap:websocket,omitempty"`
Task string `json:"urn:ietf:params:jmap:task,omitempty"`
Calendar string `json:"urn:ietf:params:jmap:calendar,omitempty"`
Contact string `json:"urn:ietf:params:jmap:contact,omitempty"`
Calendars string `json:"urn:ietf:params:jmap:calendars,omitempty"`
CalendarsParse string `json:"urn:ietf:params:jmap:calendars:parse,omitempty"`
Contacts string `json:"urn:ietf:params:jmap:contacts,omitempty"`
ContactsParse string `json:"urn:ietf:params:jmap:contacts:parse,omitempty"`
}
type SessionState string

View File

@@ -587,7 +587,7 @@ const (
// PhoneFeatures.
PhoneFeatureMobile = PhoneFeature("mobile")
PhoneFeatureMobile = PhoneFeature("cell") // TODO the spec says 'mobile', but Stalwart only supports 'cell'
PhoneFeatureVoice = PhoneFeature("voice")
PhoneFeatureText = PhoneFeature("text")
PhoneFeatureVideo = PhoneFeature("video")

View File

@@ -55,18 +55,50 @@ func Index[K comparable, V any](source []V, indexer func(V) K) map[K]V {
return result
}
func Map[E any, R any](source []E, indexer func(E) R) []R {
func Map[E any, R any](source []E, mapper func(E) R) []R {
if source == nil {
var zero []R
return zero
}
result := make([]R, len(source))
for i, e := range source {
result[i] = indexer(e)
result[i] = mapper(e)
}
return result
}
func MapValues[K comparable, S any, T any](m map[K]S, mapper func(S) T) map[K]T {
r := make(map[K]T, len(m))
for k, s := range m {
r[k] = mapper(s)
}
return r
}
func MapValues2[K comparable, S any, T any](m map[K]S, mapper func(K, S) T) map[K]T {
r := make(map[K]T, len(m))
for k, s := range m {
r[k] = mapper(k, s)
}
return r
}
func MapKeys[S comparable, T comparable, V any](m map[S]V, mapper func(S) T) map[T]V {
r := make(map[T]V, len(m))
for s, v := range m {
r[mapper(s)] = v
}
return r
}
func MapKeys2[S comparable, T comparable, V any](m map[S]V, mapper func(S, V) T) map[T]V {
r := make(map[T]V, len(m))
for s, v := range m {
r[mapper(s, v)] = v
}
return r
}
func ToBoolMap[E comparable](source []E) map[E]bool {
m := make(map[E]bool, len(source))
for _, v := range source {

View File

@@ -148,6 +148,32 @@ func (g *Groupware) GetContactsInAddressbook(w http.ResponseWriter, r *http.Requ
})
}
func (g *Groupware) GetContactById(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
ok, accountId, resp := req.needContactWithAccount()
if !ok {
return resp
}
l := req.logger.With()
contactId := chi.URLParam(r, UriParamContactId)
l = l.Str(UriParamContactId, log.SafeString(contactId))
logger := log.From(l)
contactsById, sessionState, state, lang, jerr := g.jmap.GetContactCardsById(accountId, req.session, req.ctx, logger, req.language(), []string{contactId})
if jerr != nil {
return req.errorResponseFromJmap(jerr)
}
if contact, ok := contactsById[contactId]; ok {
return etagResponse(contact, sessionState, state, lang)
} else {
return notFoundResponse(sessionState)
}
})
}
func (g *Groupware) CreateContact(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
ok, accountId, resp := req.needContactWithAccount()

View File

@@ -148,6 +148,7 @@ func (g *Groupware) Route(r chi.Router) {
r.Route("/contacts", func(r chi.Router) {
r.Post("/", g.CreateContact)
r.Delete("/{contactid}", g.DeleteContact)
r.Get("/{contactid}", g.GetContactById)
})
r.Route("/calendars", func(r chi.Router) {
r.Get("/", g.GetCalendars)