Refactor html template handling

This commit is contained in:
Martin Boehm 2023-03-26 00:40:10 +01:00
parent 41a2c5472b
commit a200f5cc83
4 changed files with 495 additions and 440 deletions

299
server/html_templates.go Normal file
View File

@ -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(`<span tt="` + f + `">` + r + " ago</span>")
}
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(`<span`)
if class != "" {
rv.WriteString(` class="`)
rv.WriteString(class)
rv.WriteString(`"`)
}
if txDate != "" {
rv.WriteString(` tm="`)
rv.WriteString(txDate)
rv.WriteString(`"`)
}
rv.WriteString(">")
i := strings.IndexByte(amount, '.')
if i < 0 {
appendSeparatedNumberSpans(rv, amount, "nc")
} else {
appendSeparatedNumberSpans(rv, amount[:i], "nc")
rv.WriteString(`.`)
rv.WriteString(`<span class="amt-dec">`)
appendLeftSeparatedNumberSpans(rv, amount[i+1:], "ns")
rv.WriteString("</span>")
}
if shortcut != "" {
rv.WriteString(" ")
rv.WriteString(shortcut)
}
rv.WriteString("</span>")
}
func appendAmountWrapperSpan(rv *strings.Builder, primary, symbol, classes string) {
rv.WriteString(`<span class="amt`)
if classes != "" {
rv.WriteString(` `)
rv.WriteString(classes)
}
rv.WriteString(`" cc="`)
rv.WriteString(primary)
rv.WriteString(" ")
rv.WriteString(symbol)
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(`<span class="`)
rv.WriteString(separatorClass)
rv.WriteString(`">`)
rv.WriteString(s[i : i+3])
rv.WriteString("</span>")
}
}
}
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(`<span class="`)
rv.WriteString(separatorClass)
rv.WriteString(`">`)
e := i + 3
if e > l {
e = l
}
rv.WriteString(s[i:e])
rv.WriteString("</span>")
}
}
}
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())
}

View File

@ -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, `1<span class="ns">234</span>`},
{"91234", 91234, `91<span class="ns">234</span>`},
{"891234", 891234, `891<span class="ns">234</span>`},
{"7891234", 7891234, `7<span class="ns">891</span><span class="ns">234</span>`},
{"67891234", 67891234, `67<span class="ns">891</span><span class="ns">234</span>`},
{"567891234", 567891234, `567<span class="ns">891</span><span class="ns">234</span>`},
{"4567891234", 4567891234, `4<span class="ns">567</span><span class="ns">891</span><span class="ns">234</span>`},
}
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: `<span tt="2020-12-23 15:16:17">630 days 21 hours ago</span>`,
},
{
name: "2022-08-23 11:12:13",
want: `<span tt="2022-08-23 11:12:13">23 days 1 hour ago</span>`,
},
{
name: "2022-09-14 11:12:13",
want: `<span tt="2022-09-14 11:12:13">1 day 1 hour ago</span>`,
},
{
name: "2022-09-14 14:12:13",
want: `<span tt="2022-09-14 14:12:13">22 hours 31 mins ago</span>`,
},
{
name: "2022-09-15 09:33:26",
want: `<span tt="2022-09-15 09:33:26">3 hours 10 mins ago</span>`,
},
{
name: "2022-09-15 12:23:56",
want: `<span tt="2022-09-15 12:23:56">20 mins ago</span>`,
},
{
name: "2022-09-15 12:24:07",
want: `<span tt="2022-09-15 12:24:07">19 mins ago</span>`,
},
{
name: "2022-09-15 12:43:21",
want: `<span tt="2022-09-15 12:43:21">35 secs ago</span>`,
},
{
name: "2022-09-15 12:43:56",
want: `<span tt="2022-09-15 12:43:56">0 secs ago</span>`,
},
{
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: `<span class="prim-amt">1.<span class="amt-dec">234<span class="ns">567</span><span class="ns">89</span></span> BTC</span>`,
},
{
name: "prim-amt 1432134.23456 BTC",
class: "prim-amt",
amount: "1432134.23456",
shortcut: "BTC",
want: `<span class="prim-amt">1<span class="nc">432</span><span class="nc">134</span>.<span class="amt-dec">234<span class="ns">56</span></span> BTC</span>`,
},
{
name: "sec-amt 1 EUR",
class: "sec-amt",
amount: "1",
shortcut: "EUR",
want: `<span class="sec-amt">1 EUR</span>`,
},
{
name: "sec-amt -1 EUR",
class: "sec-amt",
amount: "-1",
shortcut: "EUR",
want: `<span class="sec-amt">-1 EUR</span>`,
},
{
name: "sec-amt 432109.23 EUR",
class: "sec-amt",
amount: "432109.23",
shortcut: "EUR",
want: `<span class="sec-amt">432<span class="nc">109</span>.<span class="amt-dec">23</span> EUR</span>`,
},
{
name: "sec-amt -432109.23 EUR",
class: "sec-amt",
amount: "-432109.23",
shortcut: "EUR",
want: `<span class="sec-amt">-432<span class="nc">109</span>.<span class="amt-dec">23</span> EUR</span>`,
},
{
name: "sec-amt 43141.29 EUR",
class: "sec-amt",
amount: "43141.29",
shortcut: "EUR",
txDate: "2022-03-14",
want: `<span class="sec-amt" tm="2022-03-14">43<span class="nc">141</span>.<span class="amt-dec">29</span> EUR</span>`,
},
{
name: "sec-amt -43141.29 EUR",
class: "sec-amt",
amount: "-43141.29",
shortcut: "EUR",
txDate: "2022-03-14",
want: `<span class="sec-amt" tm="2022-03-14">-43<span class="nc">141</span>.<span class="amt-dec">29</span> EUR</span>`,
},
}
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)
}
})
}
}

