mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-11-08 21:43:00 -05:00
groupware: add Groupware integration testing
* split off the Stalwart integration test boilerplate into a new jmaptest package, in order to be able to use it from the groupware package as well * remove the JMAP Session patching in tests and, instead, pick random free local ports for HTTP and IMAPS manually, and set those explicitly in the Stalwart testcontainer, so that we can set the PUBLIC_URL environment variable that Stalwart then uses to define the URLs that are served as part of the JMAP Sessions (API URL, etc...)
This commit is contained in:
1 parent
e3d635c25c
commit
ee03db134d
14 files changed
+529
-270
No files matched your search
+4
-4
@@ -44,8 +44,8 @@ func (j *Client) Api() ApiClient {
|
||||
return j.api
|
||||
}
|
||||
|
||||
func NewClient(session SessionClient, api ApiClient, blob BlobClient, ws WsClientFactory) Client {
|
||||
return Client{
|
||||
func NewClient(session SessionClient, api ApiClient, blob BlobClient, ws WsClientFactory) *Client {
|
||||
return &Client{
|
||||
session: session,
|
||||
api: api,
|
||||
blob: blob,
|
||||
@@ -73,11 +73,11 @@ func (j *Client) OnNotification(username string, stateChange StateChange) {
|
||||
|
||||
// Retrieve JMAP well-known data from the Stalwart server and create a Session from that.
|
||||
func (j *Client) FetchSession(ctx context.Context, sessionUrl *url.URL, username string, logger *log.Logger) (Session, Error) {
|
||||
wk, err := j.session.GetSession(ctx, sessionUrl, username, logger)
|
||||
sessionResponse, err := j.session.GetSession(ctx, sessionUrl, username, logger)
|
||||
if err != nil {
|
||||
return Session{}, err
|
||||
}
|
||||
return newSession(wk)
|
||||
return newSession(sessionResponse)
|
||||
}
|
||||
|
||||
func (j *Client) logger(operation string, ctx Context) *log.Logger {
|
||||
|
||||
+1
-1
@@ -377,7 +377,7 @@ func (h *HttpJmapClient) Command(operation Operation, request Request, ctx Conte
|
||||
if res.StatusCode < 200 || res.StatusCode > 299 {
|
||||
h.listener.OnFailedRequestWithStatus(endpoint, operation, res.StatusCode)
|
||||
logger.Error().Str(logEndpoint, endpoint).Str(logHttpStatus, log.SafeString(res.Status)).Msg("HTTP response status code is not 2xx") //NOSONAR
|
||||
return nil, language, jmapError(err, JmapErrorServerResponse)
|
||||
return nil, language, jmapError(fmt.Errorf("JMAP server responsed with '%s'", res.Status), JmapErrorServerResponse)
|
||||
}
|
||||
|
||||
h.listener.OnSuccessfulRequest(endpoint, operation, res.StatusCode)
|
||||
|
||||
+7
-2
@@ -26,7 +26,7 @@ var UintPtrZero *uint = uintPtr(0)
|
||||
|
||||
type eventListeners[T any] struct {
|
||||
listeners []T
|
||||
m sync.Mutex
|
||||
m sync.Mutex // TODO get rid of the mutex to avoid lock contention, use a lock-free alternative, or get rid of it altogether
|
||||
}
|
||||
|
||||
func (e *eventListeners[T]) add(listener T) {
|
||||
@@ -81,6 +81,9 @@ func command[T any](client Cmdr, //NOSONAR
|
||||
if jmapErr != nil {
|
||||
return ZeroResult[T](single(duration)), jmapErr
|
||||
}
|
||||
if responseBody == nil {
|
||||
return ZeroResult[T](single(duration)), jmapError(fmt.Errorf("empty response body"), JmapErrorInvalidJmapResponsePayload)
|
||||
}
|
||||
|
||||
var response Response
|
||||
if err := json.NewDecoder(responseBody).Decode(&response); err != nil {
|
||||
@@ -450,6 +453,8 @@ func ptr[T any | int | uint | bool | string](t T) *T {
|
||||
|
||||
func dumpHttpRequest(req *http.Request, maxBodySize int64, closure func(method string, uri string, content string, truncated bool)) error {
|
||||
var logBuilder bytes.Buffer
|
||||
uri := req.URL.String()
|
||||
fmt.Fprintf(&logBuilder, "%s %s\n", req.Method, uri)
|
||||
for key, values := range req.Header {
|
||||
if key == "Authorization" {
|
||||
continue
|
||||
@@ -474,7 +479,7 @@ func dumpHttpRequest(req *http.Request, maxBodySize int64, closure func(method s
|
||||
req.Body = io.NopCloser(fullBodyReader)
|
||||
}
|
||||
}
|
||||
closure(req.Method, req.URL.String(), logBuilder.String(), truncated)
|
||||
closure(req.Method, uri, logBuilder.String(), truncated)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
package jmaptest
|
||||
@@ -47,13 +47,13 @@ func TestAddressBooks(t *testing.T) {
|
||||
list,
|
||||
getid,
|
||||
func(s *StalwartTest, accountId AccountId, ids []string, ctx Context) (Result[AddressBookGetResponse], error) {
|
||||
return s.client.GetAddressbooks(accountId, ids, ctx)
|
||||
return s.Client.GetAddressbooks(accountId, ids, ctx)
|
||||
},
|
||||
func(s *StalwartTest, accountId AccountId, id string, change AddressBookChange, ctx Context) (Result[AddressBook], error) { //NOSONAR
|
||||
return s.client.UpdateAddressBook(accountId, id, change, ctx)
|
||||
return s.Client.UpdateAddressBook(accountId, id, change, ctx)
|
||||
},
|
||||
func(s *StalwartTest, accountId AccountId, ids []string, ctx Context) (Result[map[string]SetError], error) { //NOSONAR
|
||||
return s.client.DeleteAddressBook(accountId, ids, ctx)
|
||||
return s.Client.DeleteAddressBook(accountId, ids, ctx)
|
||||
},
|
||||
func(s *StalwartTest, t *testing.T, accountId AccountId, count uint, ctx Context, user User, principalIds []PrincipalId) (AddressBookBoxes, []AddressBook, SessionState, State, error) {
|
||||
return s.fillAddressBook(t, accountId, count, ctx, user, principalIds)
|
||||
@@ -86,7 +86,7 @@ func TestContacts(t *testing.T) {
|
||||
defer s.Close()
|
||||
|
||||
user := pickUser()
|
||||
session := s.Session(user.email)
|
||||
session := s.Session(user.Email)
|
||||
ctx := s.Context(session)
|
||||
|
||||
accountId, addressbookId, expectedContactCardsById, boxes, err := s.fillContacts(t, count, session, ctx, user)
|
||||
@@ -105,7 +105,7 @@ func TestContacts(t *testing.T) {
|
||||
ss := EmptySessionState
|
||||
os := EmptyState
|
||||
{
|
||||
result, err := s.client.QueryContactCards(ToNullQueryParams([]AccountId{accountId}), nil, filter, sortBy, true, ctx)
|
||||
result, err := s.Client.QueryContactCards(ToNullQueryParams([]AccountId{accountId}), nil, filter, sortBy, true, ctx)
|
||||
require.NoError(err)
|
||||
|
||||
require.Len(result.Payload, 1)
|
||||
@@ -134,7 +134,7 @@ func TestContacts(t *testing.T) {
|
||||
// retrieve all objects at once
|
||||
{
|
||||
ids := structs.Map(results.Results, func(c ContactCard) string { return c.Id })
|
||||
result, err := s.client.GetContactCards(accountId, ids, ctx)
|
||||
result, err := s.Client.GetContactCards(accountId, ids, ctx)
|
||||
require.NoError(err)
|
||||
require.Empty(result.Payload.NotFound)
|
||||
require.Len(result.Payload.List, len(ids))
|
||||
@@ -148,7 +148,7 @@ func TestContacts(t *testing.T) {
|
||||
|
||||
// retrieve each object one by one
|
||||
for _, actual := range results.Results {
|
||||
result, err := s.client.GetContactCards(accountId, []string{actual.Id}, ctx)
|
||||
result, err := s.Client.GetContactCards(accountId, []string{actual.Id}, ctx)
|
||||
require.NoError(err)
|
||||
require.Len(result.Payload.List, 1)
|
||||
matchContact(t, result.Payload.List[0], actual)
|
||||
@@ -162,7 +162,7 @@ func TestContacts(t *testing.T) {
|
||||
for i := range slices {
|
||||
position := int(i * limit)
|
||||
page := min(remainder, limit)
|
||||
result, err := s.client.QueryContactCards(map[AccountId]QueryParams{accountId: {Position: position}}, &limit, filter, sortBy, true, ctx)
|
||||
result, err := s.Client.QueryContactCards(map[AccountId]QueryParams{accountId: {Position: position}}, &limit, filter, sortBy, true, ctx)
|
||||
require.NoError(err)
|
||||
require.Len(result.Payload, 1)
|
||||
require.Contains(result.Payload, accountId)
|
||||
@@ -187,7 +187,7 @@ func TestContacts(t *testing.T) {
|
||||
offset := 0
|
||||
i := 0
|
||||
for chunk := range slices.Chunk(results.Results, chunkSize) {
|
||||
result, err := s.client.QueryContactCards(map[AccountId]QueryParams{accountId: {Anchor: anchor, AnchorOffset: &offset}}, uintPtr(chunkSize), filter, sortBy, true, ctx)
|
||||
result, err := s.Client.QueryContactCards(map[AccountId]QueryParams{accountId: {Anchor: anchor, AnchorOffset: &offset}}, uintPtr(chunkSize), filter, sortBy, true, ctx)
|
||||
require.Equal(ss, result.GetSessionState())
|
||||
require.NoError(err)
|
||||
require.Len(result.Payload, 1)
|
||||
@@ -217,7 +217,7 @@ func TestContacts(t *testing.T) {
|
||||
Language: ptr("xyz"),
|
||||
Updated: ptr(now),
|
||||
}
|
||||
result, err := s.client.UpdateContactCard(accountId, event.Id, change, ctx)
|
||||
result, err := s.Client.UpdateContactCard(accountId, event.Id, change, ctx)
|
||||
require.NoError(err)
|
||||
require.Equal("xyz", result.Payload.Language)
|
||||
require.Equal(now, result.Payload.Updated)
|
||||
@@ -228,7 +228,7 @@ func TestContacts(t *testing.T) {
|
||||
}
|
||||
{
|
||||
ids := structs.Map(slices.Collect(maps.Values(expectedContactCardsById)), func(e ContactCard) string { return e.Id })
|
||||
result, err := s.client.DeleteContactCard(accountId, ids, ctx)
|
||||
result, err := s.Client.DeleteContactCard(accountId, ids, ctx)
|
||||
require.NoError(err)
|
||||
require.Empty(result.Payload)
|
||||
|
||||
@@ -237,7 +237,7 @@ func TestContacts(t *testing.T) {
|
||||
os = result.GetState()
|
||||
}
|
||||
{
|
||||
result, err := s.client.QueryContactCards(ToNullQueryParams([]AccountId{accountId}), nil, filter, sortBy, true, ctx)
|
||||
result, err := s.Client.QueryContactCards(ToNullQueryParams([]AccountId{accountId}), nil, filter, sortBy, true, ctx)
|
||||
require.NoError(err)
|
||||
require.Contains(result.Payload, accountId)
|
||||
resp := result.Payload[accountId]
|
||||
@@ -330,7 +330,7 @@ func (s *StalwartTest) fillAddressBook( //NOSONAR
|
||||
abook.ShareWith = m
|
||||
}
|
||||
|
||||
result, err := s.client.CreateAddressBook(accountId, abook, ctx)
|
||||
result, err := s.Client.CreateAddressBook(accountId, abook, ctx)
|
||||
if err != nil {
|
||||
return boxes, created, ss, as, err
|
||||
}
|
||||
@@ -360,7 +360,7 @@ func (s *StalwartTest) fillContacts( //NOSONAR
|
||||
user User,
|
||||
) (AccountId, string, map[string]ContactCard, ContactsBoxes, error) {
|
||||
require := require.New(t)
|
||||
c, err := NewTestJmapClient(session, user.email, user.password, true, true)
|
||||
c, err := NewTestJmapClient(session, user.Email, user.Password, true, true)
|
||||
require.NoError(err)
|
||||
defer c.Close()
|
||||
|
||||
@@ -655,7 +655,7 @@ func (s *StalwartTest) fillContacts( //NOSONAR
|
||||
return "", "", nil, boxes, err
|
||||
}
|
||||
|
||||
result, err := s.client.CreateContactCard(accountId, card, ctx)
|
||||
result, err := s.Client.CreateContactCard(accountId, card, ctx)
|
||||
if err != nil {
|
||||
return accountId, addressbookId, filled, boxes, err
|
||||
}
|
||||
|
||||
@@ -37,13 +37,13 @@ func TestCalendars(t *testing.T) { //NOSONAR
|
||||
func(resp CalendarGetResponse) []Calendar { return resp.List },
|
||||
func(obj Calendar) string { return obj.Id },
|
||||
func(s *StalwartTest, accountId AccountId, ids []string, ctx Context) (Result[CalendarGetResponse], error) {
|
||||
return s.client.GetCalendars(accountId, ids, ctx)
|
||||
return s.Client.GetCalendars(accountId, ids, ctx)
|
||||
},
|
||||
func(s *StalwartTest, accountId AccountId, id string, change CalendarChange, ctx Context) (Result[Calendar], error) { //NOSONAR
|
||||
return s.client.UpdateCalendar(accountId, id, change, ctx)
|
||||
return s.Client.UpdateCalendar(accountId, id, change, ctx)
|
||||
},
|
||||
func(s *StalwartTest, accountId AccountId, ids []string, ctx Context) (Result[map[string]SetError], error) { //NOSONAR
|
||||
return s.client.DeleteCalendar(accountId, ids, ctx)
|
||||
return s.Client.DeleteCalendar(accountId, ids, ctx)
|
||||
},
|
||||
func(s *StalwartTest, t *testing.T, accountId AccountId, count uint, ctx Context, user User, principalIds []PrincipalId) (CalendarBoxes, []Calendar, SessionState, State, error) {
|
||||
return s.fillCalendar(t, accountId, count, ctx, user, principalIds)
|
||||
@@ -76,7 +76,7 @@ func TestEvents(t *testing.T) {
|
||||
defer s.Close()
|
||||
|
||||
user := pickUser()
|
||||
session := s.Session(user.email)
|
||||
session := s.Session(user.Email)
|
||||
ctx := s.Context(session)
|
||||
|
||||
accountId, calendarId, expectedEventsById, boxes, err := s.fillEvents(t, count, ctx, user)
|
||||
@@ -95,7 +95,7 @@ func TestEvents(t *testing.T) {
|
||||
os := EmptyState
|
||||
var results *CalendarEventSearchResults
|
||||
{
|
||||
result, err := s.client.QueryCalendarEvents(ToNullQueryParams([]AccountId{accountId}), nil, filter, sortBy, true, ctx)
|
||||
result, err := s.Client.QueryCalendarEvents(ToNullQueryParams([]AccountId{accountId}), nil, filter, sortBy, true, ctx)
|
||||
require.NoError(err)
|
||||
|
||||
require.Len(result.Payload, 1)
|
||||
@@ -128,7 +128,7 @@ func TestEvents(t *testing.T) {
|
||||
for i := range slices {
|
||||
position := int(i * limit)
|
||||
page := min(remainder, limit)
|
||||
result, err := s.client.QueryCalendarEvents(map[AccountId]QueryParams{accountId: {Position: position}}, &limit, filter, sortBy, true, ctx)
|
||||
result, err := s.Client.QueryCalendarEvents(map[AccountId]QueryParams{accountId: {Position: position}}, &limit, filter, sortBy, true, ctx)
|
||||
require.NoError(err)
|
||||
require.Len(result.Payload, 1)
|
||||
require.Contains(result.Payload, accountId)
|
||||
@@ -153,7 +153,7 @@ func TestEvents(t *testing.T) {
|
||||
offset := 0
|
||||
i := 0
|
||||
for chunk := range slices.Chunk(results.Results, chunkSize) {
|
||||
result, err := s.client.QueryCalendarEvents(map[AccountId]QueryParams{accountId: {Anchor: anchor, AnchorOffset: &offset}}, uintPtr(chunkSize), filter, sortBy, true, ctx)
|
||||
result, err := s.Client.QueryCalendarEvents(map[AccountId]QueryParams{accountId: {Anchor: anchor, AnchorOffset: &offset}}, uintPtr(chunkSize), filter, sortBy, true, ctx)
|
||||
require.Equal(ss, result.GetSessionState())
|
||||
require.NoError(err)
|
||||
require.Len(result.Payload, 1)
|
||||
@@ -186,7 +186,7 @@ func TestEvents(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
result, err := s.client.UpdateCalendarEvent(accountId, event.Id, change, ctx)
|
||||
result, err := s.Client.UpdateCalendarEvent(accountId, event.Id, change, ctx)
|
||||
require.NoError(err)
|
||||
require.Equal(jscalendar.StatusCancelled, result.Payload.Status)
|
||||
require.Equal(uint(99), result.Payload.Sequence)
|
||||
@@ -198,7 +198,7 @@ func TestEvents(t *testing.T) {
|
||||
|
||||
{
|
||||
ids := structs.Map(slices.Collect(maps.Values(expectedEventsById)), func(e CalendarEvent) string { return e.Id })
|
||||
result, err := s.client.DeleteCalendarEvent(accountId, ids, ctx)
|
||||
result, err := s.Client.DeleteCalendarEvent(accountId, ids, ctx)
|
||||
require.NoError(err)
|
||||
require.Empty(result.Payload)
|
||||
|
||||
@@ -208,7 +208,7 @@ func TestEvents(t *testing.T) {
|
||||
}
|
||||
|
||||
{
|
||||
result, err := s.client.QueryCalendarEvents(ToNullQueryParams([]AccountId{accountId}), nil, filter, sortBy, true, ctx)
|
||||
result, err := s.Client.QueryCalendarEvents(ToNullQueryParams([]AccountId{accountId}), nil, filter, sortBy, true, ctx)
|
||||
require.NoError(err)
|
||||
require.Contains(result.Payload, accountId)
|
||||
resp := result.Payload[accountId]
|
||||
@@ -348,7 +348,7 @@ func (s *StalwartTest) fillCalendar( //NOSONAR
|
||||
cal.ShareWith = m
|
||||
}
|
||||
|
||||
result, err := s.client.CreateCalendar(accountId, cal, ctx)
|
||||
result, err := s.Client.CreateCalendar(accountId, cal, ctx)
|
||||
if err != nil {
|
||||
return boxes, created, ss, as, err
|
||||
}
|
||||
@@ -383,7 +383,7 @@ func (s *StalwartTest) fillEvents( //NOSONAR
|
||||
user User,
|
||||
) (AccountId, string, map[string]CalendarEvent, EventsBoxes, error) {
|
||||
require := require.New(t)
|
||||
c, err := NewTestJmapClient(ctx.Session, user.email, user.password, true, true)
|
||||
c, err := NewTestJmapClient(ctx.Session, user.Email, user.Password, true, true)
|
||||
require.NoError(err)
|
||||
defer c.Close()
|
||||
|
||||
@@ -572,7 +572,7 @@ func (s *StalwartTest) fillEvents( //NOSONAR
|
||||
obj.RecurrenceRule = &rr
|
||||
}
|
||||
|
||||
result, err := s.client.CreateCalendarEvent(accountId, obj, ctx)
|
||||
result, err := s.Client.CreateCalendarEvent(accountId, obj, ctx)
|
||||
if err != nil {
|
||||
return accountId, calendarId, nil, boxes, err
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ func TestEmails(t *testing.T) {
|
||||
defer s.Close()
|
||||
|
||||
user := pickUser()
|
||||
session := s.Session(user.email)
|
||||
session := s.Session(user.Email)
|
||||
ctx := s.Context(session)
|
||||
|
||||
accountId := session.PrimaryAccounts.Mail
|
||||
@@ -57,18 +57,18 @@ func TestEmails(t *testing.T) {
|
||||
|
||||
{
|
||||
{
|
||||
result, err := s.client.GetIdentities(accountId, []string{}, ctx)
|
||||
result, err := s.Client.GetIdentities(accountId, []string{}, ctx)
|
||||
require.NoError(err)
|
||||
require.Equal(session.State, result.GetSessionState())
|
||||
require.Len(result.Payload.List, 2)
|
||||
emailMatches := structs.Filter(result.Payload.List, func(i Identity) bool { return i.Email == user.email })
|
||||
emailMatches := structs.Filter(result.Payload.List, func(i Identity) bool { return i.Email == user.Email })
|
||||
require.Len(emailMatches, 1)
|
||||
aliasMatches := structs.Filter(result.Payload.List, func(i Identity) bool { return i.Email == user.alias })
|
||||
aliasMatches := structs.Filter(result.Payload.List, func(i Identity) bool { return i.Email == user.Alias })
|
||||
require.Len(aliasMatches, 1)
|
||||
}
|
||||
|
||||
{
|
||||
result, err := s.client.GetAllMailboxes([]AccountId{accountId}, ctx)
|
||||
result, err := s.Client.GetAllMailboxes([]AccountId{accountId}, ctx)
|
||||
require.NoError(err)
|
||||
require.Equal(session.State, result.GetSessionState())
|
||||
require.Len(result.Payload, 1)
|
||||
@@ -84,7 +84,7 @@ func TestEmails(t *testing.T) {
|
||||
}
|
||||
|
||||
{
|
||||
result, err := s.client.GetAllEmailsInMailbox(accountId, inboxId, NullQueryParams, nil, true, false, 0, true, ctx)
|
||||
result, err := s.Client.GetAllEmailsInMailbox(accountId, inboxId, NullQueryParams, nil, true, false, 0, true, ctx)
|
||||
require.NoError(err)
|
||||
require.Equal(session.State, result.GetSessionState())
|
||||
|
||||
@@ -98,7 +98,7 @@ func TestEmails(t *testing.T) {
|
||||
}
|
||||
|
||||
{
|
||||
result, err := s.client.GetAllEmailsInMailbox(accountId, inboxId, NullQueryParams, nil, false, false, 0, true, ctx)
|
||||
result, err := s.Client.GetAllEmailsInMailbox(accountId, inboxId, NullQueryParams, nil, false, false, 0, true, ctx)
|
||||
require.NoError(err)
|
||||
require.Equal(session.State, result.GetSessionState())
|
||||
|
||||
@@ -125,29 +125,29 @@ func TestSendingEmails(t *testing.T) {
|
||||
defer s.Close()
|
||||
|
||||
from := pickUser()
|
||||
session := s.Session(from.email)
|
||||
session := s.Session(from.Email)
|
||||
ctx := s.Context(session)
|
||||
accountId := session.PrimaryAccounts.Mail
|
||||
|
||||
var to User
|
||||
{
|
||||
others := structs.Filter(users[:], func(u User) bool { return u.name != from.name })
|
||||
others := structs.Filter(users[:], func(u User) bool { return u.Name != from.Name })
|
||||
to = others[rand.Intn(len(others))]
|
||||
}
|
||||
toSession := s.Session(to.email)
|
||||
toSession := s.Session(to.Email)
|
||||
toAccountId := toSession.PrimaryAccounts.Mail
|
||||
|
||||
var cc User
|
||||
{
|
||||
others := structs.Filter(users[:], func(u User) bool { return u.name != from.name && u.name != to.name })
|
||||
others := structs.Filter(users[:], func(u User) bool { return u.Name != from.Name && u.Name != to.Name })
|
||||
cc = others[rand.Intn(len(others))]
|
||||
}
|
||||
ccSession := s.Session(cc.email)
|
||||
ccSession := s.Session(cc.Email)
|
||||
ccAccountId := ccSession.PrimaryAccounts.Mail
|
||||
|
||||
var mailboxPerRole map[string]Mailbox
|
||||
{
|
||||
result, err := s.client.GetAllMailboxes([]AccountId{accountId}, ctx)
|
||||
result, err := s.Client.GetAllMailboxes([]AccountId{accountId}, ctx)
|
||||
require.NoError(err)
|
||||
mailboxPerRole = structs.Index(result.Payload[accountId], func(m Mailbox) string { return m.Role })
|
||||
require.Contains(mailboxPerRole, JmapMailboxRoleInbox)
|
||||
@@ -157,7 +157,7 @@ func TestSendingEmails(t *testing.T) {
|
||||
}
|
||||
{
|
||||
roles := []string{JmapMailboxRoleDrafts, JmapMailboxRoleSent, JmapMailboxRoleInbox}
|
||||
result, err := s.client.SearchMailboxIdsPerRole([]AccountId{accountId}, roles, ctx)
|
||||
result, err := s.Client.SearchMailboxIdsPerRole([]AccountId{accountId}, roles, ctx)
|
||||
require.NoError(err)
|
||||
require.Contains(result.Payload, accountId)
|
||||
a := result.Payload[accountId]
|
||||
@@ -177,7 +177,7 @@ func TestSendingEmails(t *testing.T) {
|
||||
Logger: ctx.Logger,
|
||||
AcceptLanguage: ctx.AcceptLanguage,
|
||||
}
|
||||
result, err := s.client.GetAllMailboxes([]AccountId{u.accountId}, uctx)
|
||||
result, err := s.Client.GetAllMailboxes([]AccountId{u.accountId}, uctx)
|
||||
require.NoError(err)
|
||||
for _, mailbox := range result.Payload[u.accountId] {
|
||||
require.Equal(0, mailbox.TotalEmails)
|
||||
@@ -185,16 +185,16 @@ func TestSendingEmails(t *testing.T) {
|
||||
}
|
||||
|
||||
subject := fmt.Sprintf("Test Subject %d", 10000+rand.Intn(90000))
|
||||
fromName := fmt.Sprintf("%s (test %d)", from.name, 1000+rand.Intn(9000))
|
||||
sender := EmailAddress{Email: from.email, Name: from.description}
|
||||
fromName := fmt.Sprintf("%s (test %d)", from.Name, 1000+rand.Intn(9000))
|
||||
sender := EmailAddress{Email: from.Email, Name: from.Description}
|
||||
|
||||
{
|
||||
var identity Identity
|
||||
{
|
||||
result, err := s.client.GetIdentities(accountId, []string{}, ctx)
|
||||
result, err := s.Client.GetIdentities(accountId, []string{}, ctx)
|
||||
require.NoError(err)
|
||||
require.Len(result.Payload.List, 2)
|
||||
matchesAlias := structs.Filter(result.Payload.List, func(i Identity) bool { return i.Email == from.alias })
|
||||
matchesAlias := structs.Filter(result.Payload.List, func(i Identity) bool { return i.Email == from.Alias })
|
||||
require.Len(matchesAlias, 1)
|
||||
identity = matchesAlias[0]
|
||||
}
|
||||
@@ -206,14 +206,14 @@ func TestSendingEmails(t *testing.T) {
|
||||
}
|
||||
var created *Email
|
||||
{
|
||||
result, err := s.client.CreateEmail(accountId, create, "", ctx)
|
||||
result, err := s.Client.CreateEmail(accountId, create, "", ctx)
|
||||
require.NoError(err)
|
||||
created = result.Payload
|
||||
require.NotEmpty(created.Id)
|
||||
}
|
||||
|
||||
{
|
||||
result, err := s.client.GetEmails(accountId, []string{created.Id}, true, 0, false, false, ctx)
|
||||
result, err := s.Client.GetEmails(accountId, []string{created.Id}, true, 0, false, false, ctx)
|
||||
require.NoError(err)
|
||||
require.Len(result.Payload.List, 1)
|
||||
require.Empty(result.Payload.NotFound)
|
||||
@@ -224,9 +224,9 @@ func TestSendingEmails(t *testing.T) {
|
||||
}
|
||||
|
||||
update := EmailChange{
|
||||
From: []EmailAddress{{Name: fromName, Email: from.email}},
|
||||
To: []EmailAddress{{Name: to.description, Email: to.email}},
|
||||
Cc: []EmailAddress{{Name: cc.description, Email: cc.email}},
|
||||
From: []EmailAddress{{Name: fromName, Email: from.Email}},
|
||||
To: []EmailAddress{{Name: to.Description, Email: to.Email}},
|
||||
Cc: []EmailAddress{{Name: cc.Description, Email: cc.Email}},
|
||||
Sender: []EmailAddress{sender},
|
||||
Keywords: toBoolMapS("test"),
|
||||
Subject: subject,
|
||||
@@ -234,7 +234,7 @@ func TestSendingEmails(t *testing.T) {
|
||||
}
|
||||
var updated *Email
|
||||
{
|
||||
result, err := s.client.CreateEmail(accountId, update, created.Id, ctx)
|
||||
result, err := s.Client.CreateEmail(accountId, update, created.Id, ctx)
|
||||
require.NoError(err)
|
||||
updated = result.Payload
|
||||
require.NotNil(updated)
|
||||
@@ -244,7 +244,7 @@ func TestSendingEmails(t *testing.T) {
|
||||
|
||||
var updatedMailboxId string
|
||||
{
|
||||
result, err := s.client.GetEmails(accountId, []string{created.Id, updated.Id}, true, 0, false, false, ctx)
|
||||
result, err := s.Client.GetEmails(accountId, []string{created.Id, updated.Id}, true, 0, false, false, ctx)
|
||||
require.NoError(err)
|
||||
require.Len(result.Payload.List, 1)
|
||||
require.Len(result.Payload.NotFound, 1)
|
||||
@@ -265,7 +265,7 @@ func TestSendingEmails(t *testing.T) {
|
||||
|
||||
var sub EmailSubmission
|
||||
{
|
||||
result, err := s.client.SubmitEmail(accountId, identity.Id, updated.Id, &move, ctx)
|
||||
result, err := s.Client.SubmitEmail(accountId, identity.Id, updated.Id, &move, ctx)
|
||||
require.NoError(err)
|
||||
sub = result.Payload
|
||||
require.NotEmpty(sub.Id)
|
||||
@@ -275,20 +275,20 @@ func TestSendingEmails(t *testing.T) {
|
||||
require.Equal(sub.UndoStatus, UndoStatusPending) // this *might* be fragile: if the server is fast enough, would we get "final" here?
|
||||
require.Empty(sub.DsnBlobIds)
|
||||
require.Empty(sub.MdnBlobIds)
|
||||
require.Equal(from.alias, sub.Envelope.MailFrom.Email)
|
||||
require.Equal(from.Alias, sub.Envelope.MailFrom.Email)
|
||||
require.Nil(sub.Envelope.MailFrom.Parameters)
|
||||
require.Len(sub.Envelope.RcptTo, 2)
|
||||
require.Contains(sub.Envelope.RcptTo, Address{Email: to.email})
|
||||
require.Contains(sub.Envelope.RcptTo, Address{Email: cc.email})
|
||||
require.Contains(sub.Envelope.RcptTo, Address{Email: to.Email})
|
||||
require.Contains(sub.Envelope.RcptTo, Address{Email: cc.Email})
|
||||
require.NotZero(sub.SendAt)
|
||||
require.Len(sub.DeliveryStatus, 2)
|
||||
require.Contains(sub.DeliveryStatus, to.email)
|
||||
require.Contains(sub.DeliveryStatus, cc.email)
|
||||
require.Contains(sub.DeliveryStatus, to.Email)
|
||||
require.Contains(sub.DeliveryStatus, cc.Email)
|
||||
}
|
||||
|
||||
a := 0
|
||||
maxAttempts := 3
|
||||
delivery := sub.DeliveryStatus[to.email].Delivered
|
||||
delivery := sub.DeliveryStatus[to.Email].Delivered
|
||||
|
||||
for delivery != DeliveredYes {
|
||||
require.NotEqual(DeliveredNo, delivery)
|
||||
@@ -298,13 +298,13 @@ func TestSendingEmails(t *testing.T) {
|
||||
}
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
result, err := s.client.GetEmailSubmissionStatus(accountId, []string{sub.Id}, ctx)
|
||||
result, err := s.Client.GetEmailSubmissionStatus(accountId, []string{sub.Id}, ctx)
|
||||
require.NoError(err)
|
||||
require.Empty(result.Payload.NotFound)
|
||||
submittedIds := structs.Map(result.Payload.List, func(s EmailSubmission) string { return s.Id })
|
||||
require.Contains(submittedIds, sub.Id)
|
||||
subs := structs.Index(result.Payload.List, func(s EmailSubmission) string { return s.Id })
|
||||
delivery = subs[sub.Id].DeliveryStatus[to.email].Delivered
|
||||
delivery = subs[sub.Id].DeliveryStatus[to.Email].Delivered
|
||||
}
|
||||
|
||||
require.Contains([]DeliveryStatusDelivered{DeliveredYes, DeliveredUnknown}, delivery)
|
||||
@@ -322,7 +322,7 @@ func TestSendingEmails(t *testing.T) {
|
||||
}
|
||||
inboxId := ""
|
||||
{
|
||||
result, err := s.client.GetAllMailboxes([]AccountId{r.accountId}, rctx)
|
||||
result, err := s.Client.GetAllMailboxes([]AccountId{r.accountId}, rctx)
|
||||
require.NoError(err)
|
||||
for _, mailbox := range result.Payload[r.accountId] {
|
||||
if mailbox.Role == JmapMailboxRoleInbox {
|
||||
@@ -330,26 +330,26 @@ func TestSendingEmails(t *testing.T) {
|
||||
require.Equal(1, mailbox.TotalEmails)
|
||||
}
|
||||
}
|
||||
require.NotEmpty(inboxId, "failed to find the Mailbox with the 'inbox' role for %v", r.user.email)
|
||||
require.NotEmpty(inboxId, "failed to find the Mailbox with the 'inbox' role for %v", r.user.Email)
|
||||
}
|
||||
|
||||
result, err := s.client.QueryEmails([]AccountId{r.accountId}, EmailFilterCondition{InMailbox: inboxId}, 0, 0, true, 0, rctx)
|
||||
result, err := s.Client.QueryEmails([]AccountId{r.accountId}, EmailFilterCondition{InMailbox: inboxId}, 0, 0, true, 0, rctx)
|
||||
require.NoError(err)
|
||||
require.Contains(result.Payload, r.accountId)
|
||||
require.Len(result.Payload[r.accountId].Results, 1)
|
||||
received := result.Payload[r.accountId].Results[0]
|
||||
require.Len(received.From, 1)
|
||||
require.Equal(from.email, received.From[0].Email)
|
||||
require.Equal(from.Email, received.From[0].Email)
|
||||
require.Equal(fromName, received.From[0].Name)
|
||||
require.Len(received.Sender, 1)
|
||||
require.Equal(from.email, received.Sender[0].Email)
|
||||
require.Equal(from.description, received.Sender[0].Name)
|
||||
require.Equal(from.Email, received.Sender[0].Email)
|
||||
require.Equal(from.Description, received.Sender[0].Name)
|
||||
require.Len(received.To, 1)
|
||||
require.Equal(to.email, received.To[0].Email)
|
||||
require.Equal(to.description, received.To[0].Name)
|
||||
require.Equal(to.Email, received.To[0].Email)
|
||||
require.Equal(to.Description, received.To[0].Name)
|
||||
require.Len(received.Cc, 1)
|
||||
require.Equal(cc.email, received.Cc[0].Email)
|
||||
require.Equal(cc.description, received.Cc[0].Name)
|
||||
require.Equal(cc.Email, received.Cc[0].Email)
|
||||
require.Equal(cc.Description, received.Cc[0].Name)
|
||||
require.Equal(subject, received.Subject)
|
||||
}
|
||||
}
|
||||
@@ -392,7 +392,7 @@ func matchEmail(t *testing.T, actual Email, expected filledMail, hasBodies bool)
|
||||
|
||||
func (s *StalwartTest) findInbox(t *testing.T, accountId AccountId, ctx Context) (string, string) {
|
||||
require := require.New(t)
|
||||
result, err := s.client.GetAllMailboxes([]AccountId{accountId}, ctx)
|
||||
result, err := s.Client.GetAllMailboxes([]AccountId{accountId}, ctx)
|
||||
require.NoError(err)
|
||||
require.Equal(ctx.Session.State, result.GetSessionState())
|
||||
require.Len(result.Payload, 1)
|
||||
@@ -521,7 +521,7 @@ var allKeywords = map[string]imap.Flag{
|
||||
}
|
||||
|
||||
func (s *StalwartTest) fillEmailsWithImap(folder string, count int, empty bool, user User) ([]filledMail, int, error) { //NOSONAR
|
||||
to := fmt.Sprintf("%s <%s>", user.description, user.email)
|
||||
to := fmt.Sprintf("%s <%s>", user.Description, user.Email)
|
||||
ccEvery := 2
|
||||
bccEvery := 3
|
||||
attachmentEvery := 2
|
||||
@@ -543,7 +543,7 @@ func (s *StalwartTest) fillEmailsWithImap(folder string, count int, empty bool,
|
||||
}
|
||||
}(c)
|
||||
|
||||
if err = c.Login(user.email, user.password).Wait(); err != nil {
|
||||
if err = c.Login(user.Email, user.Password).Wait(); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
@@ -591,7 +591,7 @@ func (s *StalwartTest) fillEmailsWithImap(folder string, count int, empty bool,
|
||||
domain := addressParts[0][2]
|
||||
|
||||
toName := displayName
|
||||
toAddress := fmt.Sprintf("%s@%s", user.email, domain)
|
||||
toAddress := fmt.Sprintf("%s@%s", user.Email, domain)
|
||||
ccName1 := "Team Lead"
|
||||
ccAddress1 := fmt.Sprintf("lead@%s", domain)
|
||||
ccName2 := "Coworker"
|
||||
|
||||
@@ -69,7 +69,7 @@ func TestWs(t *testing.T) {
|
||||
defer s.Close()
|
||||
|
||||
user := pickUser()
|
||||
session := s.Session(user.email)
|
||||
session := s.Session(user.Email)
|
||||
ctx := s.Context(session)
|
||||
|
||||
mailAccountId := session.PrimaryAccounts.Mail
|
||||
@@ -78,8 +78,8 @@ func TestWs(t *testing.T) {
|
||||
_, inboxFolder = s.findInbox(t, mailAccountId, ctx)
|
||||
}
|
||||
|
||||
l := &testWsPushListener{t: t, username: user.email, logger: s.logger, mailAccountId: mailAccountId}
|
||||
s.client.AddWsPushListener(l)
|
||||
l := &testWsPushListener{t: t, username: user.Email, logger: s.logger, mailAccountId: mailAccountId}
|
||||
s.Client.AddWsPushListener(l)
|
||||
|
||||
require.Equal(uint32(0), l.calls.Load())
|
||||
{
|
||||
@@ -92,7 +92,7 @@ func TestWs(t *testing.T) {
|
||||
|
||||
var initialState State
|
||||
{
|
||||
result, err := s.client.GetEmailChanges(mailAccountId, EmptyState, true, 0, 0, ctx)
|
||||
result, err := s.Client.GetEmailChanges(mailAccountId, EmptyState, true, 0, 0, ctx)
|
||||
require.NoError(err)
|
||||
require.Equal(session.State, result.GetSessionState())
|
||||
require.NotEmpty(result.GetState())
|
||||
@@ -106,7 +106,7 @@ func TestWs(t *testing.T) {
|
||||
require.NotEmpty(initialState)
|
||||
|
||||
{
|
||||
result, err := s.client.GetEmailChanges(mailAccountId, initialState, true, 0, 0, ctx)
|
||||
result, err := s.Client.GetEmailChanges(mailAccountId, initialState, true, 0, 0, ctx)
|
||||
require.NoError(err)
|
||||
require.Equal(session.State, result.GetSessionState())
|
||||
require.Equal(initialState, result.GetState())
|
||||
@@ -116,7 +116,7 @@ func TestWs(t *testing.T) {
|
||||
require.Empty(result.Payload.Updated)
|
||||
}
|
||||
|
||||
wsc, err := s.client.EnablePushNotifications(cotx, initialState, func() (*Session, error) { return session, nil })
|
||||
wsc, err := s.Client.EnablePushNotifications(cotx, initialState, func() (*Session, error) { return session, nil })
|
||||
require.NoError(err)
|
||||
defer wsc.Close()
|
||||
|
||||
@@ -149,7 +149,7 @@ func TestWs(t *testing.T) {
|
||||
}
|
||||
var lastState State
|
||||
{
|
||||
result, err := s.client.GetEmailChanges(mailAccountId, initialState, true, 0, 0, ctx)
|
||||
result, err := s.Client.GetEmailChanges(mailAccountId, initialState, true, 0, 0, ctx)
|
||||
require.NoError(err)
|
||||
require.Equal(session.State, result.GetSessionState())
|
||||
require.NotEqual(initialState, result.GetState())
|
||||
@@ -183,7 +183,7 @@ func TestWs(t *testing.T) {
|
||||
l.m.Unlock()
|
||||
}
|
||||
{
|
||||
result, err := s.client.GetEmailChanges(mailAccountId, lastState, true, 0, 0, ctx)
|
||||
result, err := s.Client.GetEmailChanges(mailAccountId, lastState, true, 0, 0, ctx)
|
||||
require.NoError(err)
|
||||
require.Equal(session.State, result.GetSessionState())
|
||||
require.NotEqual(lastState, result.GetState())
|
||||
@@ -217,7 +217,7 @@ func TestWs(t *testing.T) {
|
||||
l.m.Unlock()
|
||||
}
|
||||
{
|
||||
result, err := s.client.GetEmailChanges(mailAccountId, lastState, true, 0, 0, ctx)
|
||||
result, err := s.Client.GetEmailChanges(mailAccountId, lastState, true, 0, 0, ctx)
|
||||
require.NoError(err)
|
||||
require.Equal(session.State, result.GetSessionState())
|
||||
require.NotEqual(lastState, result.GetState())
|
||||
|
||||
+106
-91
@@ -14,6 +14,7 @@ import (
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"os"
|
||||
"reflect"
|
||||
@@ -25,6 +26,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
dockercontainer "github.com/moby/moby/api/types/container"
|
||||
dockernetwork "github.com/moby/moby/api/types/network"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
@@ -75,11 +77,11 @@ const (
|
||||
)
|
||||
|
||||
type User struct {
|
||||
name string
|
||||
description string
|
||||
alias string
|
||||
password string
|
||||
email string
|
||||
Name string
|
||||
Description string
|
||||
Alias string
|
||||
Password string
|
||||
Email string
|
||||
}
|
||||
|
||||
func userpassword() string {
|
||||
@@ -92,7 +94,7 @@ func userpassword() string {
|
||||
|
||||
func mkuser(name string, description string, alias string) User {
|
||||
parts := strings.Split(alias, "@")
|
||||
return User{name: name, description: description, alias: alias, email: name + "@" + parts[1], password: userpassword()}
|
||||
return User{Name: name, Description: description, Alias: alias, Email: name + "@" + parts[1], Password: userpassword()}
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -214,16 +216,20 @@ func Skip(t *testing.T) bool {
|
||||
}
|
||||
|
||||
type StalwartTest struct {
|
||||
t *testing.T
|
||||
ip string
|
||||
imapPort uint16
|
||||
container *testcontainers.DockerContainer
|
||||
ctx context.Context
|
||||
cancelCtx context.CancelFunc
|
||||
client *jmap.Client
|
||||
logger *oclog.Logger
|
||||
jmapBaseUrl *url.URL
|
||||
sessionUrl *url.URL
|
||||
t *testing.T
|
||||
ip string
|
||||
imapPort uint16
|
||||
container *testcontainers.DockerContainer
|
||||
ctx context.Context
|
||||
cancelCtx context.CancelFunc
|
||||
logger *oclog.Logger
|
||||
sessionUrl *url.URL
|
||||
|
||||
Client *jmap.Client
|
||||
JmapBaseUrl *url.URL
|
||||
MasterUsername string
|
||||
MasterPassword string
|
||||
Users []User
|
||||
|
||||
io.Closer
|
||||
}
|
||||
@@ -249,43 +255,11 @@ func (s *StalwartTest) Context(session *jmap.Session) jmap.Context {
|
||||
}
|
||||
|
||||
func (s *StalwartTest) Session(username string) *jmap.Session {
|
||||
session, err := s.client.FetchSession(s.ctx, s.sessionUrl, username, s.logger)
|
||||
session, err := s.Client.FetchSession(s.ctx, s.sessionUrl, username, s.logger)
|
||||
require.NoError(s.t, err, "failed to authenticate user '%s' and/or retrieve their JMAP session using the URL '%s'", username, s.sessionUrl.String())
|
||||
require.NotNil(s.t, session.Capabilities.Mail)
|
||||
require.NotNil(s.t, session.Capabilities.Calendars)
|
||||
require.NotNil(s.t, session.Capabilities.Contacts)
|
||||
|
||||
// we have to overwrite the hostname in JMAP URL because the container
|
||||
// will know its name to be a random Docker container identifier, or
|
||||
// "localhost" as we defined the hostname in the Stalwart configuration,
|
||||
// and we also need to overwrite the port number as its not mapped
|
||||
|
||||
session.JmapUrl.Host = s.jmapBaseUrl.Host
|
||||
session.JmapUrl.Scheme = "http" // replace https with http
|
||||
session.WebsocketUrl.Host = s.jmapBaseUrl.Host
|
||||
session.WebsocketUrl.Scheme = "ws" // replace wss with ws
|
||||
|
||||
if v, err := replaceHostProto(session.ApiUrl, "http", s.jmapBaseUrl.Host); err != nil {
|
||||
require.NoError(s.t, err)
|
||||
} else {
|
||||
session.ApiUrl = v
|
||||
}
|
||||
if v, err := replaceHostProto(session.DownloadUrl, "http", s.jmapBaseUrl.Host); err != nil {
|
||||
require.NoError(s.t, err)
|
||||
} else {
|
||||
session.DownloadUrl = v
|
||||
}
|
||||
if v, err := replaceHostProto(session.UploadUrl, "http", s.jmapBaseUrl.Host); err != nil {
|
||||
require.NoError(s.t, err)
|
||||
} else {
|
||||
session.UploadUrl = v
|
||||
}
|
||||
if v, err := replaceHostProto(session.EventSourceUrl, "http", s.jmapBaseUrl.Host); err != nil {
|
||||
require.NoError(s.t, err)
|
||||
} else {
|
||||
session.EventSourceUrl = v
|
||||
}
|
||||
|
||||
return &session
|
||||
}
|
||||
|
||||
@@ -609,10 +583,10 @@ func createManagementObject(ctx context.Context, h http.Client, url string, admi
|
||||
}
|
||||
}
|
||||
|
||||
func createJmapClient(container *testcontainers.DockerContainer, ctx context.Context, auth jmap.HttpJmapClientAuthenticator, logger *oclog.Logger) (jmap.Client, *url.URL, *url.URL, error) {
|
||||
func createJmapClient(container *testcontainers.DockerContainer, ctx context.Context, auth jmap.HttpJmapClientAuthenticator, logger *oclog.Logger) (*jmap.Client, *url.URL, *url.URL, error) {
|
||||
ip, err := container.Host(ctx)
|
||||
if err != nil {
|
||||
return jmap.Client{}, nil, nil, err
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
tlsConfig := &tls.Config{InsecureSkipVerify: true}
|
||||
@@ -630,7 +604,7 @@ func createJmapClient(container *testcontainers.DockerContainer, ctx context.Con
|
||||
|
||||
jmapPort, err := container.MappedPort(ctx, httpPort)
|
||||
if err != nil {
|
||||
return jmap.Client{}, nil, nil, err
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
jmapBaseUrl := &url.URL{
|
||||
Scheme: "http",
|
||||
@@ -649,11 +623,11 @@ func createJmapClient(container *testcontainers.DockerContainer, ctx context.Con
|
||||
cmd := []string{Wireshark, "-pkSl", "-i", "lo", "-f", fmt.Sprintf("port %d", jmapPort.Num()), "-Y", "http||websocket"}
|
||||
process, err := os.StartProcess(Wireshark, cmd, &attr)
|
||||
if err != nil {
|
||||
return jmap.Client{}, nil, nil, err
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
err = process.Release()
|
||||
if err != nil {
|
||||
return jmap.Client{}, nil, nil, err
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
time.Sleep(10 * time.Second)
|
||||
@@ -665,7 +639,7 @@ func createJmapClient(container *testcontainers.DockerContainer, ctx context.Con
|
||||
|
||||
wscf, err := jmap.NewHttpWsClientFactory(wsd, auth, logger, eventListener, true, 8192)
|
||||
if err != nil {
|
||||
return jmap.Client{}, nil, nil, err
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
return jmap.NewClient(api, api, api, wscf), jmapBaseUrl, sessionUrl, nil
|
||||
@@ -741,17 +715,17 @@ func NewStalwartTest(t *testing.T, options ...func(*importSettings)) (*StalwartT
|
||||
option(&settings)
|
||||
}
|
||||
|
||||
var net *testcontainers.DockerNetwork
|
||||
var containerNetwork *testcontainers.DockerNetwork
|
||||
{
|
||||
if useNetwork {
|
||||
if n, err := network.New(ctx); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
testcontainers.CleanupNetwork(t, n)
|
||||
net = n
|
||||
containerNetwork = n
|
||||
}
|
||||
} else {
|
||||
net = nil
|
||||
containerNetwork = nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -791,8 +765,8 @@ func NewStalwartTest(t *testing.T, options ...func(*importSettings)) (*StalwartT
|
||||
},
|
||||
),
|
||||
}
|
||||
if net != nil {
|
||||
opts = append(opts, network.WithNetwork([]string{recoveryAlias}, net))
|
||||
if containerNetwork != nil {
|
||||
opts = append(opts, network.WithNetwork([]string{recoveryAlias}, containerNetwork))
|
||||
}
|
||||
|
||||
stalwartImage := fmt.Sprintf(stalwartImageTemplate, StalwartVersion)
|
||||
@@ -805,7 +779,7 @@ func NewStalwartTest(t *testing.T, options ...func(*importSettings)) (*StalwartT
|
||||
|
||||
// now that the container is running in recovery mode, we can use the stalwart CLI to import
|
||||
// the configuration
|
||||
if output, err := importConfig(t, recovery, ctx, net, recoveryAlias, adminUsername, adminPassword, settings, false); err != nil {
|
||||
if output, err := importConfig(t, recovery, ctx, containerNetwork, recoveryAlias, adminUsername, adminPassword, settings, false); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
t.Logf("Output of applying configuration:\n%s", strings.Join(output, "")) //NOSONAR
|
||||
@@ -819,13 +793,45 @@ func NewStalwartTest(t *testing.T, options ...func(*importSettings)) (*StalwartT
|
||||
}
|
||||
}
|
||||
|
||||
httpHostPort, err := FreeLocalhostPort()
|
||||
require.NoError(t, err)
|
||||
imapsHostPort, err := FreeLocalhostPort()
|
||||
require.NoError(t, err)
|
||||
|
||||
publicUrl := fmt.Sprintf("http://127.0.0.1:%d", httpHostPort)
|
||||
|
||||
// and now we start the container in "proper" mode (not recovery), re-using the same volume for data
|
||||
var container *testcontainers.DockerContainer
|
||||
{
|
||||
parsedHttpPort, err := dockernetwork.ParsePort(httpPort + "/tcp")
|
||||
require.NoError(t, err)
|
||||
parsedImapsPort, err := dockernetwork.ParsePort(imapsPort + "/tcp")
|
||||
require.NoError(t, err)
|
||||
address, err := netip.ParseAddr("127.0.0.1")
|
||||
require.NoError(t, err)
|
||||
|
||||
opts := []testcontainers.ContainerCustomizer{
|
||||
testcontainers.WithLogConsumers(&printingLogConsumer{prefix: "STALWART"}),
|
||||
testcontainers.WithExposedPorts(httpPort+"/tcp", imapsPort+"/tcp"),
|
||||
testcontainers.WithHostConfigModifier(func(hc *dockercontainer.HostConfig) {
|
||||
hc.PortBindings = dockernetwork.PortMap{
|
||||
dockernetwork.Port(parsedHttpPort): []dockernetwork.PortBinding{
|
||||
{
|
||||
HostIP: address,
|
||||
HostPort: fmt.Sprintf("%d", httpHostPort),
|
||||
},
|
||||
},
|
||||
dockernetwork.Port(parsedImapsPort): []dockernetwork.PortBinding{
|
||||
{
|
||||
HostIP: address,
|
||||
HostPort: fmt.Sprintf("%d", imapsHostPort),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
}),
|
||||
testcontainers.WithEnv(map[string]string{
|
||||
"STALWART_PUBLIC_URL": publicUrl,
|
||||
"STALWART_RECOVERY_ADMIN": strings.Join([]string{adminUsername, adminPassword}, ":"),
|
||||
}),
|
||||
testcontainers.WithFiles(testcontainers.ContainerFile{
|
||||
@@ -850,8 +856,8 @@ func NewStalwartTest(t *testing.T, options ...func(*importSettings)) (*StalwartT
|
||||
),
|
||||
}
|
||||
|
||||
if net != nil {
|
||||
opts = append(opts, network.WithNetwork([]string{containerAlias}, net))
|
||||
if containerNetwork != nil {
|
||||
opts = append(opts, network.WithNetwork([]string{containerAlias}, containerNetwork))
|
||||
}
|
||||
|
||||
stalwartImage := fmt.Sprintf(stalwartImageTemplate, StalwartVersion)
|
||||
@@ -866,7 +872,7 @@ func NewStalwartTest(t *testing.T, options ...func(*importSettings)) (*StalwartT
|
||||
if !useRecoveryContainer {
|
||||
// we didn't use a recovery container to initialize the configuration, we will use the
|
||||
// regular container to do so
|
||||
if output, err := importConfig(t, container, ctx, net, containerAlias, adminUsername, adminPassword, settings, true); err != nil {
|
||||
if output, err := importConfig(t, container, ctx, containerNetwork, containerAlias, adminUsername, adminPassword, settings, true); err != nil {
|
||||
t.Logf("Output of applying configuration:\n%s", strings.Join(output, ""))
|
||||
return nil, fmt.Errorf("failed to import configuration: %w: output: %s", err, strings.Join(output, ""))
|
||||
} else {
|
||||
@@ -887,7 +893,7 @@ func NewStalwartTest(t *testing.T, options ...func(*importSettings)) (*StalwartT
|
||||
}
|
||||
|
||||
domains := structs.Uniq(structs.Map(users[:], func(u User) string {
|
||||
parts := strings.Split(u.email, "@")
|
||||
parts := strings.Split(u.Email, "@")
|
||||
return parts[1]
|
||||
}))
|
||||
slices.Sort(domains)
|
||||
@@ -948,26 +954,26 @@ func NewStalwartTest(t *testing.T, options ...func(*importSettings)) (*StalwartT
|
||||
}
|
||||
|
||||
for _, user := range users {
|
||||
fmt.Printf("Creating individual '%v'\n", user.name)
|
||||
fmt.Printf("Creating individual '%v'\n", user.Name)
|
||||
|
||||
localPart := ""
|
||||
domain := ""
|
||||
domainId := ""
|
||||
{
|
||||
parts := strings.Split(user.alias, "@")
|
||||
parts := strings.Split(user.Alias, "@")
|
||||
localPart = parts[0]
|
||||
domain = parts[1]
|
||||
if v, ok := domainIds[domain]; ok {
|
||||
domainId = v
|
||||
} else {
|
||||
require.FailNow(t, "failed to find id for domain '%s' in user email address '%s'", domain, user.email)
|
||||
require.FailNow(t, "failed to find id for domain '%s' in user email address '%s'", domain, user.Email)
|
||||
}
|
||||
}
|
||||
|
||||
_, err := createManagementObject(ctx, h, url, adminUsername, adminPassword, "Account", map[string]any{
|
||||
"@type": "User",
|
||||
"name": user.name,
|
||||
"description": user.description,
|
||||
"name": user.Name,
|
||||
"description": user.Description,
|
||||
"aliases": map[string]any{
|
||||
"0": map[string]any{
|
||||
"enabled": true,
|
||||
@@ -978,7 +984,7 @@ func NewStalwartTest(t *testing.T, options ...func(*importSettings)) (*StalwartT
|
||||
"credentials": map[string]any{
|
||||
"0": map[string]any{
|
||||
"@type": "Password",
|
||||
"secret": user.password,
|
||||
"secret": user.Password,
|
||||
"allowedIps": map[string]any{},
|
||||
"expiresAt": nil,
|
||||
},
|
||||
@@ -1012,7 +1018,7 @@ func NewStalwartTest(t *testing.T, options ...func(*importSettings)) (*StalwartT
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
usersByName := structs.Index(users[:], func(u User) string { return u.name })
|
||||
usersByName := structs.Index(users[:], func(u User) string { return u.Name })
|
||||
{
|
||||
mr := result["methodResponses"].([]any)
|
||||
f := mr[0].([]any)
|
||||
@@ -1023,11 +1029,11 @@ func NewStalwartTest(t *testing.T, options ...func(*importSettings)) (*StalwartT
|
||||
name := u["name"].(string)
|
||||
email := u["emailAddress"].(string)
|
||||
if match, ok := usersByName[name]; ok {
|
||||
require.Equal(t, match.email, email)
|
||||
require.Equal(t, match.Email, email)
|
||||
aliases := u["aliases"].(map[string]any)
|
||||
require.Len(t, aliases, 1)
|
||||
for _, alias := range aliases {
|
||||
parts := strings.Split(match.alias, "@")
|
||||
parts := strings.Split(match.Alias, "@")
|
||||
alias := alias.(map[string]any)
|
||||
require.True(t, alias["enabled"].(bool))
|
||||
require.Equal(t, parts[0], alias["name"].(string))
|
||||
@@ -1058,12 +1064,18 @@ func NewStalwartTest(t *testing.T, options ...func(*importSettings)) (*StalwartT
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// check whether we can fetch a session for the provisioned users
|
||||
// check whether we can fetch a session for the provisioned users,
|
||||
// but since these are flaky once in a blue moon, we will retry
|
||||
for _, user := range users {
|
||||
ctx := context.WithValue(ctx, "password", user.password)
|
||||
session, err := j.FetchSession(ctx, sessionUrl, user.email, oclog.From(logger.With().Str("username", user.email).Str("password", user.password)))
|
||||
require.NoError(t, err, "failed to retrieve JMAP session for newly created principal '%s'", user.email)
|
||||
require.Equal(t, user.email, session.Username)
|
||||
require.Eventually(t, func() bool {
|
||||
ctx := context.WithValue(ctx, "password", user.Password)
|
||||
session, err := j.FetchSession(ctx, sessionUrl, user.Email, oclog.From(logger.With().Str("username", user.Email).Str("password", user.Password)))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
require.Equal(t, user.Email, session.Username)
|
||||
return true
|
||||
}, 5*time.Second, 500*time.Millisecond, "failed to retrieve JMAP session for newly created principal '%s'", user.Email)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1079,16 +1091,19 @@ func NewStalwartTest(t *testing.T, options ...func(*importSettings)) (*StalwartT
|
||||
}
|
||||
|
||||
return &StalwartTest{
|
||||
t: t,
|
||||
ip: ip,
|
||||
imapPort: imapPort.Num(),
|
||||
container: container,
|
||||
ctx: ctx,
|
||||
cancelCtx: cancel,
|
||||
client: &j,
|
||||
logger: logger,
|
||||
jmapBaseUrl: jmapBaseUrl,
|
||||
sessionUrl: sessionUrl,
|
||||
t: t,
|
||||
ip: ip,
|
||||
imapPort: imapPort.Num(),
|
||||
container: container,
|
||||
ctx: ctx,
|
||||
cancelCtx: cancel,
|
||||
logger: logger,
|
||||
sessionUrl: sessionUrl,
|
||||
Client: j,
|
||||
JmapBaseUrl: jmapBaseUrl,
|
||||
MasterUsername: masterUsername,
|
||||
MasterPassword: masterPassword,
|
||||
Users: users[:],
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -1767,7 +1782,7 @@ func containerTest[OBJ jmap.Idable, RESP jmap.GetResponse[OBJ], BOXES any, CHANG
|
||||
defer s.Close()
|
||||
|
||||
user := pickUser()
|
||||
session := s.Session(user.email)
|
||||
session := s.Session(user.Email)
|
||||
ctx := s.Context(session)
|
||||
|
||||
accountId := acc(session)
|
||||
@@ -1775,7 +1790,7 @@ func containerTest[OBJ jmap.Idable, RESP jmap.GetResponse[OBJ], BOXES any, CHANG
|
||||
// we first need to retrieve the list of all the Principals in order to be able to use and test sharing
|
||||
principalIds := []jmap.PrincipalId{}
|
||||
{
|
||||
result, err := s.client.GetPrincipals(accountId, []jmap.PrincipalId{}, ctx)
|
||||
result, err := s.Client.GetPrincipals(accountId, []jmap.PrincipalId{}, ctx)
|
||||
require.NoError(err)
|
||||
require.NotEmpty(result.Payload.List)
|
||||
principalIds = structs.Map(result.Payload.List, func(p jmap.Principal) jmap.PrincipalId { return jmap.PrincipalId(p.Id) })
|
||||
|
||||
+25
-1
@@ -1,6 +1,11 @@
|
||||
package jmaptest
|
||||
|
||||
import "github.com/opencloud-eu/opencloud/pkg/jmap"
|
||||
import (
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
"github.com/opencloud-eu/opencloud/pkg/jmap"
|
||||
)
|
||||
|
||||
var (
|
||||
truep = ptr(true)
|
||||
@@ -26,3 +31,22 @@ func firstKey[K comparable, V any](m map[K]V) (K, bool) {
|
||||
var zero K
|
||||
return zero, false
|
||||
}
|
||||
|
||||
var freeLocalhostPortSync = sync.Mutex{}
|
||||
|
||||
func FreeLocalhostPort() (int, error) {
|
||||
addr, err := net.ResolveTCPAddr("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
freeLocalhostPortSync.Lock()
|
||||
defer freeLocalhostPortSync.Unlock()
|
||||
l, err := net.ListenTCP("tcp", addr)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer l.Close()
|
||||
|
||||
return l.Addr().(*net.TCPAddr).Port, nil
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package groupware
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGroupwareIndex(t *testing.T) {
|
||||
require := require.New(t)
|
||||
g, err := newGroupwareTest(t)
|
||||
require.NoError(err)
|
||||
require.NotEmpty(g.Users)
|
||||
|
||||
client := &http.Client{}
|
||||
for _, u := range g.Users {
|
||||
req, err := http.NewRequest(http.MethodGet, g.BaseURL, nil)
|
||||
require.NoError(err)
|
||||
req.SetBasicAuth(u.Name, u.Password)
|
||||
resp, err := client.Do(req)
|
||||
require.NoError(err)
|
||||
require.Equal(200, resp.StatusCode)
|
||||
defer resp.Body.Close()
|
||||
index := IndexResponse{}
|
||||
err = json.NewDecoder(resp.Body).Decode(&index)
|
||||
require.NoError(err)
|
||||
require.Len(index.Accounts, 1)
|
||||
require.Equal(u.Email, index.Accounts[0].Name)
|
||||
require.Len(index.Accounts[0].Identities, 2) // email + alias
|
||||
}
|
||||
}
|
||||
@@ -179,7 +179,7 @@ func (r groupwareHttpJmapApiClientMetricsRecorder) OnFailedWsHandshakeRequestWit
|
||||
// TODO metrics for WSS
|
||||
}
|
||||
|
||||
func NewGroupware(config *config.Config, logger *log.Logger, mux *chi.Mux, prometheusRegistry prometheus.Registerer) (*Groupware, error) { //NOSONAR
|
||||
func NewGroupwareUsingJmapClient(jmapClient *jmap.Client, config *config.Config, logger *log.Logger, mux *chi.Mux, prometheusRegistry prometheus.Registerer) (*Groupware, error) { //NOSONAR
|
||||
baseUrl, err := url.Parse(config.Mail.BaseUrl)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msgf("failed to parse configured Mail.Baseurl '%v'", config.Mail.BaseUrl)
|
||||
@@ -192,11 +192,9 @@ func NewGroupware(config *config.Config, logger *log.Logger, mux *chi.Mux, prome
|
||||
maxBodyValueBytes := max(config.Mail.MaxBodyValueBytes, 0)
|
||||
sendDurationsResponse := config.HTTP.SendDurationsResponse
|
||||
defaultContactLimit := ptrIfNot(max(config.Mail.DefaultContactLimit, 0), 0)
|
||||
responseHeaderTimeout := max(config.Mail.ResponseHeaderTimeout, 0)
|
||||
sessionCacheMaxCapacity := uint64(max(config.Mail.SessionCache.MaxCapacity, 0))
|
||||
sessionCacheTtl := max(config.Mail.SessionCache.Ttl, 0)
|
||||
sessionFailureCacheTtl := max(config.Mail.SessionCache.FailureTtl, 0)
|
||||
wsHandshakeTimeout := config.Mail.PushHandshakeTimeout
|
||||
|
||||
eventChannelSize := 100 // TODO make channel queue buffering size configurable
|
||||
workerQueueSize := 100 // TODO configuration setting
|
||||
@@ -207,8 +205,6 @@ func NewGroupware(config *config.Config, logger *log.Logger, mux *chi.Mux, prome
|
||||
|
||||
useDnsForSessionResolution := false // TODO configuration setting, although still experimental, needs proper unit tests first
|
||||
|
||||
insecureTls := config.HTTP.Insecure
|
||||
|
||||
sanitize := true // TODO make configurable
|
||||
|
||||
m, err := metrics.New(prometheusRegistry, logger)
|
||||
@@ -218,79 +214,6 @@ func NewGroupware(config *config.Config, logger *log.Logger, mux *chi.Mux, prome
|
||||
|
||||
userProvider := newRevaContextUsernameProvider()
|
||||
|
||||
jmapMetricsAdapter := groupwareHttpJmapApiClientMetricsRecorder{m: m}
|
||||
|
||||
var jmapClient jmap.Client
|
||||
{
|
||||
var auth jmap.HttpJmapClientAuthenticator
|
||||
{
|
||||
masterUsername := config.Mail.Master.Username
|
||||
masterPassword := config.Mail.Master.Password
|
||||
if masterUsername != "" && masterPassword != "" {
|
||||
auth = jmap.NewMasterAuthHttpJmapClientAuthenticator(masterUsername, masterPassword)
|
||||
} else {
|
||||
auth = newRevaBearerHttpJmapClientAuthenticator()
|
||||
}
|
||||
}
|
||||
|
||||
var api *jmap.HttpJmapClient
|
||||
{
|
||||
// TODO add timeouts and other meaningful configuration settings for the HTTP client
|
||||
var httpClient http.Client
|
||||
{
|
||||
httpTransport := http.DefaultTransport.(*http.Transport).Clone()
|
||||
httpTransport.ResponseHeaderTimeout = responseHeaderTimeout
|
||||
if insecureTls {
|
||||
tlsConfig := &tls.Config{InsecureSkipVerify: true} // #nosec G402 insecure TLS is a configuration option for development
|
||||
httpTransport.TLSClientConfig = tlsConfig
|
||||
}
|
||||
httpClient = *http.DefaultClient
|
||||
httpClient.Transport = httpTransport
|
||||
}
|
||||
|
||||
api = jmap.NewHttpJmapClient(
|
||||
&httpClient,
|
||||
auth,
|
||||
jmapMetricsAdapter,
|
||||
config.HTTP.TraceRequests,
|
||||
config.HTTP.TraceMaxRequestBodySize,
|
||||
config.HTTP.TraceResponses,
|
||||
config.HTTP.TraceMaxResponseBodySize,
|
||||
)
|
||||
defer func() {
|
||||
if err := api.Close(); err != nil {
|
||||
logger.Error().Err(err).Msgf("failed to close HTTP JMAP API client")
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
var wsf *jmap.HttpWsClientFactory
|
||||
{
|
||||
wsDialer := &websocket.Dialer{
|
||||
HandshakeTimeout: wsHandshakeTimeout,
|
||||
}
|
||||
if insecureTls {
|
||||
wsDialer.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} // #nosec G402 insecure TLS is a configuration option for development
|
||||
}
|
||||
|
||||
wsf, err = jmap.NewHttpWsClientFactory(wsDialer, auth, logger, jmapMetricsAdapter,
|
||||
config.HTTP.TraceResponses, config.HTTP.TraceMaxResponseBodySize,
|
||||
)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("failed to create websocket client")
|
||||
return nil, GroupwareInitializationError{Message: "failed to create websocket client", Err: err}
|
||||
}
|
||||
}
|
||||
|
||||
// api implements all three interfaces:
|
||||
jmapClient = jmap.NewClient(api, api, api, wsf)
|
||||
defer func() {
|
||||
if err := jmapClient.Close(); err != nil {
|
||||
logger.Error().Err(err).Msgf("failed to close JMAP client")
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
sessionCacheBuilder := newSessionCacheBuilder(
|
||||
sessionUrl,
|
||||
logger,
|
||||
@@ -404,8 +327,8 @@ func NewGroupware(config *config.Config, logger *log.Logger, mux *chi.Mux, prome
|
||||
var suppliers Suppliers
|
||||
{
|
||||
impls := []Supplier[jmap.Foo]{
|
||||
newJmapAddressBookSupplier(&jmapClient),
|
||||
newJmapContactCardSupplier(&jmapClient),
|
||||
newJmapAddressBookSupplier(jmapClient),
|
||||
newJmapContactCardSupplier(jmapClient),
|
||||
}
|
||||
if config.EnableMockData {
|
||||
impls = append(impls,
|
||||
@@ -434,7 +357,7 @@ func NewGroupware(config *config.Config, logger *log.Logger, mux *chi.Mux, prome
|
||||
logger: logger,
|
||||
sessionCache: sessionCache,
|
||||
userProvider: userProvider,
|
||||
jmap: &jmapClient,
|
||||
jmap: jmapClient,
|
||||
defaults: groupwareDefaults{
|
||||
emailLimit: defaultEmailLimit,
|
||||
contactLimit: defaultContactLimit,
|
||||
@@ -470,6 +393,92 @@ func NewGroupware(config *config.Config, logger *log.Logger, mux *chi.Mux, prome
|
||||
return g, nil
|
||||
}
|
||||
|
||||
func NewGroupware(config *config.Config, logger *log.Logger, mux *chi.Mux, prometheusRegistry prometheus.Registerer) (*Groupware, error) { //NOSONAR
|
||||
responseHeaderTimeout := max(config.Mail.ResponseHeaderTimeout, 0)
|
||||
wsHandshakeTimeout := config.Mail.PushHandshakeTimeout
|
||||
insecureTls := config.HTTP.Insecure
|
||||
|
||||
m, err := metrics.New(prometheusRegistry, logger)
|
||||
if err != nil {
|
||||
return nil, GroupwareInitializationError{Message: fmt.Sprintf("failed to configure metrics: %v", err), Err: err}
|
||||
}
|
||||
|
||||
jmapMetricsAdapter := groupwareHttpJmapApiClientMetricsRecorder{m: m}
|
||||
|
||||
var jmapClient *jmap.Client
|
||||
{
|
||||
var auth jmap.HttpJmapClientAuthenticator
|
||||
{
|
||||
masterUsername := config.Mail.Master.Username
|
||||
masterPassword := config.Mail.Master.Password
|
||||
if masterUsername != "" && masterPassword != "" {
|
||||
auth = jmap.NewMasterAuthHttpJmapClientAuthenticator(masterUsername, masterPassword)
|
||||
} else {
|
||||
auth = newRevaBearerHttpJmapClientAuthenticator()
|
||||
}
|
||||
}
|
||||
|
||||
var api *jmap.HttpJmapClient
|
||||
{
|
||||
// TODO add timeouts and other meaningful configuration settings for the HTTP client
|
||||
var httpClient http.Client
|
||||
{
|
||||
httpTransport := http.DefaultTransport.(*http.Transport).Clone()
|
||||
httpTransport.ResponseHeaderTimeout = responseHeaderTimeout
|
||||
if insecureTls {
|
||||
tlsConfig := &tls.Config{InsecureSkipVerify: true} // #nosec G402 insecure TLS is a configuration option for development
|
||||
httpTransport.TLSClientConfig = tlsConfig
|
||||
}
|
||||
httpClient = *http.DefaultClient
|
||||
httpClient.Transport = httpTransport
|
||||
}
|
||||
|
||||
api = jmap.NewHttpJmapClient(
|
||||
&httpClient,
|
||||
auth,
|
||||
jmapMetricsAdapter,
|
||||
config.HTTP.TraceRequests,
|
||||
config.HTTP.TraceMaxRequestBodySize,
|
||||
config.HTTP.TraceResponses,
|
||||
config.HTTP.TraceMaxResponseBodySize,
|
||||
)
|
||||
defer func() {
|
||||
if err := api.Close(); err != nil {
|
||||
logger.Error().Err(err).Msgf("failed to close HTTP JMAP API client")
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
var wsf *jmap.HttpWsClientFactory
|
||||
{
|
||||
wsDialer := &websocket.Dialer{
|
||||
HandshakeTimeout: wsHandshakeTimeout,
|
||||
}
|
||||
if insecureTls {
|
||||
wsDialer.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} // #nosec G402 insecure TLS is a configuration option for development
|
||||
}
|
||||
|
||||
wsf, err = jmap.NewHttpWsClientFactory(wsDialer, auth, logger, jmapMetricsAdapter,
|
||||
config.HTTP.TraceResponses, config.HTTP.TraceMaxResponseBodySize,
|
||||
)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("failed to create websocket client")
|
||||
return nil, GroupwareInitializationError{Message: "failed to create websocket client", Err: err}
|
||||
}
|
||||
}
|
||||
|
||||
// api implements all three interfaces:
|
||||
jmapClient = jmap.NewClient(api, api, api, wsf)
|
||||
defer func() {
|
||||
if err := jmapClient.Close(); err != nil {
|
||||
logger.Error().Err(err).Msgf("failed to close JMAP client")
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
return NewGroupwareUsingJmapClient(jmapClient, config, logger, mux, prometheusRegistry)
|
||||
}
|
||||
|
||||
func (g *Groupware) worker(jobs <-chan Job, busy *atomic.Int32) {
|
||||
for job := range jobs {
|
||||
busy.Add(1)
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
package groupware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/opencloud-eu/opencloud/pkg/cors"
|
||||
"github.com/opencloud-eu/opencloud/pkg/jmaptest"
|
||||
opencloudmiddleware "github.com/opencloud-eu/opencloud/pkg/middleware"
|
||||
"github.com/opencloud-eu/opencloud/pkg/shared"
|
||||
"github.com/opencloud-eu/opencloud/pkg/structs"
|
||||
"github.com/opencloud-eu/opencloud/services/groupware/pkg/config"
|
||||
"github.com/opencloud-eu/opencloud/services/groupware/pkg/logging"
|
||||
groupwaremiddleware "github.com/opencloud-eu/opencloud/services/groupware/pkg/middleware"
|
||||
revactx "github.com/opencloud-eu/reva/v2/pkg/ctx"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func init() {
|
||||
chi.RegisterMethod("REPORT")
|
||||
}
|
||||
|
||||
type GroupwareTest struct {
|
||||
BaseURL string
|
||||
Users []jmaptest.User
|
||||
}
|
||||
|
||||
func newGroupwareTest(t *testing.T) (GroupwareTest, error) {
|
||||
require := require.New(t)
|
||||
s, err := jmaptest.NewStalwartTest(t)
|
||||
t.Cleanup(func() { s.Close() })
|
||||
require.NoError(err)
|
||||
|
||||
root := ""
|
||||
{
|
||||
const charset = "abcdefghijklmnopqrstuvwxyz"
|
||||
l := 4 + rand.Intn(27)
|
||||
var sb strings.Builder
|
||||
sb.Grow(l)
|
||||
for range l {
|
||||
r := rand.Intn(len(charset))
|
||||
sb.WriteByte(charset[r])
|
||||
}
|
||||
root = sb.String()
|
||||
}
|
||||
|
||||
httpPort, err := jmaptest.FreeLocalhostPort()
|
||||
require.NoError(err)
|
||||
|
||||
config := &config.Config{
|
||||
Service: config.Service{
|
||||
Name: "TestGroupware",
|
||||
},
|
||||
Log: &config.Log{
|
||||
Level: "trace",
|
||||
Pretty: true,
|
||||
Color: false,
|
||||
},
|
||||
Mail: config.Mail{
|
||||
Master: config.MailMasterAuth{
|
||||
Username: s.MasterUsername,
|
||||
Password: s.MasterPassword,
|
||||
},
|
||||
BaseUrl: s.JmapBaseUrl.String(),
|
||||
SessionCache: config.MailSessionCache{
|
||||
MaxCapacity: 1,
|
||||
},
|
||||
},
|
||||
HTTP: config.HTTP{
|
||||
Addr: fmt.Sprintf("127.0.0.1:%d", httpPort),
|
||||
Insecure: true,
|
||||
Root: "/" + root,
|
||||
TLS: shared.HTTPServiceTLS{
|
||||
Enabled: false,
|
||||
},
|
||||
Namespace: "eu.opencloud.web",
|
||||
OpenCloudPublicURL: fmt.Sprintf("http://127.0.0.1:%d", httpPort),
|
||||
SendDurationsResponse: true,
|
||||
CORS: config.CORS{
|
||||
AllowedOrigins: []string{"*"},
|
||||
AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "REPORT"},
|
||||
AllowedHeaders: []string{"Authorization", "Origin", "Content-Type", "Accept", "X-Requested-With", "X-Request-Id", "Trace-Id", "Cache-Control"},
|
||||
AllowCredentials: true,
|
||||
},
|
||||
TraceRequests: true,
|
||||
TraceMaxRequestBodySize: 8 * 1024,
|
||||
TraceResponses: true,
|
||||
TraceMaxResponseBodySize: 8 * 1024,
|
||||
},
|
||||
TokenManager: &config.TokenManager{
|
||||
JWTSecret: "some-opencloud-jwt-secret",
|
||||
},
|
||||
}
|
||||
|
||||
logger := logging.Configure("TestGroupware", config.Log)
|
||||
prom := prometheus.NewRegistry()
|
||||
m := chi.NewMux()
|
||||
|
||||
gw, err := NewGroupwareUsingJmapClient(s.Client, config, &logger, m, prom)
|
||||
require.NoError(err)
|
||||
|
||||
auth := func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
username, password, ok := r.BasicAuth()
|
||||
if !ok {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
if match, ok := structs.First(s.Users, func(u jmaptest.User) bool {
|
||||
return u.Name == username
|
||||
}); !ok {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
} else if match.Password != password {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
} else {
|
||||
u := &userv1beta1.User{
|
||||
Username: match.Email,
|
||||
Mail: match.Email,
|
||||
}
|
||||
ctx = revactx.ContextSetUser(ctx, u)
|
||||
}
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
|
||||
m.Use(
|
||||
opencloudmiddleware.Cors(
|
||||
cors.Logger(logger),
|
||||
cors.AllowedOrigins(config.HTTP.CORS.AllowedOrigins),
|
||||
cors.AllowedMethods(config.HTTP.CORS.AllowedMethods),
|
||||
cors.AllowedHeaders(config.HTTP.CORS.AllowedHeaders),
|
||||
cors.AllowCredentials(config.HTTP.CORS.AllowCredentials),
|
||||
),
|
||||
groupwaremiddleware.GroupwareLogger(logger),
|
||||
auth,
|
||||
)
|
||||
m.Route(config.HTTP.Root, gw.Route)
|
||||
|
||||
baseURL := ""
|
||||
{
|
||||
l, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", httpPort))
|
||||
require.NoError(err)
|
||||
ts := &httptest.Server{
|
||||
Listener: l,
|
||||
Config: &http.Server{
|
||||
Handler: gw,
|
||||
},
|
||||
}
|
||||
ts.Start()
|
||||
t.Cleanup(ts.Close)
|
||||
|
||||
baseURL, err = url.JoinPath(ts.URL, config.HTTP.Root)
|
||||
require.NoError(err)
|
||||
}
|
||||
|
||||
return GroupwareTest{
|
||||
BaseURL: baseURL,
|
||||
Users: s.Users,
|
||||
}, nil
|
||||
}
|
||||
@@ -25,6 +25,8 @@ func authOptions(opts ...account.Option) account.Options {
|
||||
return opt
|
||||
}
|
||||
|
||||
const RequestIdHeader = "X-Request-ID"
|
||||
|
||||
func Auth(opts ...account.Option) func(http.Handler) http.Handler {
|
||||
opt := authOptions(opts...)
|
||||
tokenManager, err := jwt.New(map[string]any{
|
||||
@@ -39,7 +41,7 @@ func Auth(opts ...account.Option) func(http.Handler) http.Handler {
|
||||
ctx := r.Context()
|
||||
t := r.Header.Get(revactx.TokenHeader)
|
||||
if t == "" {
|
||||
requestID := r.Header.Get("X-Request-ID")
|
||||
requestID := r.Header.Get(RequestIdHeader)
|
||||
traceID := GetTraceID(ctx)
|
||||
l := opt.Logger.Error().Str(log.RequestIDString, log.SafeString(requestID))
|
||||
if traceID != "" {
|
||||
@@ -52,7 +54,7 @@ func Auth(opts ...account.Option) func(http.Handler) http.Handler {
|
||||
|
||||
u, tokenScope, err := tokenManager.DismantleToken(r.Context(), t)
|
||||
if err != nil {
|
||||
requestID := r.Header.Get("X-Request-ID")
|
||||
requestID := r.Header.Get(RequestIdHeader)
|
||||
traceID := GetTraceID(ctx)
|
||||
l := opt.Logger.Error().Str(log.RequestIDString, log.SafeString(requestID))
|
||||
if traceID != "" {
|
||||
@@ -63,7 +65,7 @@ func Auth(opts ...account.Option) func(http.Handler) http.Handler {
|
||||
return
|
||||
}
|
||||
if ok, err := scope.VerifyScope(ctx, tokenScope, r); err != nil || !ok {
|
||||
requestID := r.Header.Get("X-Request-ID")
|
||||
requestID := r.Header.Get(RequestIdHeader)
|
||||
traceID := GetTraceID(ctx)
|
||||
l := opt.Logger.Error().Str(log.RequestIDString, log.SafeString(requestID))
|
||||
if traceID != "" {
|
||||
|
||||
Reference in new issue
Block a user