Files
fn_registry/functions/infra/synapse_msc3861_enable_test.go
T
egutierrez daef7ea190 feat(matrix): MAS migration helpers + 2 flows + 15 issues + capability group
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>
2026-05-24 22:53:33 +02:00

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")
}
}
}