tag_test.go 13.5 KB
Newer Older
1 2 3 4 5 6 7 8 9
// 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"
	"fmt"
10
	"io/ioutil"
11
	"net/http"
12
	"sort"
13 14 15
	"strconv"
	"testing"

16
	"github.com/ethersphere/bee/pkg/logging"
acud's avatar
acud committed
17
	mockpost "github.com/ethersphere/bee/pkg/postage/mock"
18 19
	statestore "github.com/ethersphere/bee/pkg/statestore/mock"

20 21 22 23
	"github.com/ethersphere/bee/pkg/api"
	"github.com/ethersphere/bee/pkg/jsonhttp"
	"github.com/ethersphere/bee/pkg/jsonhttp/jsonhttptest"
	"github.com/ethersphere/bee/pkg/storage/mock"
acud's avatar
acud committed
24
	testingc "github.com/ethersphere/bee/pkg/storage/testing"
25 26 27 28 29 30 31 32 33 34
	"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
35 36
func tagsWithIdResource(id uint32) string { return fmt.Sprintf("/tags/%d", id) }

37 38
func TestTags(t *testing.T) {
	var (
39
		bzzResource    = "/bzz"
acud's avatar
acud committed
40
		bytesResource  = "/bytes"
41
		chunksResource = "/chunks"
acud's avatar
acud committed
42
		tagsResource   = "/tags"
acud's avatar
acud committed
43
		chunk          = testingc.GenerateTestRandomChunk()
acud's avatar
acud committed
44 45 46 47
		mockStatestore = statestore.NewStateStore()
		logger         = logging.New(ioutil.Discard, 0)
		tag            = tags.NewTags(mockStatestore, logger)
		client, _, _   = newTestServer(t, testServerOptions{
48 49
			Storer: mock.NewStorer(),
			Tags:   tag,
50
			Logger: logger,
acud's avatar
acud committed
51
			Post:   mockpost.New(mockpost.WithAcceptAll()),
52 53 54
		})
	)

55 56 57 58 59 60
	// list tags without anything pinned
	t.Run("list tags zero", func(t *testing.T) {
		jsonhttptest.Request(t, client, http.MethodGet, tagsResource, http.StatusOK,
			jsonhttptest.WithExpectedJSONResponse(api.ListTagsResponse{
				Tags: []api.TagResponse{},
			}),
61
		)
62 63
	})

64
	t.Run("create tag", func(t *testing.T) {
65
		tr := api.TagResponse{}
66
		jsonhttptest.Request(t, client, http.MethodPost, tagsResource, http.StatusCreated,
67
			jsonhttptest.WithJSONRequestBody(api.TagRequest{}),
68 69
			jsonhttptest.WithUnmarshalJSONResponse(&tr),
		)
70 71
	})

acud's avatar
acud committed
72
	t.Run("create tag with invalid id", func(t *testing.T) {
73
		jsonhttptest.Request(t, client, http.MethodPost, chunksResource, http.StatusBadRequest,
acud's avatar
acud committed
74
			jsonhttptest.WithRequestBody(bytes.NewReader(chunk.Data())),
75
			jsonhttptest.WithExpectedJSONResponse(jsonhttp.StatusResponse{
76 77
				Message: "cannot get tag",
				Code:    http.StatusBadRequest,
78
			}),
79
			jsonhttptest.WithRequestHeader(api.SwarmTagHeader, "invalid_id.jpg"), // the value should be uint32
80
		)
81 82
	})

acud's avatar
acud committed
83
	t.Run("get invalid tags", func(t *testing.T) {
84
		// invalid tag
85 86 87 88 89 90
		jsonhttptest.Request(t, client, http.MethodGet, tagsResource+"/foobar", http.StatusBadRequest,
			jsonhttptest.WithExpectedJSONResponse(jsonhttp.StatusResponse{
				Message: "invalid id",
				Code:    http.StatusBadRequest,
			}),
		)
91 92

		// non-existent tag
93 94 95 96 97 98
		jsonhttptest.Request(t, client, http.MethodDelete, tagsWithIdResource(uint32(333)), http.StatusNotFound,
			jsonhttptest.WithExpectedJSONResponse(jsonhttp.StatusResponse{
				Message: "tag not present",
				Code:    http.StatusNotFound,
			}),
		)
99 100
	})

acud's avatar
acud committed
101
	t.Run("create tag upload chunk", func(t *testing.T) {
102 103
		// create a tag using the API
		tr := api.TagResponse{}
104
		jsonhttptest.Request(t, client, http.MethodPost, tagsResource, http.StatusCreated,
105
			jsonhttptest.WithJSONRequestBody(api.TagRequest{}),
106 107
			jsonhttptest.WithUnmarshalJSONResponse(&tr),
		)
108

109
		_ = jsonhttptest.Request(t, client, http.MethodPost, chunksResource, http.StatusCreated,
acud's avatar
acud committed
110
			jsonhttptest.WithRequestHeader(api.SwarmPostageBatchIdHeader, batchOkStr),
acud's avatar
acud committed
111
			jsonhttptest.WithRequestBody(bytes.NewReader(chunk.Data())),
112
			jsonhttptest.WithExpectedJSONResponse(api.ChunkAddressResponse{Reference: chunk.Address()}),
113
		)
114

115
		rcvdHeaders := jsonhttptest.Request(t, client, http.MethodPost, chunksResource, http.StatusCreated,
acud's avatar
acud committed
116
			jsonhttptest.WithRequestHeader(api.SwarmPostageBatchIdHeader, batchOkStr),
acud's avatar
acud committed
117
			jsonhttptest.WithRequestBody(bytes.NewReader(chunk.Data())),
118
			jsonhttptest.WithExpectedJSONResponse(api.ChunkAddressResponse{Reference: chunk.Address()}),
119
			jsonhttptest.WithRequestHeader(api.SwarmTagHeader, strconv.FormatUint(uint64(tr.Uid), 10)),
120
		)
121

122 123
		isTagFoundInResponse(t, rcvdHeaders, &tr)
		tagValueTest(t, tr.Uid, 1, 1, 1, 0, 0, 0, swarm.ZeroAddress, client)
124 125
	})

126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161
	t.Run("list tags", func(t *testing.T) {
		// list all current tags
		var resp api.ListTagsResponse
		jsonhttptest.Request(t, client, http.MethodGet, tagsResource, http.StatusOK,
			jsonhttptest.WithUnmarshalJSONResponse(&resp),
		)

		// create 2 new tags
		tRes1 := api.TagResponse{}
		jsonhttptest.Request(t, client, http.MethodPost, tagsResource, http.StatusCreated,
			jsonhttptest.WithJSONRequestBody(api.TagRequest{}),
			jsonhttptest.WithUnmarshalJSONResponse(&tRes1),
		)

		tRes2 := api.TagResponse{}
		jsonhttptest.Request(t, client, http.MethodPost, tagsResource, http.StatusCreated,
			jsonhttptest.WithJSONRequestBody(api.TagRequest{}),
			jsonhttptest.WithUnmarshalJSONResponse(&tRes2),
		)

		expectedTags := []api.TagResponse{
			tRes1,
			tRes2,
		}
		expectedTags = append(expectedTags, resp.Tags...)

		sort.Slice(expectedTags, func(i, j int) bool { return expectedTags[i].Uid < expectedTags[j].Uid })

		// check if listing returns expected tags
		jsonhttptest.Request(t, client, http.MethodGet, tagsResource, http.StatusOK,
			jsonhttptest.WithExpectedJSONResponse(api.ListTagsResponse{
				Tags: expectedTags,
			}),
		)
	})

acud's avatar
acud committed
162
	t.Run("delete tag error", func(t *testing.T) {
163
		// try to delete invalid tag
164 165 166 167 168 169
		jsonhttptest.Request(t, client, http.MethodDelete, tagsResource+"/foobar", http.StatusBadRequest,
			jsonhttptest.WithExpectedJSONResponse(jsonhttp.StatusResponse{
				Message: "invalid id",
				Code:    http.StatusBadRequest,
			}),
		)
170 171

		// try to delete non-existent tag
172 173 174 175 176 177
		jsonhttptest.Request(t, client, http.MethodDelete, tagsWithIdResource(uint32(333)), http.StatusNotFound,
			jsonhttptest.WithExpectedJSONResponse(jsonhttp.StatusResponse{
				Message: "tag not present",
				Code:    http.StatusNotFound,
			}),
		)
178 179
	})

acud's avatar
acud committed
180
	t.Run("delete tag", func(t *testing.T) {
181 182
		// create a tag through API
		tRes := api.TagResponse{}
183
		jsonhttptest.Request(t, client, http.MethodPost, tagsResource, http.StatusCreated,
184
			jsonhttptest.WithJSONRequestBody(api.TagRequest{}),
185 186
			jsonhttptest.WithUnmarshalJSONResponse(&tRes),
		)
187 188

		// delete tag through API
189 190 191
		jsonhttptest.Request(t, client, http.MethodDelete, tagsWithIdResource(tRes.Uid), http.StatusNoContent,
			jsonhttptest.WithNoResponseBody(),
		)
192 193

		// try to get tag
194 195 196 197 198 199
		jsonhttptest.Request(t, client, http.MethodGet, tagsWithIdResource(tRes.Uid), http.StatusNotFound,
			jsonhttptest.WithExpectedJSONResponse(jsonhttp.StatusResponse{
				Message: "tag not present",
				Code:    http.StatusNotFound,
			}),
		)
200 201
	})

acud's avatar
acud committed
202
	t.Run("done split error", func(t *testing.T) {
203
		// invalid tag
204 205 206 207 208 209 210
		jsonhttptest.Request(t, client, http.MethodPatch, tagsResource+"/foobar", http.StatusBadRequest,
			jsonhttptest.WithJSONRequestBody(api.TagResponse{}),
			jsonhttptest.WithExpectedJSONResponse(jsonhttp.StatusResponse{
				Message: "invalid id",
				Code:    http.StatusBadRequest,
			}),
		)
211 212

		// non-existent tag
213 214 215 216 217 218 219
		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,
			}),
		)
