diff --git a/.gitignore b/.gitignore index c681117..d3d6d37 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ data/* +testdata # this is the binary go-jamming diff --git a/app/server.go b/app/server.go index 6e1c28b..c152531 100644 --- a/app/server.go +++ b/app/server.go @@ -32,7 +32,9 @@ func (s *server) authorizedOnly(h http.HandlerFunc) http.HandlerFunc { func Start() { r := mux.NewRouter() - server := &server{router: r, conf: common.Configure()} + config := common.Configure() + config.SetupDataDirs() + server := &server{router: r, conf: config} server.routes() http.Handle("/", r) diff --git a/app/webmention/handler.go b/app/webmention/handler.go index 5a2f158..a078ee3 100644 --- a/app/webmention/handler.go +++ b/app/webmention/handler.go @@ -43,6 +43,7 @@ func HandlePost(conf *common.Config) http.HandlerFunc { } recv := &receiver{ restClient: httpClient, + conf: conf, } go recv.receive(wm) diff --git a/app/webmention/receive.go b/app/webmention/receive.go index 5d21452..b822e6d 100644 --- a/app/webmention/receive.go +++ b/app/webmention/receive.go @@ -3,7 +3,10 @@ package webmention import ( "fmt" + "os" + "crypto/md5" + "github.com/wgroeneveld/go-jamming/common" "github.com/wgroeneveld/go-jamming/rest" "github.com/rs/zerolog/log" @@ -18,10 +21,17 @@ func (wm *webmention) String() string { return fmt.Sprintf("source: %s, target: %s", wm.source, wm.target) } +func (wm *webmention) asPath(conf *common.Config) string { + filename := fmt.Sprintf("%x", md5.Sum([]byte("source=" + wm.source + ",target=" + wm.target))) + domain, _ := conf.FetchDomain(wm.target) + return conf.DataPath + "/" + domain + "/" + filename + ".json" +} + // used as a "class" to iject dependencies, just to be able to test. Do NOT like htis. // Is there a better way? e.g. in validate, I just pass rest.Client as an arg. Not great either. type receiver struct { restClient rest.Client + conf *common.Config } func (recv *receiver) receive(wm webmention) { @@ -38,7 +48,7 @@ func (recv *receiver) receive(wm webmention) { } func (recv *receiver) deletePossibleOlderWebmention(wm webmention) { - + os.Remove(wm.asPath(recv.conf)) } func (recv *receiver) processSourceBody(body string, wm webmention) { diff --git a/app/webmention/receive_test.go b/app/webmention/receive_test.go new file mode 100644 index 0000000..3f8c7dc --- /dev/null +++ b/app/webmention/receive_test.go @@ -0,0 +1,64 @@ + +package webmention + +import ( + "testing" + "os" + "errors" + + "github.com/wgroeneveld/go-jamming/common" + "github.com/wgroeneveld/go-jamming/mocks" +) + +var conf = &common.Config{ + AllowedWebmentionSources: []string { + "jefklakscodex.com", + }, + DataPath: "testdata", +} + + +func TestConvertWebmentionToPath(t *testing.T) { + wm := webmention{ + source: "https://brainbaking.com", + target: "https://jefklakscodex.com/articles", + } + + result := wm.asPath(conf) + if result != "testdata/jefklakscodex.com/99be66594fdfcf482545fead8e7e4948.json" { + t.Fatalf("md5 hash check failed, got " + result) + } +} + +func writeSomethingTo(filename string) { + file, _ := os.Create(filename) + file.WriteString("lolz") + defer file.Close() +} + +func TestReceiveTargetDoesNotExistAnymoreDeletesPossiblyOlderWebmention(t *testing.T) { + os.MkdirAll("testdata/jefklakscodex.com", os.ModePerm) + defer os.RemoveAll("testdata") + + wm := webmention{ + source: "https://brainbaking.com", + target: "https://jefklakscodex.com/articles", + } + filename := wm.asPath(conf) + writeSomethingTo(filename) + + client := &mocks.RestClientMock{ + GetBodyFunc: func(url string) (string, error) { + return "", errors.New("whoops") + }, + } + receiver := &receiver { + conf: conf, + restClient: client, + } + + receiver.receive(wm) + if _, err := os.Stat(filename); err == nil { + t.Fatalf("Expected possibly older webmention to be deleted, but it wasn't!") + } +} \ No newline at end of file diff --git a/app/webmention/validate.go b/app/webmention/validate.go index 9bfbbb1..19802e9 100644 --- a/app/webmention/validate.go +++ b/app/webmention/validate.go @@ -16,12 +16,11 @@ func isValidUrl(url string) bool { } func isValidDomain(url string, conf *common.Config) bool { - for _, domain := range conf.AllowedWebmentionSources { - if strings.Index(url, domain) != -1 { - return true - } + _, err := conf.FetchDomain(url) + if err != nil { + return false } - return false + return true } func isValidTargetUrl(url string, httpClient rest.Client) bool { diff --git a/app/webmention/validate_test.go b/app/webmention/validate_test.go index 6c8e7c6..026cd5c 100644 --- a/app/webmention/validate_test.go +++ b/app/webmention/validate_test.go @@ -7,6 +7,7 @@ import ( "net/http" "github.com/wgroeneveld/go-jamming/common" + "github.com/wgroeneveld/go-jamming/mocks" ) type httpReqMock struct { @@ -114,21 +115,8 @@ func TestValidate(t *testing.T) { } } -// neat trick! https://medium.com/@matryer/meet-moq-easily-mock-interfaces-in-go-476444187d10 -type restClientMock struct { - GetFunc func(string) (*http.Response, error) -} - -// although these are still requied to match the rest.Client interface. -func (m *restClientMock) Get(url string) (*http.Response, error) { - return m.GetFunc(url) -} -func (m *restClientMock) GetBody(url string) (string, error) { - return "", nil -} - func TestIsValidTargetUrlFalseIfGetFails(t *testing.T) { - client := &restClientMock{ + client := &mocks.RestClientMock{ GetFunc: func(url string) (*http.Response, error) { return nil, errors.New("whoops") }, @@ -140,7 +128,7 @@ func TestIsValidTargetUrlFalseIfGetFails(t *testing.T) { } func TestIsValidTargetUrlTrueIfGetSucceeds(t *testing.T) { - client := &restClientMock{ + client := &mocks.RestClientMock{ GetFunc: func(url string) (*http.Response, error) { return nil, nil }, diff --git a/common/config.go b/common/config.go index d4e446e..d2caf1f 100644 --- a/common/config.go +++ b/common/config.go @@ -4,16 +4,37 @@ package common import ( "os" "strconv" + "errors" + "strings" + + "github.com/rs/zerolog/log" ) type Config struct { Port int Token string UtcOffset int + DataPath string AllowedWebmentionSources []string DisallowedWebmentionDomains []string } +func (c *Config) FetchDomain(url string) (string, error) { + for _, domain := range c.AllowedWebmentionSources { + if strings.Index(url, domain) != -1 { + return domain, nil + } + } + return "", errors.New("no allowed domain found for url " + url) +} + +func (c *Config) SetupDataDirs() { + for _, domain := range c.AllowedWebmentionSources { + os.MkdirAll(c.DataPath + "/" + domain, os.ModePerm) + log.Info().Str("allowedDomain", domain).Msg("Configured") + } +} + func Configure() (c *Config) { portstr := os.Getenv("PORT") port, err := strconv.Atoi(portstr) @@ -29,6 +50,7 @@ func Configure() (c *Config) { Port: port, Token: token, UtcOffset: 60, + DataPath: "data", AllowedWebmentionSources: []string{ "brainbaking.com", "jefklakscodex.com" }, DisallowedWebmentionDomains: []string{ "youtube.com" }, } diff --git a/mocks/restclient.go b/mocks/restclient.go new file mode 100644 index 0000000..6663aa8 --- /dev/null +++ b/mocks/restclient.go @@ -0,0 +1,21 @@ + +package mocks + +import ( + "net/http" +) + +// neat trick! https://medium.com/@matryer/meet-moq-easily-mock-interfaces-in-go-476444187d10 +type RestClientMock struct { + GetFunc func(string) (*http.Response, error) + GetBodyFunc func(string) (string, error) +} + +// although these are still requied to match the rest.Client interface. +func (m *RestClientMock) Get(url string) (*http.Response, error) { + return m.GetFunc(url) +} +func (m *RestClientMock) GetBody(url string) (string, error) { + return m.GetBodyFunc(url) +} +