Files
opencloud/pkg/jmaptest/integration_ws_test.go
Pascal Bleser 6c57f37b3b 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...)
2026-07-09 14:30:14 +02:00

244 lines
7.2 KiB
Go

package jmaptest
import (
"slices"
"sync"
"sync/atomic"
"testing"
"time"
. "github.com/opencloud-eu/opencloud/pkg/jmap"
"github.com/opencloud-eu/opencloud/pkg/log"
"github.com/opencloud-eu/opencloud/pkg/structs"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type testWsPushListener struct {
t *testing.T
logger *log.Logger
username string
mailAccountId AccountId
calls atomic.Uint32
m sync.Mutex
emailStates []string
threadStates []string
mailboxStates []string
}
func (l *testWsPushListener) OnNotification(username string, pushState StateChange) {
assert.Equal(l.t, l.username, username)
l.calls.Add(1)
// pushState is currently not supported by Stalwart, let's use the object states instead
l.logger.Debug().Msgf("received %T: %v", pushState, pushState)
if changed, ok := pushState.Changed[l.mailAccountId]; ok {
l.m.Lock()
if st, ok := changed[EmailName]; ok {
l.emailStates = append(l.emailStates, st)
}
if st, ok := changed[ThreadName]; ok {
l.threadStates = append(l.threadStates, st)
}
if st, ok := changed[MailboxName]; ok {
l.mailboxStates = append(l.mailboxStates, st)
}
l.m.Unlock()
unsupportedKeys := structs.Filter(structs.Keys(changed), func(o ObjectTypeName) bool { return o != EmailName && o != ThreadName && o != MailboxName })
assert.Empty(l.t, unsupportedKeys)
}
unsupportedAccounts := structs.Filter(structs.Keys(pushState.Changed), func(s AccountId) bool { return s != l.mailAccountId })
assert.Empty(l.t, unsupportedAccounts)
}
var _ WsPushListener = &testWsPushListener{}
func TestWs(t *testing.T) {
if Skip(t) {
return
}
assert.NoError(t, nil)
require := require.New(t)
cotx := t.Context()
s, err := NewStalwartTest(t)
require.NoError(err)
defer s.Close()
user := pickUser()
session := s.Session(user.Email)
ctx := s.Context(session)
mailAccountId := session.PrimaryAccounts.Mail
inboxFolder := ""
{
_, inboxFolder = s.findInbox(t, mailAccountId, ctx)
}
l := &testWsPushListener{t: t, username: user.Email, logger: s.logger, mailAccountId: mailAccountId}
s.Client.AddWsPushListener(l)
require.Equal(uint32(0), l.calls.Load())
{
l.m.Lock()
require.Len(l.emailStates, 0)
require.Len(l.mailboxStates, 0)
require.Len(l.threadStates, 0)
l.m.Unlock()
}
var initialState State
{
result, err := s.Client.GetEmailChanges(mailAccountId, EmptyState, true, 0, 0, ctx)
require.NoError(err)
require.Equal(session.State, result.GetSessionState())
require.NotEmpty(result.GetState())
//fmt.Printf("\x1b[45;1;4mChanges [%s]:\x1b[0m\n", state)
//for _, c := range changes.Created { fmt.Printf("%s %s\n", c.Id, c.Subject) }
initialState = result.GetState()
require.Empty(result.Payload.Created)
require.Empty(result.Payload.Destroyed)
require.Empty(result.Payload.Updated)
}
require.NotEmpty(initialState)
{
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())
require.Equal(initialState, result.Payload.NewState)
require.Empty(result.Payload.Created)
require.Empty(result.Payload.Destroyed)
require.Empty(result.Payload.Updated)
}
wsc, err := s.Client.EnablePushNotifications(cotx, initialState, func() (*Session, error) { return session, nil })
require.NoError(err)
defer wsc.Close()
require.Equal(uint32(0), l.calls.Load())
{
l.m.Lock()
require.Len(l.emailStates, 0)
require.Len(l.mailboxStates, 0)
require.Len(l.threadStates, 0)
l.m.Unlock()
}
emailIds := []string{}
{
_, n, err := s.fillEmailsWithImap(inboxFolder, 1, false, user)
require.NoError(err)
require.Equal(1, n)
}
require.Eventually(func() bool {
return l.calls.Load() == uint32(1)
}, 3*time.Second, 200*time.Millisecond, "WS push listener was not called after first email state change")
{
l.m.Lock()
require.Len(l.emailStates, 1)
require.Len(l.mailboxStates, 1)
require.Len(l.threadStates, 1)
l.m.Unlock()
}
var lastState State
{
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())
require.NotEqual(initialState, result.Payload.NewState)
require.Equal(result.GetState(), result.Payload.NewState)
require.Len(result.Payload.Created, 1)
require.Empty(result.Payload.Destroyed)
require.Empty(result.Payload.Updated)
lastState = result.GetState()
emailIds = append(emailIds, structs.Map(result.Payload.Created, func(e Email) string { return e.Id })...)
}
{
_, n, err := s.fillEmailsWithImap(inboxFolder, 1, false, user)
require.NoError(err)
require.Equal(1, n)
}
require.Eventually(func() bool {
return l.calls.Load() == uint32(2)
}, 3*time.Second, 200*time.Millisecond, "WS push listener was not called after second email state change")
{
l.m.Lock()
require.Len(l.emailStates, 2)
require.Len(l.mailboxStates, 2)
require.Len(l.threadStates, 2)
assert.NotEqual(t, l.emailStates[0], l.emailStates[1])
assert.NotEqual(t, l.mailboxStates[0], l.mailboxStates[1])
assert.NotEqual(t, l.threadStates[0], l.threadStates[1])
l.m.Unlock()
}
{
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())
require.NotEqual(lastState, result.Payload.NewState)
require.Equal(result.GetState(), result.Payload.NewState)
require.Len(result.Payload.Created, 1)
require.Empty(result.Payload.Destroyed)
require.Empty(result.Payload.Updated)
lastState = result.GetState()
emailIds = append(emailIds, structs.Map(result.Payload.Created, func(e Email) string { return e.Id })...)
}
{
_, n, err := s.fillEmailsWithImap(inboxFolder, 0, true, user)
require.NoError(err)
require.Equal(0, n)
}
require.Eventually(func() bool {
return l.calls.Load() == uint32(3)
}, 3*time.Second, 200*time.Millisecond, "WS push listener was not called after third email state change")
{
l.m.Lock()
require.Len(l.emailStates, 3)
require.Len(l.mailboxStates, 3)
require.Len(l.threadStates, 3)
assert.NotEqual(t, l.emailStates[1], l.emailStates[2])
assert.NotEqual(t, l.mailboxStates[1], l.mailboxStates[2])
assert.NotEqual(t, l.threadStates[1], l.threadStates[2])
l.m.Unlock()
}
{
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())
require.NotEqual(lastState, result.Payload.NewState)
require.Equal(result.GetState(), result.Payload.NewState)
require.Empty(result.Payload.Created)
require.Len(result.Payload.Destroyed, 2)
{
a := make([]string, len(emailIds))
copy(a, emailIds)
slices.Sort(emailIds)
b := make([]string, len(result.Payload.Destroyed))
copy(b, result.Payload.Destroyed)
slices.Sort(b)
require.EqualValues(a, b)
}
require.Empty(result.Payload.Updated)
lastState = result.GetState()
}
err = wsc.DisableNotifications()
require.NoError(err)
}