tag_test.go 14.1 KB
Newer Older
1 2 3 4 5 6 7 8
// Copyright 2020 The Swarm 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 api_test

import (
	"bytes"
9
	"encoding/json"
10
	"fmt"
11
	"io/ioutil"
12 13 14 15 16
	"net/http"
	"strconv"
	"strings"
	"testing"

17 18 19
	"github.com/ethersphere/bee/pkg/logging"
	statestore "github.com/ethersphere/bee/pkg/statestore/mock"

20 21 22 23 24
	"github.com/ethersphere/bee/pkg/api"
	"github.com/ethersphere/bee/pkg/jsonhttp"
	"github.com/ethersphere/bee/pkg/jsonhttp/jsonhttptest"
	mp "github.com/ethersphere/bee/pkg/pusher/mock"
	"github.com/ethersphere/bee/pkg/storage/mock"
acud's avatar
acud committed
25
	testingc "github.com/ethersphere/bee/pkg/storage/testing"
26 27 28 29 30 31 32 33 34 35
	"github.com/ethersphere/bee/pkg/swarm"
	"github.com/ethersphere/bee/pkg/swarm/test"
	"github.com/ethersphere/bee/pkg/tags"
	"gitlab.com/nolash/go-mockbytes"
)

type fileUploadResponse struct {
	Reference swarm.Address `json:"reference"`
}

acud's avatar
acud committed
36 37
func tagsWithIdResource(id uint32) string { return fmt.Sprintf("/tags/%d", id) }