View File

@ -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(`<span tt="` + f + `">` + r + " ago</span>")
}
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(`<span`)
if class != "" {
rv.WriteString(` class="`)
rv.WriteString(class)
rv.WriteString(`"`)
}
if txDate != "" {
rv.WriteString(` tm="`)
rv.WriteString(txDate)
rv.WriteString(`"`)
}
rv.WriteString(">")
i := strings.IndexByte(amount, '.')
if i < 0 {
appendSeparatedNumberSpans(rv, amount, "nc")
} else {
appendSeparatedNumberSpans(rv, amount[:i], "nc")
rv.WriteString(`.`)
rv.WriteString(`<span class="amt-dec">`)
appendLeftSeparatedNumberSpans(rv, amount[i+1:], "ns")
rv.WriteString("</span>")
}
if shortcut != "" {
rv.WriteString(" ")
rv.WriteString(shortcut)
}
rv.WriteString("</span>")
}
func appendAmountWrapperSpan(rv *strings.Builder, primary, symbol, classes string) {
rv.WriteString(`<span class="amt`)
if classes != "" {
rv.WriteString(` `)
rv.WriteString(classes)
}
rv.WriteString(`" cc="`)
rv.WriteString(primary)
rv.WriteString(" ")
rv.WriteString(symbol)
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(`<span class="`)
rv.WriteString(separatorClass)
rv.WriteString(`">`)
rv.WriteString(s[i : i+3])
rv.WriteString("</span>")
}
}
}
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(`<span class="`)
rv.WriteString(separatorClass)
rv.WriteString(`">`)
e := i + 3
if e > l {
e = l
}
rv.WriteString(s[i:e])
rv.WriteString("</span>")
}
}
}
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 {

View File

@ -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, `1<span class="ns">234</span>`},
{"91234", 91234, `91<span class="ns">234</span>`},
{"891234", 891234, `891<span class="ns">234</span>`},
{"7891234", 7891234, `7<span class="ns">891</span><span class="ns">234</span>`},
{"67891234", 67891234, `67<span class="ns">891</span><span class="ns">234</span>`},
{"567891234", 567891234, `567<span class="ns">891</span><span class="ns">234</span>`},
{"4567891234", 4567891234, `4<span class="ns">567</span><span class="ns">891</span><span class="ns">234</span>`},
}
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: `<span tt="2020-12-23 15:16:17">630 days 21 hours ago</span>`,
},
{
name: "2022-08-23 11:12:13",
want: `<span tt="2022-08-23 11:12:13">23 days 1 hour ago</span>`,
},
{
name: "2022-09-14 11:12:13",
want: `<span tt="2022-09-14 11:12:13">1 day 1 hour ago</span>`,
},
{
name: "2022-09-14 14:12:13",
want: `<span tt="2022-09-14 14:12:13">22 hours 31 mins ago</span>`,
},
{
name: "2022-09-15 09:33:26",
want: `<span tt="2022-09-15 09:33:26">3 hours 10 mins ago</span>`,
},
{
name: "2022-09-15 12:23:56",
want: `<span tt="2022-09-15 12:23:56">20 mins ago</span>`,
},
{
name: "2022-09-15 12:24:07",
want: `<span tt="2022-09-15 12:24:07">19 mins ago</span>`,
},
{
name: "2022-09-15 12:43:21",
want: `<span tt="2022-09-15 12:43:21">35 secs ago</span>`,
},
{
name: "2022-09-15 12:43:56",
want: `<span tt="2022-09-15 12:43:56">0 secs ago</span>`,
},
{
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: `<span class="prim-amt">1.<span class="amt-dec">234<span class="ns">567</span><span class="ns">89</span></span> BTC</span>`,
},
{
name: "prim-amt 1432134.23456 BTC",
class: "prim-amt",
amount: "1432134.23456",
shortcut: "BTC",
want: `<span class="prim-amt">1<span class="nc">432</span><span class="nc">134</span>.<span class="amt-dec">234<span class="ns">56</span></span> BTC</span>`,
},
{
name: "sec-amt 1 EUR",
class: "sec-amt",
amount: "1",
shortcut: "EUR",
want: `<span class="sec-amt">1 EUR</span>`,
},
{
name: "sec-amt -1 EUR",
class: "sec-amt",
amount: "-1",
shortcut: "EUR",
want: `<span class="sec-amt">-1 EUR</span>`,
},
{
name: "sec-amt 432109.23 EUR",
class: "sec-amt",
amount: "432109.23",
shortcut: "EUR",
want: `<span class="sec-amt">432<span class="nc">109</span>.<span class="amt-dec">23</span> EUR</span>`,
},
{
name: "sec-amt -432109.23 EUR",
class: "sec-amt",
amount: "-432109.23",
shortcut: "EUR",
want: `<span class="sec-amt">-432<span class="nc">109</span>.<span class="amt-dec">23</span> EUR</span>`,
},
{
name: "sec-amt 43141.29 EUR",
class: "sec-amt",
amount: "43141.29",
shortcut: "EUR",
txDate: "2022-03-14",
want: `<span class="sec-amt" tm="2022-03-14">43<span class="nc">141</span>.<span class="amt-dec">29</span> EUR</span>`,
},
{
name: "sec-amt -43141.29 EUR",
class: "sec-amt",
amount: "-43141.29",
shortcut: "EUR",
txDate: "2022-03-14",
want: `<span class="sec-amt" tm="2022-03-14">-43<span class="nc">141</span>.<span class="amt-dec">29</span> EUR</span>`,
},
}
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)
}
})
}
}