forked from wgroeneveld/go-jamming
wm/pingback sending, concurrent impl, e2e test
This commit is contained in:
parent
d9ded09383
commit
714b90d594
|
@ -7,22 +7,30 @@ import (
|
||||||
"net/url"
|
"net/url"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// this should be passed along as a value object, not as a pointer
|
||||||
type Mention struct {
|
type Mention struct {
|
||||||
Source string
|
Source string
|
||||||
Target string
|
Target string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (wm *Mention) String() string {
|
func (wm Mention) AsFormValues() url.Values {
|
||||||
|
values := url.Values{}
|
||||||
|
values.Add("source", wm.Source)
|
||||||
|
values.Add("target", wm.Target)
|
||||||
|
return values
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wm Mention) String() string {
|
||||||
return fmt.Sprintf("source: %s, target: %s", wm.Source, wm.Target)
|
return fmt.Sprintf("source: %s, target: %s", wm.Source, wm.Target)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (wm *Mention) AsPath(conf *common.Config) string {
|
func (wm Mention) AsPath(conf *common.Config) string {
|
||||||
filename := fmt.Sprintf("%x", md5.Sum([]byte("source="+wm.Source+",target="+wm.Target)))
|
filename := fmt.Sprintf("%x", md5.Sum([]byte("source="+wm.Source+",target="+wm.Target)))
|
||||||
domain, _ := conf.FetchDomain(wm.Target)
|
domain, _ := conf.FetchDomain(wm.Target)
|
||||||
return conf.DataPath + "/" + domain + "/" + filename + ".json"
|
return conf.DataPath + "/" + domain + "/" + filename + ".json"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (wm *Mention) SourceUrl() *url.URL {
|
func (wm Mention) SourceUrl() *url.URL {
|
||||||
url, _ := url.Parse(wm.Source)
|
url, _ := url.Parse(wm.Source)
|
||||||
return url
|
return url
|
||||||
}
|
}
|
||||||
|
|
|
@ -72,6 +72,15 @@ func Map(mf *microformats.Microformat, key string) map[string]string {
|
||||||
return mapVal
|
return mapVal
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func HEntry(data *microformats.Data) *microformats.Microformat {
|
||||||
|
for _, itm := range data.Items {
|
||||||
|
if common.Includes(itm.Type, "h-entry") {
|
||||||
|
return itm
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func Prop(mf *microformats.Microformat, key string) *microformats.Microformat {
|
func Prop(mf *microformats.Microformat, key string) *microformats.Microformat {
|
||||||
val := mf.Properties[key]
|
val := mf.Properties[key]
|
||||||
if len(val) == 0 {
|
if len(val) == 0 {
|
||||||
|
|
|
@ -44,7 +44,8 @@ type Sender struct {
|
||||||
func (sender *Sender) SendPingbackToEndpoint(endpoint string, mention mf.Mention) {
|
func (sender *Sender) SendPingbackToEndpoint(endpoint string, mention mf.Mention) {
|
||||||
err := sender.RestClient.Post(endpoint, "text/xml", body.fill(mention))
|
err := sender.RestClient.Post(endpoint, "text/xml", body.fill(mention))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Err(err).Str("wm", mention.String()).Msg("Unable to send pingback")
|
log.Err(err).Str("endpoint", endpoint).Str("wm", mention.String()).Msg("Unable to send pingback")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
log.Info().Str("wm", mention.String()).Msg("Pingback sent")
|
log.Info().Str("endpoint", endpoint).Str("wm", mention.String()).Msg("Pingback sent")
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,7 @@ type Receiver struct {
|
||||||
|
|
||||||
func (recv *Receiver) Receive(wm mf.Mention) {
|
func (recv *Receiver) Receive(wm mf.Mention) {
|
||||||
log.Info().Str("Webmention", wm.String()).Msg("OK: looks valid")
|
log.Info().Str("Webmention", wm.String()).Msg("OK: looks valid")
|
||||||
body, geterr := recv.RestClient.GetBody(wm.Source)
|
_, body, geterr := recv.RestClient.GetBody(wm.Source)
|
||||||
|
|
||||||
if geterr != nil {
|
if geterr != nil {
|
||||||
log.Warn().Str("source", wm.Source).Msg(" ABORT: invalid url")
|
log.Warn().Str("source", wm.Source).Msg(" ABORT: invalid url")
|
||||||
|
@ -39,15 +39,6 @@ func (recv *Receiver) deletePossibleOlderWebmention(wm mf.Mention) {
|
||||||
os.Remove(wm.AsPath(recv.Conf))
|
os.Remove(wm.AsPath(recv.Conf))
|
||||||
}
|
}
|
||||||
|
|
||||||
func getHEntry(data *microformats.Data) *microformats.Microformat {
|
|
||||||
for _, itm := range data.Items {
|
|
||||||
if common.Includes(itm.Type, "h-entry") {
|
|
||||||
return itm
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (recv *Receiver) processSourceBody(body string, wm mf.Mention) {
|
func (recv *Receiver) processSourceBody(body string, wm mf.Mention) {
|
||||||
if !strings.Contains(body, wm.Target) {
|
if !strings.Contains(body, wm.Target) {
|
||||||
log.Warn().Str("target", wm.Target).Msg("ABORT: no mention of target found in html src of source!")
|
log.Warn().Str("target", wm.Target).Msg("ABORT: no mention of target found in html src of source!")
|
||||||
|
@ -55,7 +46,7 @@ func (recv *Receiver) processSourceBody(body string, wm mf.Mention) {
|
||||||
}
|
}
|
||||||
|
|
||||||
data := microformats.Parse(strings.NewReader(body), wm.SourceUrl())
|
data := microformats.Parse(strings.NewReader(body), wm.SourceUrl())
|
||||||
indieweb := recv.convertBodyToIndiewebData(body, wm, getHEntry(data))
|
indieweb := recv.convertBodyToIndiewebData(body, wm, mf.HEntry(data))
|
||||||
|
|
||||||
recv.saveWebmentionToDisk(wm, indieweb)
|
recv.saveWebmentionToDisk(wm, indieweb)
|
||||||
log.Info().Str("file", wm.AsPath(recv.Conf)).Msg("OK: Webmention processed.")
|
log.Info().Str("file", wm.AsPath(recv.Conf)).Msg("OK: Webmention processed.")
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
@ -132,8 +133,8 @@ func TestReceiveTargetDoesNotExistAnymoreDeletesPossiblyOlderWebmention(t *testi
|
||||||
writeSomethingTo(filename)
|
writeSomethingTo(filename)
|
||||||
|
|
||||||
client := &mocks.RestClientMock{
|
client := &mocks.RestClientMock{
|
||||||
GetBodyFunc: func(url string) (string, error) {
|
GetBodyFunc: func(url string) (http.Header, string, error) {
|
||||||
return "", errors.New("whoops")
|
return nil, "", errors.New("whoops")
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
receiver := &Receiver{
|
receiver := &Receiver{
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
package send
|
||||||
|
|
||||||
|
import (
|
||||||
|
"brainbaking.com/go-jamming/rest"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"strings"
|
||||||
|
"willnorris.com/go/microformats"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
TypeWebmention string = "webmention"
|
||||||
|
TypeUnknown string = "unknown"
|
||||||
|
TypePingback string = "pingback"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (sndr *Sender) discover(target string) (link string, mentionType string) {
|
||||||
|
mentionType = TypeUnknown
|
||||||
|
header, body, err := sndr.RestClient.GetBody(target)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn().Str("target", target).Msg("Failed to discover possible endpoint, aborting send")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(header.Get("link"), TypeWebmention) {
|
||||||
|
return buildWebmentionHeaderLink(header.Get("link")), TypeWebmention
|
||||||
|
}
|
||||||
|
if header.Get("X-Pingback") != "" {
|
||||||
|
return header.Get("X-Pingback"), TypePingback
|
||||||
|
}
|
||||||
|
|
||||||
|
// this also complies with w3.org regulations: relative endpoint could be possible
|
||||||
|
format := microformats.Parse(strings.NewReader(body), rest.BaseUrlOf(target))
|
||||||
|
if len(format.Rels[TypeWebmention]) > 0 {
|
||||||
|
mentionType = TypeWebmention
|
||||||
|
link = format.Rels[TypeWebmention][0]
|
||||||
|
} else if len(format.Rels[TypePingback]) > 0 {
|
||||||
|
mentionType = TypePingback
|
||||||
|
link = format.Rels[TypePingback][0]
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildWebmentionHeaderLink(link string) string {
|
||||||
|
// e.g. Link: <http://aaronpk.example/webmention-endpoint>; rel="webmention"
|
||||||
|
raw := strings.Split(link, ";")[0][1:]
|
||||||
|
return raw[:len(raw)-1]
|
||||||
|
}
|
|
@ -0,0 +1,78 @@
|
||||||
|
package send
|
||||||
|
|
||||||
|
import (
|
||||||
|
"brainbaking.com/go-jamming/mocks"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDiscover(t *testing.T) {
|
||||||
|
var sender = &Sender{
|
||||||
|
RestClient: &mocks.RestClientMock{
|
||||||
|
GetBodyFunc: mocks.RelPathGetBodyFunc(t, "../../../mocks/"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
label string
|
||||||
|
url string
|
||||||
|
expectedLink string
|
||||||
|
expectedType string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"discover 'unknown' if no link is present",
|
||||||
|
"https://brainbaking.com/link-discover-test-none.html",
|
||||||
|
"",
|
||||||
|
TypeUnknown,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prefer webmentions over pingbacks if both links are present",
|
||||||
|
"https://brainbaking.com/link-discover-bothtypes.html",
|
||||||
|
"http://aaronpk.example/webmention-endpoint",
|
||||||
|
TypeWebmention,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pingbacks: discover link if present in header",
|
||||||
|
"https://brainbaking.com/pingback-discover-test.html",
|
||||||
|
"http://aaronpk.example/pingback-endpoint",
|
||||||
|
TypePingback,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pingbacks: discover link if sole entry somewhere in html",
|
||||||
|
"https://brainbaking.com/pingback-discover-test-single.html",
|
||||||
|
"http://aaronpk.example/pingback-endpoint-body",
|
||||||
|
TypePingback,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pingbacks: use link in header if multiple present in html",
|
||||||
|
"https://brainbaking.com/pingback-discover-test-multiple.html",
|
||||||
|
"http://aaronpk.example/pingback-endpoint-header",
|
||||||
|
TypePingback,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"webmentions: discover link if present in header",
|
||||||
|
"https://brainbaking.com/link-discover-test.html",
|
||||||
|
"http://aaronpk.example/webmention-endpoint",
|
||||||
|
TypeWebmention,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"webmentions: discover link if sole entry somewhere in html",
|
||||||
|
"https://brainbaking.com/link-discover-test-single.html",
|
||||||
|
"http://aaronpk.example/webmention-endpoint-body",
|
||||||
|
TypeWebmention,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"webmentions: use link in header if multiple present in html",
|
||||||
|
"https://brainbaking.com/link-discover-test-multiple.html",
|
||||||
|
"http://aaronpk.example/webmention-endpoint-header",
|
||||||
|
TypeWebmention,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.label, func(t *testing.T) {
|
||||||
|
link, mentionType := sender.discover(tc.url)
|
||||||
|
assert.Equal(t, tc.expectedLink, link)
|
||||||
|
assert.Equal(t, tc.expectedType, mentionType)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"brainbaking.com/go-jamming/common"
|
"brainbaking.com/go-jamming/common"
|
||||||
"brainbaking.com/go-jamming/rest"
|
"brainbaking.com/go-jamming/rest"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -16,22 +17,65 @@ type Sender struct {
|
||||||
|
|
||||||
func (snder *Sender) Send(domain string, since string) {
|
func (snder *Sender) Send(domain string, since string) {
|
||||||
log.Info().Str("domain", domain).Str("since", since).Msg(` OK: someone wants to send mentions`)
|
log.Info().Str("domain", domain).Str("since", since).Msg(` OK: someone wants to send mentions`)
|
||||||
feed, err := snder.RestClient.GetBody("https://" + domain + "/index.xml")
|
_, feed, err := snder.RestClient.GetBody("https://" + domain + "/index.xml")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Err(err).Str("domain", domain).Msg("Unable to retrieve RSS feed, aborting send")
|
log.Err(err).Str("domain", domain).Msg("Unable to retrieve RSS feed, aborting send")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
snder.parseRssFeed(feed, common.IsoToTime(since))
|
snder.parseRssFeed(feed, common.IsoToTime(since))
|
||||||
|
log.Info().Str("domain", domain).Str("since", since).Msg(` OK: sending done.`)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (snder *Sender) parseRssFeed(feed string, since time.Time) {
|
func (snder *Sender) parseRssFeed(feed string, since time.Time) {
|
||||||
|
items, err := snder.Collect(feed, since)
|
||||||
}
|
if err != nil {
|
||||||
|
log.Err(err).Msg("Unable to parse RSS fed, aborting send")
|
||||||
func mention() {
|
return
|
||||||
pingbackSender := &send.Sender{
|
|
||||||
RestClient: nil,
|
|
||||||
}
|
}
|
||||||
pingbackSender.SendPingbackToEndpoint("endpoint", mf.Mention{})
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
for _, item := range items {
|
||||||
|
for _, href := range item.hrefs {
|
||||||
|
mention := mf.Mention{
|
||||||
|
// SOURCE is own domain this time, TARGET = outbound
|
||||||
|
Source: item.link,
|
||||||
|
Target: href,
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
snder.sendMention(mention)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
var mentionFuncs = map[string]func(snder *Sender, mention mf.Mention, endpoint string){
|
||||||
|
TypeUnknown: func(snder *Sender, mention mf.Mention, endpoint string) {},
|
||||||
|
TypeWebmention: sendMentionAsWebmention,
|
||||||
|
TypePingback: sendMentionAsPingback,
|
||||||
|
}
|
||||||
|
|
||||||
|
func (snder *Sender) sendMention(mention mf.Mention) {
|
||||||
|
endpoint, mentionType := snder.discover(mention.Target)
|
||||||
|
mentionFuncs[mentionType](snder, mention, endpoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendMentionAsWebmention(snder *Sender, mention mf.Mention, endpoint string) {
|
||||||
|
err := snder.RestClient.PostForm(endpoint, mention.AsFormValues())
|
||||||
|
if err != nil {
|
||||||
|
log.Err(err).Str("endpoint", endpoint).Str("wm", mention.String()).Msg("Webmention send failed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Info().Str("endpoint", endpoint).Str("wm", mention.String()).Msg("OK: webmention sent.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendMentionAsPingback(snder *Sender, mention mf.Mention, endpoint string) {
|
||||||
|
pingbackSender := &send.Sender{
|
||||||
|
RestClient: snder.RestClient,
|
||||||
|
}
|
||||||
|
pingbackSender.SendPingbackToEndpoint(endpoint, mention)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,81 @@
|
||||||
|
package send
|
||||||
|
|
||||||
|
import (
|
||||||
|
"brainbaking.com/go-jamming/app/mf"
|
||||||
|
"brainbaking.com/go-jamming/common"
|
||||||
|
"brainbaking.com/go-jamming/mocks"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"net/url"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSendMentionAsWebmention(t *testing.T) {
|
||||||
|
passedFormValues := url.Values{}
|
||||||
|
snder := Sender{
|
||||||
|
RestClient: &mocks.RestClientMock{
|
||||||
|
PostFormFunc: func(endpoint string, formValues url.Values) error {
|
||||||
|
passedFormValues = formValues
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
sendMentionAsWebmention(&snder, mf.Mention{
|
||||||
|
Source: "mysource",
|
||||||
|
Target: "mytarget",
|
||||||
|
}, "someendpoint")
|
||||||
|
|
||||||
|
assert.Equal(t, "mysource", passedFormValues.Get("source"))
|
||||||
|
assert.Equal(t, "mytarget", passedFormValues.Get("target"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSendIntegrationTestCanSendBothWebmentionsAndPingbacks(t *testing.T) {
|
||||||
|
posted := map[string]interface{}{}
|
||||||
|
var lock = sync.RWMutex{}
|
||||||
|
|
||||||
|
snder := Sender{
|
||||||
|
Conf: common.Configure(),
|
||||||
|
RestClient: &mocks.RestClientMock{
|
||||||
|
GetBodyFunc: mocks.RelPathGetBodyFunc(t, "./../../../mocks/"),
|
||||||
|
PostFunc: func(url string, contentType string, body string) error {
|
||||||
|
lock.RLock()
|
||||||
|
defer lock.RUnlock()
|
||||||
|
posted[url] = body
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
PostFormFunc: func(endpoint string, formValues url.Values) error {
|
||||||
|
lock.RLock()
|
||||||
|
defer lock.RUnlock()
|
||||||
|
posted[endpoint] = formValues
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
snder.Send("brainbaking.com", "2021-03-16T16:00:00.000Z")
|
||||||
|
assert.Equal(t, 3, len(posted))
|
||||||
|
|
||||||
|
wmPost1 := posted["http://aaronpk.example/webmention-endpoint-header"].(url.Values)
|
||||||
|
assert.Equal(t, "https://brainbaking.com/notes/2021/03/16h17m07s14/", wmPost1.Get("source"))
|
||||||
|
assert.Equal(t, "https://brainbaking.com/link-discover-test-multiple.html", wmPost1.Get("target"))
|
||||||
|
|
||||||
|
wmPost2 := posted["http://aaronpk.example/pingback-endpoint-body"].(string)
|
||||||
|
expectedPost2 := `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<methodCall>
|
||||||
|
<methodName>pingback.ping</methodName>
|
||||||
|
<params>
|
||||||
|
<param>
|
||||||
|
<value><string>https://brainbaking.com/notes/2021/03/16h17m07s14/</string></value>
|
||||||
|
</param>
|
||||||
|
<param>
|
||||||
|
<value><string>https://brainbaking.com/pingback-discover-test-single.html</string></value>
|
||||||
|
</param>
|
||||||
|
</params>
|
||||||
|
</methodCall>`
|
||||||
|
assert.Equal(t, expectedPost2, wmPost2)
|
||||||
|
|
||||||
|
wmPost3 := posted["http://aaronpk.example/webmention-endpoint-body"].(url.Values)
|
||||||
|
assert.Equal(t, "https://brainbaking.com/notes/2021/03/16h17m07s14/", wmPost3.Get("source"))
|
||||||
|
assert.Equal(t, "https://brainbaking.com/link-discover-test-single.html", wmPost3.Get("target"))
|
||||||
|
}
|
|
@ -38,3 +38,12 @@ func (set *Set) Keys() []string {
|
||||||
}
|
}
|
||||||
return keys
|
return keys
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Includes(slice []string, elem string) bool {
|
||||||
|
for _, el := range slice {
|
||||||
|
if el == elem {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
package common
|
|
||||||
|
|
||||||
func Includes(slice []string, elem string) bool {
|
|
||||||
for _, el := range slice {
|
|
||||||
if el == elem {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
|
@ -1,33 +1,50 @@
|
||||||
package mocks
|
package mocks
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
// neat trick! https://medium.com/@matryer/meet-moq-easily-mock-interfaces-in-go-476444187d10
|
// neat trick! https://medium.com/@matryer/meet-moq-easily-mock-interfaces-in-go-476444187d10
|
||||||
type RestClientMock struct {
|
type RestClientMock struct {
|
||||||
GetFunc func(string) (*http.Response, error)
|
GetFunc func(string) (*http.Response, error)
|
||||||
GetBodyFunc func(string) (string, error)
|
GetBodyFunc func(string) (http.Header, string, error)
|
||||||
PostFunc func(string, string, string) error
|
PostFunc func(string, string, string) error
|
||||||
|
PostFormFunc func(string, url.Values) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// although these are still requied to match the rest.Client interface.
|
// although these are still requied to match the rest.Client interface.
|
||||||
func (m *RestClientMock) Get(url string) (*http.Response, error) {
|
func (m *RestClientMock) Get(url string) (*http.Response, error) {
|
||||||
return m.GetFunc(url)
|
return m.GetFunc(url)
|
||||||
}
|
}
|
||||||
func (m *RestClientMock) GetBody(url string) (string, error) {
|
func (m *RestClientMock) GetBody(url string) (http.Header, string, error) {
|
||||||
return m.GetBodyFunc(url)
|
return m.GetBodyFunc(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (client *RestClientMock) PostForm(url string, formData url.Values) error {
|
||||||
|
return client.PostFormFunc(url, formData)
|
||||||
|
}
|
||||||
|
|
||||||
func (m *RestClientMock) Post(url string, contentType string, body string) error {
|
func (m *RestClientMock) Post(url string, contentType string, body string) error {
|
||||||
return m.PostFunc(url, contentType, body)
|
return m.PostFunc(url, contentType, body)
|
||||||
}
|
}
|
||||||
|
|
||||||
func RelPathGetBodyFunc(t *testing.T, relPath string) func(string) (string, error) {
|
func toHttpHeader(header map[string]interface{}) http.Header {
|
||||||
return func(url string) (string, error) {
|
httpHeader := http.Header{}
|
||||||
|
for key, value := range header {
|
||||||
|
httpHeader.Add(key, value.(string))
|
||||||
|
}
|
||||||
|
return httpHeader
|
||||||
|
}
|
||||||
|
|
||||||
|
func RelPathGetBodyFunc(t *testing.T, relPath string) func(string) (http.Header, string, error) {
|
||||||
|
return func(url string) (http.Header, string, error) {
|
||||||
|
fmt.Println(" - GET call at " + url)
|
||||||
// url: https://brainbaking.com/something-something.html
|
// url: https://brainbaking.com/something-something.html
|
||||||
// want: ../../mocks/something-something.html
|
// want: ../../mocks/something-something.html
|
||||||
mockfile := relPath + strings.ReplaceAll(url, "https://brainbaking.com/", "")
|
mockfile := relPath + strings.ReplaceAll(url, "https://brainbaking.com/", "")
|
||||||
|
@ -35,7 +52,15 @@ func RelPathGetBodyFunc(t *testing.T, relPath string) func(string) (string, erro
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
return string(html), nil
|
|
||||||
|
headerData, headerFileErr := ioutil.ReadFile(strings.ReplaceAll(mockfile, ".html", "-headers.json"))
|
||||||
|
if headerFileErr != nil {
|
||||||
|
return http.Header{}, string(html), nil
|
||||||
|
}
|
||||||
|
headerJson := map[string]interface{}{}
|
||||||
|
json.Unmarshal(headerData, &headerJson)
|
||||||
|
|
||||||
|
return toHttpHeader(headerJson), string(html), nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,46 +4,74 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Client interface {
|
type Client interface {
|
||||||
Get(url string) (*http.Response, error)
|
Get(url string) (*http.Response, error)
|
||||||
Post(url string, contentType string, body string) error
|
Post(url string, contentType string, body string) error
|
||||||
GetBody(url string) (string, error)
|
GetBody(url string) (http.Header, string, error)
|
||||||
|
PostForm(url string, formData url.Values) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type HttpClient struct {
|
type HttpClient struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (client *HttpClient) Post(url string, contenType string, body string) error {
|
func (client *HttpClient) PostForm(url string, formData url.Values) error {
|
||||||
_, err := http.Post(url, contenType, strings.NewReader(body))
|
resp, err := http.PostForm(url, formData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if !isStatusOk(resp) {
|
||||||
|
return fmt.Errorf("POST Form to %s: Status code is not OK (%d)", url, resp.StatusCode)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *HttpClient) Post(url string, contenType string, body string) error {
|
||||||
|
resp, err := http.Post(url, contenType, strings.NewReader(body))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !isStatusOk(resp) {
|
||||||
|
return fmt.Errorf("POST to %s: Status code is not OK (%d)", url, resp.StatusCode)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// something like this? https://freshman.tech/snippets/go/http-response-to-string/
|
// something like this? https://freshman.tech/snippets/go/http-response-to-string/
|
||||||
func (client *HttpClient) GetBody(url string) (string, error) {
|
func (client *HttpClient) GetBody(url string) (http.Header, string, error) {
|
||||||
resp, geterr := http.Get(url)
|
resp, geterr := client.Get(url)
|
||||||
if geterr != nil {
|
if geterr != nil {
|
||||||
return "", geterr
|
return nil, "", geterr
|
||||||
}
|
}
|
||||||
|
|
||||||
if resp.StatusCode < 200 || resp.StatusCode > 299 {
|
body, err := ReadBodyFromResponse(resp)
|
||||||
return "", fmt.Errorf("Status code for %s is not OK (%d)", url, resp.StatusCode)
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp.Header, body, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadBodyFromResponse(resp *http.Response) (string, error) {
|
||||||
|
if !isStatusOk(resp) {
|
||||||
|
return "", fmt.Errorf("Status code is not OK (%d)", resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
defer resp.Body.Close()
|
|
||||||
body, readerr := ioutil.ReadAll(resp.Body)
|
body, readerr := ioutil.ReadAll(resp.Body)
|
||||||
|
defer resp.Body.Close()
|
||||||
if readerr != nil {
|
if readerr != nil {
|
||||||
return "", readerr
|
return "", readerr
|
||||||
}
|
}
|
||||||
|
|
||||||
return string(body), nil
|
return string(body), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isStatusOk(resp *http.Response) bool {
|
||||||
|
return resp.StatusCode >= 200 && resp.StatusCode <= 299
|
||||||
|
}
|
||||||
|
|
||||||
func (client *HttpClient) Get(url string) (*http.Response, error) {
|
func (client *HttpClient) Get(url string) (*http.Response, error) {
|
||||||
return http.Get(url)
|
return http.Get(url)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package rest
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
)
|
)
|
||||||
|
|
||||||
func BadRequest(w http.ResponseWriter) {
|
func BadRequest(w http.ResponseWriter) {
|
||||||
|
@ -12,3 +13,10 @@ func Accept(w http.ResponseWriter) {
|
||||||
w.WriteHeader(202)
|
w.WriteHeader(202)
|
||||||
w.Write([]byte("Thanks, bro. Will process this soon, pinky swear!"))
|
w.Write([]byte("Thanks, bro. Will process this soon, pinky swear!"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// assumes the URL is well-formed.
|
||||||
|
func BaseUrlOf(link string) *url.URL {
|
||||||
|
obj, _ := url.Parse(link)
|
||||||
|
baseUrl, _ := url.Parse(obj.Scheme + "://" + obj.Host)
|
||||||
|
return baseUrl
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue