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
337 lines
15 KiB
Go
337 lines
15 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package sqlstore
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
|
"github.com/mattermost/mattermost/server/v8/channels/db"
|
|
)
|
|
|
|
func readMigrationSQL(t *testing.T, filename string) string {
|
|
t.Helper()
|
|
data, err := db.Assets().ReadFile("migrations/postgres/" + filename)
|
|
require.NoError(t, err, "failed to read migration file %s", filename)
|
|
return string(data)
|
|
}
|
|
|
|
func TestMigration000185(t *testing.T) {
|
|
logger := mlog.CreateTestLogger(t)
|
|
|
|
settings, err := makeSqlSettings(model.DatabaseDriverPostgres)
|
|
if err != nil {
|
|
t.Skip(err)
|
|
}
|
|
|
|
store, err := New(*settings, logger, nil)
|
|
require.NoError(t, err)
|
|
defer store.Close()
|
|
|
|
master := store.GetMaster()
|
|
|
|
upSQL := readMigrationSQL(t, "000176_migrate_cpa_to_access_control.up.sql")
|
|
downSQL := readMigrationSQL(t, "000176_migrate_cpa_to_access_control.down.sql")
|
|
|
|
// Insert a group simulating pre-migration CPA state.
|
|
groupID := model.NewId()
|
|
_, err = master.Exec("INSERT INTO PropertyGroups (ID, Name) VALUES (?, ?)", groupID, "custom_profile_attributes")
|
|
require.NoError(t, err)
|
|
|
|
t.Cleanup(func() {
|
|
master.Exec("DELETE FROM PropertyValues WHERE GroupID = ?", groupID) //nolint:errcheck
|
|
master.Exec("DELETE FROM PropertyFields WHERE GroupID = ?", groupID) //nolint:errcheck
|
|
master.Exec("DELETE FROM PropertyGroups WHERE ID = ?", groupID) //nolint:errcheck
|
|
})
|
|
|
|
now := model.GetMillis()
|
|
|
|
// Insert active fields with old format (no ObjectType, no permissions).
|
|
// fieldID1 and fieldID2 are non-managed; fieldID3 is admin-managed.
|
|
fieldID1 := model.NewId()
|
|
fieldID2 := model.NewId()
|
|
fieldID3 := model.NewId()
|
|
for _, f := range []struct {
|
|
id string
|
|
name string
|
|
ftype string
|
|
attrs string
|
|
}{
|
|
{fieldID1, "Text Field", "text", `{"visibility":"always","sort_order":1}`},
|
|
{fieldID2, "Select Field", "select", `{"options":[{"id":"opt1","name":"Option 1"}]}`},
|
|
{fieldID3, "Admin Managed Field", "text", `{"visibility":"always","sort_order":3,"managed":"admin"}`},
|
|
} {
|
|
_, err = master.Exec(
|
|
`INSERT INTO PropertyFields
|
|
(ID, GroupID, Name, Type, Attrs, TargetID, TargetType, ObjectType, CreateAt, UpdateAt, DeleteAt, Protected)
|
|
VALUES (?, ?, ?, ?, ?::jsonb, '', '', '', ?, ?, 0, false)`,
|
|
f.id, groupID, f.name, f.ftype, f.attrs, now, now,
|
|
)
|
|
require.NoError(t, err, "inserting field %s", f.name)
|
|
}
|
|
|
|
// Insert a soft-deleted field to verify all fields are migrated.
|
|
deletedFieldID := model.NewId()
|
|
_, err = master.Exec(
|
|
`INSERT INTO PropertyFields
|
|
(ID, GroupID, Name, Type, Attrs, TargetID, TargetType, ObjectType, CreateAt, UpdateAt, DeleteAt, Protected)
|
|
VALUES (?, ?, 'Deleted Field', 'text', '{}'::jsonb, '', '', '', ?, ?, ?, false)`,
|
|
deletedFieldID, groupID, now, now, now,
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
// Insert a property value.
|
|
valueID := model.NewId()
|
|
targetUserID := model.NewId()
|
|
_, err = master.Exec(
|
|
`INSERT INTO PropertyValues
|
|
(ID, TargetID, TargetType, GroupID, FieldID, Value, CreateAt, UpdateAt, DeleteAt)
|
|
VALUES (?, ?, 'user', ?, ?, '"hello"'::jsonb, ?, ?, 0)`,
|
|
valueID, targetUserID, groupID, fieldID1, now, now,
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
// ---- Run UP migration ----
|
|
_, err = master.ExecNoTimeout(upSQL)
|
|
require.NoError(t, err, "up migration should succeed")
|
|
|
|
// Verify: group renamed.
|
|
var groupName string
|
|
require.NoError(t, master.Get(&groupName, "SELECT Name FROM PropertyGroups WHERE ID = ?", groupID))
|
|
assert.Equal(t, "access_control", groupName)
|
|
|
|
// Verify: all fields (including soft-deleted) have new metadata.
|
|
// Non-managed fields get PermissionValues = 'member'.
|
|
// Admin-managed fields get PermissionValues = 'sysadmin'.
|
|
for _, tc := range []struct {
|
|
id string
|
|
label string
|
|
expectedPermissionValues string
|
|
}{
|
|
{fieldID1, "non-managed text field", "member"},
|
|
{fieldID2, "non-managed select field", "member"},
|
|
{fieldID3, "admin-managed field", "sysadmin"},
|
|
{deletedFieldID, "soft-deleted non-managed field", "member"},
|
|
} {
|
|
var f struct {
|
|
ObjectType string `db:"objecttype"`
|
|
TargetType string `db:"targettype"`
|
|
PermissionField sql.NullString `db:"permissionfield"`
|
|
PermissionValues sql.NullString `db:"permissionvalues"`
|
|
PermissionOptions sql.NullString `db:"permissionoptions"`
|
|
}
|
|
require.NoError(t, master.Get(&f, "SELECT ObjectType, TargetType, PermissionField, PermissionValues, PermissionOptions FROM PropertyFields WHERE ID = ?", tc.id))
|
|
assert.Equal(t, "user", f.ObjectType, "%s ObjectType", tc.label)
|
|
assert.Equal(t, "system", f.TargetType, "%s TargetType", tc.label)
|
|
assert.True(t, f.PermissionField.Valid, "%s PermissionField should be set", tc.label)
|
|
assert.Equal(t, "sysadmin", f.PermissionField.String, "%s PermissionField", tc.label)
|
|
assert.True(t, f.PermissionValues.Valid, "%s PermissionValues should be set", tc.label)
|
|
assert.Equal(t, tc.expectedPermissionValues, f.PermissionValues.String, "%s PermissionValues", tc.label)
|
|
assert.True(t, f.PermissionOptions.Valid, "%s PermissionOptions should be set", tc.label)
|
|
assert.Equal(t, "sysadmin", f.PermissionOptions.String, "%s PermissionOptions", tc.label)
|
|
}
|
|
|
|
// Verify: property value is unchanged (GroupID still references the same ID).
|
|
var val struct {
|
|
GroupID string `db:"groupid"`
|
|
TargetID string `db:"targetid"`
|
|
TargetType string `db:"targettype"`
|
|
}
|
|
require.NoError(t, master.Get(&val, "SELECT GroupID, TargetID, TargetType FROM PropertyValues WHERE ID = ?", valueID))
|
|
assert.Equal(t, groupID, val.GroupID, "value GroupID should be unchanged")
|
|
assert.Equal(t, targetUserID, val.TargetID, "value TargetID should be unchanged")
|
|
assert.Equal(t, "user", val.TargetType, "value TargetType should be unchanged")
|
|
|
|
// Verify: the user attribute matview exists and includes the ObjectType
|
|
// filter (user-type fields only). Since migration 000216 the single
|
|
// AttributeView is split per object type; the user row lives in UserAttributeView.
|
|
var viewDef string
|
|
err = master.Get(&viewDef, "SELECT definition FROM pg_matviews WHERE matviewname = 'userattributeview'")
|
|
require.NoError(t, err, "UserAttributeView should exist")
|
|
assert.Contains(t, viewDef, "pf.objecttype", "view definition should filter by pf.ObjectType")
|
|
|
|
// Verify: materialized view contains expected data after refresh.
|
|
_, err = master.ExecNoTimeout("REFRESH MATERIALIZED VIEW UserAttributeView")
|
|
require.NoError(t, err, "refreshing UserAttributeView should succeed")
|
|
|
|
var viewRow struct {
|
|
GroupID string `db:"groupid"`
|
|
TargetID string `db:"targetid"`
|
|
TargetType string `db:"targettype"`
|
|
Attributes []byte `db:"attributes"`
|
|
}
|
|
err = master.Get(&viewRow, "SELECT GroupID, TargetID, TargetType, Attributes FROM UserAttributeView WHERE TargetID = ?", targetUserID)
|
|
require.NoError(t, err, "UserAttributeView should contain a row for the target user")
|
|
assert.Equal(t, groupID, viewRow.GroupID)
|
|
assert.Equal(t, targetUserID, viewRow.TargetID)
|
|
assert.Equal(t, "user", viewRow.TargetType)
|
|
|
|
// The text field value "hello" should appear under the field name "Text Field".
|
|
var attrs map[string]json.RawMessage
|
|
require.NoError(t, json.Unmarshal(viewRow.Attributes, &attrs))
|
|
assert.JSONEq(t, `"hello"`, string(attrs["Text Field"]), "text field value should be materialized")
|
|
|
|
// ---- Run DOWN migration ----
|
|
_, err = master.ExecNoTimeout(downSQL)
|
|
require.NoError(t, err, "down migration should succeed")
|
|
|
|
// Verify: group name reverted.
|
|
require.NoError(t, master.Get(&groupName, "SELECT Name FROM PropertyGroups WHERE ID = ?", groupID))
|
|
assert.Equal(t, "custom_profile_attributes", groupName)
|
|
|
|
// Verify: fields reverted.
|
|
for _, fid := range []string{fieldID1, fieldID2, fieldID3, deletedFieldID} {
|
|
var f struct {
|
|
ObjectType string `db:"objecttype"`
|
|
TargetType string `db:"targettype"`
|
|
PermissionField sql.NullString `db:"permissionfield"`
|
|
PermissionValues sql.NullString `db:"permissionvalues"`
|
|
PermissionOptions sql.NullString `db:"permissionoptions"`
|
|
}
|
|
require.NoError(t, master.Get(&f, "SELECT ObjectType, TargetType, PermissionField, PermissionValues, PermissionOptions FROM PropertyFields WHERE ID = ?", fid))
|
|
assert.Equal(t, "", f.ObjectType, "field %s ObjectType should revert", fid)
|
|
assert.Equal(t, "", f.TargetType, "field %s TargetType should revert", fid)
|
|
assert.False(t, f.PermissionField.Valid, "field %s PermissionField should be NULL", fid)
|
|
assert.False(t, f.PermissionValues.Valid, "field %s PermissionValues should be NULL", fid)
|
|
assert.False(t, f.PermissionOptions.Valid, "field %s PermissionOptions should be NULL", fid)
|
|
}
|
|
|
|
// Verify: value still unchanged after down migration.
|
|
require.NoError(t, master.Get(&val, "SELECT GroupID, TargetID, TargetType FROM PropertyValues WHERE ID = ?", valueID))
|
|
assert.Equal(t, groupID, val.GroupID, "value GroupID should remain unchanged after down")
|
|
}
|
|
|
|
func TestMigration000185DownPreservesNonUserFields(t *testing.T) {
|
|
logger := mlog.CreateTestLogger(t)
|
|
|
|
settings, err := makeSqlSettings(model.DatabaseDriverPostgres)
|
|
if err != nil {
|
|
t.Skip(err)
|
|
}
|
|
|
|
store, err := New(*settings, logger, nil)
|
|
require.NoError(t, err)
|
|
defer store.Close()
|
|
|
|
master := store.GetMaster()
|
|
|
|
upSQL := readMigrationSQL(t, "000176_migrate_cpa_to_access_control.up.sql")
|
|
downSQL := readMigrationSQL(t, "000176_migrate_cpa_to_access_control.down.sql")
|
|
|
|
groupID := model.NewId()
|
|
_, err = master.Exec("INSERT INTO PropertyGroups (ID, Name) VALUES (?, ?)", groupID, "custom_profile_attributes")
|
|
require.NoError(t, err)
|
|
|
|
t.Cleanup(func() {
|
|
master.Exec("DELETE FROM PropertyFields WHERE GroupID = ?", groupID) //nolint:errcheck
|
|
master.Exec("DELETE FROM PropertyGroups WHERE ID = ?", groupID) //nolint:errcheck
|
|
})
|
|
|
|
now := model.GetMillis()
|
|
|
|
// Insert a legacy user field that the up migration will touch.
|
|
userFieldID := model.NewId()
|
|
_, err = master.Exec(
|
|
`INSERT INTO PropertyFields
|
|
(ID, GroupID, Name, Type, Attrs, TargetID, TargetType, ObjectType, CreateAt, UpdateAt, DeleteAt, Protected)
|
|
VALUES (?, ?, 'Legacy User Field', 'text', '{}'::jsonb, '', '', '', ?, ?, 0, false)`,
|
|
userFieldID, groupID, now, now,
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
// Run UP migration — legacy user field gets ObjectType='user', TargetType='system'.
|
|
_, err = master.ExecNoTimeout(upSQL)
|
|
require.NoError(t, err, "up migration should succeed")
|
|
|
|
// Simulate a post-migration channel-scoped field created via the
|
|
// generic property API against the (now renamed) access_control
|
|
// group.
|
|
channelFieldID := model.NewId()
|
|
channelTargetID := model.NewId()
|
|
_, err = master.Exec(
|
|
`INSERT INTO PropertyFields
|
|
(ID, GroupID, Name, Type, Attrs, TargetID, TargetType, ObjectType, PermissionField, PermissionValues, PermissionOptions, CreateAt, UpdateAt, DeleteAt, Protected)
|
|
VALUES (?, ?, 'Channel Classification', 'select', '{}'::jsonb, ?, 'channel', 'channel', 'sysadmin', 'member', 'sysadmin', ?, ?, 0, false)`,
|
|
channelFieldID, groupID, channelTargetID, now, now,
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
// Run DOWN migration — must revert only user/system fields, not the channel one.
|
|
_, err = master.ExecNoTimeout(downSQL)
|
|
require.NoError(t, err, "down migration should succeed")
|
|
|
|
// The original user field reverts to legacy metadata.
|
|
var userField struct {
|
|
ObjectType string `db:"objecttype"`
|
|
TargetType string `db:"targettype"`
|
|
PermissionField sql.NullString `db:"permissionfield"`
|
|
PermissionValues sql.NullString `db:"permissionvalues"`
|
|
PermissionOptions sql.NullString `db:"permissionoptions"`
|
|
}
|
|
require.NoError(t, master.Get(&userField, "SELECT ObjectType, TargetType, PermissionField, PermissionValues, PermissionOptions FROM PropertyFields WHERE ID = ?", userFieldID))
|
|
assert.Equal(t, "", userField.ObjectType, "user field ObjectType should revert")
|
|
assert.Equal(t, "", userField.TargetType, "user field TargetType should revert")
|
|
assert.False(t, userField.PermissionField.Valid, "user field PermissionField should be NULL")
|
|
|
|
// The post-migration channel field keeps its PSAv2 metadata intact.
|
|
var channelField struct {
|
|
ObjectType string `db:"objecttype"`
|
|
TargetType string `db:"targettype"`
|
|
TargetID string `db:"targetid"`
|
|
PermissionField sql.NullString `db:"permissionfield"`
|
|
PermissionValues sql.NullString `db:"permissionvalues"`
|
|
PermissionOptions sql.NullString `db:"permissionoptions"`
|
|
}
|
|
require.NoError(t, master.Get(&channelField, "SELECT ObjectType, TargetType, TargetID, PermissionField, PermissionValues, PermissionOptions FROM PropertyFields WHERE ID = ?", channelFieldID))
|
|
assert.Equal(t, "channel", channelField.ObjectType, "channel field ObjectType must survive rollback")
|
|
assert.Equal(t, "channel", channelField.TargetType, "channel field TargetType must survive rollback")
|
|
assert.Equal(t, channelTargetID, channelField.TargetID, "channel field TargetID must survive rollback")
|
|
assert.True(t, channelField.PermissionField.Valid, "channel field PermissionField must survive rollback")
|
|
assert.Equal(t, "sysadmin", channelField.PermissionField.String)
|
|
assert.True(t, channelField.PermissionValues.Valid)
|
|
assert.Equal(t, "member", channelField.PermissionValues.String)
|
|
assert.True(t, channelField.PermissionOptions.Valid)
|
|
assert.Equal(t, "sysadmin", channelField.PermissionOptions.String)
|
|
}
|
|
|
|
func TestMigration000185NoOpOnFreshDB(t *testing.T) {
|
|
logger := mlog.CreateTestLogger(t)
|
|
|
|
settings, err := makeSqlSettings(model.DatabaseDriverPostgres)
|
|
if err != nil {
|
|
t.Skip(err)
|
|
}
|
|
|
|
store, err := New(*settings, logger, nil)
|
|
require.NoError(t, err)
|
|
defer store.Close()
|
|
|
|
master := store.GetMaster()
|
|
|
|
upSQL := readMigrationSQL(t, "000176_migrate_cpa_to_access_control.up.sql")
|
|
downSQL := readMigrationSQL(t, "000176_migrate_cpa_to_access_control.down.sql")
|
|
|
|
// On a fresh database with no CPA group, both up and down should be
|
|
// safe no-ops (the UPDATE statements match zero rows).
|
|
_, err = master.ExecNoTimeout(upSQL)
|
|
assert.NoError(t, err, "up migration should be a safe no-op on fresh DB")
|
|
|
|
// The attribute matviews exist in the final schema. Since migration 000216
|
|
// the single AttributeView is split into per-object-type views.
|
|
for _, view := range []string{"userattributeview", "channelattributeview"} {
|
|
var viewExists bool
|
|
require.NoError(t, master.Get(&viewExists, "SELECT EXISTS (SELECT 1 FROM pg_matviews WHERE matviewname = $1)", view))
|
|
assert.True(t, viewExists, "%s should exist after up migration on fresh DB", view)
|
|
}
|
|
|
|
_, err = master.ExecNoTimeout(downSQL)
|
|
assert.NoError(t, err, "down migration should be a safe no-op on fresh DB")
|
|
}
|