From ca8d1a8c2ad6f8285f26779c7ae8575d85a2f71e Mon Sep 17 00:00:00 2001
From: Pascal Bleser
Date: Wed, 8 Jul 2026 16:09:53 +0200
Subject: [PATCH] groupware: add test for vacation responses, and fix issue
found during testing
---
pkg/jmap/api_vacation.go | 4 +-
pkg/jmap/model.go | 4 +-
pkg/jmap/model_examples.go | 4 +-
pkg/jmap/tools.go | 64 +++++++++----------
.../groupware/pkg/groupware/api_index_test.go | 10 +--
.../pkg/groupware/api_vacation_test.go | 44 +++++++++++++
.../groupware/groupware_integration_test.go | 31 ++++++++-
services/groupware/pkg/groupware/templates.go | 15 +++--
8 files changed, 124 insertions(+), 52 deletions(-)
create mode 100644 services/groupware/pkg/groupware/api_vacation_test.go
diff --git a/pkg/jmap/api_vacation.go b/pkg/jmap/api_vacation.go
index 792fa67704..f80a4ec41a 100644
--- a/pkg/jmap/api_vacation.go
+++ b/pkg/jmap/api_vacation.go
@@ -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"`
diff --git a/pkg/jmap/model.go b/pkg/jmap/model.go
index 61fd76003d..9f28950841 100644
--- a/pkg/jmap/model.go
+++ b/pkg/jmap/model.go
@@ -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.
diff --git a/pkg/jmap/model_examples.go b/pkg/jmap/model_examples.go
index 6fe57415c3..4065ccad7d 100644
--- a/pkg/jmap/model_examples.go
+++ b/pkg/jmap/model_examples.go
@@ -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.",
}
diff --git a/pkg/jmap/tools.go b/pkg/jmap/tools.go
index e929e719ae..720e6f3285 100644
--- a/pkg/jmap/tools.go
+++ b/pkg/jmap/tools.go
@@ -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) {
diff --git a/services/groupware/pkg/groupware/api_index_test.go b/services/groupware/pkg/groupware/api_index_test.go
index 33d4e3118b..f5c4529a23 100644
--- a/services/groupware/pkg/groupware/api_index_test.go
+++ b/services/groupware/pkg/groupware/api_index_test.go
@@ -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)
}
diff --git a/services/groupware/pkg/groupware/api_vacation_test.go b/services/groupware/pkg/groupware/api_vacation_test.go
new file mode 100644
index 0000000000..96ec2fe019
--- /dev/null
+++ b/services/groupware/pkg/groupware/api_vacation_test.go
@@ -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)
+ }
+}
diff --git a/services/groupware/pkg/groupware/groupware_integration_test.go b/services/groupware/pkg/groupware/groupware_integration_test.go
index 551a773fef..037a004825 100644
--- a/services/groupware/pkg/groupware/groupware_integration_test.go
+++ b/services/groupware/pkg/groupware/groupware_integration_test.go
@@ -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)
diff --git a/services/groupware/pkg/groupware/templates.go b/services/groupware/pkg/groupware/templates.go
index 76e82b8148..dd36d41bf2 100644
--- a/services/groupware/pkg/groupware/templates.go
+++ b/services/groupware/pkg/groupware/templates.go
@@ -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)
}