38 39
func TestTags(t *testing.T) {
	var (
acud's avatar
acud committed
40 41 42 43 44
		filesResource  = "/files"
		dirResource    = "/dirs"
		bytesResource  = "/bytes"
		chunksResource = func(addr swarm.Address) string { return "/chunks/" + addr.String() }
		tagsResource   = "/tags"
acud's avatar
acud committed
45
		chunk          = testingc.GenerateTestRandomChunk()
acud's avatar
acud committed
46 47 48 49 50 51
		someTagName    = "file.jpg"
		mockStatestore = statestore.NewStateStore()
		logger         = logging.New(ioutil.Discard, 0)
		tag            = tags.NewTags(mockStatestore, logger)
		mockPusher     = mp.NewMockPusher(tag)
		client, _, _   = newTestServer(t, testServerOptions{
52 53 54 55 56
			Storer: mock.NewStorer(),
			Tags:   tag,
		})
	)

acud's avatar
acud committed
57
	t.Run("create unnamed tag", func(t *testing.T) {
58
		tr := api.TagResponse{}
59 60 61 62
		jsonhttptest.Request(t, client, http.MethodPost, tagsResource, http.StatusCreated,
			jsonhttptest.WithJSONRequestBody(api.TagRequest{}),
			jsonhttptest.WithUnmarshalJSONResponse(&tr),
		)
63 64 65 66 67 68

		if !strings.Contains(tr.Name, "unnamed_tag_") {
			t.Fatalf("expected tag name to contain %s but is %s instead", "unnamed_tag_", tr.Name)
		}
	})

acud's avatar
acud committed
69
	t.Run("create tag with name", func(t *testing.T) {
70
		tr := api.TagResponse{}
71 72 73 74 75 76
		jsonhttptest.Request(t, client, http.MethodPost, tagsResource, http.StatusCreated,
			jsonhttptest.WithJSONRequestBody(api.TagRequest{
				Name: someTagName,
			}),
			jsonhttptest.WithUnmarshalJSONResponse(&tr),
		)
77 78 79 80 81 82

		if tr.Name != someTagName {
			t.Fatalf("expected tag name to be %s but is %s instead", someTagName, tr.Name)
		}
	})

acud's avatar
acud committed
83
	t.Run("create tag with invalid id", func(t *testing.T) {
acud's avatar
acud committed
84 85
		jsonhttptest.Request(t, client, http.MethodPost, chunksResource(chunk.Address()), http.StatusInternalServerError,
			jsonhttptest.WithRequestBody(bytes.NewReader(chunk.Data())),
86 87 88 89 90 91
			jsonhttptest.WithExpectedJSONResponse(jsonhttp.StatusResponse{
				Message: "cannot get or create tag",
				Code:    http.StatusInternalServerError,
			}),
			jsonhttptest.WithRequestHeader(api.SwarmTagUidHeader, "invalid_id.jpg"), // the value should be uint32
		)
92 93
	})

acud's avatar
acud committed
94
	t.Run("get invalid tags", func(t *testing.T) {
95
		// invalid tag
96 97 98 99 100 101
		jsonhttptest.Request(t, client, http.MethodGet, tagsResource+"/foobar", http.StatusBadRequest,
			jsonhttptest.WithExpectedJSONResponse(jsonhttp.StatusResponse{
				Message: "invalid id",
				Code:    http.StatusBadRequest,
			}),
		)
102 103

		// non-existent tag
104 105 106 107 108 109
		jsonhttptest.Request(t, client, http.MethodDelete, tagsWithIdResource(uint32(333)), http.StatusNotFound,
			jsonhttptest.WithExpectedJSONResponse(jsonhttp.StatusResponse{
				Message: "tag not present",
				Code:    http.StatusNotFound,
			}),
		)
110 111
	})

acud's avatar
acud committed
112
	t.Run("tag id in chunk upload", func(t *testing.T) {
acud's avatar
acud committed
113 114
		rcvdHeaders := jsonhttptest.Request(t, client, http.MethodPost, chunksResource(chunk.Address()), http.StatusOK,
			jsonhttptest.WithRequestBody(bytes.NewReader(chunk.Data())),
115 116 117 118 119
			jsonhttptest.WithExpectedJSONResponse(jsonhttp.StatusResponse{
				Message: http.StatusText(http.StatusOK),
				Code:    http.StatusOK,
			}),
		)
120 121 122 123

		isTagFoundInResponse(t, rcvdHeaders, nil)
	})

acud's avatar
acud committed
124
	t.Run("create tag upload chunk", func(t *testing.T) {
125 126
		// create a tag using the API
		tr := api.TagResponse{}
127 128 129 130 131 132
		jsonhttptest.Request(t, client, http.MethodPost, tagsResource, http.StatusCreated,
			jsonhttptest.WithJSONRequestBody(api.TagResponse{
				Name: someTagName,
			}),
			jsonhttptest.WithUnmarshalJSONResponse(&tr),
		)
133 134 135 136 137 138

		if tr.Name != someTagName {
			t.Fatalf("sent tag name %s does not match received tag name %s", someTagName, tr.Name)
		}

		// now upload a chunk and see if we receive a tag with the same id
acud's avatar
acud committed
139 140
		rcvdHeaders := jsonhttptest.Request(t, client, http.MethodPost, chunksResource(chunk.Address()), http.StatusOK,
			jsonhttptest.WithRequestBody(bytes.NewReader(chunk.Data())),
141 142 143 144 145 146
			jsonhttptest.WithExpectedJSONResponse(jsonhttp.StatusResponse{
				Message: http.StatusText(http.StatusOK),
				Code:    http.StatusOK,
			}),
			jsonhttptest.WithRequestHeader(api.SwarmTagUidHeader, strconv.FormatUint(uint64(tr.Uid), 10)),
		)
147 148

		isTagFoundInResponse(t, rcvdHeaders, &tr)
acud's avatar
acud committed
149
		tagValueTest(t, tr.Uid, 1, 1, 1, 0, 0, 0, swarm.ZeroAddress, client)
150 151
	})

acud's avatar
acud committed
152
	t.Run("tag counters", func(t *testing.T) {
acud's avatar
acud committed
153 154
		rcvdHeaders := jsonhttptest.Request(t, client, http.MethodPost, chunksResource(chunk.Address()), http.StatusOK,
			jsonhttptest.WithRequestBody(bytes.NewReader(chunk.Data())),
155 156 157 158 159
			jsonhttptest.WithExpectedJSONResponse(jsonhttp.StatusResponse{
				Message: http.StatusText(http.StatusOK),
				Code:    http.StatusOK,
			}),
		)
160 161
		id := isTagFoundInResponse(t, rcvdHeaders, nil)

acud's avatar
acud committed
162
		tag, err := tag.Get(id)
163 164 165 166 167 168 169 170 171 172 173 174
		if err != nil {
			t.Fatal(err)
		}
		err = mockPusher.SendChunk(id)
		if err != nil {
			t.Fatal(err)
		}
		err = mockPusher.RcvdReceipt(id)
		if err != nil {
			t.Fatal(err)
		}

acud's avatar
acud committed
175
		tagValueTest(t, id, tag.Split, tag.Stored, tag.Seen, tag.Sent, tag.Synced, tag.Total, swarm.ZeroAddress, client)
176 177
	})

acud's avatar
acud committed
178
	t.Run("delete tag error", func(t *testing.T) {
179
		// try to delete invalid tag
180 181 182 183 184 185
		jsonhttptest.Request(t, client, http.MethodDelete, tagsResource+"/foobar", http.StatusBadRequest,
			jsonhttptest.WithExpectedJSONResponse(jsonhttp.StatusResponse{
				Message: "invalid id",
				Code:    http.StatusBadRequest,
			}),
		)
186 187

		// try to delete non-existent tag
188 189 190 191 192 193
		jsonhttptest.Request(t, client, http.MethodDelete, tagsWithIdResource(uint32(333)), http.StatusNotFound,
			jsonhttptest.WithExpectedJSONResponse(jsonhttp.StatusResponse{
				Message: "tag not present",
				Code:    http.StatusNotFound,
			}),
		)
194 195
	})

acud's avatar
acud committed
196
	t.Run("delete tag", func(t *testing.T) {
197 198
		// create a tag through API
		tRes := api.TagResponse{}
199 200 201 202 203 204
		jsonhttptest.Request(t, client, http.MethodPost, tagsResource, http.StatusCreated,
			jsonhttptest.WithJSONRequestBody(api.TagResponse{
				Name: someTagName,
			}),
			jsonhttptest.WithUnmarshalJSONResponse(&tRes),
		)
205 206

		// delete tag through API
207 208 209
		jsonhttptest.Request(t, client, http.MethodDelete, tagsWithIdResource(tRes.Uid), http.StatusNoContent,
			jsonhttptest.WithNoResponseBody(),
		)
210 211

		// try to get tag
212 213 214 215 216 217
		jsonhttptest.Request(t, client, http.MethodGet, tagsWithIdResource(tRes.Uid), http.StatusNotFound,
			jsonhttptest.WithExpectedJSONResponse(jsonhttp.StatusResponse{
				Message: "tag not present",
				Code:    http.StatusNotFound,
			}),
		)
218 219
	})

acud's avatar
acud committed
220
	t.Run("done split error", func(t *testing.T) {
221
		// invalid tag
222 223 224 225 226 227 228
		jsonhttptest.Request(t, client, http.MethodPatch, tagsResource+"/foobar", http.StatusBadRequest,
			jsonhttptest.WithJSONRequestBody(api.TagResponse{}),
			jsonhttptest.WithExpectedJSONResponse(jsonhttp.StatusResponse{
				Message: "invalid id",
				Code:    http.StatusBadRequest,
			}),
		)
229 230

		// non-existent tag
231 232 233 234 235 236 237
		jsonhttptest.Request(t, client, http.MethodPatch, tagsWithIdResource(uint32(333)), http.StatusNotFound,
			jsonhttptest.WithJSONRequestBody(api.TagResponse{}),
			jsonhttptest.WithExpectedJSONResponse(jsonhttp.StatusResponse{
				Message: "tag not present",
				Code:    http.StatusNotFound,
			}),
		)
238 239
	})

acud's avatar
acud committed
240
	t.Run("done split", func(t *testing.T) {
241 242
		// create a tag through API
		tRes := api.TagResponse{}
243 244 245 246 247 248
		jsonhttptest.Request(t, client, http.MethodPost, tagsResource, http.StatusCreated,
			jsonhttptest.WithJSONRequestBody(api.TagResponse{
				Name: someTagName,
			}),
			jsonhttptest.WithUnmarshalJSONResponse(&tRes),
		)
249 250 251 252 253 254
		tagId := tRes.Uid

		// generate address to be supplied to the done split
		addr := test.RandomAddress()

		// upload content with tag
acud's avatar
acud committed
255 256
		jsonhttptest.Request(t, client, http.MethodPost, chunksResource(chunk.Address()), http.StatusOK,
			jsonhttptest.WithRequestBody(bytes.NewReader(chunk.Data())),
257 258
			jsonhttptest.WithRequestHeader(api.SwarmTagUidHeader, fmt.Sprint(tagId)),
		)
259 260

		// call done split
261 262 263 264 265 266 267 268 269
		jsonhttptest.Request(t, client, http.MethodPatch, tagsWithIdResource(tagId), http.StatusOK,
			jsonhttptest.WithJSONRequestBody(api.TagRequest{
				Address: addr,
			}),
			jsonhttptest.WithExpectedJSONResponse(jsonhttp.StatusResponse{
				Message: "ok",
				Code:    http.StatusOK,
			}),
		)
acud's avatar
acud committed
270
		tagValueTest(t, tagId, 1, 1, 1, 0, 0, 1, addr, client)
271 272 273 274 275

		// try different address value
		addr = test.RandomAddress()

		// call done split
276 277 278 279 280 281 282 283 284
		jsonhttptest.Request(t, client, http.MethodPatch, tagsWithIdResource(tagId), http.StatusOK,
			jsonhttptest.WithJSONRequestBody(api.TagRequest{
				Address: addr,
			}),
			jsonhttptest.WithExpectedJSONResponse(jsonhttp.StatusResponse{
				Message: "ok",
				Code:    http.StatusOK,
			}),
		)
acud's avatar
acud committed
285
		tagValueTest(t, tagId, 1, 1, 1, 0, 0, 1, addr, client)
286 287
	})

acud's avatar
acud committed
288
	t.Run("file tags", func(t *testing.T) {
289 290 291 292
		// upload a file without supplying tag
		expectedHash := swarm.MustParseHexAddress("8e27bb803ff049e8c2f4650357026723220170c15ebf9b635a7026539879a1a8")
		expectedResponse := api.FileUploadResponse{Reference: expectedHash}

293 294 295 296 297
		respHeaders := jsonhttptest.Request(t, client, http.MethodPost, filesResource, http.StatusOK,
			jsonhttptest.WithRequestBody(bytes.NewReader([]byte("some data"))),
			jsonhttptest.WithExpectedJSONResponse(expectedResponse),
			jsonhttptest.WithRequestHeader("Content-Type", "application/octet-stream"),
		)
298 299 300 301 302

		tagId, err := strconv.Atoi(respHeaders.Get(api.SwarmTagUidHeader))
		if err != nil {
			t.Fatal(err)
		}
acud's avatar
acud committed
303
		tagValueTest(t, uint32(tagId), 3, 3, 0, 0, 0, 3, expectedHash, client)
304 305
	})

acud's avatar
acud committed
306
	t.Run("dir tags", func(t *testing.T) {
307 308 309 310 311 312
		// upload a dir without supplying tag
		tarReader := tarFiles(t, []f{{
			data: []byte("some data"),
			name: "binary-file",
		}})

313
		var respBytes []byte
314

315 316 317
		respHeaders := jsonhttptest.Request(t, client, http.MethodPost, dirResource, http.StatusOK,
			jsonhttptest.WithRequestBody(tarReader),
			jsonhttptest.WithRequestHeader("Content-Type", api.ContentTypeTar),
318
			jsonhttptest.WithPutResponseBody(&respBytes),
319
		)
320

321 322 323 324 325 326 327 328 329
		read := bytes.NewReader(respBytes)

		// get the reference as everytime it will change because of random encryption key
		var resp api.FileUploadResponse
		err := json.NewDecoder(read).Decode(&resp)
		if err != nil {
			t.Fatal(err)
		}

330 331 332 333
		tagId, err := strconv.Atoi(respHeaders.Get(api.SwarmTagUidHeader))
		if err != nil {
			t.Fatal(err)
		}
acud's avatar
acud committed
334
		tagValueTest(t, uint32(tagId), 7, 7, 1, 0, 0, 7, resp.Reference, client)
335 336
	})

acud's avatar
acud committed
337
	t.Run("bytes tags", func(t *testing.T) {
338 339
		// create a tag using the API
		tr := api.TagResponse{}
340 341 342 343 344 345
		jsonhttptest.Request(t, client, http.MethodPost, tagsResource, http.StatusCreated,
			jsonhttptest.WithJSONRequestBody(api.TagResponse{
				Name: someTagName,
			}),
			jsonhttptest.WithUnmarshalJSONResponse(&tr),
		)
346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364
		if tr.Name != someTagName {
			t.Fatalf("sent tag name %s does not match received tag name %s", someTagName, tr.Name)
		}

		sentHeaders := make(http.Header)
		sentHeaders.Set(api.SwarmTagUidHeader, strconv.FormatUint(uint64(tr.Uid), 10))

		g := mockbytes.New(0, mockbytes.MockTypeStandard).WithModulus(255)
		dataChunk, err := g.SequentialBytes(swarm.ChunkSize)
		if err != nil {
			t.Fatal(err)
		}

		rootAddress := swarm.MustParseHexAddress("5e2a21902f51438be1adbd0e29e1bd34c53a21d3120aefa3c7275129f2f88de9")

		content := make([]byte, swarm.ChunkSize*2)
		copy(content[swarm.ChunkSize:], dataChunk)
		copy(content[:swarm.ChunkSize], dataChunk)

365 366 367 368 369 370 371
		rcvdHeaders := jsonhttptest.Request(t, client, http.MethodPost, bytesResource, http.StatusOK,
			jsonhttptest.WithRequestBody(bytes.NewReader(content)),
			jsonhttptest.WithExpectedJSONResponse(fileUploadResponse{
				Reference: rootAddress,
			}),
			jsonhttptest.WithRequestHeader(api.SwarmTagUidHeader, strconv.FormatUint(uint64(tr.Uid), 10)),
		)
372 373 374 375 376 377 378 379 380 381
		id := isTagFoundInResponse(t, rcvdHeaders, nil)

		tagToVerify, err := tag.Get(id)
		if err != nil {
			t.Fatal(err)
		}

		if tagToVerify.Uid != tr.Uid {
			t.Fatalf("expected tag id to be %d but is %d", tagToVerify.Uid, tr.Uid)
		}
acud's avatar
acud committed
382
		tagValueTest(t, id, 3, 3, 1, 0, 0, 0, swarm.ZeroAddress, client)
383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404
	})
}