220 221
	})

acud's avatar
acud committed
222
	t.Run("done split", func(t *testing.T) {
223 224
		// create a tag through API
		tRes := api.TagResponse{}
225
		jsonhttptest.Request(t, client, http.MethodPost, tagsResource, http.StatusCreated,
226
			jsonhttptest.WithJSONRequestBody(api.TagRequest{}),
227 228
			jsonhttptest.WithUnmarshalJSONResponse(&tRes),
		)
229 230 231 232 233 234
		tagId := tRes.Uid

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

		// upload content with tag
235
		jsonhttptest.Request(t, client, http.MethodPost, chunksResource, http.StatusCreated,
acud's avatar
acud committed
236
			jsonhttptest.WithRequestHeader(api.SwarmPostageBatchIdHeader, batchOkStr),
acud's avatar
acud committed
237
			jsonhttptest.WithRequestBody(bytes.NewReader(chunk.Data())),
238
			jsonhttptest.WithRequestHeader(api.SwarmTagHeader, fmt.Sprint(tagId)),
239
		)
240 241

		// call done split
242 243 244 245 246 247 248 249 250
		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
251
		tagValueTest(t, tagId, 1, 1, 1, 0, 0, 1, addr, client)
252 253 254 255 256

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

		// call done split
257 258 259 260 261 262 263 264 265
		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
266
		tagValueTest(t, tagId, 1, 1, 1, 0, 0, 1, addr, client)
267 268
	})

