Add Telegram Messenger notifications
Signed-off-by: Julien Riou <julien@riou.xyz>
This commit is contained in:
parent
e67ab63ca8
commit
0a9ae46d19
11 changed files with 255 additions and 39 deletions
3
Makefile
3
Makefile
|
@ -13,5 +13,8 @@ build:
|
|||
release:
|
||||
go build -ldflags "${LDFLAGS}" -o bin/restockbot-${APPVERSION}-${PLATFORM}-${ARCH} *.go
|
||||
|
||||
test:
|
||||
go test
|
||||
|
||||
clean:
|
||||
rm -rf bin
|
45
README.md
45
README.md
|
@ -22,6 +22,47 @@ Follow [this procedure](https://github.com/jouir/twitter-login) to generate all
|
|||
* `access_token`
|
||||
* `access_token_secret`
|
||||
|
||||
### Telegram (optional)
|
||||
|
||||
Follow [this procedure](https://core.telegram.org/bots#3-how-do-i-create-a-bot) to create a bot `token`.
|
||||
|
||||
Then you have two possible destinations to send messages:
|
||||
* channel using a `channel_name` (string)
|
||||
* chat using a `chat_id` (integer)
|
||||
|
||||
For testing purpose, you should store the token in a variable for next sections:
|
||||
```
|
||||
read -s TOKEN
|
||||
```
|
||||
|
||||
#### Chat
|
||||
|
||||
To get the chat identifier, you can send a message to your bot then read messages using the API:
|
||||
|
||||
```
|
||||
curl -s -XGET "https://api.telegram.org/bot${TOKEN}/getUpdates" | jq -r ".result[].message.chat.id"
|
||||
```
|
||||
|
||||
You can test to send messages to a chat with:
|
||||
|
||||
```
|
||||
read CHAT_ID
|
||||
curl -s -XGET "https://api.telegram.org/bot${TOKEN}/sendMessage?chat_id=${CHAT_ID}&text=hello" | jq
|
||||
```
|
||||
|
||||
#### Channel
|
||||
|
||||
Public channel names can be used (example: `@mychannel`). For private channels, you should use a `chat_id` instead.
|
||||
|
||||
You can test to send messages to a channel with:
|
||||
|
||||
```
|
||||
read CHANNEL_NAME
|
||||
curl -s -XGET "https://api.telegram.org/bot${TOKEN}/sendMessage?chat_id=${CHANNEL_NAME}&text=hello" | jq
|
||||
```
|
||||
|
||||
Don't forget to prefix the channel name with an `@`.
|
||||
|
||||
## Compilation
|
||||
|
||||
### With pre-built binaries
|
||||
|
@ -74,6 +115,10 @@ Options:
|
|||
* `access_token`: authentication token generated for your Twitter account
|
||||
* `access_token_secret`: authentication token secret generated for your Twitter account
|
||||
* `hashtags`: list of key/value used to append hashtags to each tweet. Key is the pattern to match in the product name, value is the string to append to the tweet. For example, `{"twitter": {"hashtags": [{"rtx 3090": "#nvidia #rtx3090"}]}}` will detect `rtx 3090` to append `#nvidia #rtx3090` at the end of the tweet.
|
||||
* `telegram` (optional):
|
||||
* `channel_name`: send message to a channel (ex: `@channel`)
|
||||
* `chat_id`: send message to a chat (ex: `1234`)
|
||||
* `token`: key returned by BotFather
|
||||
* `include_regex` (optional): include products with a name matching this regexp
|
||||
* `exclude_regex` (optional): exclude products with a name matching this regexp
|
||||
* `browser_address` (optional): set headless browser address (ex: `http://127.0.0.1:9222`)
|
||||
|
|
13
config.go
13
config.go
|
@ -9,6 +9,7 @@ import (
|
|||
// Config to store JSON configuration
|
||||
type Config struct {
|
||||
TwitterConfig `json:"twitter"`
|
||||
TelegramConfig `json:"telegram"`
|
||||
URLs []string `json:"urls"`
|
||||
IncludeRegex string `json:"include_regex"`
|
||||
ExcludeRegex string `json:"exclude_regex"`
|
||||
|
@ -24,6 +25,13 @@ type TwitterConfig struct {
|
|||
Hashtags []map[string]string `json:"hashtags"`
|
||||
}
|
||||
|
||||
// TelegramConfig to store Telegram API key
|
||||
type TelegramConfig struct {
|
||||
Token string `json:"token"`
|
||||
ChatID int64 `json:"chat_id"`
|
||||
ChannelName string `json:"channel_name"`
|
||||
}
|
||||
|
||||
// NewConfig creates a Config struct
|
||||
func NewConfig() *Config {
|
||||
return &Config{}
|
||||
|
@ -52,3 +60,8 @@ func (c *Config) Read(file string) error {
|
|||
func (c *Config) HasTwitter() bool {
|
||||
return (c.TwitterConfig.AccessToken != "" && c.TwitterConfig.AccessTokenSecret != "" && c.TwitterConfig.ConsumerKey != "" && c.TwitterConfig.ConsumerSecret != "")
|
||||
}
|
||||
|
||||
// HasTelegram returns true when Telegram has been configured
|
||||
func (c *Config) HasTelegram() bool {
|
||||
return c.TelegramConfig.Token != "" && (c.TelegramConfig.ChatID != 0 || c.TelegramConfig.ChannelName != "")
|
||||
}
|
||||
|
|
1
go.mod
1
go.mod
|
@ -6,6 +6,7 @@ require (
|
|||
github.com/MontFerret/ferret v0.13.0
|
||||
github.com/dghubble/go-twitter v0.0.0-20201011215211-4b180d0cc78d
|
||||
github.com/dghubble/oauth1 v0.7.0
|
||||
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.0.0-rc1
|
||||
github.com/sirupsen/logrus v1.8.0
|
||||
gorm.io/driver/sqlite v1.1.4
|
||||
gorm.io/gorm v1.20.12
|
||||
|
|
8
go.sum
8
go.sum
|
@ -36,6 +36,8 @@ github.com/dghubble/oauth1 v0.7.0 h1:AlpZdbRiJM4XGHIlQ8BuJ/wlpGwFEJNnB4Mc+78tA/w
|
|||
github.com/dghubble/oauth1 v0.7.0/go.mod h1:8pFdfPkv/jr8mkChVbNVuJ0suiHe278BtWI4Tk1ujxk=
|
||||
github.com/dghubble/sling v1.3.0 h1:pZHjCJq4zJvc6qVQ5wN1jo5oNZlNE0+8T/h0XeXBUKU=
|
||||
github.com/dghubble/sling v1.3.0/go.mod h1:XXShWaBWKzNLhu2OxikSNFrlsvowtz4kyRuXUG7oQKY=
|
||||
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.0.0-rc1 h1:Mr8jIV7wDfLw5Fw6BPupm0aduTFdLjhI3wFuIIZKvO4=
|
||||
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.0.0-rc1/go.mod h1:2s/IzRcxCszyNh760IjJiqoYHTnifk8ZeNYL33z8Pww=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/google/go-cmp v0.4.1 h1:/exdXoGamhu5ONeUJH0deniYLWYvQwW66yvlfiiKTu0=
|
||||
|
@ -59,6 +61,7 @@ github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7
|
|||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mafredri/cdp v0.30.0 h1:Lvcwjajq6wB6Uk8dYeCLrF26LG85rUdpMxgrwdEvU0o=
|
||||
github.com/mafredri/cdp v0.30.0/go.mod h1:71D84qPmWUvBWYj24Zp+U69mrUof4o8qL2X1fQJ/lHc=
|
||||
|
@ -76,6 +79,7 @@ github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9
|
|||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk=
|
||||
github.com/ngdinhtoan/glide-cleanup v0.2.0/go.mod h1:UQzsmiDOb8YV3nOsCxK/c9zPpCZVNoHScRE3EO9pVMM=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
|
@ -100,6 +104,8 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
|
|||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQDeX7m2XsSOlQEnM=
|
||||
github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog=
|
||||
github.com/wI2L/jettison v0.7.1 h1:XNq/WvSOAiJhFww9F5JZZcBZtKFL2Y/9WHHEHLDq9TE=
|
||||
github.com/wI2L/jettison v0.7.1/go.mod h1:dj49nOP41M7x6Jql62BqqF/+nW+XJgBaWzJR0hd6M84=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
|
@ -134,8 +140,10 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
|
|||
golang.org/x/tools v0.0.0-20200601175630-2caf76543d99/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
|
|
7
main.go
7
main.go
|
@ -125,6 +125,13 @@ func main() {
|
|||
}
|
||||
notifiers = append(notifiers, twitterNotifier)
|
||||
}
|
||||
if config.HasTelegram() {
|
||||
telegramNotifier, err := NewTelegramNotifier(&config.TelegramConfig, db)
|
||||
if err != nil {
|
||||
log.Fatalf("cannot create telegram client: %s", err)
|
||||
}
|
||||
notifiers = append(notifiers, telegramNotifier)
|
||||
}
|
||||
}
|
||||
|
||||
// Group links by shop
|
||||
|
|
19
notifier.go
19
notifier.go
|
@ -1,9 +1,26 @@
|
|||
package main
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Notifier interface to notify when a product becomes available or is sold out again
|
||||
type Notifier interface {
|
||||
NotifyWhenAvailable(string, string, float64, string, string) error
|
||||
NotifyWhenNotAvailable(string, time.Duration) error
|
||||
}
|
||||
|
||||
// formatPrice using internationalization rules
|
||||
// euro sign is placed after the value
|
||||
// default the currency, or symbol if applicable, is placed before the value
|
||||
func formatPrice(value float64, currency string) string {
|
||||
switch {
|
||||
case currency == "EUR":
|
||||
return fmt.Sprintf("%.2f€", value)
|
||||
case currency == "USD":
|
||||
return fmt.Sprintf("$%.2f", value)
|
||||
default:
|
||||
return fmt.Sprintf("%s%.2f", currency, value)
|
||||
}
|
||||
}
|
||||
|
|
129
notifier_telegram.go
Normal file
129
notifier_telegram.go
Normal file
|
@ -0,0 +1,129 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
telegram "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// TelegramMessage to store relationship between a Product and a Telegram notification
|
||||
type TelegramMessage struct {
|
||||
gorm.Model
|
||||
MessageID int `gorm:"not null;unique"`
|
||||
ProductURL string
|
||||
Product Product `gorm:"foreignKey:ProductURL"`
|
||||
}
|
||||
|
||||
// TelegramNotifier to manage notifications to Twitter
|
||||
type TelegramNotifier struct {
|
||||
db *gorm.DB
|
||||
bot *telegram.BotAPI
|
||||
chatID int64
|
||||
channelName string
|
||||
timeout int
|
||||
}
|
||||
|
||||
// NewTelegramNotifier to create a Notifier with Telegram capabilities
|
||||
func NewTelegramNotifier(config *TelegramConfig, db *gorm.DB) (*TelegramNotifier, error) {
|
||||
// create table
|
||||
err := db.AutoMigrate(&TelegramMessage{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// create client
|
||||
bot, err := telegram.NewBotAPI(config.Token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Debugf("connected to telegram as %s", bot.Self.UserName)
|
||||
|
||||
return &TelegramNotifier{
|
||||
db: db,
|
||||
bot: bot,
|
||||
chatID: config.ChatID,
|
||||
channelName: config.ChannelName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NotifyWhenAvailable create a Telegram message for announcing that a product is available
|
||||
// implements the Notifier interface
|
||||
func (n *TelegramNotifier) NotifyWhenAvailable(shopName string, productName string, productPrice float64, productCurrency string, productURL string) error {
|
||||
// TODO: check if message exists in the database to avoid flood
|
||||
|
||||
// send message to telegram
|
||||
formattedPrice := formatPrice(productPrice, productCurrency)
|
||||
message := fmt.Sprintf("🟢 %s: %s for %s is available at %s", shopName, productName, formattedPrice, productURL)
|
||||
messageID, err := n.sendMessage(message, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// save telegram message to database
|
||||
m := TelegramMessage{MessageID: messageID, ProductURL: productURL}
|
||||
trx := n.db.Create(&m)
|
||||
if trx.Error != nil {
|
||||
return fmt.Errorf("failed to save telegram message %d to database: %s", m.MessageID, trx.Error)
|
||||
}
|
||||
log.Debugf("telegram message %d saved to database", m.MessageID)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NotifyWhenNotAvailable create a Telegram message replying to the NotifyWhenAvailable message to say it's gone
|
||||
// implements the Notifier interface
|
||||
func (n *TelegramNotifier) NotifyWhenNotAvailable(productURL string, duration time.Duration) error {
|
||||
// find message in the database
|
||||
var m TelegramMessage
|
||||
trx := n.db.Where(TelegramMessage{ProductURL: productURL}).First(&m)
|
||||
if trx.Error != nil {
|
||||
return fmt.Errorf("failed to find telegram message in database for product with url %s: %s", productURL, trx.Error)
|
||||
}
|
||||
if m.MessageID == 0 {
|
||||
log.Warnf("telegram message for product with url %s not found, skipping close notification", productURL)
|
||||
return nil
|
||||
}
|
||||
|
||||
// format message
|
||||
text := fmt.Sprintf("🔴 And it's gone (%s)", duration)
|
||||
|
||||
// send reply on telegram
|
||||
_, err := n.sendMessage(text, m.MessageID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to reply on telegram: %s", err)
|
||||
}
|
||||
log.Infof("reply to telegram message %d sent", m.MessageID)
|
||||
|
||||
// remove message from database
|
||||
trx = n.db.Unscoped().Delete(&m)
|
||||
if trx.Error != nil {
|
||||
return fmt.Errorf("failed to remove message %d from database: %s", m.MessageID, trx.Error)
|
||||
}
|
||||
log.Debugf("telegram message removed from database")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *TelegramNotifier) sendMessage(text string, reply int) (int, error) {
|
||||
log.Debugf("sending message %s to telegram", text)
|
||||
var request telegram.MessageConfig
|
||||
if n.chatID != 0 {
|
||||
request = telegram.NewMessage(n.chatID, text)
|
||||
} else {
|
||||
request = telegram.NewMessageToChannel(n.channelName, text)
|
||||
}
|
||||
request.DisableWebPagePreview = true
|
||||
|
||||
if reply != 0 {
|
||||
request.ReplyToMessageID = reply
|
||||
}
|
||||
|
||||
response, err := n.bot.Send(request)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
log.Infof("message %d sent to telegram", response.MessageID)
|
||||
return response.MessageID, nil
|
||||
}
|
30
notifier_test.go
Normal file
30
notifier_test.go
Normal file
|
@ -0,0 +1,30 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFormatPrice(t *testing.T) {
|
||||
tests := []struct {
|
||||
value float64
|
||||
currency string
|
||||
expected string
|
||||
}{
|
||||
{999.99, "EUR", "999.99€"},
|
||||
{999.99, "USD", "$999.99"},
|
||||
{999.99, "CHF", "CHF999.99"},
|
||||
{999.99, "", "999.99"},
|
||||
}
|
||||
|
||||
for i, tc := range tests {
|
||||
t.Run(fmt.Sprintf("TestFormatPrice#%d", i), func(t *testing.T) {
|
||||
got := formatPrice(tc.value, tc.currency)
|
||||
if got != tc.expected {
|
||||
t.Errorf("for value %0.2f and currency %s, got %s, want %s", tc.value, tc.currency, got, tc.expected)
|
||||
} else {
|
||||
t.Logf("for value %0.2f and currency %s, got %s, want %s", tc.value, tc.currency, got, tc.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -94,23 +94,10 @@ func (c *TwitterNotifier) buildHashtags(productName string) string {
|
|||
return ""
|
||||
}
|
||||
|
||||
// formatPrice using internationalization rules
|
||||
// euro sign is placed after the value
|
||||
// default the currency, or symbol if applicable, is placed before the value
|
||||
func formatPrice(value float64, currency string) string {
|
||||
switch {
|
||||
case currency == "EUR":
|
||||
return fmt.Sprintf("%.2f€", value)
|
||||
case currency == "USD":
|
||||
return fmt.Sprintf("$%.2f", value)
|
||||
default:
|
||||
return fmt.Sprintf("%s%.2f", currency, value)
|
||||
}
|
||||
}
|
||||
|
||||
// NotifyWhenAvailable create a Twitter status for announcing that a product is available
|
||||
// implements the Notifier interface
|
||||
func (c *TwitterNotifier) NotifyWhenAvailable(shopName string, productName string, productPrice float64, productCurrency string, productURL string) error {
|
||||
// TODO: check if message exists in the database to avoid flood
|
||||
hashtags := c.buildHashtags(productName)
|
||||
message := formatAvailableTweet(shopName, productName, productPrice, productCurrency, productURL, hashtags)
|
||||
// create thread
|
|
@ -56,30 +56,6 @@ func TestBuildHashtags(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestFormatPrice(t *testing.T) {
|
||||
tests := []struct {
|
||||
value float64
|
||||
currency string
|
||||
expected string
|
||||
}{
|
||||
{999.99, "EUR", "999.99€"},
|
||||
{999.99, "USD", "$999.99"},
|
||||
{999.99, "CHF", "CHF999.99"},
|
||||
{999.99, "", "999.99"},
|
||||
}
|
||||
|
||||
for i, tc := range tests {
|
||||
t.Run(fmt.Sprintf("TestFormatPrice#%d", i), func(t *testing.T) {
|
||||
got := formatPrice(tc.value, tc.currency)
|
||||
if got != tc.expected {
|
||||
t.Errorf("for value %0.2f and currency %s, got %s, want %s", tc.value, tc.currency, got, tc.expected)
|
||||
} else {
|
||||
t.Logf("for value %0.2f and currency %s, got %s, want %s", tc.value, tc.currency, got, tc.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatAvailableTweet(t *testing.T) {
|
||||
tests := []struct {
|
||||
shopName string
|
Reference in a new issue