From 9959190e396814c009c2b22fee79fb2d25efff70 Mon Sep 17 00:00:00 2001
From: Michael Eischer <michael.eischer@fau.de>
Date: Sun, 14 Nov 2021 17:38:56 +0100
Subject: [PATCH] lock: Add integration test

The tests check that the wrapped context is properly canceled whenever
the repository is unlock or when the lock refresh fails.
---
 cmd/restic/lock_test.go | 130 ++++++++++++++++++++++++++++++++++++++++
 1 file changed, 130 insertions(+)
 create mode 100644 cmd/restic/lock_test.go

diff --git a/cmd/restic/lock_test.go b/cmd/restic/lock_test.go
new file mode 100644
index 000000000..70e864448
--- /dev/null
+++ b/cmd/restic/lock_test.go
@@ -0,0 +1,130 @@
+package main
+
+import (
+	"context"
+	"fmt"
+	"testing"
+	"time"
+
+	"github.com/restic/restic/internal/repository"
+	"github.com/restic/restic/internal/restic"
+	rtest "github.com/restic/restic/internal/test"
+)
+
+func openTestRepo(t *testing.T, wrapper backendWrapper) (*repository.Repository, func(), *testEnvironment) {
+	env, cleanup := withTestEnvironment(t)
+	if wrapper != nil {
+		env.gopts.backendTestHook = wrapper
+	}
+	testRunInit(t, env.gopts)
+
+	repo, err := OpenRepository(context.TODO(), env.gopts)
+	rtest.OK(t, err)
+	return repo, cleanup, env
+}
+
+func checkedLockRepo(ctx context.Context, t *testing.T, repo restic.Repository) (*restic.Lock, context.Context) {
+	lock, wrappedCtx, err := lockRepo(ctx, repo)
+	rtest.OK(t, err)
+	rtest.OK(t, wrappedCtx.Err())
+	if lock.Stale() {
+		t.Fatal("lock returned stale lock")
+	}
+	return lock, wrappedCtx
+}
+
+func TestLock(t *testing.T) {
+	repo, cleanup, _ := openTestRepo(t, nil)
+	defer cleanup()
+
+	lock, wrappedCtx := checkedLockRepo(context.Background(), t, repo)
+	unlockRepo(lock)
+	if wrappedCtx.Err() == nil {
+		t.Fatal("unlock did not cancel context")
+	}
+}
+
+func TestLockCancel(t *testing.T) {
+	repo, cleanup, _ := openTestRepo(t, nil)
+	defer cleanup()
+
+	ctx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+	lock, wrappedCtx := checkedLockRepo(ctx, t, repo)
+	cancel()
+	if wrappedCtx.Err() == nil {
+		t.Fatal("canceled parent context did not cancel context")
+	}
+
+	// unlockRepo should not crash
+	unlockRepo(lock)
+}
+
+func TestLockUnlockAll(t *testing.T) {
+	repo, cleanup, _ := openTestRepo(t, nil)
+	defer cleanup()
+
+	lock, wrappedCtx := checkedLockRepo(context.Background(), t, repo)
+	_, err := unlockAll(0)
+	rtest.OK(t, err)
+	if wrappedCtx.Err() == nil {
+		t.Fatal("canceled parent context did not cancel context")
+	}
+
+	// unlockRepo should not crash
+	unlockRepo(lock)
+}
+
+func TestLockConflict(t *testing.T) {
+	repo, cleanup, env := openTestRepo(t, nil)
+	defer cleanup()
+	repo2, err := OpenRepository(context.TODO(), env.gopts)
+	rtest.OK(t, err)
+
+	lock, _, err := lockRepoExclusive(context.Background(), repo)
+	rtest.OK(t, err)
+	defer unlockRepo(lock)
+	_, _, err = lockRepo(context.Background(), repo2)
+	if err == nil {
+		t.Fatal("second lock should have failed")
+	}
+}
+
+type writeOnceBackend struct {
+	restic.Backend
+	written bool
+}
+
+func (b *writeOnceBackend) Save(ctx context.Context, h restic.Handle, rd restic.RewindReader) error {
+	if b.written {
+		return fmt.Errorf("fail after first write")
+	}
+	b.written = true
+	return b.Backend.Save(ctx, h, rd)
+}
+
+func TestLockFailedRefresh(t *testing.T) {
+	repo, cleanup, _ := openTestRepo(t, func(r restic.Backend) (restic.Backend, error) {
+		return &writeOnceBackend{Backend: r}, nil
+	})
+	defer cleanup()
+
+	// reduce locking intervals to be suitable for testing
+	ri, rt := refreshInterval, refreshabilityTimeout
+	refreshInterval = 20 * time.Millisecond
+	refreshabilityTimeout = 100 * time.Millisecond
+	defer func() {
+		refreshInterval, refreshabilityTimeout = ri, rt
+	}()
+
+	lock, wrappedCtx := checkedLockRepo(context.Background(), t, repo)
+
+	select {
+	case <-wrappedCtx.Done():
+		// expected lock refresh failure
+	case <-time.After(time.Second):
+		t.Fatal("failed lock refresh did not cause context cancellation")
+	}
+	// unlockRepo should not crash
+	unlockRepo(lock)
+}