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) }