From 85dcb8b9e7e89977584922709b74e597139d5f23 Mon Sep 17 00:00:00 2001 From: Doug Lauder Date: Wed, 25 Mar 2026 11:53:56 -0400 Subject: [PATCH] MM-67944: Add shared channel integration test tool (#35639) * Add shared channel integration test tool (MM-67944) Add tools/sharedchannel-test, a standalone Go tool that validates shared channel synchronization between two real Mattermost Enterprise instances. The tool builds and manages two server processes with separate databases, establishes a remote cluster connection, and runs integration tests for membership, post, and reaction sync. Test coverage: - Membership: add, remove, re-add, bulk remove - Posts: create, edit, delete - Reactions: add, remove Uses mlog with dual console targets (stdout for info, stderr for errors) and exits non-zero on failure for CI integration. --- .gitignore | 1 + tools/README.md | 22 ++ tools/sharedchannel-test/go.mod | 54 ++++ tools/sharedchannel-test/go.sum | 298 +++++++++++++++++++ tools/sharedchannel-test/main.go | 117 ++++++++ tools/sharedchannel-test/runner.go | 302 ++++++++++++++++++++ tools/sharedchannel-test/server.go | 255 +++++++++++++++++ tools/sharedchannel-test/test_membership.go | 129 +++++++++ tools/sharedchannel-test/test_posts.go | 113 ++++++++ tools/sharedchannel-test/test_reactions.go | 109 +++++++ 10 files changed, 1400 insertions(+) create mode 100644 tools/sharedchannel-test/go.mod create mode 100644 tools/sharedchannel-test/go.sum create mode 100644 tools/sharedchannel-test/main.go create mode 100644 tools/sharedchannel-test/runner.go create mode 100644 tools/sharedchannel-test/server.go create mode 100644 tools/sharedchannel-test/test_membership.go create mode 100644 tools/sharedchannel-test/test_posts.go create mode 100644 tools/sharedchannel-test/test_reactions.go diff --git a/.gitignore b/.gitignore index 8ed1d5c54e1..a5717922566 100644 --- a/.gitignore +++ b/.gitignore @@ -64,6 +64,7 @@ go.work.sum .npminstall .yarninstall /prepackaged_plugins +tools/sharedchannel-test/sharedchannel-test # Compiled Object files, Static and Dynamic libs (Shared Objects) *.o diff --git a/tools/README.md b/tools/README.md index 7b85a7fe287..ffdfa2b9a77 100644 --- a/tools/README.md +++ b/tools/README.md @@ -5,6 +5,7 @@ This directory aims to provide a set of tools that simplify and enhance various ## Included tools * **mmgotool**: is a CLI to help with i18n related checks for the mattermost/server development. +* **sharedchannel-test**: integration test tool that validates shared channel synchronization (posts, reactions, membership) between two real Mattermost server instances. ## Installation & Usage @@ -20,3 +21,24 @@ Make sure you have the necessary prerequisites such as [Go](https://go.dev/) com * `check-empty-src`: Check for empty translation source strings * `clean-empty`: Clean empty translations * `extract`: Extract translations + +### sharedchannel-test + +Stands up two Mattermost Enterprise instances, creates a remote cluster connection, and runs integration tests for shared channel synchronization (membership, posts, reactions). + +**Prerequisites:** +- `make start-docker` (Postgres and friends running) +- Enterprise repo present at `../../enterprise` +- An enterprise license file + +**Usage:** + +```bash +# Managed mode (builds server, starts/stops both instances automatically) +cd tools/sharedchannel-test +go run . --license /path/to/license.mattermost-license --server-dir ../../server + +# External mode (connect to already-running instances) +go run . --license /path/to/license.mattermost-license --manage=false \ + --server-a http://localhost:9065 --server-b http://localhost:9066 +``` diff --git a/tools/sharedchannel-test/go.mod b/tools/sharedchannel-test/go.mod new file mode 100644 index 00000000000..473d29c5fcd --- /dev/null +++ b/tools/sharedchannel-test/go.mod @@ -0,0 +1,54 @@ +module github.com/mattermost/mattermost/tools/sharedchannel-test + +go 1.24.13 + +require github.com/mattermost/mattermost/server/public v0.1.12 + +require ( + github.com/blang/semver/v4 v4.0.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/dyatlov/go-opengraph/opengraph v0.0.0-20220524092352-606d7b1e5f8a // indirect + github.com/fatih/color v1.18.0 // indirect + github.com/francoispqt/gojay v1.2.13 // indirect + github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect + github.com/goccy/go-yaml v1.18.0 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-hclog v1.6.3 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-plugin v1.7.0 // indirect + github.com/hashicorp/yamux v0.1.2 // indirect + github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404 // indirect + github.com/mattermost/ldap v0.0.0-20231116144001-0f480c025956 // indirect + github.com/mattermost/logr/v2 v2.0.22 // indirect + github.com/mattermost/mattermost/server/v8 v8.0.0-20251014075701-833e0125320d // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/oklog/run v1.2.0 // indirect + github.com/pborman/uuid v1.2.1 // indirect + github.com/pelletier/go-toml v1.9.5 // indirect + github.com/philhofer/fwd v1.2.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/stretchr/testify v1.11.1 // indirect + github.com/tinylib/msgp v1.4.0 // indirect + github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect + github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect + github.com/wiggin77/merror v1.0.5 // indirect + github.com/wiggin77/srslog v1.0.1 // indirect + golang.org/x/crypto v0.43.0 // indirect + golang.org/x/mod v0.29.0 // indirect + golang.org/x/net v0.46.0 // indirect + golang.org/x/sys v0.37.0 // indirect + golang.org/x/text v0.30.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251007200510-49b9836ed3ff // indirect + google.golang.org/grpc v1.76.0 // indirect + google.golang.org/protobuf v1.36.10 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +replace github.com/mattermost/mattermost/server/public => ../../server/public diff --git a/tools/sharedchannel-test/go.sum b/tools/sharedchannel-test/go.sum new file mode 100644 index 00000000000..2a95d2dfed5 --- /dev/null +++ b/tools/sharedchannel-test/go.sum @@ -0,0 +1,298 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo= +dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU= +dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU= +dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4= +dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= +git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= +github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw= +github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= +github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dyatlov/go-opengraph/opengraph v0.0.0-20220524092352-606d7b1e5f8a h1:etIrTD8BQqzColk9nKRusM9um5+1q0iOEJLqfBMIK64= +github.com/dyatlov/go-opengraph/opengraph v0.0.0-20220524092352-606d7b1e5f8a/go.mod h1:emQhSYTXqB0xxjLITTw4EaWZ+8IIQYw+kx9GqNUKdLg= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= +github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= +github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= +github.com/go-asn1-ber/asn1-ber v1.5.7 h1:DTX+lbVTWaTw1hQ+PbZPlnDZPEIs0SS/GCZAl535dDk= +github.com/go-asn1-ber/asn1-ber v1.5.7/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= +github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= +github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= +github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-plugin v1.7.0 h1:YghfQH/0QmPNc/AZMTFE3ac8fipZyZECHdDPshfk+mA= +github.com/hashicorp/go-plugin v1.7.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8= +github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= +github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= +github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= +github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94= +github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= +github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404 h1:Khvh6waxG1cHc4Cz5ef9n3XVCxRWpAKUtqg9PJl5+y8= +github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404/go.mod h1:RyS7FDNQlzF1PsjbJWHRI35exqaKGSO9qD4iv8QjE34= +github.com/mattermost/ldap v0.0.0-20231116144001-0f480c025956 h1:Y1Tu/swM31pVwwb2BTCsOdamENjjWCI6qmfHLbk6OZI= +github.com/mattermost/ldap v0.0.0-20231116144001-0f480c025956/go.mod h1:SRl30Lb7/QoYyohYeVBuqYvvmXSZJxZgiV3Zf6VbxjI= +github.com/mattermost/logr/v2 v2.0.22 h1:npFkXlkAWR9J8payh8ftPcCZvLbHSI125mAM5/r/lP4= +github.com/mattermost/logr/v2 v2.0.22/go.mod h1:0sUKpO+XNMZApeumaid7PYaUZPBIydfuWZ0dqixXo+s= +github.com/mattermost/mattermost/server/v8 v8.0.0-20251014075701-833e0125320d h1:etRyN6FNd6fc7BGZ8X+XB2u/5Hb2HNz5/K53YZNvfrs= +github.com/mattermost/mattermost/server/v8 v8.0.0-20251014075701-833e0125320d/go.mod h1:HILhsra+xY4SNEFhuPbobH3I8a0aeXJcTJ6RWPX85nI= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= +github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= +github.com/oklog/run v1.2.0 h1:O8x3yXwah4A73hJdlrwo/2X6J62gE5qTMusH0dvz60E= +github.com/oklog/run v1.2.0/go.mod h1:mgDbKRSwPhJfesJ4PntqFUbKQRZ50NgmZTSPlFA0YFk= +github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= +github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw= +github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= +github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= +github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= +github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0= +github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= +github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= +github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw= +github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI= +github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU= +github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag= +github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg= +github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw= +github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y= +github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= +github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q= +github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ= +github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I= +github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0= +github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ= +github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk= +github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= +github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= +github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= +github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= +github.com/tinylib/msgp v1.4.0 h1:SYOeDRiydzOw9kSiwdYp9UcBgPFtLU2WDHaJXyHruf8= +github.com/tinylib/msgp v1.4.0/go.mod h1:cvjFkb4RiC8qSBOPMGPSzSAx47nAsfhLVTCZZNuHv5o= +github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= +github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= +github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= +github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/wiggin77/merror v1.0.5 h1:P+lzicsn4vPMycAf2mFf7Zk6G9eco5N+jB1qJ2XW3ME= +github.com/wiggin77/merror v1.0.5/go.mod h1:H2ETSu7/bPE0Ymf4bEwdUoo73OOEkdClnoRisfw0Nm0= +github.com/wiggin77/srslog v1.0.1 h1:gA2XjSMy3DrRdX9UqLuDtuVAAshb8bE1NhX1YK0Qe+8= +github.com/wiggin77/srslog v1.0.1/go.mod h1:fehkyYDq1QfuYn60TDPu9YdY2bB85VUW2mvN1WynEls= +go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= +go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= +go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= +go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= +go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= +go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= +go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= +go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= +go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= +golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= +golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= +google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251007200510-49b9836ed3ff h1:A90eA31Wq6HOMIQlLfzFwzqGKBTuaVztYu/g8sn+8Zc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251007200510-49b9836ed3ff/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= +google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= +google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck= +sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= diff --git a/tools/sharedchannel-test/main.go b/tools/sharedchannel-test/main.go new file mode 100644 index 00000000000..3057c97366b --- /dev/null +++ b/tools/sharedchannel-test/main.go @@ -0,0 +1,117 @@ +// sharedchannel-test is an integration test tool that validates shared channel +// synchronization between two real Mattermost server instances. +// +// It can either manage the server lifecycle itself (build, start, stop) or +// connect to already-running instances. +// +// Usage: +// +// go run . --license /path/to/license.mattermost-license +// go run . --server-a http://already:9065 --server-b http://running:9066 --manage=false +package main + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "os" + "os/signal" + "syscall" + + "github.com/mattermost/mattermost/server/public/shared/mlog" +) + +type Config struct { + ServerAURL string + ServerBURL string + LicensePath string + ServerDir string + Manage bool + AdminUser string + AdminPass string +} + +func main() { + cfg := Config{} + + flag.StringVar(&cfg.ServerAURL, "server-a", "http://localhost:9065", "Server A URL") + flag.StringVar(&cfg.ServerBURL, "server-b", "http://localhost:9066", "Server B URL") + flag.StringVar(&cfg.LicensePath, "license", "", "Path to enterprise license file (required)") + flag.StringVar(&cfg.ServerDir, "server-dir", "", "Path to server directory (for managed mode)") + flag.BoolVar(&cfg.Manage, "manage", true, "Manage server lifecycle (build/start/stop)") + flag.StringVar(&cfg.AdminUser, "admin-user", "admin", "Admin username to create") + flag.StringVar(&cfg.AdminPass, "admin-pass", "Admin1234!", "Admin password") + flag.Parse() + + if cfg.LicensePath == "" { + fmt.Fprintln(os.Stderr, "error: --license is required") + flag.Usage() + os.Exit(1) + } + + logger, err := newLogger() + if err != nil { + fmt.Fprintf(os.Stderr, "error creating logger: %v\n", err) + os.Exit(1) + } + defer logger.Flush() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) + go func() { + <-sigCh + logger.Info("Received signal, shutting down...") + cancel() + }() + + runner := NewTestRunner(cfg, logger) + if err := runner.Run(ctx); err != nil { + logger.Error("Test run failed", mlog.Err(err)) + logger.Flush() + os.Exit(1) + } +} + +// newLogger creates an mlog.Logger with two console targets: +// - stdout: info, warn, and debug (progress messages, PASS results) +// - stderr: error, fatal, panic (FAIL results, fatal errors) +// +// Both use the plain formatter with color enabled so level names +// are visually distinct (cyan=info, yellow=warn, red=error). +func newLogger() (*mlog.Logger, error) { + logger, err := mlog.NewLogger() + if err != nil { + return nil, err + } + + formatOpts := json.RawMessage(`{"enable_color": true, "enable_caller": false}`) + + logCfg := mlog.LoggerConfiguration{ + "stdout": mlog.TargetCfg{ + Type: "console", + Format: "plain", + Levels: []mlog.Level{mlog.LvlInfo, mlog.LvlWarn, mlog.LvlDebug}, + Options: json.RawMessage(`{"out": "stdout"}`), + FormatOptions: formatOpts, + MaxQueueSize: 1000, + }, + "stderr": mlog.TargetCfg{ + Type: "console", + Format: "plain", + Levels: []mlog.Level{mlog.LvlError, mlog.LvlFatal, mlog.LvlPanic}, + Options: json.RawMessage(`{"out": "stderr"}`), + FormatOptions: formatOpts, + MaxQueueSize: 1000, + }, + } + + if err := logger.ConfigureTargets(logCfg, nil); err != nil { + return nil, fmt.Errorf("configure log targets: %w", err) + } + + return logger, nil +} diff --git a/tools/sharedchannel-test/runner.go b/tools/sharedchannel-test/runner.go new file mode 100644 index 00000000000..0d7ac96713a --- /dev/null +++ b/tools/sharedchannel-test/runner.go @@ -0,0 +1,302 @@ +package main + +import ( + "context" + "fmt" + "time" + + "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/shared/mlog" +) + +// TestResult tracks the outcome of a single test case. +type TestResult struct { + Name string + Passed bool + Detail string +} + +// TestRunner orchestrates the full integration test sequence. +type TestRunner struct { + cfg Config + logger *mlog.Logger + mgr *ServerManager + clientA *model.Client4 + clientB *model.Client4 + teamA *model.Team + teamB *model.Team + remoteID string // single remote cluster connection, reused for all tests + suffix string // random suffix for unique names across repeated runs + results []TestResult +} + +func NewTestRunner(cfg Config, logger *mlog.Logger) *TestRunner { + return &TestRunner{ + cfg: cfg, + logger: logger, + suffix: model.NewId()[:6], + } +} + +func (tr *TestRunner) pass(name string) { + tr.results = append(tr.results, TestResult{Name: name, Passed: true}) + tr.logger.Info("PASS", mlog.String("test", name)) +} + +func (tr *TestRunner) fail(name, detail string) { + tr.results = append(tr.results, TestResult{Name: name, Passed: false, Detail: detail}) + tr.logger.Error("FAIL", mlog.String("test", name), mlog.String("detail", detail)) +} + +// Run executes the full test suite. +func (tr *TestRunner) Run(ctx context.Context) error { + // Lifecycle management + if tr.cfg.Manage { + tr.mgr = NewServerManager(tr.cfg, tr.logger) + defer tr.mgr.Teardown() + if err := tr.mgr.Setup(ctx); err != nil { + return fmt.Errorf("server setup: %w", err) + } + } + + // Provision + if err := tr.provision(ctx); err != nil { + return fmt.Errorf("provision: %w", err) + } + + // Run test suites + if err := tr.runMembershipTests(ctx); err != nil { + tr.logger.Error("Membership tests had errors", mlog.Err(err)) + } + if err := tr.runPostTests(ctx); err != nil { + tr.logger.Error("Post tests had errors", mlog.Err(err)) + } + if err := tr.runReactionTests(ctx); err != nil { + tr.logger.Error("Reaction tests had errors", mlog.Err(err)) + } + + // Report + return tr.report() +} + +func (tr *TestRunner) provision(ctx context.Context) error { + var err error + + // Create admin users and login + tr.logger.Info("Provisioning admin users...") + tr.clientA, err = ProvisionAdmin(ctx, tr.cfg.ServerAURL, "mattermost_test", + tr.cfg.AdminUser, "admin-a@test.local", tr.cfg.AdminPass) + if err != nil { + return fmt.Errorf("provision server A: %w", err) + } + + tr.clientB, err = ProvisionAdmin(ctx, tr.cfg.ServerBURL, "mattermost_node_test", + tr.cfg.AdminUser, "admin-b@test.local", tr.cfg.AdminPass) + if err != nil { + return fmt.Errorf("provision server B: %w", err) + } + + // Upload license + tr.logger.Info("Uploading license...") + if err := UploadLicense(ctx, tr.clientA, tr.cfg.LicensePath); err != nil { + return fmt.Errorf("license server A: %w", err) + } + if err := UploadLicense(ctx, tr.clientB, tr.cfg.LicensePath); err != nil { + return fmt.Errorf("license server B: %w", err) + } + + // Wait for remote cluster service to become available after license upload + if err := tr.waitFor(ctx, 30*time.Second, func() bool { + _, _, err := tr.clientA.GetRemoteClusters(ctx, 0, 1, model.RemoteClusterQueryFilter{}) + return err == nil + }); err != nil { + return fmt.Errorf("remote cluster service not ready on server A: %w", err) + } + + // Create teams + tr.logger.Info("Creating teams...") + tr.teamA, _, err = tr.clientA.CreateTeam(ctx, &model.Team{ + Name: "team-a-" + tr.suffix, + DisplayName: "Team A", + Type: model.TeamOpen, + }) + if err != nil { + return fmt.Errorf("create team A: %w", err) + } + + tr.teamB, _, err = tr.clientB.CreateTeam(ctx, &model.Team{ + Name: "team-b-" + tr.suffix, + DisplayName: "Team B", + Type: model.TeamOpen, + }) + if err != nil { + return fmt.Errorf("create team B: %w", err) + } + + // Create a single remote cluster connection for all tests + tr.logger.Info("Setting up remote cluster connection...") + invitePassword := "TestInvite123!" + invite, _, err := tr.clientA.CreateRemoteCluster(ctx, &model.RemoteClusterWithPassword{ + RemoteCluster: &model.RemoteCluster{ + Name: "server-b-" + tr.suffix, + DisplayName: "Server B", + DefaultTeamId: tr.teamA.Id, + }, + Password: invitePassword, + }) + if err != nil { + return fmt.Errorf("create remote cluster: %w", err) + } + tr.remoteID = invite.RemoteCluster.RemoteId + + _, _, err = tr.clientB.RemoteClusterAcceptInvite(ctx, &model.RemoteClusterAcceptInvite{ + Name: "server-a-" + tr.suffix, + DisplayName: "Server A", + DefaultTeamId: tr.teamB.Id, + Invite: invite.Invite, + Password: invitePassword, + }) + if err != nil { + return fmt.Errorf("accept invite: %w", err) + } + + if err := tr.waitForRemoteOnline(ctx, tr.remoteID); err != nil { + return fmt.Errorf("remote cluster not online: %w", err) + } + tr.logger.Info("Remote cluster connection established") + + return nil +} + +// setupSharedChannel creates a channel on Server A, shares it with Server B via +// the pre-established remote cluster, and returns both channel IDs. +func (tr *TestRunner) setupSharedChannel(ctx context.Context, channelName string) (channelA, channelB string, err error) { + uniqueName := channelName + "-" + tr.suffix + ch, _, err := tr.clientA.CreateChannel(ctx, &model.Channel{ + TeamId: tr.teamA.Id, + Name: uniqueName, + DisplayName: "Shared: " + channelName, + Type: model.ChannelTypeOpen, + }) + if err != nil { + return "", "", fmt.Errorf("create channel: %w", err) + } + channelA = ch.Id + + // Share the channel through the existing remote + _, err = tr.clientA.InviteRemoteClusterToChannel(ctx, tr.remoteID, channelA) + if err != nil { + return "", "", fmt.Errorf("share channel: %w", err) + } + + // Wait for channel to appear on B + tr.logger.Info("Waiting for channel to sync to Server B...", mlog.String("channel", channelName)) + if err := tr.waitFor(ctx, 30*time.Second, func() bool { + channels, _, err := tr.clientB.GetAllSharedChannels(ctx, tr.teamB.Id, 0, 100) + if err != nil { + return false + } + for _, sc := range channels { + if sc.ShareName == uniqueName { + channelB = sc.ChannelId + return true + } + } + return false + }); err != nil { + return "", "", fmt.Errorf("channel did not appear on server B: %w", err) + } + + return channelA, channelB, nil +} + +func (tr *TestRunner) waitForRemoteOnline(ctx context.Context, remoteID string) error { + return tr.waitFor(ctx, 30*time.Second, func() bool { + rc, _, err := tr.clientA.GetRemoteCluster(ctx, remoteID) + if err != nil { + return false + } + return rc.LastPingAt > 0 + }) +} + +func (tr *TestRunner) waitFor(ctx context.Context, timeout time.Duration, condition func() bool) error { + deadline := time.After(timeout) + ticker := time.NewTicker(2 * time.Second) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return ctx.Err() + case <-deadline: + return fmt.Errorf("timed out after %s", timeout) + case <-ticker.C: + if condition() { + return nil + } + } + } +} + +// createTestUser creates a user on Server A, adds them to the team, and returns their ID. +func (tr *TestRunner) createTestUser(ctx context.Context, username string) (string, error) { + uniqueName := username + "-" + tr.suffix + user, _, err := tr.clientA.CreateUser(ctx, &model.User{ + Username: uniqueName, + Email: uniqueName + "@test.local", + Password: "TestPass123!", + }) + if err != nil { + return "", fmt.Errorf("create user %s: %w", username, err) + } + + _, _, err = tr.clientA.AddTeamMember(ctx, tr.teamA.Id, user.Id) + if err != nil { + return "", fmt.Errorf("add user %s to team: %w", username, err) + } + + return user.Id, nil +} + +// verifyMemberOnB checks whether a user (by username prefix) is a member of a channel on Server B. +func (tr *TestRunner) verifyMemberOnB(ctx context.Context, channelB, usernamePrefix string) bool { + users, _, err := tr.clientB.GetUsersInChannel(ctx, channelB, 0, 200, "") + if err != nil { + return false + } + for _, u := range users { + if len(u.Username) >= len(usernamePrefix) && u.Username[:len(usernamePrefix)] == usernamePrefix { + return true + } + } + return false +} + +// report prints the final summary. Errors go to stderr so that a clean +// stderr signals success in CI/scripted environments. Returns non-nil +// on any failure so the process exits with a non-zero code. +func (tr *TestRunner) report() error { + passed, failed := 0, 0 + for _, r := range tr.results { + if r.Passed { + passed++ + } else { + failed++ + } + } + + tr.logger.Info("=========================================") + tr.logger.Info(fmt.Sprintf("Results: %d passed, %d failed", passed, failed)) + tr.logger.Info("=========================================") + + if failed > 0 { + for _, r := range tr.results { + if !r.Passed { + tr.logger.Error("FAIL", mlog.String("test", r.Name), mlog.String("detail", r.Detail)) + } + } + return fmt.Errorf("%d test(s) failed", failed) + } + return nil +} diff --git a/tools/sharedchannel-test/server.go b/tools/sharedchannel-test/server.go new file mode 100644 index 00000000000..7abf5ba7ba2 --- /dev/null +++ b/tools/sharedchannel-test/server.go @@ -0,0 +1,255 @@ +package main + +import ( + "context" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "time" + + "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/shared/mlog" +) + +// ServerManager handles building, starting, and stopping Mattermost server instances. +type ServerManager struct { + cfg Config + logger *mlog.Logger + procA *exec.Cmd + procB *exec.Cmd + binary string +} + +func NewServerManager(cfg Config, logger *mlog.Logger) *ServerManager { + return &ServerManager{cfg: cfg, logger: logger} +} + +// Setup builds the server binary, resets databases, and starts both instances. +func (sm *ServerManager) Setup(ctx context.Context) error { + if err := sm.build(ctx); err != nil { + return fmt.Errorf("build: %w", err) + } + if err := sm.resetDatabases(ctx); err != nil { + return fmt.Errorf("reset databases: %w", err) + } + if err := sm.startServers(ctx); err != nil { + return fmt.Errorf("start servers: %w", err) + } + return nil +} + +// Teardown stops both server processes. +func (sm *ServerManager) Teardown() { + sm.logger.Info("Stopping servers...") + stopProc(sm.procA) + stopProc(sm.procB) +} + +// stopProc sends SIGINT and waits up to 10 seconds for graceful shutdown, +// then sends SIGKILL if the process is still running. +func stopProc(cmd *exec.Cmd) { + if cmd == nil || cmd.Process == nil { + return + } + + _ = cmd.Process.Signal(os.Interrupt) + + done := make(chan struct{}) + go func() { + _ = cmd.Wait() + close(done) + }() + + select { + case <-done: + case <-time.After(10 * time.Second): + _ = cmd.Process.Kill() + <-done + } +} + +func (sm *ServerManager) build(ctx context.Context) error { + sm.logger.Info("Building server binary with enterprise...") + sm.binary = filepath.Join(sm.cfg.ServerDir, "bin", "mattermost") + + cmd := exec.CommandContext(ctx, "go", "build", + "-tags", "enterprise", + "-ldflags", `-X "github.com/mattermost/mattermost/server/public/model.BuildEnterpriseReady=true"`, + "-o", sm.binary, + "./cmd/mattermost", + ) + cmd.Dir = sm.cfg.ServerDir + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + if err := cmd.Run(); err != nil { + return fmt.Errorf("go build: %w", err) + } + sm.logger.Info("Binary built", mlog.String("path", sm.binary)) + return nil +} + +func (sm *ServerManager) resetDatabases(ctx context.Context) error { + sm.logger.Info("Resetting databases...") + for _, db := range []string{"mattermost_test", "mattermost_node_test"} { + for _, sql := range []string{ + fmt.Sprintf("DROP DATABASE IF EXISTS %s", db), + fmt.Sprintf("CREATE DATABASE %s", db), + } { + cmd := exec.CommandContext(ctx, "docker", "exec", + "-e", "PGPASSWORD=mostest", + "mattermost-postgres", + "psql", "-U", "mmuser", "-d", "postgres", "-c", sql, + ) + if out, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("psql %q: %w\n%s", sql, err, out) + } + } + } + return nil +} + +func (sm *ServerManager) startServers(ctx context.Context) error { + commonEnv := []string{ + "MM_CONNECTEDWORKSPACESSETTINGS_ENABLESHAREDCHANNELS=true", + "MM_CONNECTEDWORKSPACESSETTINGS_ENABLEREMOTECLUSTERSERVICE=true", + "MM_FEATUREFLAGS_ENABLESHAREDCHANNELSMEMBERSYNC=true", + "MM_SERVICESETTINGS_ENABLELOCALMODE=false", + "MM_TEAMSETTINGS_ENABLEOPENSERVER=true", + "MM_LOGSETTINGS_CONSOLELEVEL=ERROR", + "MM_LOGSETTINGS_FILELEVEL=INFO", + "MM_SQLSETTINGS_DRIVERNAME=postgres", + } + + logsDir := filepath.Join(sm.cfg.ServerDir, "logs") + _ = os.MkdirAll(logsDir, 0o755) + + // Server A + sm.logger.Info("Starting Server A", mlog.String("url", sm.cfg.ServerAURL)) + sm.procA = exec.Command(sm.binary, "server") + sm.procA.Dir = sm.cfg.ServerDir + sm.procA.Env = append(os.Environ(), commonEnv...) + sm.procA.Env = append(sm.procA.Env, + "MM_SERVICESETTINGS_SITEURL="+sm.cfg.ServerAURL, + "MM_SERVICESETTINGS_LISTENADDRESS=:9065", + "MM_SQLSETTINGS_DATASOURCE=postgres://mmuser:mostest@localhost/mattermost_test?sslmode=disable&connect_timeout=10&binary_parameters=yes", + "MM_LOGSETTINGS_FILELOCATION="+filepath.Join(logsDir, "server_a.log"), + ) + outA, err := os.Create(filepath.Join(logsDir, "server_a_stdout.log")) + if err != nil { + return fmt.Errorf("create server A log file: %w", err) + } + sm.procA.Stdout = outA + sm.procA.Stderr = outA + if err := sm.procA.Start(); err != nil { + return fmt.Errorf("start server A: %w", err) + } + + // Server B + sm.logger.Info("Starting Server B", mlog.String("url", sm.cfg.ServerBURL)) + sm.procB = exec.Command(sm.binary, "server") + sm.procB.Dir = sm.cfg.ServerDir + sm.procB.Env = append(os.Environ(), commonEnv...) + sm.procB.Env = append(sm.procB.Env, + "MM_SERVICESETTINGS_SITEURL="+sm.cfg.ServerBURL, + "MM_SERVICESETTINGS_LISTENADDRESS=:9066", + "MM_SQLSETTINGS_DATASOURCE=postgres://mmuser:mostest@localhost/mattermost_node_test?sslmode=disable&connect_timeout=10&binary_parameters=yes", + "MM_LOGSETTINGS_FILELOCATION="+filepath.Join(logsDir, "server_b.log"), + ) + outB, err := os.Create(filepath.Join(logsDir, "server_b_stdout.log")) + if err != nil { + stopProc(sm.procA) + return fmt.Errorf("create server B log file: %w", err) + } + sm.procB.Stdout = outB + sm.procB.Stderr = outB + if err := sm.procB.Start(); err != nil { + stopProc(sm.procA) + return fmt.Errorf("start server B: %w", err) + } + + // Wait for both — stop started servers on failure + if err := sm.waitForServer(ctx, sm.cfg.ServerAURL, "Server A"); err != nil { + sm.Teardown() + return err + } + if err := sm.waitForServer(ctx, sm.cfg.ServerBURL, "Server B"); err != nil { + sm.Teardown() + return err + } + return nil +} + +func (sm *ServerManager) waitForServer(ctx context.Context, url, name string) error { + client := model.NewAPIv4Client(url) + deadline := time.After(2 * time.Minute) + ticker := time.NewTicker(2 * time.Second) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return ctx.Err() + case <-deadline: + return fmt.Errorf("%s did not become ready within 2 minutes", name) + case <-ticker.C: + _, _, err := client.GetPingWithServerStatus(ctx) + if err == nil { + sm.logger.Info("Server is ready", mlog.String("name", name)) + return nil + } + } + } +} + +// ProvisionAdmin creates an admin user on a server, promotes via DB, and returns an authenticated client. +func ProvisionAdmin(ctx context.Context, serverURL, dbName, username, email, password string) (*model.Client4, error) { + client := model.NewAPIv4Client(serverURL) + + // Create user + _, _, err := client.CreateUser(ctx, &model.User{ + Username: username, + Email: email, + Password: password, + }) + if err != nil { + return nil, fmt.Errorf("create admin user: %w", err) + } + + // Promote to system_admin via DB using parameterized query via stdin + // (psql -v variable substitution is not supported with -c) + cmd := exec.CommandContext(ctx, "docker", "exec", + "-i", + "-e", "PGPASSWORD=mostest", + "mattermost-postgres", + "psql", "-U", "mmuser", "-d", dbName, + "-v", "uname="+username, + ) + cmd.Stdin = strings.NewReader("UPDATE users SET roles='system_admin system_user' WHERE username=:'uname';") + if out, err := cmd.CombinedOutput(); err != nil { + return nil, fmt.Errorf("promote admin: %w\n%s", err, out) + } + + // Login + _, _, err = client.Login(ctx, username, password) + if err != nil { + return nil, fmt.Errorf("login: %w", err) + } + + return client, nil +} + +// UploadLicense reads and uploads a license file to a server. +func UploadLicense(ctx context.Context, client *model.Client4, licensePath string) error { + data, err := os.ReadFile(licensePath) + if err != nil { + return fmt.Errorf("read license file: %w", err) + } + _, err = client.UploadLicenseFile(ctx, data) + if err != nil { + return fmt.Errorf("upload license: %w", err) + } + return nil +} diff --git a/tools/sharedchannel-test/test_membership.go b/tools/sharedchannel-test/test_membership.go new file mode 100644 index 00000000000..ead93f89b24 --- /dev/null +++ b/tools/sharedchannel-test/test_membership.go @@ -0,0 +1,129 @@ +package main + +import ( + "context" + "fmt" + "time" +) + +func (tr *TestRunner) runMembershipTests(ctx context.Context) error { + tr.logger.Info("=== Membership Sync Tests ===") + + channelA, channelB, err := tr.setupSharedChannel(ctx, "membership-test") + if err != nil { + tr.fail("membership/setup", err.Error()) + return err + } + + // Create test users + userIDs := make([]string, 3) + for i := range 3 { + name := fmt.Sprintf("memtest%d", i+1) + id, err := tr.createTestUser(ctx, name) + if err != nil { + tr.fail("membership/create-user-"+name, err.Error()) + return err + } + userIDs[i] = id + } + + // ── Test: Add users and verify sync ───────────────────── + tr.logger.Info("Adding users to shared channel...") + for i, uid := range userIDs { + _, _, err := tr.clientA.AddChannelMember(ctx, channelA, uid) + if err != nil { + tr.fail(fmt.Sprintf("membership/add-user-%d", i+1), err.Error()) + return err + } + } + + tr.logger.Info("Waiting for membership sync...") + for i := range 3 { + name := fmt.Sprintf("memtest%d", i+1) + testName := "membership/add-sync-" + name + err := tr.waitFor(ctx, 30*time.Second, func() bool { + return tr.verifyMemberOnB(ctx, channelB, name) + }) + if err != nil { + tr.fail(testName, fmt.Sprintf("user %s did not sync to Server B: %v", name, err)) + } else { + tr.pass(testName) + } + } + + // ── Test: Remove one user and verify sync ─────────────── + tr.logger.Info("Removing memtest3 from shared channel...") + _, err = tr.clientA.RemoveUserFromChannel(ctx, channelA, userIDs[2]) + if err != nil { + tr.fail("membership/remove-memtest3", err.Error()) + } else { + testName := "membership/remove-sync-memtest3" + err := tr.waitFor(ctx, 30*time.Second, func() bool { + return !tr.verifyMemberOnB(ctx, channelB, "memtest3") + }) + if err != nil { + tr.fail(testName, "memtest3 still present on Server B after removal") + } else { + tr.pass(testName) + } + + // Verify others still present + for _, name := range []string{"memtest1", "memtest2"} { + testName := "membership/still-present-" + name + if tr.verifyMemberOnB(ctx, channelB, name) { + tr.pass(testName) + } else { + tr.fail(testName, name+" unexpectedly missing after memtest3 removal") + } + } + } + + // ── Test: Re-add removed user ─────────────────────────── + tr.logger.Info("Re-adding memtest3...") + _, _, err = tr.clientA.AddChannelMember(ctx, channelA, userIDs[2]) + if err != nil { + tr.fail("membership/re-add-memtest3", err.Error()) + } else { + testName := "membership/re-add-sync-memtest3" + err := tr.waitFor(ctx, 30*time.Second, func() bool { + return tr.verifyMemberOnB(ctx, channelB, "memtest3") + }) + if err != nil { + tr.fail(testName, "memtest3 re-add did not sync to Server B") + } else { + tr.pass(testName) + } + } + + // ── Test: Bulk removal ────────────────────────────────── + tr.logger.Info("Bulk removing all 3 users...") + for _, uid := range userIDs { + _, _ = tr.clientA.RemoveUserFromChannel(ctx, channelA, uid) + } + + testName := "membership/bulk-remove-sync" + err = tr.waitFor(ctx, 30*time.Second, func() bool { + for i := range 3 { + if tr.verifyMemberOnB(ctx, channelB, fmt.Sprintf("memtest%d", i+1)) { + return false + } + } + return true + }) + if err != nil { + tr.fail(testName, "not all users removed from Server B after bulk removal") + } else { + tr.pass(testName) + } + + return nil +} + +// verifyChannelMemberCount returns the number of members in a channel on Server B. +func (tr *TestRunner) verifyChannelMemberCount(ctx context.Context, channelB string) int { + members, _, err := tr.clientB.GetChannelMembers(ctx, channelB, 0, 200, "") + if err != nil { + return -1 + } + return len(members) +} diff --git a/tools/sharedchannel-test/test_posts.go b/tools/sharedchannel-test/test_posts.go new file mode 100644 index 00000000000..d20e3f847f4 --- /dev/null +++ b/tools/sharedchannel-test/test_posts.go @@ -0,0 +1,113 @@ +package main + +import ( + "context" + "time" + + "github.com/mattermost/mattermost/server/public/model" +) + +func (tr *TestRunner) runPostTests(ctx context.Context) error { + tr.logger.Info("=== Post Sync Tests ===") + + channelA, channelB, err := tr.setupSharedChannel(ctx, "post-test") + if err != nil { + tr.fail("post/setup", err.Error()) + return err + } + + // Create a user and add to channel + userID, err := tr.createTestUser(ctx, "postuser1") + if err != nil { + tr.fail("post/create-user", err.Error()) + return err + } + _, _, err = tr.clientA.AddChannelMember(ctx, channelA, userID) + if err != nil { + tr.fail("post/add-user-to-channel", err.Error()) + return err + } + + // Wait for user to sync before posting + if err := tr.waitFor(ctx, 15*time.Second, func() bool { + return tr.verifyMemberOnB(ctx, channelB, "postuser1") + }); err != nil { + tr.fail("post/user-sync", "postuser1 did not sync to Server B before posting") + return err + } + + // ── Test: Create post and verify sync ─────────────────── + tr.logger.Info("Creating post on Server A...") + postMessage := "Hello from Server A! " + model.NewId()[:8] + post, _, err := tr.clientA.CreatePost(ctx, &model.Post{ + ChannelId: channelA, + Message: postMessage, + }) + if err != nil { + tr.fail("post/create", err.Error()) + return err + } + + testName := "post/sync-create" + err = tr.waitFor(ctx, 30*time.Second, func() bool { + return tr.findPostOnB(ctx, channelB, postMessage) != "" + }) + if err != nil { + tr.fail(testName, "post did not sync to Server B") + } else { + tr.pass(testName) + } + + // ── Test: Edit post and verify sync ───────────────────── + tr.logger.Info("Editing post on Server A...") + editedMessage := "Edited: " + postMessage + post.Message = editedMessage + _, _, err = tr.clientA.UpdatePost(ctx, post.Id, post) + if err != nil { + tr.fail("post/edit", err.Error()) + } else { + testName := "post/sync-edit" + err := tr.waitFor(ctx, 30*time.Second, func() bool { + return tr.findPostOnB(ctx, channelB, editedMessage) != "" + }) + if err != nil { + tr.fail(testName, "post edit did not sync to Server B") + } else { + tr.pass(testName) + } + } + + // ── Test: Delete post and verify sync ─────────────────── + tr.logger.Info("Deleting post on Server A...") + _, err = tr.clientA.DeletePost(ctx, post.Id) + if err != nil { + tr.fail("post/delete", err.Error()) + } else { + testName := "post/sync-delete" + err := tr.waitFor(ctx, 30*time.Second, func() bool { + return tr.findPostOnB(ctx, channelB, editedMessage) == "" + }) + if err != nil { + tr.fail(testName, "post delete did not sync to Server B") + } else { + tr.pass(testName) + } + } + + return nil +} + +// findPostOnB searches for a post with the given message in a channel on Server B. +// Returns the post ID if found, empty string otherwise. +func (tr *TestRunner) findPostOnB(ctx context.Context, channelB, message string) string { + postList, _, err := tr.clientB.GetPostsForChannel(ctx, channelB, 0, 100, "", false, false) + if err != nil { + return "" + } + for _, post := range postList.Posts { + if post.Message == message { + return post.Id + } + } + return "" +} diff --git a/tools/sharedchannel-test/test_reactions.go b/tools/sharedchannel-test/test_reactions.go new file mode 100644 index 00000000000..44a05fea2b0 --- /dev/null +++ b/tools/sharedchannel-test/test_reactions.go @@ -0,0 +1,109 @@ +package main + +import ( + "context" + "time" + + "github.com/mattermost/mattermost/server/public/model" +) + +func (tr *TestRunner) runReactionTests(ctx context.Context) error { + tr.logger.Info("=== Reaction Sync Tests ===") + + channelA, channelB, err := tr.setupSharedChannel(ctx, "reaction-test") + if err != nil { + tr.fail("reaction/setup", err.Error()) + return err + } + + // Get admin's user ID (the authenticated user on clientA) + me, _, err := tr.clientA.GetMe(ctx, "") + if err != nil { + tr.fail("reaction/get-me", err.Error()) + return err + } + adminUserID := me.Id + + // Create a post to react to + postMsg := "React to this! " + model.NewId()[:8] + post, _, err := tr.clientA.CreatePost(ctx, &model.Post{ + ChannelId: channelA, + Message: postMsg, + }) + if err != nil { + tr.fail("reaction/create-post", err.Error()) + return err + } + + // Wait for post to sync + var postBID string + if err := tr.waitFor(ctx, 30*time.Second, func() bool { + postBID = tr.findPostOnB(ctx, channelB, postMsg) + return postBID != "" + }); err != nil { + tr.fail("reaction/post-sync", "post did not sync to Server B before adding reactions") + return err + } + + // ── Test: Add reaction and verify sync ────────────────── + tr.logger.Info("Adding reaction on Server A...") + _, _, err = tr.clientA.SaveReaction(ctx, &model.Reaction{ + UserId: adminUserID, + PostId: post.Id, + EmojiName: "thumbsup", + }) + if err != nil { + tr.fail("reaction/add", err.Error()) + return err + } + + testName := "reaction/sync-add" + err = tr.waitFor(ctx, 30*time.Second, func() bool { + return tr.hasReactionOnB(ctx, postBID, "thumbsup") + }) + if err != nil { + tr.fail(testName, "reaction did not sync to Server B") + } else { + tr.pass(testName) + } + + // ── Test: Remove reaction and verify sync ─────────────── + tr.logger.Info("Removing reaction on Server A...") + _, err = tr.clientA.DeleteReaction(ctx, &model.Reaction{ + UserId: adminUserID, + PostId: post.Id, + EmojiName: "thumbsup", + }) + if err != nil { + tr.fail("reaction/remove", err.Error()) + } else { + testName := "reaction/sync-remove" + err := tr.waitFor(ctx, 30*time.Second, func() bool { + return !tr.hasReactionOnB(ctx, postBID, "thumbsup") + }) + if err != nil { + tr.fail(testName, "reaction removal did not sync to Server B") + } else { + tr.pass(testName) + } + } + + return nil +} + +// hasReactionOnB checks if a post on Server B has a specific emoji reaction. +func (tr *TestRunner) hasReactionOnB(ctx context.Context, postID, emojiName string) bool { + if postID == "" { + return false + } + reactions, _, err := tr.clientB.GetReactions(ctx, postID) + if err != nil { + return false + } + for _, r := range reactions { + if r.EmojiName == emojiName { + return true + } + } + return false +}