2024-10-15 09:31:18 +00:00
|
|
|
package frostfs
|
|
|
|
|
|
|
|
// Tests for our FrostFS client code
|
|
|
|
|
|
|
|
import (
|
2024-10-16 09:05:26 +00:00
|
|
|
"bytes"
|
2024-10-15 09:31:18 +00:00
|
|
|
"context"
|
|
|
|
"crypto/ecdsa"
|
2024-10-16 09:05:26 +00:00
|
|
|
"crypto/sha256"
|
2024-10-15 09:47:54 +00:00
|
|
|
"os"
|
2024-10-15 09:31:18 +00:00
|
|
|
"sync"
|
2024-10-15 13:52:53 +00:00
|
|
|
"testing"
|
2024-10-16 13:18:20 +00:00
|
|
|
"time"
|
2024-10-15 12:42:46 +00:00
|
|
|
|
|
|
|
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
|
2024-10-16 09:05:26 +00:00
|
|
|
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
|
|
|
"github.com/nspcc-dev/neo-go/pkg/wallet"
|
2024-10-15 09:31:18 +00:00
|
|
|
)
|
|
|
|
|
2024-10-15 13:52:53 +00:00
|
|
|
// Initialize storage backend for tests.
|
2024-10-15 09:31:18 +00:00
|
|
|
func openStorage(t *testing.T) *Storage {
|
2024-10-15 13:52:53 +00:00
|
|
|
t.Helper()
|
2024-10-16 09:05:26 +00:00
|
|
|
cid := os.Getenv("FROSTFS_CONTAINER") // sample: "348WWfBKbS79Wbmm38MRE3oBoEDM5Ga1XXbGKGNyisDM"
|
2024-10-15 13:52:53 +00:00
|
|
|
endpoint := os.Getenv("FROSTFS_ENDPOINT") // sample: "grpc://localhost:8802"
|
2024-10-15 09:47:54 +00:00
|
|
|
if cid == "" || endpoint == "" {
|
2024-10-16 09:05:26 +00:00
|
|
|
t.Skipf("one or more environment variables not set: FROSTFS_ENDPOINT, FROSTFS_CONTAINER")
|
2024-10-15 09:47:54 +00:00
|
|
|
}
|
2024-10-16 09:05:26 +00:00
|
|
|
key, err := getKey(
|
|
|
|
os.Getenv("FROSTFS_WALLET"),
|
|
|
|
os.Getenv("FROSTFS_WALLET_ACCOUNT"),
|
|
|
|
os.Getenv("FROSTFS_WALLET_PASSWORD"),
|
|
|
|
)
|
2024-10-15 09:31:18 +00:00
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
2024-10-15 09:47:54 +00:00
|
|
|
storage, err := Open(endpoint, cid, key)
|
2024-10-15 09:31:18 +00:00
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
return storage
|
|
|
|
}
|
|
|
|
|
2024-10-15 13:52:53 +00:00
|
|
|
// Save some bytes to FrostFS and clean up after.
|
2024-10-15 09:31:18 +00:00
|
|
|
func TestObjectPutDelete(t *testing.T) {
|
2024-10-15 13:52:53 +00:00
|
|
|
payload := []byte("Hello FrostFS!\n")
|
2024-10-15 09:31:18 +00:00
|
|
|
storage := openStorage(t)
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
t.Cleanup(cancel)
|
|
|
|
oid, err := storage.Save(ctx, payload, "FileName", "hello_from_sdk.txt", "Tag", "foobar")
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
t.Logf("saved object: %s", oid)
|
|
|
|
err = storage.Delete(ctx, oid)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
t.Logf("deleted object: %s", oid)
|
|
|
|
}
|
|
|
|
|
2024-10-15 13:52:53 +00:00
|
|
|
// Check that FrostFS is in fact a content addressable storage.
|
2024-10-15 09:31:18 +00:00
|
|
|
func TestMulipleObjects(t *testing.T) {
|
2024-10-15 13:52:53 +00:00
|
|
|
payload := []byte("Multiple objects with same content should get the same object ID")
|
2024-10-15 09:31:18 +00:00
|
|
|
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
t.Cleanup(cancel)
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
|
|
|
|
objects := make(chan string)
|
|
|
|
go func() {
|
|
|
|
baseline := ""
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case <-ctx.Done():
|
|
|
|
return
|
|
|
|
case obj, ok := <-objects:
|
|
|
|
if !ok {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if baseline == "" {
|
|
|
|
baseline = obj
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if obj != baseline {
|
2024-10-16 09:05:26 +00:00
|
|
|
t.Errorf("non-identical object id: %s != %s", baseline, obj)
|
|
|
|
return
|
2024-10-15 09:31:18 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
const testCount = 5
|
|
|
|
storage := openStorage(t)
|
2024-10-15 13:52:53 +00:00
|
|
|
for i := 0; i < testCount; i++ { //nolint:intrange
|
2024-10-15 09:31:18 +00:00
|
|
|
wg.Add(1)
|
|
|
|
go func() {
|
|
|
|
defer wg.Done()
|
|
|
|
oid, err := storage.Save(ctx, payload, "FileName", "CAS.txt", "Tag", "test")
|
|
|
|
if err != nil {
|
2024-10-16 09:05:26 +00:00
|
|
|
t.Error(err)
|
|
|
|
return
|
2024-10-15 09:31:18 +00:00
|
|
|
}
|
|
|
|
err = storage.Delete(ctx, oid)
|
|
|
|
if err != nil {
|
2024-10-16 09:05:26 +00:00
|
|
|
t.Error(err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
select {
|
|
|
|
case objects <- oid:
|
|
|
|
case <-ctx.Done():
|
|
|
|
return
|
2024-10-15 09:31:18 +00:00
|
|
|
}
|
|
|
|
t.Log(oid)
|
|
|
|
}()
|
|
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
}
|
2024-10-15 12:42:46 +00:00
|
|
|
|
2024-10-15 13:52:53 +00:00
|
|
|
// Check opening wallet from file system.
|
2024-10-15 12:42:46 +00:00
|
|
|
func TestLoadWallet(t *testing.T) {
|
|
|
|
const (
|
|
|
|
walletPath = "client_test_wallet.json"
|
|
|
|
walletAccount = "NWZnjbTKbzwtX6w1q5R3kbEKrnJ5bp1kn7"
|
|
|
|
)
|
|
|
|
key, err := getKey(walletPath, "", "")
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
if key2addr(key) != walletAccount {
|
|
|
|
t.Fatalf("incorrect address for default account: want %s, got %s", walletAccount, key2addr(key))
|
|
|
|
}
|
|
|
|
key, err = getKey(walletPath, walletAccount, "")
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
if key2addr(key) != walletAccount {
|
|
|
|
t.Fatalf("incorrect address for specific account: want %s, got %s", walletAccount, key2addr(key))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func key2addr(k *ecdsa.PrivateKey) string {
|
|
|
|
var owner user.ID
|
|
|
|
user.IDFromKey(&owner, k.PublicKey)
|
2024-10-15 13:52:53 +00:00
|
|
|
return owner.String()
|
2024-10-15 12:42:46 +00:00
|
|
|
}
|
2024-10-16 09:05:26 +00:00
|
|
|
|
|
|
|
// Check that loaded wallet key generates valid signature.
|
|
|
|
func TestLoadWalletAndSign(t *testing.T) {
|
|
|
|
const (
|
|
|
|
walletPath = "client_test_wallet.json"
|
|
|
|
walletAccount = "NWZnjbTKbzwtX6w1q5R3kbEKrnJ5bp1kn7"
|
|
|
|
walletAccountPassword = ""
|
|
|
|
walletAccountIndex = 0
|
|
|
|
dataString = "some data to sign in this test"
|
|
|
|
)
|
|
|
|
w, err := wallet.NewWalletFromFile(walletPath)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("wallet from file: %v", err)
|
|
|
|
}
|
|
|
|
accountFromWallet := w.Accounts[walletAccountIndex]
|
|
|
|
err = accountFromWallet.Decrypt(walletAccountPassword, w.Scrypt)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("wallet decrypt: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
key, err := getKey(walletPath, walletAccount, walletAccountPassword)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
wrappedKey := &keys.PrivateKey{PrivateKey: *key}
|
|
|
|
accountFromKey := wallet.NewAccountFromPrivateKey(wrappedKey)
|
|
|
|
|
|
|
|
if !accountFromKey.PublicKey().Equal(accountFromWallet.PublicKey()) {
|
|
|
|
t.Fatalf("corrupted key: want %s, got %s", accountFromWallet.PublicKey().Address(), accountFromKey.PublicKey().Address())
|
|
|
|
}
|
|
|
|
data := []byte(dataString)
|
|
|
|
sig := accountFromKey.PrivateKey().Sign(data)
|
|
|
|
hash := sha256.Sum256(data)
|
|
|
|
hashSig := accountFromKey.PrivateKey().SignHash(hash)
|
|
|
|
if !bytes.Equal(sig, hashSig) {
|
|
|
|
t.Fatalf("different signatures for data (%x) and for hash (%x)", sig, hashSig)
|
|
|
|
}
|
|
|
|
ok := accountFromKey.PublicKey().Verify(sig, hash[:])
|
|
|
|
if !ok {
|
|
|
|
t.Errorf("signature %x (%d bytes): check failed: signing key", sig, len(sig))
|
|
|
|
}
|
|
|
|
ok = accountFromWallet.PublicKey().Verify(sig, hash[:])
|
|
|
|
if !ok {
|
|
|
|
t.Errorf("signature %x (%d bytes): check failed: wallet key", sig, len(sig))
|
|
|
|
}
|
|
|
|
}
|
2024-10-16 13:18:20 +00:00
|
|
|
|
|
|
|
// Show epoch values for nearby times.
|
|
|
|
//
|
|
|
|
// This test is barely useful when run unattended: it will only validate "no
|
|
|
|
// errors, no panic" result.
|
|
|
|
// Human operator is required to check if epoch calculations actually make
|
|
|
|
// sense for network configuration being used.
|
|
|
|
func TestEpoch(t *testing.T) {
|
|
|
|
storage := openStorage(t)
|
|
|
|
now := time.Now()
|
|
|
|
var i time.Duration
|
|
|
|
for i = -10; i < 10; i++ {
|
|
|
|
timestamp := now.Add(i * time.Minute) //nolint:durationcheck
|
|
|
|
epoch, err := storage.Epoch(context.Background(), timestamp)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
t.Logf("%s: %d", timestamp, epoch)
|
|
|
|
}
|
|
|
|
}
|