GPLv3's "How to Apply These Terms" asks for a notice in each source file; this is the modern two-line SPDX form of it rather than the full paragraph. 82 files, including tests. The blank line after the header is load-bearing. In Go a comment block immediately preceding `package X` becomes the package doc comment, so without the separator the SPDX lines would be absorbed into the doc for the eleven packages whose doc.go (or main.go) opens with one, and `go doc` would print them. Verified it doesn't.
179 lines
6.0 KiB
Go
179 lines
6.0 KiB
Go
// SPDX-FileCopyrightText: 2026 LINUXexpert-org
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package backup
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestDownloadFileAcceptsMatchingChecksum(t *testing.T) {
|
|
content := "print('fake migration script')\n"
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Write([]byte(content))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
sum := sha256.Sum256([]byte(content))
|
|
expected := hex.EncodeToString(sum[:])
|
|
|
|
dest := filepath.Join(t.TempDir(), "script.py")
|
|
got, err := DownloadFile(context.Background(), nil, srv.URL, dest, expected)
|
|
if err != nil {
|
|
t.Fatalf("DownloadFile: %v", err)
|
|
}
|
|
if got != expected {
|
|
t.Errorf("returned checksum = %s, want %s", got, expected)
|
|
}
|
|
data, _ := os.ReadFile(dest)
|
|
if string(data) != content {
|
|
t.Errorf("downloaded content = %q, want %q", data, content)
|
|
}
|
|
}
|
|
|
|
func TestDownloadFileRejectsMismatchedChecksum(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Write([]byte("unexpected content"))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
dest := filepath.Join(t.TempDir(), "script.py")
|
|
_, err := DownloadFile(context.Background(), nil, srv.URL, dest, "0000000000000000000000000000000000000000000000000000000000000000")
|
|
if err == nil {
|
|
t.Fatal("DownloadFile should reject a checksum mismatch")
|
|
}
|
|
if _, statErr := os.Stat(dest); !os.IsNotExist(statErr) {
|
|
t.Error("DownloadFile should remove the file it wrote after a checksum mismatch")
|
|
}
|
|
}
|
|
|
|
func TestDownloadFileWithoutPinReturnsComputedHash(t *testing.T) {
|
|
content := "arbitrary content"
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Write([]byte(content))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
dest := filepath.Join(t.TempDir(), "script.py")
|
|
got, err := DownloadFile(context.Background(), nil, srv.URL, dest, "")
|
|
if err != nil {
|
|
t.Fatalf("DownloadFile: %v", err)
|
|
}
|
|
sum := sha256.Sum256([]byte(content))
|
|
want := hex.EncodeToString(sum[:])
|
|
if got != want {
|
|
t.Errorf("returned checksum = %s, want %s", got, want)
|
|
}
|
|
}
|
|
|
|
func TestRunSettingsDumpInvokesScriptWithFlags(t *testing.T) {
|
|
dir := t.TempDir()
|
|
log := argsFile(t, dir)
|
|
pythonDir := withFakeExecutable(t, "python3", fakeScriptLoggingArgs(log, "exit 0"))
|
|
|
|
err := RunSettingsDump(context.Background(), SettingsDumpOptions{
|
|
PythonPath: filepath.Join(pythonDir, "python3"),
|
|
ScriptPath: "/opt/migrate_v016.py",
|
|
URL: "https://mail.example.com",
|
|
Username: "admin",
|
|
Password: "hunter2",
|
|
SettingsPath: filepath.Join(dir, "settings.json"),
|
|
PrincipalsPath: filepath.Join(dir, "principals.json"),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("RunSettingsDump: %v", err)
|
|
}
|
|
got := readArgsFile(t, log)
|
|
for _, want := range []string{"/opt/migrate_v016.py", "dump", "--url https://mail.example.com", "--username admin"} {
|
|
if !strings.Contains(got, want) {
|
|
t.Errorf("script invoked with %q, missing %q", got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRunSettingsDumpPropagatesFailure(t *testing.T) {
|
|
pythonDir := withFakeExecutable(t, "python3", "#!/bin/sh\necho 'auth failed' >&2\nexit 1\n")
|
|
err := RunSettingsDump(context.Background(), SettingsDumpOptions{
|
|
PythonPath: filepath.Join(pythonDir, "python3"),
|
|
ScriptPath: "/opt/migrate_v016.py",
|
|
})
|
|
if err == nil {
|
|
t.Fatal("RunSettingsDump should error when the script exits non-zero")
|
|
}
|
|
if !strings.Contains(err.Error(), "auth failed") {
|
|
t.Errorf("error = %v, want it to include the script's stderr", err)
|
|
}
|
|
}
|
|
|
|
func TestRunSettingsConvertInvokesScriptWithFlags(t *testing.T) {
|
|
dir := t.TempDir()
|
|
log := argsFile(t, dir)
|
|
pythonDir := withFakeExecutable(t, "python3", fakeScriptLoggingArgs(log, "exit 0"))
|
|
|
|
err := RunSettingsConvert(context.Background(), SettingsConvertOptions{
|
|
PythonPath: filepath.Join(pythonDir, "python3"),
|
|
ScriptPath: "/opt/migrate_v016.py",
|
|
SettingsPath: filepath.Join(dir, "settings.json"),
|
|
PrincipalsPath: filepath.Join(dir, "principals.json"),
|
|
ConfigPath: filepath.Join(dir, "config.json"),
|
|
OutputPath: filepath.Join(dir, "export.json"),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("RunSettingsConvert: %v", err)
|
|
}
|
|
got := readArgsFile(t, log)
|
|
for _, want := range []string{"convert", "--settings", "--config", "--output"} {
|
|
if !strings.Contains(got, want) {
|
|
t.Errorf("script invoked with %q, missing %q", got, want)
|
|
}
|
|
}
|
|
if strings.Contains(got, "--patch-paths") {
|
|
t.Errorf("script invoked with %q, should not include --patch-paths when none given", got)
|
|
}
|
|
}
|
|
|
|
func TestRunSettingsConvertWithPatchPaths(t *testing.T) {
|
|
dir := t.TempDir()
|
|
log := argsFile(t, dir)
|
|
pythonDir := withFakeExecutable(t, "python3", fakeScriptLoggingArgs(log, "exit 0"))
|
|
|
|
err := RunSettingsConvert(context.Background(), SettingsConvertOptions{
|
|
PythonPath: filepath.Join(pythonDir, "python3"),
|
|
ScriptPath: "/opt/migrate_v016.py",
|
|
SettingsPath: filepath.Join(dir, "settings.json"),
|
|
PrincipalsPath: filepath.Join(dir, "principals.json"),
|
|
ConfigPath: filepath.Join(dir, "config.json"),
|
|
OutputPath: filepath.Join(dir, "export.json"),
|
|
PatchPaths: map[string]string{"/var/lib/stalwart": "/tmp/sandbox/stalwart"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("RunSettingsConvert: %v", err)
|
|
}
|
|
got := readArgsFile(t, log)
|
|
if !strings.Contains(got, "--patch-paths /var/lib/stalwart=/tmp/sandbox/stalwart") {
|
|
t.Errorf("script invoked with %q, missing the expected --patch-paths flag", got)
|
|
}
|
|
}
|
|
|
|
func TestRunSettingsConvertPropagatesFailure(t *testing.T) {
|
|
pythonDir := withFakeExecutable(t, "python3", "#!/bin/sh\necho 'unsupported settings key' >&2\nexit 1\n")
|
|
err := RunSettingsConvert(context.Background(), SettingsConvertOptions{
|
|
PythonPath: filepath.Join(pythonDir, "python3"),
|
|
ScriptPath: "/opt/migrate_v016.py",
|
|
})
|
|
if err == nil {
|
|
t.Fatal("RunSettingsConvert should error when the script exits non-zero")
|
|
}
|
|
if !strings.Contains(err.Error(), "unsupported settings key") {
|
|
t.Errorf("error = %v, want it to include the script's stderr", err)
|
|
}
|
|
}
|