snapshot of https://github.com/mattermost/mattermost.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s

This commit is contained in:
upstream-mirror
2026-09-16 00:29:55 +05:00
commit 240db4e2a7
15309 changed files with 2173406 additions and 0 deletions

View File

@@ -0,0 +1,486 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"context"
"io"
"net/http"
"os"
"strings"
"testing"
"github.com/golang-jwt/jwt/v5"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost/server/public/model"
)
const (
wipeSignatureAckId = "ackid"
wipeSignatureDeviceId = "testdevice"
)
// signWipePush mints the signature a session wipe push carries for the given user, which the
// client echoes back when confirming its wipe.
func signWipePush(t *testing.T, th *TestHelper, userID string) string {
t.Helper()
signature, err := jwt.NewWithClaims(jwt.SigningMethodES256, jwt.MapClaims{
"ack_id": wipeSignatureAckId,
"device_id": wipeSignatureDeviceId,
"user_id": userID,
}).SignedString(th.App.AsymmetricSigningKey())
require.NoError(t, err)
return signature
}
// setupEphemeralModeAuditTest starts a TestHelper with a file-based audit target so tests can
// read back the audit entries the ephemeral mode endpoints log.
func setupEphemeralModeAuditTest(t *testing.T) (*TestHelper, *os.File) {
logFile, err := os.CreateTemp("", "ephemeral_mode_audit.log")
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, logFile.Close())
require.NoError(t, os.Remove(logFile.Name()))
})
th := SetupWithServerOptionsAndConfig(t, nil, func(cfg *model.Config) {
cfg.ExperimentalAuditSettings.FileEnabled = new(true)
cfg.ExperimentalAuditSettings.FileName = new(logFile.Name())
}).InitBasic(t)
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
t.Cleanup(func() { th.RemoveLicense(t) })
return th, logFile
}
func readAuditEntry(t *testing.T, th *TestHelper, logFile *os.File, eventName, userID string) *AuditEntry {
require.NoError(t, th.Server.Audit.Flush())
require.NoError(t, logFile.Sync())
data, err := io.ReadAll(logFile)
require.NoError(t, err)
entry := FindAuditEntry(string(data), eventName, userID)
require.NotNil(t, entry, "should find a %s audit entry for user %s", eventName, userID)
return entry
}
func TestLogCleanup(t *testing.T) {
mainHelper.Parallel(t)
t.Run("logs success when deleted record counts and cleanup date are all provided", func(t *testing.T) {
th, logFile := setupEphemeralModeAuditTest(t)
body := mustMarshal(t, model.CleanupReport{
PostsDeleted: model.NewPointer(int64(10)),
PlaybookRunsDeleted: model.NewPointer(int64(2)),
CleanupAt: model.NewPointer(model.GetMillis()),
})
resp, err := th.Client.DoAPIPost(context.Background(), "/ephemeral_mode/cleanup", string(body))
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, http.StatusOK, resp.StatusCode)
entry := readAuditEntry(t, th, logFile, model.AuditEventAutoCacheCleanupRun, th.BasicUser.Id)
require.Equal(t, model.AuditStatusSuccess, entry.Status)
require.EqualValues(t, 10, entry.Parameters["posts_deleted"])
require.EqualValues(t, 2, entry.Parameters["playbook_runs_deleted"])
require.Contains(t, entry.Parameters, "cleanup_at")
require.Contains(t, entry.Parameters, "server_ts")
})
t.Run("succeeds when cleanup date is omitted but deleted record counts are provided", func(t *testing.T) {
th, logFile := setupEphemeralModeAuditTest(t)
body := mustMarshal(t, model.CleanupReport{
PostsDeleted: model.NewPointer(int64(5)),
PlaybookRunsDeleted: model.NewPointer(int64(1)),
})
resp, err := th.Client.DoAPIPost(context.Background(), "/ephemeral_mode/cleanup", string(body))
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, http.StatusOK, resp.StatusCode)
entry := readAuditEntry(t, th, logFile, model.AuditEventAutoCacheCleanupRun, th.BasicUser.Id)
require.Equal(t, model.AuditStatusSuccess, entry.Status)
require.EqualValues(t, 5, entry.Parameters["posts_deleted"])
require.EqualValues(t, 1, entry.Parameters["playbook_runs_deleted"])
require.NotContains(t, entry.Parameters, "cleanup_at")
})
t.Run("accepts a failure report with no deleted record counts and records no counts for it", func(t *testing.T) {
th, logFile := setupEphemeralModeAuditTest(t)
body := mustMarshal(t, model.CleanupReport{
CleanupAt: model.NewPointer(model.GetMillis()),
ErrorReason: model.NewPointer("disk full during cleanup"),
})
resp, err := th.Client.DoAPIPost(context.Background(), "/ephemeral_mode/cleanup", string(body))
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, http.StatusOK, resp.StatusCode)
entry := readAuditEntry(t, th, logFile, model.AuditEventAutoCacheCleanupRun, th.BasicUser.Id)
require.Equal(t, model.AuditStatusFail, entry.Status)
require.NotContains(t, entry.Parameters, "posts_deleted")
require.NotContains(t, entry.Parameters, "playbook_runs_deleted")
errData, ok := entry.Raw["error"].(map[string]any)
require.True(t, ok)
require.Equal(t, "disk full during cleanup", errData["description"])
})
t.Run("logs failure without a description when the client reports an empty error reason", func(t *testing.T) {
th, logFile := setupEphemeralModeAuditTest(t)
body := mustMarshal(t, model.CleanupReport{
ErrorReason: model.NewPointer(""),
})
resp, err := th.Client.DoAPIPost(context.Background(), "/ephemeral_mode/cleanup", string(body))
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, http.StatusOK, resp.StatusCode)
entry := readAuditEntry(t, th, logFile, model.AuditEventAutoCacheCleanupRun, th.BasicUser.Id)
require.Equal(t, model.AuditStatusFail, entry.Status)
errData, ok := entry.Raw["error"].(map[string]any)
require.True(t, ok)
require.NotContains(t, errData, "description")
})
t.Run("rejects a success report that provides only the cleanup date", func(t *testing.T) {
th := Setup(t).InitBasic(t)
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
defer th.RemoveLicense(t)
body := mustMarshal(t, model.CleanupReport{
CleanupAt: model.NewPointer(model.GetMillis()),
})
resp, err := th.Client.DoAPIPost(context.Background(), "/ephemeral_mode/cleanup", string(body))
require.Error(t, err)
defer resp.Body.Close()
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
})
t.Run("rejects a success report that provides only the deleted post count", func(t *testing.T) {
th := Setup(t).InitBasic(t)
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
defer th.RemoveLicense(t)
body := mustMarshal(t, model.CleanupReport{
PostsDeleted: model.NewPointer(int64(3)),
})
resp, err := th.Client.DoAPIPost(context.Background(), "/ephemeral_mode/cleanup", string(body))
require.Error(t, err)
defer resp.Body.Close()
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
})
t.Run("rejects request when license does not support ephemeral mode", func(t *testing.T) {
th := Setup(t).InitBasic(t)
body := mustMarshal(t, model.CleanupReport{
CleanupAt: model.NewPointer(model.GetMillis()),
})
resp, err := th.Client.DoAPIPost(context.Background(), "/ephemeral_mode/cleanup", string(body))
require.Error(t, err)
defer resp.Body.Close()
require.Equal(t, http.StatusNotImplemented, resp.StatusCode)
})
}
func TestLogOfflinePurge(t *testing.T) {
mainHelper.Parallel(t)
t.Run("logs success when offline time and purge date are both provided", func(t *testing.T) {
th, logFile := setupEphemeralModeAuditTest(t)
body := mustMarshal(t, model.OfflinePurgeReport{
OfflineTimeMinutes: model.NewPointer(int64(90)),
PurgeAt: model.NewPointer(model.GetMillis()),
})
resp, err := th.Client.DoAPIPost(context.Background(), "/ephemeral_mode/purge", string(body))
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, http.StatusOK, resp.StatusCode)
entry := readAuditEntry(t, th, logFile, model.AuditEventOfflinePurge, th.BasicUser.Id)
require.Equal(t, model.AuditStatusSuccess, entry.Status)
require.EqualValues(t, 90, entry.Parameters["offline_time_minutes"])
require.Contains(t, entry.Parameters, "purge_at")
require.Contains(t, entry.Parameters, "server_ts")
})
t.Run("succeeds when purge date is omitted but offline time is provided", func(t *testing.T) {
th, logFile := setupEphemeralModeAuditTest(t)
body := mustMarshal(t, model.OfflinePurgeReport{
OfflineTimeMinutes: model.NewPointer(int64(45)),
})
resp, err := th.Client.DoAPIPost(context.Background(), "/ephemeral_mode/purge", string(body))
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, http.StatusOK, resp.StatusCode)
entry := readAuditEntry(t, th, logFile, model.AuditEventOfflinePurge, th.BasicUser.Id)
require.Equal(t, model.AuditStatusSuccess, entry.Status)
require.EqualValues(t, 45, entry.Parameters["offline_time_minutes"])
require.NotContains(t, entry.Parameters, "purge_at")
})
t.Run("logs failure with the reported reason when the client reports the offline purge failed", func(t *testing.T) {
th, logFile := setupEphemeralModeAuditTest(t)
body := mustMarshal(t, model.OfflinePurgeReport{
OfflineTimeMinutes: model.NewPointer(int64(90)),
ErrorReason: model.NewPointer("could not reach storage"),
})
resp, err := th.Client.DoAPIPost(context.Background(), "/ephemeral_mode/purge", string(body))
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, http.StatusOK, resp.StatusCode)
entry := readAuditEntry(t, th, logFile, model.AuditEventOfflinePurge, th.BasicUser.Id)
require.Equal(t, model.AuditStatusFail, entry.Status)
errData, ok := entry.Raw["error"].(map[string]any)
require.True(t, ok)
require.Equal(t, "could not reach storage", errData["description"])
})
t.Run("accepts a failure report with no offline time and records no offline time for it", func(t *testing.T) {
th, logFile := setupEphemeralModeAuditTest(t)
body := mustMarshal(t, model.OfflinePurgeReport{
ErrorReason: model.NewPointer("could not read the last connected timestamp"),
})
resp, err := th.Client.DoAPIPost(context.Background(), "/ephemeral_mode/purge", string(body))
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, http.StatusOK, resp.StatusCode)
entry := readAuditEntry(t, th, logFile, model.AuditEventOfflinePurge, th.BasicUser.Id)
require.Equal(t, model.AuditStatusFail, entry.Status)
require.NotContains(t, entry.Parameters, "offline_time_minutes")
errData, ok := entry.Raw["error"].(map[string]any)
require.True(t, ok)
require.Equal(t, "could not read the last connected timestamp", errData["description"])
})
t.Run("rejects request missing offline time even when purge date is provided", func(t *testing.T) {
th := Setup(t).InitBasic(t)
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
defer th.RemoveLicense(t)
body := mustMarshal(t, model.OfflinePurgeReport{
PurgeAt: model.NewPointer(model.GetMillis()),
})
resp, err := th.Client.DoAPIPost(context.Background(), "/ephemeral_mode/purge", string(body))
require.Error(t, err)
defer resp.Body.Close()
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
})
t.Run("rejects request missing offline time and purge date", func(t *testing.T) {
th := Setup(t).InitBasic(t)
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
defer th.RemoveLicense(t)
body := mustMarshal(t, model.OfflinePurgeReport{})
resp, err := th.Client.DoAPIPost(context.Background(), "/ephemeral_mode/purge", string(body))
require.Error(t, err)
defer resp.Body.Close()
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
})
t.Run("rejects request when license does not support ephemeral mode", func(t *testing.T) {
th := Setup(t).InitBasic(t)
body := mustMarshal(t, model.OfflinePurgeReport{
OfflineTimeMinutes: model.NewPointer(int64(45)),
})
resp, err := th.Client.DoAPIPost(context.Background(), "/ephemeral_mode/purge", string(body))
require.Error(t, err)
defer resp.Body.Close()
require.Equal(t, http.StatusNotImplemented, resp.StatusCode)
})
}
func TestLogSessionWipe(t *testing.T) {
mainHelper.Parallel(t)
// The wipe confirmation is sent by a client whose session was already revoked server-side,
// so it must succeed without any authentication. The signature the wipe push carried is
// what attributes the record.
t.Run("logs success without a session and takes the actor from the signature", func(t *testing.T) {
th, logFile := setupEphemeralModeAuditTest(t)
client := th.CreateClient()
body := mustMarshal(t, model.SessionWipeReport{
Signature: signWipePush(t, th, th.BasicUser.Id),
WipeAt: model.NewPointer(model.GetMillis()),
})
resp, err := client.DoAPIPost(context.Background(), "/ephemeral_mode/wipe", string(body))
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, http.StatusOK, resp.StatusCode)
entry := readAuditEntry(t, th, logFile, model.AuditEventSessionWipe, th.BasicUser.Id)
require.Equal(t, model.AuditStatusSuccess, entry.Status)
require.Equal(t, model.RedactDeviceId(wipeSignatureDeviceId), entry.Parameters["device_id"])
require.Equal(t, wipeSignatureAckId, entry.Parameters["ack_id"])
require.Contains(t, entry.Parameters, "wipe_at")
require.Contains(t, entry.Parameters, "server_ts")
})
t.Run("succeeds when the wipe date is omitted", func(t *testing.T) {
th, logFile := setupEphemeralModeAuditTest(t)
client := th.CreateClient()
body := mustMarshal(t, model.SessionWipeReport{
Signature: signWipePush(t, th, th.BasicUser.Id),
})
resp, err := client.DoAPIPost(context.Background(), "/ephemeral_mode/wipe", string(body))
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, http.StatusOK, resp.StatusCode)
entry := readAuditEntry(t, th, logFile, model.AuditEventSessionWipe, th.BasicUser.Id)
require.Equal(t, model.AuditStatusSuccess, entry.Status)
require.NotContains(t, entry.Parameters, "wipe_at")
})
t.Run("logs failure with the reported reason when the client reports the session wipe failed", func(t *testing.T) {
th, logFile := setupEphemeralModeAuditTest(t)
client := th.CreateClient()
body := mustMarshal(t, model.SessionWipeReport{
Signature: signWipePush(t, th, th.BasicUser.Id),
ErrorReason: model.NewPointer("local storage wipe threw an exception"),
})
resp, err := client.DoAPIPost(context.Background(), "/ephemeral_mode/wipe", string(body))
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, http.StatusOK, resp.StatusCode)
entry := readAuditEntry(t, th, logFile, model.AuditEventSessionWipe, th.BasicUser.Id)
require.Equal(t, model.AuditStatusFail, entry.Status)
errData, ok := entry.Raw["error"].(map[string]any)
require.True(t, ok)
require.Equal(t, "local storage wipe threw an exception", errData["description"])
})
// An error reason must not waive the signature requirement: this endpoint is
// unauthenticated, so that would let anyone append unattributable audit records.
t.Run("rejects a failure report that omits the signature", func(t *testing.T) {
th := Setup(t).InitBasic(t)
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
defer th.RemoveLicense(t)
client := th.CreateClient()
body := mustMarshal(t, model.SessionWipeReport{
ErrorReason: model.NewPointer("local storage was already unreadable"),
})
resp, err := client.DoAPIPost(context.Background(), "/ephemeral_mode/wipe", string(body))
require.Error(t, err)
defer resp.Body.Close()
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
})
// Rejecting the request is not enough on an unauthenticated endpoint: without a verified
// signature there is no actor, so the attempt must leave no sessionWipe record behind at all.
t.Run("rejects a report whose signature does not verify and records nothing for it", func(t *testing.T) {
th, logFile := setupEphemeralModeAuditTest(t)
client := th.CreateClient()
body := mustMarshal(t, model.SessionWipeReport{
Signature: signWipePush(t, th, th.BasicUser.Id) + "tampered",
})
resp, err := client.DoAPIPost(context.Background(), "/ephemeral_mode/wipe", string(body))
require.Error(t, err)
defer resp.Body.Close()
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
require.NoError(t, th.Server.Audit.Flush())
data, err := io.ReadAll(logFile)
require.NoError(t, err)
require.Nil(t, FindAuditEntry(string(data), model.AuditEventSessionWipe, ""))
})
t.Run("rejects request when license does not support ephemeral mode", func(t *testing.T) {
th := Setup(t).InitBasic(t)
client := th.CreateClient()
body := mustMarshal(t, model.SessionWipeReport{
Signature: signWipePush(t, th, th.BasicUser.Id),
})
resp, err := client.DoAPIPost(context.Background(), "/ephemeral_mode/wipe", string(body))
require.Error(t, err)
defer resp.Body.Close()
require.Equal(t, http.StatusNotImplemented, resp.StatusCode)
})
}
// An over-long reason is truncated rather than rejected, so a client that reports a failure
// verbosely still gets its audit record. The ellipsis keeps a cut reason distinguishable from
// a complete one when the record is read back as evidence.
func TestEphemeralModeTruncatesLongErrorReasons(t *testing.T) {
mainHelper.Parallel(t)
longReason := strings.Repeat("test", errorReasonMaxRunes)
testCases := []struct {
name string
path string
event string
client func(th *TestHelper) *model.Client4
body func(t *testing.T, th *TestHelper) any
}{
{
name: "cleanup",
path: "/ephemeral_mode/cleanup",
event: model.AuditEventAutoCacheCleanupRun,
client: func(th *TestHelper) *model.Client4 { return th.Client },
body: func(_ *testing.T, _ *TestHelper) any {
return model.CleanupReport{ErrorReason: model.NewPointer(longReason)}
},
},
{
name: "offline purge",
path: "/ephemeral_mode/purge",
event: model.AuditEventOfflinePurge,
client: func(th *TestHelper) *model.Client4 { return th.Client },
body: func(_ *testing.T, _ *TestHelper) any {
return model.OfflinePurgeReport{ErrorReason: model.NewPointer(longReason)}
},
},
{
name: "session wipe",
path: "/ephemeral_mode/wipe",
event: model.AuditEventSessionWipe,
client: func(th *TestHelper) *model.Client4 { return th.CreateClient() },
body: func(t *testing.T, th *TestHelper) any {
return model.SessionWipeReport{Signature: signWipePush(t, th, th.BasicUser.Id), ErrorReason: model.NewPointer(longReason)}
},
},
}
for _, tc := range testCases {
t.Run(tc.name+" records the error reason cut to the maximum length and marked as cut", func(t *testing.T) {
th, logFile := setupEphemeralModeAuditTest(t)
body := mustMarshal(t, tc.body(t, th))
resp, err := tc.client(th).DoAPIPost(context.Background(), tc.path, string(body))
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, http.StatusOK, resp.StatusCode)
entry := readAuditEntry(t, th, logFile, tc.event, th.BasicUser.Id)
errData, ok := entry.Raw["error"].(map[string]any)
require.True(t, ok)
require.Equal(t, longReason[:errorReasonMaxRunes-3]+"...", errData["description"])
})
}
}