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