Files
kopia/internal/server/server_test.go
Jarek Kowalski 675bf4e033 Removed manifest manager refresh + server improvements (#835)
* manifest: removed explicit refresh

Instead, content manager is exposing a revision counter that changes
on each mutation or index change. Manifest manager will be invalidated
whenever this is encountered.

* server: refactored initialization API

* server: added unit tests for repository server APIs (HTTP and REST)

* server: ensure we don't upload contents that already exist

This saves bandwidth, since the client can compute hash locally
and ask the server whether the object exists before starting the upload.
2021-02-15 23:55:58 -08:00

289 lines
7.0 KiB
Go

package server_test
import (
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"io/ioutil"
"net/http/httptest"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/kopia/kopia/internal/auth"
"github.com/kopia/kopia/internal/repotesting"
"github.com/kopia/kopia/internal/server"
"github.com/kopia/kopia/internal/testlogging"
"github.com/kopia/kopia/repo"
"github.com/kopia/kopia/repo/content"
"github.com/kopia/kopia/repo/manifest"
"github.com/kopia/kopia/repo/object"
"github.com/kopia/kopia/snapshot"
)
const (
testUsername = "foo"
testHostname = "bar"
testPassword = "123"
testPathname = "/tmp/path"
)
// nolint:thelper
func startServer(ctx context.Context, t *testing.T) *repo.APIServerInfo {
var env repotesting.Environment
env.Setup(t)
t.Cleanup(func() { env.Close(ctx, t) })
s, err := server.New(ctx, server.Options{
ConfigFile: env.ConfigFile(),
Authorizer: auth.LegacyAuthorizerForUser,
Authenticator: auth.AuthenticateSingleUser(testUsername+"@"+testHostname, testPassword),
RefreshInterval: 1 * time.Minute,
})
s.SetRepository(ctx, env.Repository)
if err != nil {
t.Fatal(err)
}
hs := httptest.NewUnstartedServer(s.GRPCRouterHandler(s.APIHandlers(true)))
hs.EnableHTTP2 = true
hs.StartTLS()
t.Cleanup(hs.Close)
serverHash := sha256.Sum256(hs.Certificate().Raw)
return &repo.APIServerInfo{
BaseURL: hs.URL,
TrustedServerCertificateFingerprint: hex.EncodeToString(serverHash[:]),
}
}
func TestServer_REST(t *testing.T) {
testServer(t, true)
}
func TestServer_GRPC(t *testing.T) {
testServer(t, false)
}
// nolint:thelper
func testServer(t *testing.T, disableGRPC bool) {
ctx := testlogging.ContextWithLevel(t, testlogging.LevelDebug)
apiServerInfo := startServer(ctx, t)
apiServerInfo.DisableGRPC = disableGRPC
rep, err := repo.OpenAPIServer(ctx, apiServerInfo, repo.ClientOptions{
Username: testUsername,
Hostname: testHostname,
}, testPassword)
if err != nil {
t.Fatal(err)
}
defer rep.Close(ctx)
remoteRepositoryTest(ctx, t, rep)
}
func TestGPRServer_AuthenticationError(t *testing.T) {
ctx := testlogging.ContextWithLevel(t, testlogging.LevelDebug)
apiServerInfo := startServer(ctx, t)
if _, err := repo.OpenGRPCAPIRepository(ctx, apiServerInfo, repo.ClientOptions{
Username: "bad-username",
Hostname: "bad-hostname",
}, "bad-password"); err == nil {
t.Fatal("unexpected success when connecting with invalid username")
}
}
// nolint:thelper
func remoteRepositoryTest(ctx context.Context, t *testing.T, rep repo.Repository) {
mustListSnapshotCount(ctx, t, rep, 0)
mustGetObjectNotFound(ctx, t, rep, "abcd")
mustGetManifestNotFound(ctx, t, rep, "mnosuchmanifest")
var (
result object.ID
manifestID, manifestID2 manifest.ID
written = []byte{1, 2, 3}
srcInfo = snapshot.SourceInfo{
Host: testHostname,
UserName: testUsername,
Path: testPathname,
}
)
var uploaded int64
must(t, repo.WriteSession(ctx, rep, repo.WriteSessionOptions{
Purpose: "write test",
OnUpload: func(i int64) {
uploaded += i
},
}, func(w repo.RepositoryWriter) error {
mustGetObjectNotFound(ctx, t, w, "abcd")
mustGetManifestNotFound(ctx, t, w, "mnosuchmanifest")
mustManifestNotFound(t, w.DeleteManifest(ctx, manifestID2))
mustListSnapshotCount(ctx, t, w, 0)
result = mustWriteObject(ctx, t, w, written)
if uploaded == 0 {
t.Fatalf("did not report uploaded bytes")
}
uploaded = 0
result2 := mustWriteObject(ctx, t, w, written)
if uploaded != 0 {
t.Fatalf("unexpected upload when writing duplicate object")
}
if result != result2 {
t.Fatalf("two identical object with different IDs: %v vs %v", result, result2)
}
// verify data is read back the same.
mustReadObject(ctx, t, w, result, written)
ow := w.NewObjectWriter(ctx, object.WriterOptions{
Prefix: content.ID(manifest.ContentPrefix),
})
_, err := ow.Write([]byte{2, 3, 4})
must(t, err)
_, err = ow.Result()
if err == nil {
t.Fatalf("unexpected success writing object with 'm' prefix")
}
manifestID, err = snapshot.SaveSnapshot(ctx, w, &snapshot.Manifest{
Source: srcInfo,
Description: "written",
})
must(t, err)
mustListSnapshotCount(ctx, t, w, 1)
manifestID2, err = snapshot.SaveSnapshot(ctx, w, &snapshot.Manifest{
Source: srcInfo,
Description: "written2",
})
must(t, err)
mustListSnapshotCount(ctx, t, w, 2)
mustReadManifest(ctx, t, w, manifestID, "written")
mustReadManifest(ctx, t, w, manifestID2, "written2")
must(t, w.DeleteManifest(ctx, manifestID2))
mustListSnapshotCount(ctx, t, w, 1)
mustGetManifestNotFound(ctx, t, w, manifestID2)
mustReadManifest(ctx, t, w, manifestID, "written")
return nil
}))
// data and manifest written in a session can be read outside of it
mustReadObject(ctx, t, rep, result, written)
mustReadManifest(ctx, t, rep, manifestID, "written")
mustGetManifestNotFound(ctx, t, rep, manifestID2)
mustListSnapshotCount(ctx, t, rep, 1)
}
func mustWriteObject(ctx context.Context, t *testing.T, w repo.RepositoryWriter, data []byte) object.ID {
t.Helper()
ow := w.NewObjectWriter(ctx, object.WriterOptions{})
_, err := ow.Write(data)
must(t, err)
result, err := ow.Result()
must(t, err)
return result
}
func mustReadObject(ctx context.Context, t *testing.T, r repo.Repository, oid object.ID, want []byte) {
t.Helper()
or, err := r.OpenObject(ctx, oid)
must(t, err)
data, err := ioutil.ReadAll(or)
must(t, err)
// verify data is read back the same.
if diff := cmp.Diff(data, want); diff != "" {
t.Fatalf("invalid object data, diff: %v", diff)
}
}
func mustReadManifest(ctx context.Context, t *testing.T, r repo.Repository, manID manifest.ID, want string) {
t.Helper()
man, err := snapshot.LoadSnapshot(ctx, r, manID)
must(t, err)
// verify data is read back the same.
if diff := cmp.Diff(man.Description, want); diff != "" {
t.Fatalf("invalid manifest data, diff: %v", diff)
}
}
func mustGetObjectNotFound(ctx context.Context, t *testing.T, r repo.Repository, oid object.ID) {
t.Helper()
if _, err := r.OpenObject(ctx, oid); !errors.Is(err, object.ErrObjectNotFound) {
t.Fatalf("unexpected non-existent object error: %v", err)
}
}
func mustGetManifestNotFound(ctx context.Context, t *testing.T, r repo.Repository, manID manifest.ID) {
t.Helper()
_, err := r.GetManifest(ctx, manID, nil)
mustManifestNotFound(t, err)
}
func mustListSnapshotCount(ctx context.Context, t *testing.T, rep repo.Repository, wantCount int) {
t.Helper()
snaps, err := snapshot.ListSnapshots(ctx, rep, snapshot.SourceInfo{
UserName: testUsername,
Host: testHostname,
Path: testPathname,
})
if err != nil {
t.Fatal(err)
}
if got, want := len(snaps), wantCount; got != want {
t.Fatalf("unexpected number of snapshots: %v, want %v", got, want)
}
}
func must(t *testing.T, err error) {
t.Helper()
if err != nil {
t.Fatal(err)
}
}
func mustManifestNotFound(t *testing.T, err error) {
t.Helper()
if !errors.Is(err, manifest.ErrNotFound) {
t.Fatalf("invalid error %v, wanted manifest not found", err)
}
}