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

306 lines
8.8 KiB
Go

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))
}
if err := tr.runOfflineRecoveryTests(ctx); err != nil {
tr.logger.Error("Offline recovery 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 := model.NewTestPassword()
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: model.NewTestPassword(),
})
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
}