1// Copyright (C) 2019 Storj Labs, Inc.
2// See LICENSE for copying information.
3
4package consoleapi_test
5
6import (
7	"context"
8	"encoding/json"
9	"fmt"
10	"io"
11	"io/ioutil"
12	"net/http"
13	"testing"
14	"time"
15
16	"github.com/stretchr/testify/require"
17
18	"storj.io/common/storj"
19	"storj.io/common/testcontext"
20	"storj.io/storj/private/testplanet"
21	"storj.io/storj/storagenode/payouts"
22	"storj.io/storj/storagenode/reputation"
23)
24
25func TestHeldAmountApi(t *testing.T) {
26	testplanet.Run(t,
27		testplanet.Config{
28			SatelliteCount:   1,
29			StorageNodeCount: 1,
30		},
31		func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
32			satellite := planet.Satellites[0]
33			sno := planet.StorageNodes[0]
34			console := sno.Console
35			payoutsDB := sno.DB.Payout()
36			reputationDB := sno.DB.Reputation()
37			satellitesDB := sno.DB.Satellites()
38			baseURL := fmt.Sprintf("http://%s/api/heldamount", console.Listener.Addr())
39
40			// pause nodestats reputation cache because later tests assert a specific joinedat.
41			sno.NodeStats.Cache.Reputation.Pause()
42
43			period := "2020-03"
44			paystub := payouts.PayStub{
45				SatelliteID:    satellite.ID(),
46				Period:         period,
47				Created:        time.Now().UTC(),
48				Codes:          "qwe",
49				UsageAtRest:    1,
50				UsageGet:       2,
51				UsagePut:       3,
52				UsageGetRepair: 4,
53				UsagePutRepair: 5,
54				UsageGetAudit:  6,
55				CompAtRest:     7,
56				CompGet:        8,
57				CompPut:        9,
58				CompGetRepair:  10,
59				CompPutRepair:  11,
60				CompGetAudit:   12,
61				SurgePercent:   13,
62				Held:           14,
63				Owed:           15,
64				Disposed:       16,
65				Paid:           17,
66			}
67			err := payoutsDB.StorePayStub(ctx, paystub)
68			require.NoError(t, err)
69
70			t.Run("test SatellitePayStubMonthly", func(t *testing.T) {
71				// should return paystub inserted earlier
72				url := fmt.Sprintf("%s/paystubs/%s?id=%s", baseURL, period, satellite.ID().String())
73				res, err := httpGet(ctx, url)
74				require.NoError(t, err)
75				require.NotNil(t, res)
76				require.Equal(t, http.StatusOK, res.StatusCode)
77
78				paystub.UsageAtRest /= 720
79
80				expected, err := json.Marshal(paystub)
81				require.NoError(t, err)
82
83				defer func() {
84					err = res.Body.Close()
85					require.NoError(t, err)
86				}()
87				body, err := ioutil.ReadAll(res.Body)
88				require.NoError(t, err)
89
90				require.Equal(t, string(expected)+"\n", string(body))
91
92				// should return 404 cause no payouts for the period.
93				url = fmt.Sprintf("%s/paystubs/%s?id=%s", baseURL, "2020-01", satellite.ID().String())
94				res2, err := httpGet(ctx, url)
95				require.NoError(t, err)
96				require.NotNil(t, res2)
97				require.Equal(t, http.StatusOK, res2.StatusCode)
98
99				defer func() {
100					err = res2.Body.Close()
101					require.NoError(t, err)
102				}()
103				body2, err := ioutil.ReadAll(res2.Body)
104				require.NoError(t, err)
105
106				expected = []byte("null\n")
107				require.Equal(t, expected, body2)
108
109				// should return 400 cause of wrong satellite id.
110				url = fmt.Sprintf("%s/paystubs/%s?id=%s", baseURL, "2020-01", "123")
111				res3, err := httpGet(ctx, url)
112				require.NoError(t, err)
113				require.NotNil(t, res3)
114				require.Equal(t, http.StatusBadRequest, res3.StatusCode)
115
116				defer func() {
117					err = res3.Body.Close()
118					require.NoError(t, err)
119				}()
120			})
121
122			paystub2 := payouts.PayStub{
123				SatelliteID:    storj.NodeID{1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 0},
124				Period:         period,
125				Created:        time.Now().UTC(),
126				Codes:          "qwe",
127				UsageAtRest:    1,
128				UsageGet:       2,
129				UsagePut:       3,
130				UsageGetRepair: 4,
131				UsagePutRepair: 5,
132				UsageGetAudit:  6,
133				CompAtRest:     7,
134				CompGet:        8,
135				CompPut:        9,
136				CompGetRepair:  10,
137				CompPutRepair:  11,
138				CompGetAudit:   12,
139				SurgePercent:   13,
140				Held:           14,
141				Owed:           15,
142				Disposed:       16,
143				Paid:           17,
144			}
145			err = payoutsDB.StorePayStub(ctx, paystub2)
146			require.NoError(t, err)
147
148			t.Run("test AllPayStubsMonthly", func(t *testing.T) {
149				// should return 2 paystubs inserted earlier
150				url := fmt.Sprintf("%s/paystubs/%s", baseURL, period)
151				res, err := httpGet(ctx, url)
152				require.NoError(t, err)
153				require.NotNil(t, res)
154				require.Equal(t, http.StatusOK, res.StatusCode)
155
156				paystub2.UsageAtRest /= 720
157
158				expected, err := json.Marshal([]payouts.PayStub{paystub2, paystub})
159				require.NoError(t, err)
160
161				defer func() {
162					err = res.Body.Close()
163					require.NoError(t, err)
164				}()
165				body, err := ioutil.ReadAll(res.Body)
166				require.NoError(t, err)
167
168				require.Equal(t, string(expected)+"\n", string(body))
169
170				// should return 2 paystubs inserted earlier
171				url = fmt.Sprintf("%s/paystubs/%s", baseURL, "2020-01")
172				res2, err := httpGet(ctx, url)
173				require.NoError(t, err)
174				require.NotNil(t, res2)
175				require.Equal(t, http.StatusOK, res2.StatusCode)
176
177				defer func() {
178					err = res2.Body.Close()
179					require.NoError(t, err)
180				}()
181				body2, err := ioutil.ReadAll(res2.Body)
182				require.NoError(t, err)
183
184				require.Equal(t, "null\n", string(body2))
185			})
186
187			period2 := "2020-02"
188			paystub3 := payouts.PayStub{
189				SatelliteID:    satellite.ID(),
190				Period:         period2,
191				Created:        time.Now().UTC(),
192				Codes:          "qwe",
193				UsageAtRest:    1,
194				UsageGet:       2,
195				UsagePut:       3,
196				UsageGetRepair: 4,
197				UsagePutRepair: 5,
198				UsageGetAudit:  6,
199				CompAtRest:     7,
200				CompGet:        8,
201				CompPut:        9,
202				CompGetRepair:  10,
203				CompPutRepair:  11,
204				CompGetAudit:   12,
205				SurgePercent:   13,
206				Held:           14,
207				Owed:           15,
208				Disposed:       16,
209				Paid:           17,
210			}
211			err = payoutsDB.StorePayStub(ctx, paystub3)
212			require.NoError(t, err)
213
214			t.Run("test SatellitePayStubPeriod", func(t *testing.T) {
215				// should return all paystubs inserted earlier
216				url := fmt.Sprintf("%s/paystubs/%s/%s?id=%s", baseURL, period2, period, satellite.ID().String())
217				res, err := httpGet(ctx, url)
218				require.NoError(t, err)
219				require.NotNil(t, res)
220				require.Equal(t, http.StatusOK, res.StatusCode)
221
222				paystub3.UsageAtRest /= 720
223
224				expected, err := json.Marshal([]payouts.PayStub{paystub3, paystub})
225				require.NoError(t, err)
226
227				defer func() {
228					err = res.Body.Close()
229					require.NoError(t, err)
230				}()
231				body, err := ioutil.ReadAll(res.Body)
232				require.NoError(t, err)
233
234				require.Equal(t, string(expected)+"\n", string(body))
235
236				url = fmt.Sprintf("%s/paystubs/%s/%s?id=%s", baseURL, period, period, satellite.ID().String())
237				res2, err := httpGet(ctx, url)
238				require.NoError(t, err)
239				require.NotNil(t, res2)
240				require.Equal(t, http.StatusOK, res2.StatusCode)
241
242				expected, err = json.Marshal([]payouts.PayStub{paystub})
243				require.NoError(t, err)
244
245				defer func() {
246					err = res2.Body.Close()
247					require.NoError(t, err)
248				}()
249				body2, err := ioutil.ReadAll(res2.Body)
250				require.NoError(t, err)
251
252				require.Equal(t, string(expected)+"\n", string(body2))
253
254				url = fmt.Sprintf("%s/paystubs/%s/%s?id=%s", baseURL, period2, period, paystub2.SatelliteID.String())
255				res3, err := httpGet(ctx, url)
256				require.NoError(t, err)
257				require.NotNil(t, res3)
258				require.Equal(t, http.StatusOK, res3.StatusCode)
259
260				expected, err = json.Marshal([]payouts.PayStub{paystub2})
261				require.NoError(t, err)
262
263				defer func() {
264					err = res3.Body.Close()
265					require.NoError(t, err)
266				}()
267				body3, err := ioutil.ReadAll(res3.Body)
268				require.NoError(t, err)
269
270				require.Equal(t, string(expected)+"\n", string(body3))
271
272				// should return 400 because of bad satellite id.
273				url = fmt.Sprintf("%s/paystubs/%s/%s?id=%s", baseURL, period2, period, "1")
274				res4, err := httpGet(ctx, url)
275				require.NoError(t, err)
276				require.NotNil(t, res4)
277				require.Equal(t, http.StatusBadRequest, res4.StatusCode)
278
279				defer func() {
280					err = res4.Body.Close()
281					require.NoError(t, err)
282				}()
283
284				// should return 400 because of bad period.
285				url = fmt.Sprintf("%s/paystubs/%s/%s?id=%s", baseURL, period, period2, satellite.ID().String())
286				res5, err := httpGet(ctx, url)
287				require.NoError(t, err)
288				require.NotNil(t, res5)
289				require.Equal(t, http.StatusBadRequest, res5.StatusCode)
290
291				defer func() {
292					err = res5.Body.Close()
293					require.NoError(t, err)
294				}()
295
296				body5, err := ioutil.ReadAll(res5.Body)
297				require.NoError(t, err)
298
299				require.Equal(t, "{\"error\":\"consoleapi payouts: wrong period format: period has wrong format\"}\n", string(body5))
300			})
301
302			t.Run("test AllPayStubsPeriod", func(t *testing.T) {
303				// should return all paystubs inserted earlier
304				url := fmt.Sprintf("%s/paystubs/%s/%s", baseURL, period2, period)
305				res, err := httpGet(ctx, url)
306				require.NoError(t, err)
307				require.NotNil(t, res)
308				require.Equal(t, http.StatusOK, res.StatusCode)
309
310				expected, err := json.Marshal([]payouts.PayStub{paystub3, paystub2, paystub})
311				require.NoError(t, err)
312
313				defer func() {
314					err = res.Body.Close()
315					require.NoError(t, err)
316				}()
317				body, err := ioutil.ReadAll(res.Body)
318				require.NoError(t, err)
319
320				require.Equal(t, string(expected)+"\n", string(body))
321
322				url = fmt.Sprintf("%s/paystubs/%s/%s", baseURL, period, period)
323				res2, err := httpGet(ctx, url)
324				require.NoError(t, err)
325				require.NotNil(t, res2)
326				require.Equal(t, http.StatusOK, res2.StatusCode)
327
328				expected, err = json.Marshal([]payouts.PayStub{paystub2, paystub})
329				require.NoError(t, err)
330
331				defer func() {
332					err = res2.Body.Close()
333					require.NoError(t, err)
334				}()
335				body2, err := ioutil.ReadAll(res2.Body)
336				require.NoError(t, err)
337
338				require.Equal(t, string(expected)+"\n", string(body2))
339
340				// should return 400 because of bad period.
341				url = fmt.Sprintf("%s/paystubs/%s/%s", baseURL, period, period2)
342				res5, err := httpGet(ctx, url)
343				require.NoError(t, err)
344				require.NotNil(t, res5)
345				require.Equal(t, http.StatusBadRequest, res5.StatusCode)
346
347				defer func() {
348					err = res5.Body.Close()
349					require.NoError(t, err)
350				}()
351
352				body5, err := ioutil.ReadAll(res5.Body)
353				require.NoError(t, err)
354
355				require.Equal(t, "{\"error\":\"consoleapi payouts: wrong period format: period has wrong format\"}\n", string(body5))
356			})
357
358			t.Run("test HeldbackHistory", func(t *testing.T) {
359				date := time.Now().UTC().AddDate(0, -2, 0).Round(time.Minute)
360				err = reputationDB.Store(context.Background(), reputation.Stats{
361					SatelliteID: satellite.ID(),
362					JoinedAt:    date,
363				})
364				require.NoError(t, err)
365
366				err = satellitesDB.SetAddress(ctx, satellite.ID(), satellite.Addr())
367				require.NoError(t, err)
368
369				// should return all heldback history inserted earlier
370				url := fmt.Sprintf("%s/held-history", baseURL)
371				res, err := httpGet(ctx, url)
372				require.NoError(t, err)
373				require.NotNil(t, res)
374				require.Equal(t, http.StatusOK, res.StatusCode)
375
376				held := payouts.SatelliteHeldHistory{
377					SatelliteID:         satellite.ID(),
378					SatelliteName:       satellite.Addr(),
379					HoldForFirstPeriod:  28,
380					HoldForSecondPeriod: 0,
381					HoldForThirdPeriod:  0,
382					TotalHeld:           28,
383					TotalDisposed:       32,
384					JoinedAt:            date.Round(time.Minute),
385				}
386
387				var periods []payouts.SatelliteHeldHistory
388				periods = append(periods, held)
389
390				expected, err := json.Marshal(periods)
391				require.NoError(t, err)
392
393				defer func() {
394					err = res.Body.Close()
395					require.NoError(t, err)
396				}()
397				body, err := ioutil.ReadAll(res.Body)
398				require.NoError(t, err)
399				require.Equal(t, string(expected)+"\n", string(body))
400			})
401
402			t.Run("test Periods", func(t *testing.T) {
403				url := fmt.Sprintf("%s/periods", baseURL)
404				res, err := httpGet(ctx, url)
405				require.NoError(t, err)
406				require.NotNil(t, res)
407				require.Equal(t, http.StatusOK, res.StatusCode)
408
409				var periods []string
410				periods = append(periods, "2020-03", "2020-02")
411
412				expected, err := json.Marshal(periods)
413				require.NoError(t, err)
414
415				defer func() {
416					err = res.Body.Close()
417					require.NoError(t, err)
418				}()
419				body, err := ioutil.ReadAll(res.Body)
420				require.NoError(t, err)
421
422				require.Equal(t, string(expected)+"\n", string(body))
423
424				//
425				url = fmt.Sprintf("%s/periods?id=%s", baseURL, paystub2.SatelliteID.String())
426				res2, err := httpGet(ctx, url)
427				require.NoError(t, err)
428				require.NotNil(t, res)
429				require.Equal(t, http.StatusOK, res.StatusCode)
430
431				var periods2 []string
432				periods2 = append(periods2, "2020-03")
433
434				expected2, err := json.Marshal(periods2)
435				require.NoError(t, err)
436
437				defer func() {
438					err = res2.Body.Close()
439					require.NoError(t, err)
440				}()
441				body2, err := ioutil.ReadAll(res2.Body)
442				require.NoError(t, err)
443
444				require.Equal(t, string(expected2)+"\n", string(body2))
445			})
446		},
447	)
448}
449
450func httpGet(ctx context.Context, url string) (*http.Response, error) {
451	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
452	if err != nil {
453		return nil, err
454	}
455	return http.DefaultClient.Do(req)
456}
457
458func httpPost(ctx context.Context, url string, contentType string, b io.Reader) (*http.Response, error) {
459	req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, b)
460	if err != nil {
461		return nil, err
462	}
463	req.Header.Set("Content-Type", contentType)
464	return http.DefaultClient.Do(req)
465}
466