summaryrefslogtreecommitdiff
path: root/internal/sync/backup_test.go
blob: fd15d04f5eaa476ffb69e8ccf6d6c33dd8695a8e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
package sync

import (
	"errors"
	"fmt"
	stdsync "sync"
	"sync/atomic"
	"testing"

	"codeberg.org/snonux/gitsyncer/internal/config"
)

func TestHandlePushError_DisablesBackupForSession(t *testing.T) {
	syncer := &Syncer{}
	syncer.SetBackupEnabled(true)

	err := syncer.handlePushError("backup", &config.Organization{BackupLocation: true}, errors.New("dial tcp: connection refused"))
	if err != nil {
		t.Fatalf("expected backup push failure to be downgraded, got %v", err)
	}
	if syncer.backupActive() {
		t.Fatal("expected backup sync to be disabled for the remainder of the session")
	}
}

func TestHandlePushError_PropagatesPrimaryRemoteFailure(t *testing.T) {
	syncer := &Syncer{}
	syncer.SetBackupEnabled(true)

	pushErr := errors.New("push rejected")
	err := syncer.handlePushError("origin", &config.Organization{}, pushErr)
	if !errors.Is(err, pushErr) {
		t.Fatalf("expected primary remote error to be returned, got %v", err)
	}
}

func TestHandlePushError_BackupDisableIsIsolatedPerSyncer(t *testing.T) {
	backupOrg := &config.Organization{BackupLocation: true}

	syncerA := &Syncer{}
	syncerA.SetBackupEnabled(true)

	syncerB := &Syncer{}
	syncerB.SetBackupEnabled(true)

	err := syncerA.handlePushError("backup-a", backupOrg, errors.New("dial tcp: connection refused"))
	if err != nil {
		t.Fatalf("expected backup push failure to be downgraded, got %v", err)
	}

	if syncerA.backupActive() {
		t.Fatal("expected syncerA backup sync to be disabled for the remainder of the session")
	}
	if !syncerB.backupActive() {
		t.Fatal("expected syncerB backup session to remain active")
	}
}

func TestBackupSessionState_DisableIsThreadSafe(t *testing.T) {
	var session backupSessionState
	var firstDisableCount atomic.Int32

	const workers = 32
	var wg stdsync.WaitGroup
	wg.Add(workers)

	for i := 0; i < workers; i++ {
		go func(i int) {
			defer wg.Done()
			if session.disable(fmt.Sprintf("reason-%d", i)) {
				firstDisableCount.Add(1)
			}
		}(i)
	}

	wg.Wait()

	if got := firstDisableCount.Load(); got != 1 {
		t.Fatalf("expected exactly one successful disable transition, got %d", got)
	}

	disabled, reason := session.status()
	if !disabled {
		t.Fatal("expected backup session to be disabled")
	}
	if reason == "" {
		t.Fatal("expected disable reason to be recorded")
	}
}

func TestParseSSHLocation_SupportsSSHURLWithPort(t *testing.T) {
	t.Parallel()

	userHost, sshArgs, basePath, err := parseSSHLocation("ssh://git@r0:30022/repos")
	if err != nil {
		t.Fatalf("parseSSHLocation() error = %v", err)
	}
	if userHost != "git@r0" {
		t.Fatalf("userHost = %q, want %q", userHost, "git@r0")
	}
	if basePath != "/repos" {
		t.Fatalf("basePath = %q, want %q", basePath, "/repos")
	}

	wantArgs := []string{"-p", "30022", "git@r0"}
	if len(sshArgs) != len(wantArgs) {
		t.Fatalf("sshArgs = %#v, want %#v", sshArgs, wantArgs)
	}
	for i := range wantArgs {
		if sshArgs[i] != wantArgs[i] {
			t.Fatalf("sshArgs = %#v, want %#v", sshArgs, wantArgs)
		}
	}
}