groupware: add test for vacation responses, and fix issue found during testing

This commit is contained in:
Pascal Bleser
2026-07-08 16:09:53 +02:00
parent d811068af7
commit ca8d1a8c2a
8 changed files with 124 additions and 52 deletions

View File

@@ -29,10 +29,10 @@ type VacationResponseChange struct {
IsEnabled bool `json:"isEnabled"`
// If "isEnabled" is true, messages that arrive on or after this date-time (but before the "toDate" if defined) should receive the
// user's vacation response. If null, the vacation response is effective immediately.
FromDate time.Time `json:"fromDate,omitzero"`
FromDate *time.Time `json:"fromDate,omitzero"`
// If "isEnabled" is true, messages that arrive before this date-time but on or after the "fromDate" if defined) should receive the
// user's vacation response. If null, the vacation response is effective indefinitely.
ToDate time.Time `json:"toDate,omitzero"`
ToDate *time.Time `json:"toDate,omitzero"`
// The subject that will be used by the message sent in response to messages when the vacation response is enabled.
// If null, an appropriate subject SHOULD be set by the server.
Subject string `json:"subject,omitempty"`

View File

@@ -4163,11 +4163,11 @@ type VacationResponse struct {
// If "isEnabled" is true, messages that arrive on or after this date-time (but before the "toDate" if defined) should receive the
// user's vacation response. If null, the vacation response is effective immediately.
FromDate time.Time `json:"fromDate,omitzero"`
FromDate *time.Time `json:"fromDate,omitzero"`
// If "isEnabled" is true, messages that arrive before this date-time but on or after the "fromDate" if defined) should receive the
// user's vacation response. If null, the vacation response is effective indefinitely.
ToDate time.Time `json:"toDate,omitzero"`
ToDate *time.Time `json:"toDate,omitzero"`
// The subject that will be used by the message sent in response to messages when the vacation response is enabled.
// If null, an appropriate subject SHOULD be set by the server.

View File

@@ -883,8 +883,8 @@ func (e Exemplar) VacationResponse() VacationResponse {
return VacationResponse{
Id: "aefee7ae",
IsEnabled: true,
FromDate: from,
ToDate: to,
FromDate: &from,
ToDate: &to,
Subject: "On PTO",
TextBody: "I am currently on PTO, please contact info@example.com for any urgent matters.",
}

View File

@@ -451,38 +451,6 @@ func ptr[T any | int | uint | bool | string](t T) *T {
return &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
}
for _, value := range values {
fmt.Fprintf(&logBuilder, "%s: %s\n", key, value)
}
}
truncated := false
if maxBodySize >= 0 && req.Body != nil && req.Body != http.NoBody {
peekBuffer := new(bytes.Buffer)
limitedReader := io.LimitReader(req.Body, maxBodySize)
tee := io.TeeReader(limitedReader, peekBuffer)
if _, err := io.Copy(io.Discard, tee); err != nil {
return fmt.Errorf("failed to peek at the request body for tracing: %v", err)
} else {
logBuilder.Write(peekBuffer.Bytes())
if int64(peekBuffer.Len()) >= maxBodySize {
truncated = true
}
fullBodyReader := io.MultiReader(peekBuffer, req.Body)
req.Body = io.NopCloser(fullBodyReader)
}
}
closure(req.Method, uri, logBuilder.String(), truncated)
return nil
}
type nullReader struct {
}
@@ -543,6 +511,38 @@ func peekResponse(body io.ReadCloser, size int64,
}, peek, truncated, nil
}
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
}
for _, value := range values {
fmt.Fprintf(&logBuilder, "%s: %s\n", key, value)
}
}
truncated := false
if maxBodySize >= 0 && req.Body != nil && req.Body != http.NoBody {
peekBuffer := new(bytes.Buffer)
limitedReader := io.LimitReader(req.Body, maxBodySize)
tee := io.TeeReader(limitedReader, peekBuffer)
if _, err := io.Copy(io.Discard, tee); err != nil {
return fmt.Errorf("failed to peek at the request body for tracing: %v", err)
} else {
logBuilder.Write(peekBuffer.Bytes())
if int64(peekBuffer.Len()) >= maxBodySize {
truncated = true
}
fullBodyReader := io.MultiReader(peekBuffer, req.Body)
req.Body = io.NopCloser(fullBodyReader)
}
}
closure(req.Method, uri, logBuilder.String(), truncated)
return nil
}
func dumpHttpResponse(req *http.Request, resp *http.Response, body io.ReadCloser, maxBodySize int64,
closure func(method string, uri string, content string, truncated bool),
closeErrorClosure func(err error)) (io.ReadCloser, error) {

View File

@@ -14,14 +14,10 @@ func TestGroupwareIndex(t *testing.T) {
require.NoError(err)
index := IndexResponse{}
u := gget(g, "/", &index)
u := gget("get-index", g, "/", &index)
require.Len(index.Accounts, 1)
require.Equal(u.Email, index.Accounts[0].Name)
require.Len(index.Accounts[0].Identities, 2) // email + alias
require.Len(structs.Filter(index.Accounts[0].Identities, func(i jmap.Identity) bool {
return i.Email == u.Email
}), 1)
require.Len(structs.Filter(index.Accounts[0].Identities, func(i jmap.Identity) bool {
return i.Email == u.Alias
}), 1)
require.Len(structs.Filter(index.Accounts[0].Identities, func(i jmap.Identity) bool { return i.Email == u.Email }), 1)
require.Len(structs.Filter(index.Accounts[0].Identities, func(i jmap.Identity) bool { return i.Email == u.Alias }), 1)
}

View File

@@ -0,0 +1,44 @@
package groupware
import (
"testing"
"github.com/opencloud-eu/opencloud/pkg/jmap"
"github.com/stretchr/testify/require"
)
func TestGroupwareVacation(t *testing.T) {
require := require.New(t)
g, err := newGroupwareTest(t)
require.NoError(err)
{
resp := jmap.VacationResponse{}
gget("get-initial-vacation", g, "/accounts/_/vacation", &resp)
require.Equal("singleton", resp.Id)
require.False(resp.IsEnabled)
require.Empty(resp.Subject)
require.Empty(resp.TextBody)
require.Empty(resp.HtmlBody)
require.Nil(resp.FromDate)
require.Nil(resp.ToDate)
}
{
resp := jmap.VacationResponse{}
req := jmap.VacationResponseChange{
IsEnabled: true,
Subject: "testing",
TextBody: "text",
HtmlBody: "html",
}
gput("set-vacation", g, "/accounts/_/vacation", req, &resp)
require.Equal("singleton", resp.Id)
require.True(resp.IsEnabled)
require.Equal("testing", resp.Subject)
require.Equal("text", resp.TextBody)
require.Equal("html", resp.HtmlBody)
require.Nil(resp.FromDate)
require.Nil(resp.ToDate)
}
}

View File

@@ -1,6 +1,7 @@
package groupware
import (
"bytes"
"encoding/json"
"fmt"
"math/rand"
@@ -38,7 +39,9 @@ type GroupwareTest struct {
Users []jmaptest.User
}
func gget[T any](g GroupwareTest, path string, result *T) jmaptest.User {
func gget[T any](id string, g GroupwareTest, path string, result *T) jmaptest.User {
id = g.t.Name() + "/" + id
u, err := url.JoinPath(g.BaseURL, path)
require.NoError(g.t, err)
req, err := http.NewRequest(http.MethodGet, u, nil)
@@ -58,6 +61,32 @@ func gget[T any](g GroupwareTest, path string, result *T) jmaptest.User {
return user
}
func gput[B any, T any](id string, g GroupwareTest, path string, body B, result *T) jmaptest.User {
id = g.t.Name() + "/" + id
u, err := url.JoinPath(g.BaseURL, path)
require.NoError(g.t, err)
jsonBody, err := json.Marshal(body)
require.NoError(g.t, err)
req, err := http.NewRequest(http.MethodPut, u, bytes.NewBuffer(jsonBody))
require.NoError(g.t, err)
user := g.Users[rand.Intn(len(g.Users))] // pick a user at random
req.SetBasicAuth(user.Name, user.Password)
rid := uuid.New().String()
req.Header.Add("X-Request-Id", id)
req.Header.Add("Trace-Id", rid)
client := http.Client{}
resp, err := client.Do(req)
require.NoError(g.t, err)
require.Equal(g.t, 200, resp.StatusCode)
defer resp.Body.Close()
err = json.NewDecoder(resp.Body).Decode(result)
require.NoError(g.t, err)
return user
}
func newGroupwareTest(t *testing.T) (GroupwareTest, error) {
require := require.New(t)
s, err := jmaptest.NewStalwartTest(t)

View File

@@ -661,19 +661,22 @@ func modify[T jmap.Foo, CHANGE jmap.Change[T], CHANGES jmap.Changes[T]](
return resp
}
l := req.logger.With().Str(logAccountId, log.SafeString(accountId))
id, err := req.PathParamDoc(o.uriParamName, "The unique identifier of the object to modify")
if err != nil {
return req.errorV(accountId, err)
id := ""
if o.uriParamName != "" {
value, err := req.PathParamDoc(o.uriParamName, "The unique identifier of the object to modify")
if err != nil {
return req.errorV(accountId, err)
}
l.Str(o.uriParamName, log.SafeString(value))
id = value
}
l.Str(o.uriParamName, log.SafeString(id))
if notok, resp := req.unsupportedQueryParams(single(accountId), noSupportedQueryParams); notok {
return resp
}
var change CHANGE
err = req.body(&change)
if err != nil {
if err := req.body(&change); err != nil {
return req.errorV(accountId, err)
}