diff --git a/app/webmention_service.go b/app/webmention_service.go index 41986b5..1732d4f 100644 --- a/app/webmention_service.go +++ b/app/webmention_service.go @@ -109,7 +109,7 @@ func (s *WebmentionService) GetExistingWebmention(entryId string, source string, } for _, interaction := range inters { if webm, ok := interaction.(*interactions.Webmention); ok { - m := webm.MetaData().(interactions.WebmentionInteractionMetaData) + m := webm.MetaData().(interactions.WebmentionMetaData) if m.Source == source && m.Target == target { return webm, nil } @@ -130,6 +130,10 @@ func (s *WebmentionService) ProcessWebmention(source string, target string) erro } entryId := UrlToEntryId(target) + println(entryId) + println(entryId) + println(entryId) + println(entryId) _, err = s.EntryRepository.FindById(entryId) if err != nil { return err @@ -140,7 +144,7 @@ func (s *WebmentionService) ProcessWebmention(source string, target string) erro return err } if webmention != nil { - data := interactions.WebmentionInteractionMetaData{ + data := interactions.WebmentionMetaData{ Source: source, Target: target, Title: hEntry.Title, @@ -152,12 +156,12 @@ func (s *WebmentionService) ProcessWebmention(source string, target string) erro return err } else { webmention = &interactions.Webmention{} - data := interactions.WebmentionInteractionMetaData{ + data := interactions.WebmentionMetaData{ Source: source, Target: target, Title: hEntry.Title, } - webmention.SetMetaData(data) + webmention.SetMetaData(&data) webmention.SetEntryID(entryId) webmention.SetCreatedAt(time.Now()) err = s.InteractionRepository.Create(webmention) diff --git a/app/webmention_test.go b/app/webmention_test.go new file mode 100644 index 0000000..29d85bb --- /dev/null +++ b/app/webmention_test.go @@ -0,0 +1,222 @@ +package app_test + +import ( + "bytes" + "fmt" + "io" + "net/http" + "net/url" + "owl-blogs/app" + "owl-blogs/infra" + "owl-blogs/interactions" + "owl-blogs/test" + "testing" + + "github.com/stretchr/testify/require" +) + +// func constructResponse(html []byte) *http.Response { +// url, _ := url.Parse("http://example.com/foo/bar") +// return &http.Response{ +// Request: &http.Request{ +// URL: url, +// }, +// Body: io.NopCloser(bytes.NewReader([]byte(html))), +// } +// } + +type MockHttpClient struct { + PageContent string +} + +// Post implements owlhttp.HttpClient. +func (MockHttpClient) Post(url string, contentType string, body io.Reader) (resp *http.Response, err error) { + panic("unimplemented") +} + +// PostForm implements owlhttp.HttpClient. +func (MockHttpClient) PostForm(url string, data url.Values) (resp *http.Response, err error) { + panic("unimplemented") +} + +func (c *MockHttpClient) Get(url string) (*http.Response, error) { + return &http.Response{ + Body: io.NopCloser(bytes.NewReader([]byte(c.PageContent))), + }, nil +} + +func getWebmentionService() *app.WebmentionService { + db := test.NewMockDb() + entryRegister := app.NewEntryTypeRegistry() + entryRegister.Register(&test.MockEntry{}) + entryRepo := infra.NewEntryRepository(db, entryRegister) + + interactionRegister := app.NewInteractionTypeRegistry() + interactionRegister.Register(&interactions.Webmention{}) + + interactionRepo := infra.NewInteractionRepo(db, interactionRegister) + + http := infra.OwlHttpClient{} + return app.NewWebmentionService( + interactionRepo, entryRepo, &http, + ) +} + +// +// https://www.w3.org/TR/webmention/#h-webmention-verification +// + +func TestParseValidHEntry(t *testing.T) { + service := getWebmentionService() + html := []byte("
Foo
") + entry, err := service.ParseHEntry(&http.Response{Body: io.NopCloser(bytes.NewReader(html))}) + + require.NoError(t, err) + require.Equal(t, entry.Title, "Foo") +} + +func TestParseValidHEntryWithoutTitle(t *testing.T) { + service := getWebmentionService() + html := []byte("
Foo
") + entry, err := service.ParseHEntry(&http.Response{Body: io.NopCloser(bytes.NewReader(html))}) + + require.NoError(t, err) + require.Equal(t, entry.Title, "") +} + +func TestCreateNewWebmention(t *testing.T) { + service := getWebmentionService() + service.Http = &MockHttpClient{ + PageContent: "
Foo
", + } + entry := test.MockEntry{} + service.EntryRepository.Create(&entry) + + err := service.ProcessWebmention( + "http://example.com/foo", + fmt.Sprintf("https.//example.com/posts/%s/", entry.ID()), + ) + require.NoError(t, err) + + inters, err := service.InteractionRepository.FindAll(entry.ID()) + require.NoError(t, err) + require.Equal(t, len(inters), 1) + webm := inters[0].(*interactions.Webmention) + meta := webm.MetaData().(*interactions.WebmentionMetaData) + require.Equal(t, meta.Source, "http://example.com/foo") + require.Equal(t, meta.Target, fmt.Sprintf("https.//example.com/posts/%s/", entry.ID())) + require.Equal(t, meta.Title, "Foo") +} + +// func TestGetWebmentionEndpointLink(t *testing.T) { +// service := getWebmentionService() +// html := []byte("") +// endpoint, err := service.GetWebmentionEndpoint(constructResponse(html)) + +// require.NoError(t, err) + +// require.Equal(t, endpoint, "http://example.com/webmention") +// } + +// func TestGetWebmentionEndpointLinkA(t *testing.T) { +// service := getWebmentionService() +// html := []byte("") +// endpoint, err := service.GetWebmentionEndpoint(constructResponse(html)) + +// require.NoError(t, err) +// require.Equal(t, endpoint, "http://example.com/webmention") +// } + +// func TestGetWebmentionEndpointLinkAFakeWebmention(t *testing.T) { +// service := getWebmentionService() +// html := []byte("") +// endpoint, err := service.GetWebmentionEndpoint(constructResponse(html)) + +// require.NoError(t, err) +// require.Equal(t, endpoint, "http://example.com/webmention") +// } + +// func TestGetWebmentionEndpointLinkHeader(t *testing.T) { +// service := getWebmentionService() +// html := []byte("") +// resp := constructResponse(html) +// resp.Header = http.Header{"Link": []string{"; rel=\"webmention\""}} +// endpoint, err := service.GetWebmentionEndpoint(resp) + +// require.NoError(t, err) +// require.Equal(t, endpoint, "http://example.com/webmention") +// } + +// func TestGetWebmentionEndpointLinkHeaderCommas(t *testing.T) { +// service := getWebmentionService() +// html := []byte("") +// resp := constructResponse(html) +// resp.Header = http.Header{ +// "Link": []string{"; rel=\"other\", ; rel=\"webmention\""}, +// } +// endpoint, err := service.GetWebmentionEndpoint(resp) + +// require.NoError(t, err) +// require.Equal(t, endpoint, "https://webmention.rocks/test/19/webmention") +// } + +// func TestGetWebmentionEndpointRelativeLink(t *testing.T) { +// service := getWebmentionService() +// html := []byte("") +// endpoint, err := service.GetWebmentionEndpoint(constructResponse(html)) + +// require.NoError(t, err) +// require.Equal(t, endpoint, "http://example.com/webmention") +// } + +// func TestGetWebmentionEndpointRelativeLinkInHeader(t *testing.T) { +// service := getWebmentionService() +// html := []byte("") +// resp := constructResponse(html) +// resp.Header = http.Header{"Link": []string{"; rel=\"webmention\""}} +// endpoint, err := service.GetWebmentionEndpoint(resp) + +// require.NoError(t, err) +// require.Equal(t, endpoint, "http://example.com/webmention") +// } + +// func TestRealWorldWebmention(t *testing.T) { +// service := getWebmentionService() +// links := []string{ +// "https://webmention.rocks/test/1", +// "https://webmention.rocks/test/2", +// "https://webmention.rocks/test/3", +// "https://webmention.rocks/test/4", +// "https://webmention.rocks/test/5", +// "https://webmention.rocks/test/6", +// "https://webmention.rocks/test/7", +// "https://webmention.rocks/test/8", +// "https://webmention.rocks/test/9", +// // "https://webmention.rocks/test/10", // not supported +// "https://webmention.rocks/test/11", +// "https://webmention.rocks/test/12", +// "https://webmention.rocks/test/13", +// "https://webmention.rocks/test/14", +// "https://webmention.rocks/test/15", +// "https://webmention.rocks/test/16", +// "https://webmention.rocks/test/17", +// "https://webmention.rocks/test/18", +// "https://webmention.rocks/test/19", +// "https://webmention.rocks/test/20", +// "https://webmention.rocks/test/21", +// "https://webmention.rocks/test/22", +// "https://webmention.rocks/test/23/page", +// } + +// for _, link := range links { +// +// client := &owl.OwlHttpClient{} +// html, _ := client.Get(link) +// _, err := service.GetWebmentionEndpoint(html) + +// if err != nil { +// t.Errorf("Unable to find webmention: %v for link %v", err, link) +// } +// } + +// } diff --git a/infra/interaction_repository.go b/infra/interaction_repository.go index 68f4144..c5b4f83 100644 --- a/infra/interaction_repository.go +++ b/infra/interaction_repository.go @@ -1,19 +1,24 @@ package infra import ( + "encoding/json" + "errors" "owl-blogs/app" "owl-blogs/app/repository" "owl-blogs/domain/model" + "reflect" + "time" + "github.com/google/uuid" "github.com/jmoiron/sqlx" ) type sqlInteraction struct { - Id string `db:"id"` - Type string `db:"type"` - EntryId string `db:"entry_id"` - CreatedAt string `db:"created_at"` - MetaData string `db:"meta_data"` + Id string `db:"id"` + Type string `db:"type"` + EntryId string `db:"entry_id"` + CreatedAt time.Time `db:"created_at"` + MetaData *string `db:"meta_data"` } type DefaultInteractionRepo struct { @@ -42,8 +47,34 @@ func NewInteractionRepo(db Database, register *app.InteractionTypeRegistry) repo } // Create implements repository.InteractionRepository. -func (*DefaultInteractionRepo) Create(interaction model.Interaction) error { - panic("unimplemented") +func (repo *DefaultInteractionRepo) Create(interaction model.Interaction) error { + t, err := repo.typeRegistry.TypeName(interaction) + if err != nil { + return errors.New("interaction type not registered") + } + + if interaction.ID() == "" { + interaction.SetID(uuid.New().String()) + } + + var metaDataJson []byte + if interaction.MetaData() != nil { + metaDataJson, _ = json.Marshal(interaction.MetaData()) + } + metaDataStr := string(metaDataJson) + + _, err = repo.db.NamedExec(` + INSERT INTO interactions (id, type, entry_id, created_at, meta_data) + VALUES (:id, :type, :entry_id, :created_at, :meta_data) + `, sqlInteraction{ + Id: interaction.ID(), + Type: t, + EntryId: interaction.EntryID(), + CreatedAt: interaction.CreatedAt(), + MetaData: &metaDataStr, + }) + + return err } // Delete implements repository.InteractionRepository. @@ -52,8 +83,23 @@ func (*DefaultInteractionRepo) Delete(interaction model.Interaction) error { } // FindAll implements repository.InteractionRepository. -func (*DefaultInteractionRepo) FindAll(entryId string) ([]model.Interaction, error) { - panic("unimplemented") +func (repo *DefaultInteractionRepo) FindAll(entryId string) ([]model.Interaction, error) { + data := []sqlInteraction{} + err := repo.db.Select(&data, "SELECT * FROM interactions WHERE entry_id = ?", entryId) + if err != nil { + return nil, err + } + + interactions := []model.Interaction{} + for _, d := range data { + i, err := repo.sqlInteractionToInteraction(d) + if err != nil { + return nil, err + } + interactions = append(interactions, i) + } + + return interactions, nil } // FindById implements repository.InteractionRepository. @@ -65,3 +111,19 @@ func (*DefaultInteractionRepo) FindById(id string) (model.Interaction, error) { func (*DefaultInteractionRepo) Update(interaction model.Interaction) error { panic("unimplemented") } + +func (repo *DefaultInteractionRepo) sqlInteractionToInteraction(interaction sqlInteraction) (model.Interaction, error) { + i, err := repo.typeRegistry.Type(interaction.Type) + if err != nil { + return nil, errors.New("interaction type not registered") + } + metaData := reflect.New(reflect.TypeOf(i.MetaData()).Elem()).Interface() + json.Unmarshal([]byte(*interaction.MetaData), metaData) + i.SetID(interaction.Id) + i.SetEntryID(interaction.EntryId) + i.SetCreatedAt(interaction.CreatedAt) + i.SetMetaData(metaData) + + return i, nil + +} diff --git a/interactions/webmention.go b/interactions/webmention.go index 22247a9..09d8fc0 100644 --- a/interactions/webmention.go +++ b/interactions/webmention.go @@ -4,10 +4,10 @@ import "owl-blogs/domain/model" type Webmention struct { model.InteractionBase - meta WebmentionInteractionMetaData + meta WebmentionMetaData } -type WebmentionInteractionMetaData struct { +type WebmentionMetaData struct { Source string Target string Title string @@ -22,5 +22,5 @@ func (i *Webmention) MetaData() interface{} { } func (i *Webmention) SetMetaData(metaData interface{}) { - i.meta = *metaData.(*WebmentionInteractionMetaData) + i.meta = *metaData.(*WebmentionMetaData) }