acud's avatar
acud committed
269
	t.Run("file tags", func(t *testing.T) {
270
		// upload a file without supplying tag
271 272
		expectedHash := swarm.MustParseHexAddress("40e739ebdfd18292925bba4138cd097db9aa18c1b57e74042f48469b48da33a8")
		expectedResponse := api.BzzUploadResponse{Reference: expectedHash}
273

274
		respHeaders := jsonhttptest.Request(t, client, http.MethodPost,
275
			bzzResource+"?name=somefile", http.StatusCreated,
acud's avatar
acud committed
276
			jsonhttptest.WithRequestHeader(api.SwarmPostageBatchIdHeader, batchOkStr),
277 278 279 280
			jsonhttptest.WithRequestBody(bytes.NewReader([]byte("some data"))),
			jsonhttptest.WithExpectedJSONResponse(expectedResponse),
			jsonhttptest.WithRequestHeader("Content-Type", "application/octet-stream"),
		)
281

282
		tagId, err := strconv.Atoi(respHeaders.Get(api.SwarmTagHeader))
283 284 285
		if err != nil {
			t.Fatal(err)
		}
286
		tagValueTest(t, uint32(tagId), 4, 4, 0, 0, 0, 4, expectedHash, client)
287 288
	})

acud's avatar
acud committed
289
	t.Run("dir tags", func(t *testing.T) {
290 291
		// upload a dir without supplying tag
		tarReader := tarFiles(t, []f{{
292
			data: []byte("some dir data"),
293 294
			name: "binary-file",
		}})
295 296
		expectedHash := swarm.MustParseHexAddress("42bc27c9137c93705ffbc2945fa1aab0e8e1826f1500b7f06f6e3f86f617213b")
		expectedResponse := api.BzzUploadResponse{Reference: expectedHash}
297

298
		respHeaders := jsonhttptest.Request(t, client, http.MethodPost, bzzResource, http.StatusCreated,
acud's avatar
acud committed
299
			jsonhttptest.WithRequestHeader(api.SwarmPostageBatchIdHeader, batchOkStr),
300
			jsonhttptest.WithRequestBody(tarReader),
301
			jsonhttptest.WithRequestHeader(api.SwarmCollectionHeader, "True"),
302
			jsonhttptest.WithExpectedJSONResponse(expectedResponse),
303 304
			jsonhttptest.WithRequestHeader("Content-Type", api.ContentTypeTar),
		)
305

306
		tagId, err := strconv.Atoi(respHeaders.Get(api.SwarmTagHeader))
307 308 309
		if err != nil {
			t.Fatal(err)
		}
310
		tagValueTest(t, uint32(tagId), 3, 3, 0, 0, 0, 3, expectedHash, client)
311 312
	})

