// Copyright (C) 2019 Storj Labs, Inc. // See LICENSE for copying information. package pointerdb_test import ( "context" "crypto/tls" "crypto/x509" "errors" "fmt" "testing" "github.com/gogo/protobuf/proto" "github.com/golang/protobuf/ptypes" "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" "go.uber.org/zap" "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials" "google.golang.org/grpc/peer" "google.golang.org/grpc/status" "storj.io/storj/internal/testidentity" "storj.io/storj/pkg/auth" "storj.io/storj/pkg/pb" "storj.io/storj/pkg/pointerdb" "storj.io/storj/pkg/storage/meta" "storj.io/storj/pkg/storj" "storj.io/storj/satellite/console" "storj.io/storj/satellite/satellitedb" "storj.io/storj/storage" "storj.io/storj/storage/teststore" ) // mockAPIKeys is mock for api keys store of pointerdb type mockAPIKeys struct { info console.APIKeyInfo err error } // GetByKey return api key info for given key func (keys *mockAPIKeys) GetByKey(ctx context.Context, key console.APIKey) (*console.APIKeyInfo, error) { return &keys.info, keys.err } func TestServicePut(t *testing.T) { validAPIKey := console.APIKey{} apiKeys := &mockAPIKeys{} for i, tt := range []struct { apiKey []byte err error errString string }{ {[]byte(validAPIKey.String()), nil, ""}, {[]byte("wrong key"), nil, status.Errorf(codes.Unauthenticated, "Invalid API credential").Error()}, {nil, errors.New("put error"), status.Errorf(codes.Internal, "internal error").Error()}, } { ctx := context.Background() ctx = auth.WithAPIKey(ctx, tt.apiKey) errTag := fmt.Sprintf("Test case #%d", i) db := teststore.New() service := pointerdb.NewService(zap.NewNop(), db) s := pointerdb.NewServer(zap.NewNop(), service, nil, nil, pointerdb.Config{}, nil, apiKeys) path := "a/b/c" pr := pb.Pointer{} if tt.err != nil { db.ForceError++ } req := pb.PutRequest{Path: path, Pointer: &pr} _, err := s.Put(ctx, &req) if err != nil { assert.EqualError(t, err, tt.errString, errTag) } else { assert.NoError(t, err, errTag) } } } func TestServiceGet(t *testing.T) { ctx := context.Background() ca, err := testidentity.NewTestCA(ctx) assert.NoError(t, err) identity, err := ca.NewIdentity() assert.NoError(t, err) peerCertificates := make([]*x509.Certificate, 2) peerCertificates[0] = identity.Leaf peerCertificates[1] = identity.CA info := credentials.TLSInfo{State: tls.ConnectionState{PeerCertificates: peerCertificates}} validAPIKey := console.APIKey{} apiKeys := &mockAPIKeys{} // creating in-memory db and opening connection satdb, err := satellitedb.NewInMemory() defer func() { err = satdb.Close() assert.NoError(t, err) }() err = satdb.CreateTables() assert.NoError(t, err) for i, tt := range []struct { apiKey []byte err error errString string }{ {[]byte(validAPIKey.String()), nil, ""}, {[]byte("wrong key"), nil, status.Errorf(codes.Unauthenticated, "Invalid API credential").Error()}, {nil, errors.New("get error"), status.Errorf(codes.Internal, "internal error").Error()}, } { ctx = auth.WithAPIKey(ctx, tt.apiKey) ctx = peer.NewContext(ctx, &peer.Peer{AuthInfo: info}) errTag := fmt.Sprintf("Test case #%d", i) db := teststore.New() service := pointerdb.NewService(zap.NewNop(), db) allocation := pointerdb.NewAllocationSigner(identity, 45, satdb.CertDB()) s := pointerdb.NewServer(zap.NewNop(), service, allocation, nil, pointerdb.Config{}, identity, apiKeys) path := "a/b/c" pr := &pb.Pointer{SegmentSize: 123} prBytes, err := proto.Marshal(pr) assert.NoError(t, err, errTag) _ = db.Put(storage.Key(storj.JoinPaths(apiKeys.info.ProjectID.String(), path)), storage.Value(prBytes)) if tt.err != nil { db.ForceError++ } req := pb.GetRequest{Path: path} resp, err := s.Get(ctx, &req) if err != nil { assert.EqualError(t, err, tt.errString, errTag) } else { assert.NoError(t, err, errTag) assert.NoError(t, err, errTag) assert.True(t, pb.Equal(pr, resp.Pointer), errTag) assert.NotNil(t, resp.GetAuthorization()) assert.NotNil(t, resp.GetPba()) } } } func TestServiceDelete(t *testing.T) { validAPIKey := console.APIKey{} apiKeys := &mockAPIKeys{} for i, tt := range []struct { apiKey []byte err error errString string }{ {[]byte(validAPIKey.String()), nil, ""}, {[]byte("wrong key"), nil, status.Errorf(codes.Unauthenticated, "Invalid API credential").Error()}, {nil, errors.New("delete error"), status.Errorf(codes.Internal, "internal error").Error()}, } { ctx := context.Background() ctx = auth.WithAPIKey(ctx, tt.apiKey) errTag := fmt.Sprintf("Test case #%d", i) path := "a/b/c" db := teststore.New() _ = db.Put(storage.Key(storj.JoinPaths(apiKeys.info.ProjectID.String(), path)), storage.Value("hello")) service := pointerdb.NewService(zap.NewNop(), db) s := pointerdb.NewServer(zap.NewNop(), service, nil, nil, pointerdb.Config{}, nil, apiKeys) if tt.err != nil { db.ForceError++ } req := pb.DeleteRequest{Path: path} _, err := s.Delete(ctx, &req) if err != nil { assert.EqualError(t, err, tt.errString, errTag) } else { assert.NoError(t, err, errTag) } } } func TestServiceList(t *testing.T) { validAPIKey := console.APIKey{} apiKeys := &mockAPIKeys{} db := teststore.New() service := pointerdb.NewService(zap.NewNop(), db) server := pointerdb.NewServer(zap.NewNop(), service, nil, nil, pointerdb.Config{}, nil, apiKeys) pointer := &pb.Pointer{} pointer.CreationDate = ptypes.TimestampNow() pointerBytes, err := proto.Marshal(pointer) if err != nil { t.Fatal(err) } pointerValue := storage.Value(pointerBytes) items := []storage.ListItem{ {Key: storage.Key("sample.😶"), Value: pointerValue}, {Key: storage.Key("müsic"), Value: pointerValue}, {Key: storage.Key("müsic/söng1.mp3"), Value: pointerValue}, {Key: storage.Key("müsic/söng2.mp3"), Value: pointerValue}, {Key: storage.Key("müsic/album/söng3.mp3"), Value: pointerValue}, {Key: storage.Key("müsic/söng4.mp3"), Value: pointerValue}, {Key: storage.Key("ビデオ/movie.mkv"), Value: pointerValue}, } for i := range items { items[i].Key = storage.Key(storj.JoinPaths(apiKeys.info.ProjectID.String(), items[i].Key.String())) } err = storage.PutAll(db, items...) if err != nil { t.Fatal(err) } type Test struct { APIKey string Request pb.ListRequest Expected *pb.ListResponse Error func(i int, err error) } // TODO: ZZZ temporarily disabled until endpoint and service split // errorWithCode := func(code codes.Code) func(i int, err error) { // t.Helper() // return func(i int, err error) { // t.Helper() // if status.Code(err) != code { // t.Fatalf("%d: should fail with %v, got: %v", i, code, err) // } // } // } tests := []Test{ { APIKey: validAPIKey.String(), Request: pb.ListRequest{Recursive: true}, Expected: &pb.ListResponse{ Items: []*pb.ListResponse_Item{ {Path: "müsic"}, {Path: "müsic/album/söng3.mp3"}, {Path: "müsic/söng1.mp3"}, {Path: "müsic/söng2.mp3"}, {Path: "müsic/söng4.mp3"}, {Path: "sample.😶"}, {Path: "ビデオ/movie.mkv"}, }, }, }, { APIKey: validAPIKey.String(), Request: pb.ListRequest{Recursive: true, MetaFlags: meta.All}, Expected: &pb.ListResponse{ Items: []*pb.ListResponse_Item{ {Path: "müsic", Pointer: pointer}, {Path: "müsic/album/söng3.mp3", Pointer: pointer}, {Path: "müsic/söng1.mp3", Pointer: pointer}, {Path: "müsic/söng2.mp3", Pointer: pointer}, {Path: "müsic/söng4.mp3", Pointer: pointer}, {Path: "sample.😶", Pointer: pointer}, {Path: "ビデオ/movie.mkv", Pointer: pointer}, }, }, }, // { // TODO: ZZZ temporarily disabled until endpoint and service split // APIKey: "wrong key", // Request: pb.ListRequest{Recursive: true, MetaFlags: meta.All}, //, APIKey: []byte("wrong key")}, // Error: errorWithCode(codes.Unauthenticated), // }, { APIKey: validAPIKey.String(), Request: pb.ListRequest{Recursive: true, Limit: 3}, Expected: &pb.ListResponse{ Items: []*pb.ListResponse_Item{ {Path: "müsic"}, {Path: "müsic/album/söng3.mp3"}, {Path: "müsic/söng1.mp3"}, }, More: true, }, }, { APIKey: validAPIKey.String(), Request: pb.ListRequest{MetaFlags: meta.All}, Expected: &pb.ListResponse{ Items: []*pb.ListResponse_Item{ {Path: "müsic", Pointer: pointer}, {Path: "müsic/", IsPrefix: true}, {Path: "sample.😶", Pointer: pointer}, {Path: "ビデオ/", IsPrefix: true}, }, More: false, }, }, { APIKey: validAPIKey.String(), Request: pb.ListRequest{EndBefore: "ビデオ"}, Expected: &pb.ListResponse{ Items: []*pb.ListResponse_Item{ {Path: "müsic"}, {Path: "müsic/", IsPrefix: true}, {Path: "sample.😶"}, }, More: false, }, }, { APIKey: validAPIKey.String(), Request: pb.ListRequest{Recursive: true, Prefix: "müsic/"}, Expected: &pb.ListResponse{ Items: []*pb.ListResponse_Item{ {Path: "album/söng3.mp3"}, {Path: "söng1.mp3"}, {Path: "söng2.mp3"}, {Path: "söng4.mp3"}, }, }, }, { APIKey: validAPIKey.String(), Request: pb.ListRequest{Recursive: true, Prefix: "müsic/", StartAfter: "album/söng3.mp3"}, Expected: &pb.ListResponse{ Items: []*pb.ListResponse_Item{ {Path: "söng1.mp3"}, {Path: "söng2.mp3"}, {Path: "söng4.mp3"}, }, }, }, { APIKey: validAPIKey.String(), Request: pb.ListRequest{Prefix: "müsic/"}, Expected: &pb.ListResponse{ Items: []*pb.ListResponse_Item{ {Path: "album/", IsPrefix: true}, {Path: "söng1.mp3"}, {Path: "söng2.mp3"}, {Path: "söng4.mp3"}, }, }, }, { APIKey: validAPIKey.String(), Request: pb.ListRequest{Prefix: "müsic/", StartAfter: "söng1.mp3"}, Expected: &pb.ListResponse{ Items: []*pb.ListResponse_Item{ {Path: "söng2.mp3"}, {Path: "söng4.mp3"}, }, }, }, { APIKey: validAPIKey.String(), Request: pb.ListRequest{Prefix: "müsic/", EndBefore: "söng4.mp3"}, Expected: &pb.ListResponse{ Items: []*pb.ListResponse_Item{ {Path: "album/", IsPrefix: true}, {Path: "söng1.mp3"}, {Path: "söng2.mp3"}, }, }, }, { APIKey: validAPIKey.String(), Request: pb.ListRequest{Prefix: "müs", Recursive: true, EndBefore: "ic/söng4.mp3", Limit: 1}, Expected: &pb.ListResponse{ Items: []*pb.ListResponse_Item{ // {Path: "ic/söng2.mp3"}, }, // More: true, }, }, } // TODO: // pb.ListRequest{Prefix: "müsic/", StartAfter: "söng1.mp3", EndBefore: "söng4.mp3"}, // failing database for i, test := range tests { ctx := context.Background() ctx = auth.WithAPIKey(ctx, []byte(test.APIKey)) resp, err := server.List(ctx, &test.Request) if test.Error == nil { if err != nil { t.Fatalf("%d: failed %v", i, err) } } else { test.Error(i, err) } if diff := cmp.Diff(test.Expected, resp, cmp.Comparer(pb.Equal)); diff != "" { t.Errorf("%d: (-want +got) %v\n%s", i, test.Request.String(), diff) } } }