From a200f5cc833ca7ed81866ae812e029a766f24d0f Mon Sep 17 00:00:00 2001 From: Martin Boehm Date: Sun, 26 Mar 2023 00:40:10 +0100 Subject: [PATCH] Refactor html template handling --- server/html_templates.go | 299 ++++++++++++++++++++++++++++++++++ server/html_templates_test.go | 173 ++++++++++++++++++++ server/public.go | 299 +++------------------------------- server/public_test.go | 164 ------------------- 4 files changed, 495 insertions(+), 440 deletions(-) create mode 100644 server/html_templates.go create mode 100644 server/html_templates_test.go diff --git a/server/html_templates.go b/server/html_templates.go new file mode 100644 index 00000000..4d044c3b --- /dev/null +++ b/server/html_templates.go @@ -0,0 +1,299 @@ +package server + +import ( + "encoding/json" + "fmt" + "html/template" + "math/big" + "net/http" + "runtime/debug" + "strconv" + "strings" + "time" + + "github.com/golang/glog" + "github.com/trezor/blockbook/api" + "github.com/trezor/blockbook/common" +) + +type tpl int + +const ( + noTpl = tpl(iota) + errorTpl + errorInternalTpl +) + +// htmlTemplateHandler is a handle to public http server +type htmlTemplates[TD any] struct { + metrics *common.Metrics + templates []*template.Template + debug bool + newTemplateData func(r *http.Request) *TD + newTemplateDataWithError func(error *api.APIError, r *http.Request) *TD + parseTemplates func() []*template.Template + postHtmlTemplateHandler func(data *TD, w http.ResponseWriter, r *http.Request) +} + +func (s *htmlTemplates[TD]) htmlTemplateHandler(handler func(w http.ResponseWriter, r *http.Request) (tpl, *TD, error)) func(w http.ResponseWriter, r *http.Request) { + handlerName := getFunctionName(handler) + return func(w http.ResponseWriter, r *http.Request) { + var t tpl + var data *TD + var err error + defer func() { + if e := recover(); e != nil { + glog.Error(handlerName, " recovered from panic: ", e) + debug.PrintStack() + t = errorInternalTpl + if s.debug { + data = s.newTemplateDataWithError(&api.APIError{Text: fmt.Sprint("Internal server error: recovered from panic ", e)}, r) + } else { + data = s.newTemplateDataWithError(&api.APIError{Text: "Internal server error"}, r) + } + } + // noTpl means the handler completely handled the request + if t != noTpl { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + // return 500 Internal Server Error with errorInternalTpl + if t == errorInternalTpl { + w.WriteHeader(http.StatusInternalServerError) + } + if err := s.templates[t].ExecuteTemplate(w, "base.html", data); err != nil { + glog.Error(err) + } + } + if s.metrics != nil { + s.metrics.ExplorerPendingRequests.With((common.Labels{"method": handlerName})).Dec() + } + }() + if s.metrics != nil { + s.metrics.ExplorerPendingRequests.With((common.Labels{"method": handlerName})).Inc() + } + if s.debug { + // reload templates on each request + // to reflect changes during development + s.templates = s.parseTemplates() + } + t, data, err = handler(w, r) + if err != nil || (data == nil && t != noTpl) { + t = errorInternalTpl + if apiErr, ok := err.(*api.APIError); ok { + data = s.newTemplateDataWithError(apiErr, r) + if apiErr.Public { + t = errorTpl + } + } else { + if err != nil { + glog.Error(handlerName, " error: ", err) + } + if s.debug { + data = s.newTemplateDataWithError(&api.APIError{Text: fmt.Sprintf("Internal server error: %v, data %+v", err, data)}, r) + } else { + data = s.newTemplateDataWithError(&api.APIError{Text: "Internal server error"}, r) + } + } + } + if s.postHtmlTemplateHandler != nil { + s.postHtmlTemplateHandler(data, w, r) + } + + } +} + +func relativeTimeUnit(d int64) string { + var u string + if d < 60 { + if d == 1 { + u = " sec" + } else { + u = " secs" + } + } else if d < 3600 { + d /= 60 + if d == 1 { + u = " min" + } else { + u = " mins" + } + } else if d < 3600*24 { + d /= 3600 + if d == 1 { + u = " hour" + } else { + u = " hours" + } + } else { + d /= 3600 * 24 + if d == 1 { + u = " day" + } else { + u = " days" + } + } + return strconv.FormatInt(d, 10) + u +} + +func relativeTime(d int64) string { + r := relativeTimeUnit(d) + if d > 3600*24 { + d = d % (3600 * 24) + if d >= 3600 { + r += " " + relativeTimeUnit(d) + } + } else if d > 3600 { + d = d % 3600 + if d >= 60 { + r += " " + relativeTimeUnit(d) + } + } + return r +} + +func unixTimeSpan(ut int64) template.HTML { + t := time.Unix(ut, 0) + return timeSpan(&t) +} + +var timeNow = time.Now + +func timeSpan(t *time.Time) template.HTML { + if t == nil { + return "" + } + u := t.Unix() + if u <= 0 { + return "" + } + d := timeNow().Unix() - u + f := t.UTC().Format("2006-01-02 15:04:05") + if d < 0 { + return template.HTML(f) + } + r := relativeTime(d) + return template.HTML(`` + r + " ago") +} + +func toJSON(data interface{}) string { + json, err := json.Marshal(data) + if err != nil { + return "" + } + return string(json) +} + +func formatAmountWithDecimals(a *api.Amount, d int) string { + if a == nil { + return "0" + } + return a.DecimalString(d) +} + +func appendAmountSpan(rv *strings.Builder, class, amount, shortcut, txDate string) { + rv.WriteString(`") + i := strings.IndexByte(amount, '.') + if i < 0 { + appendSeparatedNumberSpans(rv, amount, "nc") + } else { + appendSeparatedNumberSpans(rv, amount[:i], "nc") + rv.WriteString(`.`) + rv.WriteString(``) + appendLeftSeparatedNumberSpans(rv, amount[i+1:], "ns") + rv.WriteString("") + } + if shortcut != "" { + rv.WriteString(" ") + rv.WriteString(shortcut) + } + rv.WriteString("") +} + +func appendAmountWrapperSpan(rv *strings.Builder, primary, symbol, classes string) { + rv.WriteString(``) +} + +func formatInt(i int) template.HTML { + return formatInt64(int64(i)) +} + +func formatUint32(i uint32) template.HTML { + return formatInt64(int64(i)) +} + +func appendSeparatedNumberSpans(rv *strings.Builder, s, separatorClass string) { + if len(s) > 0 && s[0] == '-' { + s = s[1:] + rv.WriteByte('-') + } + t := (len(s) - 1) / 3 + if t <= 0 { + rv.WriteString(s) + } else { + t *= 3 + rv.WriteString(s[:len(s)-t]) + for i := len(s) - t; i < len(s); i += 3 { + rv.WriteString(``) + rv.WriteString(s[i : i+3]) + rv.WriteString("") + } + } +} + +func appendLeftSeparatedNumberSpans(rv *strings.Builder, s, separatorClass string) { + l := len(s) + if l <= 3 { + rv.WriteString(s) + } else { + rv.WriteString(s[:3]) + for i := 3; i < len(s); i += 3 { + rv.WriteString(``) + e := i + 3 + if e > l { + e = l + } + rv.WriteString(s[i:e]) + rv.WriteString("") + } + } +} + +func formatInt64(i int64) template.HTML { + s := strconv.FormatInt(i, 10) + var rv strings.Builder + appendSeparatedNumberSpans(&rv, s, "ns") + return template.HTML(rv.String()) +} + +func formatBigInt(i *big.Int) template.HTML { + if i == nil { + return "" + } + s := i.String() + var rv strings.Builder + appendSeparatedNumberSpans(&rv, s, "ns") + return template.HTML(rv.String()) +} diff --git a/server/html_templates_test.go b/server/html_templates_test.go new file mode 100644 index 00000000..99c6c705 --- /dev/null +++ b/server/html_templates_test.go @@ -0,0 +1,173 @@ +//go:build unittest + +package server + +import ( + "html/template" + "reflect" + "strings" + "testing" + "time" +) + +func Test_formatInt64(t *testing.T) { + tests := []struct { + name string + n int64 + want template.HTML + }{ + {"1", 1, "1"}, + {"13", 13, "13"}, + {"123", 123, "123"}, + {"1234", 1234, `1234`}, + {"91234", 91234, `91234`}, + {"891234", 891234, `891234`}, + {"7891234", 7891234, `7891234`}, + {"67891234", 67891234, `67891234`}, + {"567891234", 567891234, `567891234`}, + {"4567891234", 4567891234, `4567891234`}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := formatInt64(tt.n); !reflect.DeepEqual(got, tt.want) { + t.Errorf("formatInt64() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_formatTime(t *testing.T) { + timeNow = fixedTimeNow + tests := []struct { + name string + want template.HTML + }{ + { + name: "2020-12-23 15:16:17", + want: `630 days 21 hours ago`, + }, + { + name: "2022-08-23 11:12:13", + want: `23 days 1 hour ago`, + }, + { + name: "2022-09-14 11:12:13", + want: `1 day 1 hour ago`, + }, + { + name: "2022-09-14 14:12:13", + want: `22 hours 31 mins ago`, + }, + { + name: "2022-09-15 09:33:26", + want: `3 hours 10 mins ago`, + }, + { + name: "2022-09-15 12:23:56", + want: `20 mins ago`, + }, + { + name: "2022-09-15 12:24:07", + want: `19 mins ago`, + }, + { + name: "2022-09-15 12:43:21", + want: `35 secs ago`, + }, + { + name: "2022-09-15 12:43:56", + want: `0 secs ago`, + }, + { + name: "2022-09-16 12:43:56", + want: `2022-09-16 12:43:56`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tm, _ := time.Parse("2006-01-02 15:04:05", tt.name) + if got := timeSpan(&tm); !reflect.DeepEqual(got, tt.want) { + t.Errorf("formatTime() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_appendAmountSpan(t *testing.T) { + tests := []struct { + name string + class string + amount string + shortcut string + txDate string + want string + }{ + { + name: "prim-amt 1.23456789 BTC", + class: "prim-amt", + amount: "1.23456789", + shortcut: "BTC", + want: `1.23456789 BTC`, + }, + { + name: "prim-amt 1432134.23456 BTC", + class: "prim-amt", + amount: "1432134.23456", + shortcut: "BTC", + want: `1432134.23456 BTC`, + }, + { + name: "sec-amt 1 EUR", + class: "sec-amt", + amount: "1", + shortcut: "EUR", + want: `1 EUR`, + }, + { + name: "sec-amt -1 EUR", + class: "sec-amt", + amount: "-1", + shortcut: "EUR", + want: `-1 EUR`, + }, + { + name: "sec-amt 432109.23 EUR", + class: "sec-amt", + amount: "432109.23", + shortcut: "EUR", + want: `432109.23 EUR`, + }, + { + name: "sec-amt -432109.23 EUR", + class: "sec-amt", + amount: "-432109.23", + shortcut: "EUR", + want: `-432109.23 EUR`, + }, + { + name: "sec-amt 43141.29 EUR", + class: "sec-amt", + amount: "43141.29", + shortcut: "EUR", + txDate: "2022-03-14", + want: `43141.29 EUR`, + }, + { + name: "sec-amt -43141.29 EUR", + class: "sec-amt", + amount: "-43141.29", + shortcut: "EUR", + txDate: "2022-03-14", + want: `-43141.29 EUR`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var rv strings.Builder + appendAmountSpan(&rv, tt.class, tt.amount, tt.shortcut, tt.txDate) + if got := rv.String(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("formatTime() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/server/public.go b/server/public.go index 73936c74..86d90251 100644 --- a/server/public.go +++ b/server/public.go @@ -40,8 +40,9 @@ const ( apiV2 ) -// PublicServer is a handle to public http server +// PublicServer provides public http server functionality type PublicServer struct { + htmlTemplates[TemplateData] binding string certFiles string socketio *SocketIoServer @@ -55,10 +56,7 @@ type PublicServer struct { api *api.Worker explorerURL string internalExplorer bool - metrics *common.Metrics is *common.InternalState - templates []*template.Template - debug bool } // NewPublicServer creates new public server http interface to blockbook and returns its handle @@ -88,6 +86,10 @@ func NewPublicServer(binding string, certFiles string, db *db.RocksDB, chain bch } s := &PublicServer{ + htmlTemplates: htmlTemplates[TemplateData]{ + metrics: metrics, + debug: debugMode, + }, binding: binding, certFiles: certFiles, https: https, @@ -101,10 +103,12 @@ func NewPublicServer(binding string, certFiles string, db *db.RocksDB, chain bch mempool: mempool, explorerURL: explorerURL, internalExplorer: explorerURL == "", - metrics: metrics, is: is, - debug: debugMode, } + s.htmlTemplates.newTemplateData = s.newTemplateData + s.htmlTemplates.newTemplateDataWithError = s.newTemplateDataWithError + s.htmlTemplates.parseTemplates = s.parseTemplates + s.htmlTemplates.postHtmlTemplateHandler = s.postHtmlTemplateHandler s.templates = s.parseTemplates() // map only basic functions, the rest is enabled by method MapFullPublicInterface @@ -396,82 +400,14 @@ func (s *PublicServer) newTemplateData(r *http.Request) *TemplateData { return t } -func (s *PublicServer) newTemplateDataWithError(text string, r *http.Request) *TemplateData { +func (s *PublicServer) newTemplateDataWithError(error *api.APIError, r *http.Request) *TemplateData { td := s.newTemplateData(r) - td.Error = &api.APIError{Text: text} + td.Error = error return td } -func (s *PublicServer) htmlTemplateHandler(handler func(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error)) func(w http.ResponseWriter, r *http.Request) { - handlerName := getFunctionName(handler) - return func(w http.ResponseWriter, r *http.Request) { - var t tpl - var data *TemplateData - var err error - defer func() { - if e := recover(); e != nil { - glog.Error(handlerName, " recovered from panic: ", e) - debug.PrintStack() - t = errorInternalTpl - if s.debug { - data = s.newTemplateDataWithError(fmt.Sprint("Internal server error: recovered from panic ", e), r) - } else { - data = s.newTemplateDataWithError("Internal server error", r) - } - } - // noTpl means the handler completely handled the request - if t != noTpl { - w.Header().Set("Content-Type", "text/html; charset=utf-8") - // return 500 Internal Server Error with errorInternalTpl - if t == errorInternalTpl { - w.WriteHeader(http.StatusInternalServerError) - } - if err := s.templates[t].ExecuteTemplate(w, "base.html", data); err != nil { - glog.Error(err) - } - } - s.metrics.ExplorerPendingRequests.With((common.Labels{"method": handlerName})).Dec() - }() - s.metrics.ExplorerPendingRequests.With((common.Labels{"method": handlerName})).Inc() - if s.debug { - // reload templates on each request - // to reflect changes during development - s.templates = s.parseTemplates() - } - t, data, err = handler(w, r) - if err != nil || (data == nil && t != noTpl) { - t = errorInternalTpl - if apiErr, ok := err.(*api.APIError); ok { - data = s.newTemplateData(r) - data.Error = apiErr - if apiErr.Public { - t = errorTpl - } - } else { - if err != nil { - glog.Error(handlerName, " error: ", err) - } - if s.debug { - data = s.newTemplateDataWithError(fmt.Sprintf("Internal server error: %v, data %+v", err, data), r) - } else { - data = s.newTemplateDataWithError("Internal server error", r) - } - } - } - // if SecondaryCoin is specified, set secondary_coin cookie - if data != nil && data.SecondaryCoin != "" { - http.SetCookie(w, &http.Cookie{Name: secondaryCoinCookieName, Value: data.SecondaryCoin + "=" + strconv.FormatBool(data.UseSecondaryCoin), Path: "/"}) - } - } -} - -type tpl int - const ( - noTpl = tpl(iota) - errorTpl - errorInternalTpl - indexTpl + indexTpl = iota + errorInternalTpl + 1 txTpl addressTpl xpubTpl @@ -481,7 +417,7 @@ const ( mempoolTpl nftDetailTpl - tplCount + publicTplCount ) // TemplateData is used to transfer data to the templates @@ -590,7 +526,7 @@ func (s *PublicServer) parseTemplates() []*template.Template { return t } } - t := make([]*template.Template, tplCount) + t := make([]*template.Template, publicTplCount) t[errorTpl] = createTemplate("./static/templates/error.html", "./static/templates/base.html") t[errorInternalTpl] = createTemplate("./static/templates/error.html", "./static/templates/base.html") t[indexTpl] = createTemplate("./static/templates/index.html", "./static/templates/base.html") @@ -611,85 +547,12 @@ func (s *PublicServer) parseTemplates() []*template.Template { return t } -func relativeTimeUnit(d int64) string { - var u string - if d < 60 { - if d == 1 { - u = " sec" - } else { - u = " secs" - } - } else if d < 3600 { - d /= 60 - if d == 1 { - u = " min" - } else { - u = " mins" - } - } else if d < 3600*24 { - d /= 3600 - if d == 1 { - u = " hour" - } else { - u = " hours" - } - } else { - d /= 3600 * 24 - if d == 1 { - u = " day" - } else { - u = " days" - } +func (s *PublicServer) postHtmlTemplateHandler(data *TemplateData, w http.ResponseWriter, r *http.Request) { + // // if SecondaryCoin is specified, set secondary_coin cookie + if data != nil && data.SecondaryCoin != "" { + http.SetCookie(w, &http.Cookie{Name: secondaryCoinCookieName, Value: data.SecondaryCoin + "=" + strconv.FormatBool(data.UseSecondaryCoin), Path: "/"}) } - return strconv.FormatInt(d, 10) + u -} -func relativeTime(d int64) string { - r := relativeTimeUnit(d) - if d > 3600*24 { - d = d % (3600 * 24) - if d >= 3600 { - r += " " + relativeTimeUnit(d) - } - } else if d > 3600 { - d = d % 3600 - if d >= 60 { - r += " " + relativeTimeUnit(d) - } - } - return r -} - -func unixTimeSpan(ut int64) template.HTML { - t := time.Unix(ut, 0) - return timeSpan(&t) -} - -var timeNow = time.Now - -func timeSpan(t *time.Time) template.HTML { - if t == nil { - return "" - } - u := t.Unix() - if u <= 0 { - return "" - } - d := timeNow().Unix() - u - f := t.UTC().Format("2006-01-02 15:04:05") - if d < 0 { - return template.HTML(f) - } - r := relativeTime(d) - return template.HTML(`` + r + " ago") -} - -func toJSON(data interface{}) string { - json, err := json.Marshal(data) - if err != nil { - return "" - } - return string(json) } func (s *PublicServer) formatAmount(a *api.Amount) string { @@ -699,63 +562,6 @@ func (s *PublicServer) formatAmount(a *api.Amount) string { return s.chainParser.AmountToDecimalString((*big.Int)(a)) } -func formatAmountWithDecimals(a *api.Amount, d int) string { - if a == nil { - return "0" - } - return a.DecimalString(d) -} - -func appendAmountSpan(rv *strings.Builder, class, amount, shortcut, txDate string) { - rv.WriteString(`") - i := strings.IndexByte(amount, '.') - if i < 0 { - appendSeparatedNumberSpans(rv, amount, "nc") - } else { - appendSeparatedNumberSpans(rv, amount[:i], "nc") - rv.WriteString(`.`) - rv.WriteString(``) - appendLeftSeparatedNumberSpans(rv, amount[i+1:], "ns") - rv.WriteString("") - } - if shortcut != "" { - rv.WriteString(" ") - rv.WriteString(shortcut) - } - rv.WriteString("") -} - -func appendAmountWrapperSpan(rv *strings.Builder, primary, symbol, classes string) { - rv.WriteString(``) -} - -func formatSecondaryAmount(a float64, td *TemplateData) string { - if td.SecondaryCoin == "BTC" || td.SecondaryCoin == "ETH" { - return strconv.FormatFloat(a, 'f', 6, 64) - } - return strconv.FormatFloat(a, 'f', 2, 64) -} - func (s *PublicServer) amountSpan(a *api.Amount, td *TemplateData, classes string) template.HTML { primary := s.formatAmount(a) var rv strings.Builder @@ -898,70 +704,11 @@ func (s *PublicServer) summaryValuesSpan(baseValue float64, secondaryValue float return template.HTML(rv.String()) } -func formatInt(i int) template.HTML { - return formatInt64(int64(i)) -} - -func formatUint32(i uint32) template.HTML { - return formatInt64(int64(i)) -} - -func appendSeparatedNumberSpans(rv *strings.Builder, s, separatorClass string) { - if len(s) > 0 && s[0] == '-' { - s = s[1:] - rv.WriteByte('-') +func formatSecondaryAmount(a float64, td *TemplateData) string { + if td.SecondaryCoin == "BTC" || td.SecondaryCoin == "ETH" { + return strconv.FormatFloat(a, 'f', 6, 64) } - t := (len(s) - 1) / 3 - if t <= 0 { - rv.WriteString(s) - } else { - t *= 3 - rv.WriteString(s[:len(s)-t]) - for i := len(s) - t; i < len(s); i += 3 { - rv.WriteString(``) - rv.WriteString(s[i : i+3]) - rv.WriteString("") - } - } -} - -func appendLeftSeparatedNumberSpans(rv *strings.Builder, s, separatorClass string) { - l := len(s) - if l <= 3 { - rv.WriteString(s) - } else { - rv.WriteString(s[:3]) - for i := 3; i < len(s); i += 3 { - rv.WriteString(``) - e := i + 3 - if e > l { - e = l - } - rv.WriteString(s[i:e]) - rv.WriteString("") - } - } -} - -func formatInt64(i int64) template.HTML { - s := strconv.FormatInt(i, 10) - var rv strings.Builder - appendSeparatedNumberSpans(&rv, s, "ns") - return template.HTML(rv.String()) -} - -func formatBigInt(i *big.Int) template.HTML { - if i == nil { - return "" - } - s := i.String() - var rv strings.Builder - appendSeparatedNumberSpans(&rv, s, "ns") - return template.HTML(rv.String()) + return strconv.FormatFloat(a, 'f', 2, 64) } func getAddressAlias(a string, td *TemplateData) *api.AddressAlias { diff --git a/server/public_test.go b/server/public_test.go index eba62945..3f862e34 100644 --- a/server/public_test.go +++ b/server/public_test.go @@ -4,13 +4,11 @@ package server import ( "encoding/json" - "html/template" "io/ioutil" "net/http" "net/http/httptest" "net/url" "os" - "reflect" "strconv" "strings" "testing" @@ -1609,165 +1607,3 @@ func Test_PublicServer_BitcoinType_ExtendedIndex(t *testing.T) { httpTestsExtendedIndex(t, ts) } - -func Test_formatInt64(t *testing.T) { - tests := []struct { - name string - n int64 - want template.HTML - }{ - {"1", 1, "1"}, - {"13", 13, "13"}, - {"123", 123, "123"}, - {"1234", 1234, `1234`}, - {"91234", 91234, `91234`}, - {"891234", 891234, `891234`}, - {"7891234", 7891234, `7891234`}, - {"67891234", 67891234, `67891234`}, - {"567891234", 567891234, `567891234`}, - {"4567891234", 4567891234, `4567891234`}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := formatInt64(tt.n); !reflect.DeepEqual(got, tt.want) { - t.Errorf("formatInt64() = %v, want %v", got, tt.want) - } - }) - } -} - -func Test_formatTime(t *testing.T) { - timeNow = fixedTimeNow - tests := []struct { - name string - want template.HTML - }{ - { - name: "2020-12-23 15:16:17", - want: `630 days 21 hours ago`, - }, - { - name: "2022-08-23 11:12:13", - want: `23 days 1 hour ago`, - }, - { - name: "2022-09-14 11:12:13", - want: `1 day 1 hour ago`, - }, - { - name: "2022-09-14 14:12:13", - want: `22 hours 31 mins ago`, - }, - { - name: "2022-09-15 09:33:26", - want: `3 hours 10 mins ago`, - }, - { - name: "2022-09-15 12:23:56", - want: `20 mins ago`, - }, - { - name: "2022-09-15 12:24:07", - want: `19 mins ago`, - }, - { - name: "2022-09-15 12:43:21", - want: `35 secs ago`, - }, - { - name: "2022-09-15 12:43:56", - want: `0 secs ago`, - }, - { - name: "2022-09-16 12:43:56", - want: `2022-09-16 12:43:56`, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tm, _ := time.Parse("2006-01-02 15:04:05", tt.name) - if got := timeSpan(&tm); !reflect.DeepEqual(got, tt.want) { - t.Errorf("formatTime() = %v, want %v", got, tt.want) - } - }) - } -} - -func Test_appendAmountSpan(t *testing.T) { - tests := []struct { - name string - class string - amount string - shortcut string - txDate string - want string - }{ - { - name: "prim-amt 1.23456789 BTC", - class: "prim-amt", - amount: "1.23456789", - shortcut: "BTC", - want: `1.23456789 BTC`, - }, - { - name: "prim-amt 1432134.23456 BTC", - class: "prim-amt", - amount: "1432134.23456", - shortcut: "BTC", - want: `1432134.23456 BTC`, - }, - { - name: "sec-amt 1 EUR", - class: "sec-amt", - amount: "1", - shortcut: "EUR", - want: `1 EUR`, - }, - { - name: "sec-amt -1 EUR", - class: "sec-amt", - amount: "-1", - shortcut: "EUR", - want: `-1 EUR`, - }, - { - name: "sec-amt 432109.23 EUR", - class: "sec-amt", - amount: "432109.23", - shortcut: "EUR", - want: `432109.23 EUR`, - }, - { - name: "sec-amt -432109.23 EUR", - class: "sec-amt", - amount: "-432109.23", - shortcut: "EUR", - want: `-432109.23 EUR`, - }, - { - name: "sec-amt 43141.29 EUR", - class: "sec-amt", - amount: "43141.29", - shortcut: "EUR", - txDate: "2022-03-14", - want: `43141.29 EUR`, - }, - { - name: "sec-amt -43141.29 EUR", - class: "sec-amt", - amount: "-43141.29", - shortcut: "EUR", - txDate: "2022-03-14", - want: `-43141.29 EUR`, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var rv strings.Builder - appendAmountSpan(&rv, tt.class, tt.amount, tt.shortcut, tt.txDate) - if got := rv.String(); !reflect.DeepEqual(got, tt.want) { - t.Errorf("formatTime() = %v, want %v", got, tt.want) - } - }) - } -}