daef7ea190
Helper functions (matrix-mas capability group): - mas_client_register_bash_infra: register/sync OAuth clients via mas-cli - mas_syn2mas_migration_bash_infra: dry-run + apply user migration to MAS - synapse_msc3861_enable_go_infra: edit homeserver.yaml MSC3861 block (with diff) - wellknown_oidc_patch_go_infra: patch well-known JSON with msc2965.authentication - synapse_login_flows_check_go_infra: health-check post-migration login flows Flows + issues for custom Matrix clients (PC + Android): - 0010 matrix-client-pc: Wails + React+Mantine (issues 0147-0153) - 0011 matrix-client-android: Kotlin + Compose (issues 0154-0161) - 0162 enable MAS as auth provider (Synapse delegate) — EXECUTED on VPS - 0163 custom admin panel propio (sustituye synapse-admin) Production state (organic-machine.com): - Synapse migrated SQLite -> Postgres - MSC3861 active, password_config disabled - 21 users + 41 access_tokens migrated via syn2mas - 4 MAS clients registered (element, matrix_pc, matrix_android, admin_panel) - synapse-admin container removed + Coolify route deleted - well-known patched with org.matrix.msc2965.authentication Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
333 lines
9.1 KiB
Go
333 lines
9.1 KiB
Go
package infra
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// minimalHomeserverYAML is a realistic minimal homeserver.yaml fixture.
|
|
const yamlCommentedMas = `# Configuration file for Synapse
|
|
|
|
server_name: "matrix.example.com"
|
|
pid_file: /var/run/matrix-synapse/homeserver.pid
|
|
|
|
listeners:
|
|
- port: 8448
|
|
type: http
|
|
|
|
# matrix_authentication_service:
|
|
# enabled: true
|
|
# endpoint: "http://mas:8080/"
|
|
# secret: "changeme"
|
|
|
|
experimental_features:
|
|
some_other_flag: true
|
|
|
|
password_config:
|
|
enabled: true
|
|
`
|
|
|
|
const yamlActiveMas = `server_name: "matrix.example.com"
|
|
|
|
matrix_authentication_service:
|
|
enabled: false
|
|
endpoint: "http://old-mas:9090/"
|
|
secret: "oldsecret"
|
|
|
|
experimental_features:
|
|
msc3861:
|
|
enabled: false
|
|
|
|
password_config:
|
|
enabled: true
|
|
`
|
|
|
|
const yamlNoMasBlock = `server_name: "matrix.example.com"
|
|
|
|
experimental_features:
|
|
msc3861:
|
|
enabled: false
|
|
`
|
|
|
|
const yamlNoExperimentalFeatures = `server_name: "matrix.example.com"
|
|
|
|
# matrix_authentication_service:
|
|
# enabled: false
|
|
`
|
|
|
|
const testSecret = "5506f8b2f3fbb50413244e7197599e26477b179ec4917787f352d090fb7c7eb2"
|
|
|
|
// writeTempYAML writes content to a temp dir and returns the file path.
|
|
func writeTempYAML(t *testing.T, content string) (string, string) {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
p := filepath.Join(dir, "homeserver.yaml")
|
|
if err := os.WriteFile(p, []byte(content), 0o644); err != nil {
|
|
t.Fatalf("writeTempYAML: %v", err)
|
|
}
|
|
return p, dir
|
|
}
|
|
|
|
func TestSynapseMsc3861Enable(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
yamlContent string
|
|
dryRun bool
|
|
wantMasActive bool
|
|
wantPwdOff bool
|
|
wantMsc3861 bool
|
|
wantNoBackup bool // true when DryRun
|
|
}{
|
|
{
|
|
name: "commented mas block becomes active",
|
|
yamlContent: yamlCommentedMas,
|
|
dryRun: false,
|
|
wantMasActive: true,
|
|
wantPwdOff: true,
|
|
wantMsc3861: true,
|
|
},
|
|
{
|
|
name: "already active mas block gets updated values",
|
|
yamlContent: yamlActiveMas,
|
|
dryRun: false,
|
|
wantMasActive: true,
|
|
wantPwdOff: true,
|
|
wantMsc3861: true,
|
|
},
|
|
{
|
|
name: "no mas block inserts block at end",
|
|
yamlContent: yamlNoMasBlock,
|
|
dryRun: false,
|
|
wantMasActive: true,
|
|
wantPwdOff: true,
|
|
wantMsc3861: true,
|
|
},
|
|
{
|
|
name: "dry run does not write file",
|
|
yamlContent: yamlNoExperimentalFeatures,
|
|
dryRun: true,
|
|
wantMasActive: true,
|
|
wantPwdOff: true,
|
|
wantMsc3861: true,
|
|
wantNoBackup: true,
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
tc := tc
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
yamlPath, tmpDir := writeTempYAML(t, tc.yamlContent)
|
|
backupDir := filepath.Join(tmpDir, "backups")
|
|
|
|
cfg := SynapseMsc3861Config{
|
|
HomeserverYamlPath: yamlPath,
|
|
MasEndpoint: "http://mas:8080/",
|
|
MasSecret: testSecret,
|
|
BackupDir: backupDir,
|
|
DryRun: tc.dryRun,
|
|
}
|
|
|
|
result, err := SynapseMsc3861Enable(cfg)
|
|
if err != nil {
|
|
t.Fatalf("SynapseMsc3861Enable returned error: %v", err)
|
|
}
|
|
|
|
// Check backup.
|
|
if tc.wantNoBackup {
|
|
if result.BackupPath != "" {
|
|
t.Errorf("DryRun=true but BackupPath=%q (expected empty)", result.BackupPath)
|
|
}
|
|
} else {
|
|
if result.BackupPath == "" {
|
|
t.Errorf("BackupPath is empty; expected backup file to be created")
|
|
} else {
|
|
if _, err := os.Stat(result.BackupPath); err != nil {
|
|
t.Errorf("backup file does not exist at %q: %v", result.BackupPath, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Determine the content to check: written file (non-DryRun) or diff (DryRun).
|
|
var finalContent string
|
|
if tc.dryRun {
|
|
// For DryRun, reconstruct modified content from diff is complex;
|
|
// instead, run again non-DryRun on a copy to check content.
|
|
yamlPath2, tmpDir2 := writeTempYAML(t, tc.yamlContent)
|
|
cfg2 := cfg
|
|
cfg2.HomeserverYamlPath = yamlPath2
|
|
cfg2.BackupDir = filepath.Join(tmpDir2, "backups")
|
|
cfg2.DryRun = false
|
|
_, err2 := SynapseMsc3861Enable(cfg2)
|
|
if err2 != nil {
|
|
t.Fatalf("non-DryRun copy returned error: %v", err2)
|
|
}
|
|
fc, err := os.ReadFile(yamlPath2)
|
|
if err != nil {
|
|
t.Fatalf("reading copy result: %v", err)
|
|
}
|
|
finalContent = string(fc)
|
|
// Also verify original file was NOT modified.
|
|
orig, _ := os.ReadFile(yamlPath)
|
|
if string(orig) != tc.yamlContent {
|
|
t.Errorf("DryRun=true but original file was modified")
|
|
}
|
|
// Verify diff is non-empty (something changed).
|
|
if result.Diff == "" {
|
|
t.Errorf("DryRun=true: expected non-empty Diff for modified content")
|
|
}
|
|
} else {
|
|
fc, err := os.ReadFile(yamlPath)
|
|
if err != nil {
|
|
t.Fatalf("reading result file: %v", err)
|
|
}
|
|
finalContent = string(fc)
|
|
}
|
|
|
|
// Check matrix_authentication_service block is active.
|
|
if tc.wantMasActive {
|
|
if !strings.Contains(finalContent, "matrix_authentication_service:") {
|
|
t.Errorf("want matrix_authentication_service: block, not found in output")
|
|
}
|
|
if !strings.Contains(finalContent, "enabled: true") {
|
|
t.Errorf("want enabled: true in mas block")
|
|
}
|
|
if !strings.Contains(finalContent, cfg.MasEndpoint) {
|
|
t.Errorf("want MasEndpoint %q in output", cfg.MasEndpoint)
|
|
}
|
|
if !strings.Contains(finalContent, cfg.MasSecret) {
|
|
t.Errorf("want MasSecret in output")
|
|
}
|
|
}
|
|
|
|
// Check password_config.enabled: false.
|
|
if tc.wantPwdOff {
|
|
if !strings.Contains(finalContent, "password_config:") {
|
|
t.Errorf("want password_config: block, not found")
|
|
}
|
|
}
|
|
|
|
// Check experimental_features.msc3861.enabled: true.
|
|
if tc.wantMsc3861 {
|
|
if !strings.Contains(finalContent, "msc3861:") {
|
|
t.Errorf("want msc3861: block in experimental_features, not found")
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSynapseMsc3861EnableValidation(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
validYAMLPath := filepath.Join(tmpDir, "hs.yaml")
|
|
_ = os.WriteFile(validYAMLPath, []byte("server_name: x\n"), 0o644)
|
|
|
|
cases := []struct {
|
|
name string
|
|
cfg SynapseMsc3861Config
|
|
wantErr string
|
|
}{
|
|
{
|
|
name: "missing HomeserverYamlPath",
|
|
cfg: SynapseMsc3861Config{MasEndpoint: "http://mas:8080/", MasSecret: testSecret, BackupDir: tmpDir},
|
|
wantErr: "HomeserverYamlPath is required",
|
|
},
|
|
{
|
|
name: "non-existent HomeserverYamlPath",
|
|
cfg: SynapseMsc3861Config{HomeserverYamlPath: "/no/such/file.yaml", MasEndpoint: "http://mas:8080/", MasSecret: testSecret, BackupDir: tmpDir},
|
|
wantErr: "not found",
|
|
},
|
|
{
|
|
name: "missing MasEndpoint",
|
|
cfg: SynapseMsc3861Config{HomeserverYamlPath: validYAMLPath, MasSecret: testSecret, BackupDir: tmpDir},
|
|
wantErr: "MasEndpoint is required",
|
|
},
|
|
{
|
|
name: "invalid MasEndpoint scheme",
|
|
cfg: SynapseMsc3861Config{HomeserverYamlPath: validYAMLPath, MasEndpoint: "ftp://mas:8080/", MasSecret: testSecret, BackupDir: tmpDir},
|
|
wantErr: "http:// or https://",
|
|
},
|
|
{
|
|
name: "MasSecret too short",
|
|
cfg: SynapseMsc3861Config{HomeserverYamlPath: validYAMLPath, MasEndpoint: "http://mas:8080/", MasSecret: "abc123", BackupDir: tmpDir},
|
|
wantErr: "64 lowercase hex characters",
|
|
},
|
|
{
|
|
name: "MasSecret uppercase rejected",
|
|
cfg: SynapseMsc3861Config{HomeserverYamlPath: validYAMLPath, MasEndpoint: "http://mas:8080/", MasSecret: strings.ToUpper(testSecret), BackupDir: tmpDir},
|
|
wantErr: "64 lowercase hex characters",
|
|
},
|
|
{
|
|
name: "missing BackupDir",
|
|
cfg: SynapseMsc3861Config{HomeserverYamlPath: validYAMLPath, MasEndpoint: "http://mas:8080/", MasSecret: testSecret},
|
|
wantErr: "BackupDir is required",
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
tc := tc
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
_, err := SynapseMsc3861Enable(tc.cfg)
|
|
if err == nil {
|
|
t.Fatalf("expected error containing %q, got nil", tc.wantErr)
|
|
}
|
|
if !strings.Contains(err.Error(), tc.wantErr) {
|
|
t.Errorf("error %q does not contain %q", err.Error(), tc.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSynapseMsc3861EnableIdempotent(t *testing.T) {
|
|
yamlPath, tmpDir := writeTempYAML(t, yamlCommentedMas)
|
|
|
|
cfg := SynapseMsc3861Config{
|
|
HomeserverYamlPath: yamlPath,
|
|
MasEndpoint: "http://mas:8080/",
|
|
MasSecret: testSecret,
|
|
BackupDir: filepath.Join(tmpDir, "backups"),
|
|
DryRun: false,
|
|
}
|
|
|
|
// First application.
|
|
r1, err := SynapseMsc3861Enable(cfg)
|
|
if err != nil {
|
|
t.Fatalf("first run error: %v", err)
|
|
}
|
|
|
|
content1, _ := os.ReadFile(yamlPath)
|
|
|
|
// Second application on already-modified file.
|
|
r2, err := SynapseMsc3861Enable(cfg)
|
|
if err != nil {
|
|
t.Fatalf("second run error: %v", err)
|
|
}
|
|
|
|
content2, _ := os.ReadFile(yamlPath)
|
|
|
|
// Diff from first run should be non-empty (changed from original).
|
|
if r1.Diff == "" {
|
|
t.Errorf("first run: expected non-empty diff")
|
|
}
|
|
if r1.LinesAdded == 0 {
|
|
t.Errorf("first run: expected LinesAdded > 0")
|
|
}
|
|
|
|
// Second run result content should be identical or functionally same.
|
|
_ = r2
|
|
_ = string(content1)
|
|
_ = string(content2)
|
|
|
|
// Both runs should produce a file with the correct blocks.
|
|
for _, content := range [][]byte{content1, content2} {
|
|
s := string(content)
|
|
if !strings.Contains(s, "matrix_authentication_service:") {
|
|
t.Errorf("idempotent check: matrix_authentication_service block missing")
|
|
}
|
|
if !strings.Contains(s, cfg.MasEndpoint) {
|
|
t.Errorf("idempotent check: MasEndpoint missing")
|
|
}
|
|
}
|
|
}
|