acud's avatar
acud committed
313
	t.Run("bytes tags", func(t *testing.T) {
314 315
		// create a tag using the API
		tr := api.TagResponse{}
316
		jsonhttptest.Request(t, client, http.MethodPost, tagsResource, http.StatusCreated,
acud's avatar
acud committed
317
			jsonhttptest.WithRequestHeader(api.SwarmPostageBatchIdHeader, batchOkStr),
318
			jsonhttptest.WithJSONRequestBody(api.TagRequest{}),
319 320
			jsonhttptest.WithUnmarshalJSONResponse(&tr),
		)
321 322

		sentHeaders := make(http.Header)
323
		sentHeaders.Set(api.SwarmTagHeader, strconv.FormatUint(uint64(tr.Uid), 10))
324 325 326 327 328 329 330 331 332 333 334 335 336

		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)

337
		rcvdHeaders := jsonhttptest.Request(t, client, http.MethodPost, bytesResource, http.StatusCreated,
acud's avatar
acud committed
338
			jsonhttptest.WithRequestHeader(api.SwarmPostageBatchIdHeader, batchOkStr),
339 340 341 342
			jsonhttptest.WithRequestBody(bytes.NewReader(content)),
			jsonhttptest.WithExpectedJSONResponse(fileUploadResponse{
				Reference: rootAddress,
			}),
343
			jsonhttptest.WithRequestHeader(api.SwarmTagHeader, strconv.FormatUint(uint64(tr.Uid), 10)),
344
		)
345 346 347 348 349 350 351 352 353 354
		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)
		}
355
		tagValueTest(t, id, 3, 3, 1, 0, 0, 3, swarm.ZeroAddress, client)
356 357 358 359 360 361
	})
}

// 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 {
362 363
	t.Helper()

364
	idStr := headers.Get(api.SwarmTagHeader)
365 366 367 368 369 370 371 372 373 374 375 376 377 378 379
	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
380 381 382 383 384 385 386 387

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),
	)

388 389
	if tag.Processed != stored {
		t.Errorf("tag processed count mismatch. got %d want %d", tag.Processed, stored)
acud's avatar
acud committed
390
	}
391 392
	if tag.Synced != seen+synced {
		t.Errorf("tag synced count mismatch. got %d want %d (seen: %d, synced: %d)", tag.Synced, seen+synced, seen, synced)
acud's avatar
acud committed
393 394 395 396 397
	}
	if tag.Total != total {
		t.Errorf("tag total count mismatch. got %d want %d", tag.Total, total)
	}
}