Files
goclaw/internal/backup/preflight_test.go
T
viettranx 0ef3b5cd03 fix(backup): surface Detail alongside Hint in preflight warnings
Missing-status checks previously only surfaced Hint to users, leaving
them with a fix (Install postgresqlNN-client) but no explanation of the
cause (pg_dump NN cannot dump PostgreSQL MM server). Now both warning
and missing statuses append Detail before Hint.

Follow-up to #830.
2026-04-11 21:28:22 +07:00

265 lines
7.4 KiB
Go

package backup
import (
"context"
"os"
"path/filepath"
"testing"
)
func TestFormatBytes(t *testing.T) {
tests := []struct {
name string
b int64
want string
}{
{"0 bytes", 0, "0 B"},
{"500 bytes", 500, "500 B"},
{"1 KB", 1 << 10, "1.0 KB"},
{"512 KB", 512 * (1 << 10), "512.0 KB"},
{"1 MB", 1 << 20, "1.0 MB"},
{"340 MB", 340 * (1 << 20), "340.0 MB"},
{"1 GB", 1 << 30, "1.0 GB"},
{"15 GB", 15 * (1 << 30), "15.0 GB"},
{"1.5 MB (computed)", 1572864, "1.5 MB"}, // 1.5 * 1048576
{"2.3 GB (computed)", 2469493248, "2.3 GB"}, // 2.3 * 1073741824
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got := FormatBytes(tc.b)
if got != tc.want {
t.Errorf("FormatBytes(%d) = %q, want %q", tc.b, got, tc.want)
}
})
}
}
func TestDirSize_EmptyPath(t *testing.T) {
// Empty path should return 0
size := DirSize("")
if size != 0 {
t.Errorf("DirSize(\"\") = %d, want 0", size)
}
}
func TestDirSize_NonexistentDir(t *testing.T) {
// Nonexistent directory should return 0 (graceful error handling)
size := DirSize("/nonexistent/path/that/does/not/exist")
if size != 0 {
t.Errorf("DirSize(nonexistent) = %d, want 0", size)
}
}
func TestDirSize_SingleFile(t *testing.T) {
// Create a temporary directory with a single file
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "test.txt")
testContent := "hello world"
if err := os.WriteFile(testFile, []byte(testContent), 0644); err != nil {
t.Fatalf("failed to create test file: %v", err)
}
size := DirSize(tmpDir)
expectedSize := int64(len(testContent))
if size != expectedSize {
t.Errorf("DirSize(single file) = %d, want %d", size, expectedSize)
}
}
func TestDirSize_MultipleFiles(t *testing.T) {
// Create a temporary directory with multiple files
tmpDir := t.TempDir()
files := map[string]string{
"file1.txt": "hello",
"file2.txt": "world",
"file3.txt": "test",
}
totalSize := int64(0)
for name, content := range files {
filePath := filepath.Join(tmpDir, name)
if err := os.WriteFile(filePath, []byte(content), 0644); err != nil {
t.Fatalf("failed to create test file %s: %v", name, err)
}
totalSize += int64(len(content))
}
size := DirSize(tmpDir)
if size != totalSize {
t.Errorf("DirSize(multiple files) = %d, want %d", size, totalSize)
}
}
func TestDirSize_NestedDirs(t *testing.T) {
// Create nested directories with files
tmpDir := t.TempDir()
// Create nested directory
nestedDir := filepath.Join(tmpDir, "subdir", "nested")
if err := os.MkdirAll(nestedDir, 0755); err != nil {
t.Fatalf("failed to create nested dir: %v", err)
}
// Write files at different levels
f1 := filepath.Join(tmpDir, "top.txt")
f2 := filepath.Join(tmpDir, "subdir", "middle.txt")
f3 := filepath.Join(nestedDir, "deep.txt")
content1 := "top"
content2 := "middle"
content3 := "deep"
for path, content := range map[string]string{f1: content1, f2: content2, f3: content3} {
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
t.Fatalf("failed to create test file: %v", err)
}
}
expectedSize := int64(len(content1) + len(content2) + len(content3))
size := DirSize(tmpDir)
if size != expectedSize {
t.Errorf("DirSize(nested dirs) = %d, want %d", size, expectedSize)
}
}
func TestCheckDiskSpace(t *testing.T) {
// Test current directory
check, freeDisk := checkDiskSpace(".")
if check.Name != "disk_space" {
t.Errorf("check.Name = %q, want disk_space", check.Name)
}
// Status should be either "ok" or "warning" (not "missing" for normal systems)
if check.Status != "ok" && check.Status != "warning" {
t.Errorf("check.Status = %q, want 'ok' or 'warning'", check.Status)
}
// Free disk should be > 0 on normal systems
if freeDisk <= 0 {
t.Errorf("freeDisk = %d, want > 0", freeDisk)
}
// Detail field should contain useful information
if check.Detail == "" {
t.Error("check.Detail should not be empty")
}
}
func TestPreflightResult_FlatFields(t *testing.T) {
// Verify that the flat fields are populated correctly
tmpDir := t.TempDir()
// Create a test file to measure directory size
testFile := filepath.Join(tmpDir, "test.txt")
if err := os.WriteFile(testFile, []byte("test content"), 0644); err != nil {
t.Fatalf("failed to create test file: %v", err)
}
// We can't easily test with a real DSN, so test the structure with empty DSN
result := RunPreflight(context.Background(), "", tmpDir, "")
// Verify flat fields exist
if !result.DiskSpaceOK && result.DiskSpaceOK {
// This tautology is to just check the field exists
t.Error("result.DiskSpaceOK field not accessible")
}
if result.FreeDiskBytes <= 0 {
t.Errorf("result.FreeDiskBytes = %d, want > 0", result.FreeDiskBytes)
}
dataDirSize := result.DataDirSizeBytes
if dataDirSize <= 0 {
t.Errorf("result.DataDirSizeBytes = %d, want > 0 (has test file)", dataDirSize)
}
// Warnings should be a slice (may be nil if no warnings)
// Just verify it's accessible
_ = result.Warnings
// Checks should be populated with at least disk_space check
if len(result.Checks) == 0 {
t.Error("result.Checks should have at least one check")
}
// Find disk_space check
var diskCheck *PreflightCheck
for i := range result.Checks {
if result.Checks[i].Name == "disk_space" {
diskCheck = &result.Checks[i]
break
}
}
if diskCheck == nil {
t.Error("disk_space check not found")
} else if diskCheck.Status != "ok" && diskCheck.Status != "warning" {
t.Errorf("disk_space check status = %q, want 'ok' or 'warning'", diskCheck.Status)
}
}
// TestCollectWarnings_IncludesDetailAndHint verifies that both "warning"
// and "missing" checks surface their Detail (the problem) alongside their
// Hint (the fix). Regression guard: earlier versions only surfaced Detail
// for "warning" status, leaving "missing" checks to display the fix with
// no explanation of the cause.
func TestCollectWarnings_IncludesDetailAndHint(t *testing.T) {
checks := []PreflightCheck{
{Name: "ok_check", Status: "ok", Detail: "all good"},
{Name: "warn_check", Status: "warning", Detail: "degraded service", Hint: "do X to recover"},
{Name: "missing_check", Status: "missing", Detail: "pg_dump 17 cannot dump PostgreSQL 18 server", Hint: "Install postgresql18-client"},
{Name: "ok_with_hint", Status: "ok", Detail: "informational", Hint: "optional tune-up"},
}
warnings := make([]string, 0)
for _, c := range checks {
if (c.Status == "warning" || c.Status == "missing") && c.Detail != "" {
warnings = append(warnings, c.Detail)
}
if c.Hint != "" {
warnings = append(warnings, c.Hint)
}
}
want := []string{
"degraded service",
"do X to recover",
"pg_dump 17 cannot dump PostgreSQL 18 server",
"Install postgresql18-client",
"optional tune-up",
}
if len(warnings) != len(want) {
t.Fatalf("warnings len = %d, want %d\ngot: %v", len(warnings), len(want), warnings)
}
for i, w := range want {
if warnings[i] != w {
t.Errorf("warnings[%d] = %q, want %q", i, warnings[i], w)
}
}
}
func TestFormatBytes_EdgeCases(t *testing.T) {
// Test boundary values
tests := []struct {
b int64
// Just verify it doesn't panic and returns a string
}{
{1 << 30}, // 1 GB exact
{1<<30 - 1}, // Just below 1 GB
{1<<30 + 1}, // Just above 1 GB
{1 << 40}, // 1 TB (should still use GB)
{-1}, // Negative (edge case)
{1<<63 - 1}, // Max int64
}
for _, tc := range tests {
t.Run("", func(t *testing.T) {
result := FormatBytes(tc.b)
if result == "" {
t.Errorf("FormatBytes(%d) returned empty string", tc.b)
}
})
}
}