mirror of
https://github.com/mattermost/mattermost.git
synced 2026-08-09 04:28:29 -05:00
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.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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
|
||||
@@ -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=
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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 ""
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user