// isTagFoundInResponse verifies that the tag id is found in the supplied HTTP headers
// if an API tag response is supplied, it also verifies that it contains an id which matches the headers
func isTagFoundInResponse(t *testing.T, headers http.Header, tr *api.TagResponse) uint32 {
	idStr := headers.Get(api.SwarmTagUidHeader)
	if idStr == "" {
		t.Fatalf("could not find tag id header in chunk upload response")
	}
	nId, err := strconv.Atoi(idStr)
	id := uint32(nId)
	if err != nil {
		t.Fatal(err)
	}
	if tr != nil {
		if id != tr.Uid {
			t.Fatalf("expected created tag id to be %d, but got %d when uploading chunk", tr.Uid, id)
		}
	}
	return id
}
acud's avatar
acud committed
405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435

func tagValueTest(t *testing.T, id uint32, split, stored, seen, sent, synced, total int64, address swarm.Address, client *http.Client) {
	t.Helper()
	tag := api.TagResponse{}
	jsonhttptest.Request(t, client, http.MethodGet, tagsWithIdResource(id), http.StatusOK,
		jsonhttptest.WithUnmarshalJSONResponse(&tag),
	)

	if tag.Split != split {
		t.Errorf("tag split count mismatch. got %d want %d", tag.Split, split)
	}
	if tag.Stored != stored {
		t.Errorf("tag stored count mismatch. got %d want %d", tag.Stored, stored)
	}
	if tag.Seen != seen {
		t.Errorf("tag seen count mismatch. got %d want %d", tag.Seen, seen)
	}
	if tag.Sent != sent {
		t.Errorf("tag sent count mismatch. got %d want %d", tag.Sent, sent)
	}
	if tag.Synced != synced {
		t.Errorf("tag synced count mismatch. got %d want %d", tag.Synced, synced)
	}
	if tag.Total != total {
		t.Errorf("tag total count mismatch. got %d want %d", tag.Total, total)
	}

	if !tag.Address.Equal(address) {
		t.Errorf("address mismatch: expected %s got %s", address.String(), tag.Address.String())
	}
}