blob: ea2e808b520b66df85dfd1d0f675d68dc4ef5769 [file] [log] [blame]
// Copyright 2016 The Upspin Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package test contains an integration test for all of Upspin.
package test
import (
"fmt"
"testing"
"upspin.io/access"
"upspin.io/bind"
"upspin.io/errors"
"upspin.io/key/usercache"
"upspin.io/path"
"upspin.io/test/testenv"
"upspin.io/upspin"
_ "upspin.io/pack/ee"
_ "upspin.io/pack/plain"
)
const (
contentsOfFile1 = "contents of file 1"
contentsOfFile2 = "contents of file 2..."
contentsOfFile3 = "===PDF PDF PDF=="
genericFileContents = "contents"
hasLocation = true
ownerName = "test@upspin.io"
snapshotUser = "test+snapshot@upspin.io"
readerName = "test-friend@upspin.io"
)
var (
errExist = errors.E(errors.Exist)
errNotExist = errors.E(errors.NotExist)
errPermission = errors.E(errors.Permission)
errPrivate = errors.E(errors.Private)
setupTemplate = testenv.Setup{
OwnerName: ownerName,
UpBox: true,
Cache: true,
Cleanup: cleanup,
}
readerConfig upspin.Config
)
func makeIntegrationTestTree(t *testing.T, r *testenv.Runner) {
// TODO(adg): The tests in this file rely on this directory tree
// existing at the root when they begin. We should probably consolidate
// these tests into a single test, as they cannot be run in isolation
// anyway.
r.As(ownerName)
r.MakeDirectory(ownerName + "/dir1")
r.MakeDirectory(ownerName + "/dir2")
r.Put(ownerName+"/dir1/file1.txt", contentsOfFile1)
r.Put(ownerName+"/dir2/file2.txt", contentsOfFile2)
r.Put(ownerName+"/dir2/file3.pdf", contentsOfFile3)
if r.Failed() {
t.Fatal(r.Diag())
}
}
func testNoReadersAllowed(t *testing.T, r *testenv.Runner) {
fileName := upspin.PathName(ownerName + "/dir1/file1.txt")
r.As(readerName)
r.Get(fileName)
if !r.Match(errPrivate) {
t.Fatal(r.Diag())
}
// But the owner can still read it.
r.As(ownerName)
r.Get(fileName)
if r.Failed() {
t.Fatal(r.Diag())
}
if r.Data != contentsOfFile1 {
t.Errorf("Expected contents %q, got %q", contentsOfFile1, r.Data)
}
}
func testAllowListAccess(t *testing.T, r *testenv.Runner) {
r.As(ownerName)
r.Put(ownerName+"/dir1/Access", "l:"+readerName)
// Check that readerClient can list file1, but can't read and therefore the Location is zeroed out.
file := ownerName + "/dir1/file1.txt"
r.As(readerName)
r.Glob(file)
if r.Failed() {
t.Fatal(r.Diag())
}
if len(r.Entries) != 1 {
t.Fatalf("Expected 1 entry, got %d", len(r.Entries))
}
checkDirEntry(t, r.Entries[0], ownerName+"/dir1/file1.txt", !hasLocation, 0)
// Ensure we can't read the data.
r.As(readerName)
r.Get(upspin.PathName(file))
if !r.Match(access.ErrPermissionDenied) {
t.Fatal(r.Diag())
}
}
func testAllowReadAccess(t *testing.T, r *testenv.Runner) {
// Owner has no delete permission (assumption tested in testDelete).
r.As(ownerName)
r.Put(ownerName+"/dir1/Access",
"l,r:"+readerName+"\nc,w,l,r:"+ownerName)
// Put file back again so we force keys to be re-wrapped.
r.Put(ownerName+"/dir1/file1.txt",
contentsOfFile1)
// Now try reading as the reader.
r.As(readerName)
r.Get(upspin.PathName(ownerName + "/dir1/file1.txt"))
if r.Failed() {
t.Fatal(r.Diag())
}
if r.Data != contentsOfFile1 {
t.Errorf("Expected contents %q, got %q", contentsOfFile1, r.Data)
}
}
func testCreateAndOpen(t *testing.T, r *testenv.Runner) {
filePath := upspin.PathName(path.Join(ownerName, "myotherfile.txt"))
r.As(ownerName)
r.Put(filePath, genericFileContents)
r.Get(filePath)
if r.Failed() {
t.Fatal(r.Diag())
}
if r.Data != genericFileContents {
t.Errorf("file content = %q, want %q", r.Data, genericFileContents)
}
}
func testGlobWithLimitedAccess(t *testing.T, r *testenv.Runner) {
dir1Pat := ownerName + "/dir1/*.txt"
dir2Pat := ownerName + "/dir2/*.txt"
bothPat := ownerName + "/dir*/*.txt"
checkDirs := func(config, pat string, want int) {
if r.Failed() {
t.Fatalf("%v globbing %v: %v", config, pat, r.Diag())
}
got := len(r.Entries)
if got == want {
return
}
for _, d := range r.Entries {
t.Log("got:", d.Name)
}
t.Fatalf("%v globbing %v saw %v dirs, want %v", config, pat, got, want)
}
// Owner sees both files.
r.As(ownerName)
r.Glob(bothPat)
checkDirs("owner", bothPat, 2)
checkDirEntry(t, r.Entries[0], ownerName+"/dir1/file1.txt", hasLocation, len(contentsOfFile1))
checkDirEntry(t, r.Entries[1], ownerName+"/dir2/file2.txt", hasLocation, len(contentsOfFile2))
// readerClient should be able to see /dir1/
r.As(readerName)
r.Glob(dir1Pat)
checkDirs("reader", dir1Pat, 1)
checkDirEntry(t, r.Entries[0], ownerName+"/dir1/file1.txt", hasLocation, len(contentsOfFile1))
// but not /dir2/
r.Glob(dir2Pat)
if !r.Match(errPrivate) {
t.Fatal(r.Diag())
}
// Without list access to the root, the reader can't glob /dir*.
r.Glob(bothPat)
if !r.Match(errPrivate) {
t.Fatal(r.Diag())
}
// Give the reader list access to the root.
r.As(ownerName)
r.Put(ownerName+"/Access",
"l:"+readerName+"\n*:"+ownerName)
// But don't give any access to /dir2/.
r.Put(ownerName+"/dir2/Access", "*:"+ownerName)
// Then try globbing the root again.
r.As(readerName)
r.Glob(bothPat)
checkDirs("reader after access", bothPat, 1)
checkDirEntry(t, r.Entries[0], ownerName+"/dir1/file1.txt", hasLocation, len(contentsOfFile1))
}
func testGlobWithPattern(t *testing.T, r *testenv.Runner) {
r.As(ownerName)
for i := 0; i <= 10; i++ {
r.MakeDirectory(upspin.PathName(fmt.Sprintf("%s/mydir%d", ownerName, i)))
}
r.Glob(ownerName + "/mydir[0-1]*")
if r.Failed() {
t.Fatal(r.Diag())
}
if len(r.Entries) != 3 {
t.Fatalf("Expected 3 paths, got %d", len(r.Entries))
}
if string(r.Entries[0].Name) != ownerName+"/mydir0" {
t.Errorf("Expected mydir0, got %s", r.Entries[0].Name)
}
if string(r.Entries[1].Name) != ownerName+"/mydir1" {
t.Errorf("Expected mydir1, got %s", r.Entries[1].Name)
}
if string(r.Entries[2].Name) != ownerName+"/mydir10" {
t.Errorf("Expected mydir10, got %s", r.Entries[2].Name)
}
}
func testDelete(t *testing.T, r *testenv.Runner) {
pathName := upspin.PathName(ownerName + "/dir2/file3.pdf")
r.As(ownerName)
r.Delete(pathName)
// Check it really deleted it (and is not being cached in memory).
r.Get(pathName)
if !r.Match(errNotExist) {
t.Fatal(r.Diag())
}
// But I can't delete files in dir1, since I lack permission.
pathName = upspin.PathName(ownerName + "/dir1/file1.txt")
r.Delete(pathName)
if !r.Match(access.ErrPermissionDenied) {
t.Fatal(r.Diag())
}
// But we can always remove the Access file.
r.Delete(upspin.PathName(ownerName + "/dir1/Access"))
// Now delete file1.txt
r.Delete(pathName)
if r.Failed() {
t.Fatal(r.Diag())
}
}
// testMetacharacters checks that we can handle files whose names
// contain Glob metacharacters.
func testMetacharacters(t *testing.T, r *testenv.Runner) {
const (
dir = ownerName + "/foo[*]bar"
subDir = dir + "/inner?"
subDirFile = subDir + "/file[]"
subDirFilePath = ownerName + "/foo???bar/in*/file??"
contents = "some text"
)
r.As(ownerName)
r.MakeDirectory(dir)
r.MakeDirectory(subDir)
r.Put(subDirFile, contents)
if r.Failed() {
t.Fatal(r.Diag())
}
r.Get(subDirFile)
if r.Failed() {
t.Fatal(r.Diag())
}
if r.Data != contents {
t.Errorf("Expected contents %q, got %q", contents, r.Data)
}
// Use Glob to access file a different way to verify it's as we expect.
r.Glob(subDirFilePath)
checkDirEntry(t, r.Entries[0], subDirFile, hasLocation, len(contents))
// Now clean up.
r.Delete(subDirFile)
r.Delete(subDir)
r.Delete(dir)
if r.Failed() {
t.Fatal(r.Diag())
}
}
func testRootDeletion(t *testing.T, r *testenv.Runner) {
r.As(readerName)
// readerName does not have a root.
r.Delete(readerName + "/")
if !r.Match(errNotExist) {
t.Fatal(r.Diag())
}
r.MakeDirectory(readerName + "/")
if r.Failed() {
t.Fatal(r.Diag())
}
// Now delete that root.
r.Delete(readerName + "/")
if r.Failed() {
t.Fatal(r.Diag())
}
// Creation can happen again.
r.MakeDirectory(readerName + "/")
if r.Failed() {
t.Fatal(r.Diag())
}
// And we delete it again so this test can be re-run on a persistent
// server.
r.Delete(readerName + "/")
if r.Failed() {
t.Fatal(r.Diag())
}
}
// integrationTests list all tests and their names. Order is important.
var integrationTests = []struct {
name string
fn func(*testing.T, *testenv.Runner)
}{
// These tests may be run independently.
{"GetErrors", testGetErrors},
{"GetLinkErrors", testGetLinkErrors},
{"PutErrors", testPutErrors},
{"PutLinkErrors", testPutLinkErrors},
{"MakeDirectoryErrors", testMakeDirectoryErrors},
{"MakeDirectoryLinkErrors", testMakeDirectoryLinkErrors},
{"WhichAccess", testWhichAccess},
{"WhichAccessErrors", testWhichAccessErrors},
{"WhichAccessLinkErrors", testWhichAccessLinkErrors},
{"GlobErrors", testGlobErrors},
{"GlobLinkErrors", testGlobLinkErrors},
{"SequenceNumbers", testSequenceNumbers},
{"RootDeletion", testRootDeletion},
{"ReadAccess", testReadAccess},
{"GroupAccess", testGroupAccess},
{"WriteReadAllAccessFile", testWriteReadAllAccessFile},
{"CreateAccessFile", testCreateAccessFile},
{"Metacharacters", testMetacharacters},
{"Watch", testWatchCurrent},
{"WatchErrors", testWatchErrors},
{"WatchNonExistentFile", testWatchNonExistentFile},
{"WatchNonExistentDir", testWatchNonExistentDir},
{"WatchForbiddenFile", testWatchForbiddenFile},
{"WatchSubtree", testWatchSubtree},
{"WatchFile", testWatchFile},
{"WatchNonExistentRoot", testWatchNonExistentRoot},
{"CopyEntries", testCopyEntries},
{"Snapshot", testSnapshot},
{"DeleteErrors", testDeleteErrors},
// Each of these tests depend on the output of the previous one.
{"NoReadersAllowed", testNoReadersAllowed},
{"AllowListAccess", testAllowListAccess},
{"AllowReadAccess", testAllowReadAccess},
{"CreateAndOpen", testCreateAndOpen},
{"GlobWithLimitedAccess", testGlobWithLimitedAccess},
{"GlobWithPattern", testGlobWithPattern},
{"Delete", testDelete},
}
const remoteTestMessage = `
error: cannot find keys for remote test users.
These tests are designed to be run against the test.upspin.io cluster,
which is only accessible by the Upspin core team at Google.
See upspin.io/key/testdata/remote/README for details.
Run the test suite with -short to skip these tests.
`
func testSelectedOnePacking(t *testing.T, setup testenv.Setup) {
usercache.ResetGlobal()
env, err := testenv.New(&setup)
if errors.Is(errors.NotExist, err) && setup.Kind == "remote" {
t.Fatal(remoteTestMessage)
}
if err != nil {
t.Fatal(err)
}
if err := cleanup(env); err != nil {
env.Exit()
t.Fatal(err)
}
readerConfig, err = env.NewUser(readerName)
if err != nil {
env.Exit()
t.Fatal(err)
}
snapshotConfig, err := env.NewUser(snapshotUser)
if err != nil {
env.Exit()
t.Fatal(err)
}
r := testenv.NewRunner()
r.AddUser(env.Config)
r.AddUser(readerConfig)
r.AddUser(snapshotConfig)
// Build the test tree (for the tests in this file).
makeIntegrationTestTree(t, r)
// The ordering here is important as each test adds state to the tree.
for _, test := range integrationTests {
t.Run(test.name, func(t *testing.T) { test.fn(t, r) })
}
if err := r.FlushCache(); err != nil {
t.Fatal(err)
}
if err := env.Exit(); err != nil {
t.Fatal(err)
}
}
func TestIntegration(t *testing.T) {
type testConfig struct {
kind string // "inprocess", "server", or "remote".
packing upspin.Packing
cache bool // Run a cacheserver for ownerName.
always bool // Always run this test, even with -test.short.
}
const noCache, cache = false, true
testConfigs := []testConfig{
{"inprocess", upspin.PlainPack, noCache, false},
{"inprocess", upspin.EEIntegrityPack, noCache, false},
{"inprocess", upspin.EEPack, noCache, true},
{"server", upspin.PlainPack, noCache, false},
{"server", upspin.EEIntegrityPack, noCache, false},
{"server", upspin.EEPack, noCache, true},
{"inprocess", upspin.EEPack, cache, true},
{"server", upspin.EEPack, cache, true},
{"remote", upspin.EEPack, noCache, false},
}
for _, config := range testConfigs {
setup := setupTemplate
setup.Kind = config.kind
setup.Packing = config.packing
setup.Cache = config.cache
if config.kind == "remote" {
setup.UpBox = false
}
name := fmt.Sprintf("kind=%v/packing=%v/cache=%t", config.kind, config.packing, config.cache)
t.Run(name, func(t *testing.T) {
if testing.Short() && !config.always {
t.Skip("skipping because -test.short is set")
}
testSelectedOnePacking(t, setup)
})
}
}
// checkDirEntry verifies a dir entry against expectations. size == 0 for don't check.
func checkDirEntry(t *testing.T, dirEntry *upspin.DirEntry, name string, hasLocation bool, size int) {
if dirEntry.Name != upspin.PathName(name) {
t.Errorf("Expected name %s, got %s", name, dirEntry.Name)
}
if loc := locationOf(dirEntry); loc == (upspin.Location{}) {
if hasLocation {
t.Errorf("%s has no location, expected one", name)
}
} else {
if !hasLocation {
t.Errorf("%s has location %v, want none", name, loc)
}
}
dSize, err := dirEntry.Size()
if err != nil {
t.Errorf("Size error: %s: %v", name, err)
}
if got, want := int(dSize), size; got != want {
t.Errorf("%s has size %d, want %d", name, got, want)
}
}
func locationOf(entry *upspin.DirEntry) upspin.Location {
if len(entry.Blocks) == 0 {
return upspin.Location{}
}
return entry.Blocks[0].Location
}
func cleanup(env *testenv.Env) error {
dir, err := bind.DirServer(env.Config, env.Config.DirEndpoint())
if err != nil {
return err
}
return deleteAll(dir, upspin.PathName(env.Config.UserName()+"/"))
}
// deleteAll recursively deletes the directory named by path through the
// provided DirServer, first deleting path/Access and then path/*.
func deleteAll(dir upspin.DirServer, path upspin.PathName) error {
if _, err := dir.Delete(path + "/Access"); err != nil {
if !errors.Is(errors.NotExist, err) {
return err
}
}
entries, err := dir.Glob(string(path + "/*"))
if err != nil && err != upspin.ErrFollowLink {
return err
}
for _, e := range entries {
if e.IsDir() {
if err := deleteAll(dir, e.Name); err != nil {
return err
}
}
if _, err := dir.Delete(e.Name); err != nil {
return err
}
}
return nil
}