package main import ( "archive/zip" "bytes" "context" "encoding/json" "fmt" "io" "math" "mime/multipart" "net/http" "sort" "strconv" "testing" "time" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neofs-sdk-go/client" "github.com/nspcc-dev/neofs-sdk-go/container" cid "github.com/nspcc-dev/neofs-sdk-go/container/id" "github.com/nspcc-dev/neofs-sdk-go/object" "github.com/nspcc-dev/neofs-sdk-go/policy" "github.com/nspcc-dev/neofs-sdk-go/pool" "github.com/spf13/viper" "github.com/stretchr/testify/require" "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/wait" ) type putResponse struct { CID string `json:"container_id"` OID string `json:"object_id"` } func TestIntegration(t *testing.T) { rootCtx := context.Background() aioImage := "nspccdev/neofs-aio-testcontainer:" versions := []string{"0.24.0", "0.25.1", "0.26.1", "latest"} key, err := keys.NewPrivateKeyFromHex("1dd37fba80fec4e6a6f13fd708d8dcb3b29def768017052f6c930fa1c5d90bbb") require.NoError(t, err) for _, version := range versions { ctx, cancel2 := context.WithCancel(rootCtx) aioContainer := createDockerContainer(ctx, t, aioImage+version) cancel := runServer() clientPool := getPool(ctx, t, key) CID := createContainer(ctx, t, clientPool) t.Run("simple put "+version, func(t *testing.T) { simplePut(ctx, t, clientPool, CID) }) t.Run("simple get "+version, func(t *testing.T) { simpleGet(ctx, t, clientPool, CID) }) t.Run("get by attribute "+version, func(t *testing.T) { getByAttr(ctx, t, clientPool, CID) }) t.Run("get zip "+version, func(t *testing.T) { getZip(ctx, t, clientPool, CID) }) cancel() err = aioContainer.Terminate(ctx) require.NoError(t, err) cancel2() } } func runServer() context.CancelFunc { cancelCtx, cancel := context.WithCancel(context.Background()) v := getDefaultConfig() l := newLogger(v) application := newApp(cancelCtx, WithConfig(v), WithLogger(l)) go application.Serve(cancelCtx) return cancel } func simplePut(ctx context.Context, t *testing.T, clientPool pool.Pool, CID *cid.ID) { content := "content of file" keyAttr, valAttr := "User-Attribute", "user value" attributes := map[string]string{ object.AttributeFileName: "newFile.txt", keyAttr: valAttr, } var buff bytes.Buffer w := multipart.NewWriter(&buff) fw, err := w.CreateFormFile("file", attributes[object.AttributeFileName]) require.NoError(t, err) _, err = io.Copy(fw, bytes.NewBufferString(content)) require.NoError(t, err) err = w.Close() require.NoError(t, err) request, err := http.NewRequest(http.MethodPost, "http://localhost:8082/upload/"+CID.String(), &buff) require.NoError(t, err) request.Header.Set("Content-Type", w.FormDataContentType()) request.Header.Set("X-Attribute-"+keyAttr, valAttr) resp, err := http.DefaultClient.Do(request) require.NoError(t, err) defer func() { err = resp.Body.Close() require.NoError(t, err) }() addr := &putResponse{} err = json.NewDecoder(resp.Body).Decode(addr) require.NoError(t, err) err = CID.Parse(addr.CID) require.NoError(t, err) oid := object.NewID() err = oid.Parse(addr.OID) require.NoError(t, err) objectAddress := object.NewAddress() objectAddress.SetContainerID(CID) objectAddress.SetObjectID(oid) payload := bytes.NewBuffer(nil) ops := new(client.GetObjectParams).WithAddress(objectAddress).WithPayloadWriter(payload) obj, err := clientPool.GetObject(ctx, ops) require.NoError(t, err) require.Equal(t, content, payload.String()) for _, attribute := range obj.Attributes() { require.Equal(t, attributes[attribute.Key()], attribute.Value()) } } func simpleGet(ctx context.Context, t *testing.T, clientPool pool.Pool, CID *cid.ID) { content := "content of file" attributes := map[string]string{ "some-attr": "some-get-value", } oid := putObject(ctx, t, clientPool, CID, content, attributes) resp, err := http.Get("http://localhost:8082/get/" + CID.String() + "/" + oid.String()) require.NoError(t, err) defer func() { err = resp.Body.Close() require.NoError(t, err) }() data, err := io.ReadAll(resp.Body) require.NoError(t, err) require.Equal(t, content, string(data)) for k, v := range attributes { require.Equal(t, v, resp.Header.Get("X-Attribute-"+k)) } } func getByAttr(ctx context.Context, t *testing.T, clientPool pool.Pool, CID *cid.ID) { keyAttr, valAttr := "some-attr", "some-get-by-attr-value" content := "content of file" attributes := map[string]string{keyAttr: valAttr} oid := putObject(ctx, t, clientPool, CID, content, attributes) expectedAttr := map[string]string{ "X-Attribute-" + keyAttr: valAttr, "x-object-id": oid.String(), "x-container-id": CID.String(), } resp, err := http.Get("http://localhost:8082/get_by_attribute/" + CID.String() + "/" + keyAttr + "/" + valAttr) require.NoError(t, err) defer func() { err = resp.Body.Close() require.NoError(t, err) }() data, err := io.ReadAll(resp.Body) require.NoError(t, err) require.Equal(t, content, string(data)) for k, v := range expectedAttr { require.Equal(t, v, resp.Header.Get(k)) } } func getZip(ctx context.Context, t *testing.T, clientPool pool.Pool, CID *cid.ID) { names := []string{"zipfolder/dir/name1.txt", "zipfolder/name2.txt"} contents := []string{"content of file1", "content of file2"} attributes1 := map[string]string{object.AttributeFileName: names[0]} attributes2 := map[string]string{object.AttributeFileName: names[1]} putObject(ctx, t, clientPool, CID, contents[0], attributes1) putObject(ctx, t, clientPool, CID, contents[1], attributes2) resp, err := http.Get("http://localhost:8082/zip/" + CID.String() + "/zipfolder") require.NoError(t, err) defer func() { err = resp.Body.Close() require.NoError(t, err) }() data, err := io.ReadAll(resp.Body) require.NoError(t, err) checkZip(t, data, resp.ContentLength, names, contents) // check nested folder resp2, err := http.Get("http://localhost:8082/zip/" + CID.String() + "/zipfolder/dir") require.NoError(t, err) defer func() { err = resp2.Body.Close() require.NoError(t, err) }() data2, err := io.ReadAll(resp2.Body) require.NoError(t, err) checkZip(t, data2, resp2.ContentLength, names[:1], contents[:1]) } func checkZip(t *testing.T, data []byte, length int64, names, contents []string) { readerAt := bytes.NewReader(data) zipReader, err := zip.NewReader(readerAt, length) require.NoError(t, err) require.Equal(t, len(names), len(zipReader.File)) sort.Slice(zipReader.File, func(i, j int) bool { return zipReader.File[i].FileHeader.Name < zipReader.File[j].FileHeader.Name }) for i, f := range zipReader.File { require.Equal(t, names[i], f.FileHeader.Name) rc, err := f.Open() require.NoError(t, err) all, err := io.ReadAll(rc) require.NoError(t, err) require.Equal(t, contents[i], string(all)) err = rc.Close() require.NoError(t, err) } } func createDockerContainer(ctx context.Context, t *testing.T, image string) testcontainers.Container { req := testcontainers.ContainerRequest{ Image: image, WaitingFor: wait.NewLogStrategy("aio container started").WithStartupTimeout(30 * time.Second), Name: "aio", Hostname: "aio", NetworkMode: "host", } aioC, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ContainerRequest: req, Started: true, }) require.NoError(t, err) return aioC } func getDefaultConfig() *viper.Viper { v := settings() v.SetDefault(cfgPeers+".0.address", "127.0.0.1:8080") v.SetDefault(cfgPeers+".0.weight", 1) return v } func getPool(ctx context.Context, t *testing.T, key *keys.PrivateKey) pool.Pool { pb := new(pool.Builder) pb.AddNode("localhost:8080", 1) opts := &pool.BuilderOptions{ Key: &key.PrivateKey, NodeConnectionTimeout: 5 * time.Second, NodeRequestTimeout: 5 * time.Second, SessionExpirationEpoch: math.MaxUint64, } clientPool, err := pb.Build(ctx, opts) require.NoError(t, err) return clientPool } func createContainer(ctx context.Context, t *testing.T, clientPool pool.Pool) *cid.ID { pp, err := policy.Parse("REP 1") require.NoError(t, err) cnr := container.New( container.WithPolicy(pp), container.WithCustomBasicACL(0x0FFFFFFF), container.WithAttribute(container.AttributeName, "friendlyName"), container.WithAttribute(container.AttributeTimestamp, strconv.FormatInt(time.Now().Unix(), 10))) cnr.SetOwnerID(clientPool.OwnerID()) CID, err := clientPool.PutContainer(ctx, cnr) require.NoError(t, err) fmt.Println(CID.String()) err = clientPool.WaitForContainerPresence(ctx, CID, &pool.ContainerPollingParams{ CreationTimeout: 15 * time.Second, PollInterval: 3 * time.Second, }) require.NoError(t, err) return CID } func putObject(ctx context.Context, t *testing.T, clientPool pool.Pool, CID *cid.ID, content string, attributes map[string]string) *object.ID { rawObject := object.NewRaw() rawObject.SetContainerID(CID) rawObject.SetOwnerID(clientPool.OwnerID()) var attrs []*object.Attribute for key, val := range attributes { attr := object.NewAttribute() attr.SetKey(key) attr.SetValue(val) attrs = append(attrs, attr) } rawObject.SetAttributes(attrs...) ops := new(client.PutObjectParams).WithObject(rawObject.Object()).WithPayloadReader(bytes.NewBufferString(content)) oid, err := clientPool.PutObject(ctx, ops) require.NoError(t, err) return oid }