diff --git a/checks/checks.go b/checks/checks.go index 69e46a0..04d9a52 100644 --- a/checks/checks.go +++ b/checks/checks.go @@ -6,8 +6,9 @@ import ( ) type Checks struct { - Carbon *Carbon - Rank *Rank + Carbon *Carbon + Rank *Rank + SocialTags *SocialTags } func NewChecks() *Checks { @@ -15,7 +16,8 @@ func NewChecks() *Checks { Timeout: 5 * time.Second, } return &Checks{ - Carbon: NewCarbon(client), - Rank: NewRank(client), + Carbon: NewCarbon(client), + Rank: NewRank(client), + SocialTags: NewSocialTags(client), } } diff --git a/checks/social_tags.go b/checks/social_tags.go new file mode 100644 index 0000000..eb398bc --- /dev/null +++ b/checks/social_tags.go @@ -0,0 +1,94 @@ +package checks + +import ( + "context" + "net/http" + + "github.com/PuerkitoBio/goquery" +) + +type SocialTagsData struct { + Title string `json:"title"` + Description string `json:"description"` + Keywords string `json:"keywords"` + CanonicalUrl string `json:"canonicalUrl"` + OgTitle string `json:"ogTitle"` + OgType string `json:"ogType"` + OgImage string `json:"ogImage"` + OgUrl string `json:"ogUrl"` + OgDescription string `json:"ogDescription"` + OgSiteName string `json:"ogSiteName"` + TwitterCard string `json:"twitterCard"` + TwitterSite string `json:"twitterSite"` + TwitterCreator string `json:"twitterCreator"` + TwitterTitle string `json:"twitterTitle"` + TwitterDescription string `json:"twitterDescription"` + TwitterImage string `json:"twitterImage"` + ThemeColor string `json:"themeColor"` + Robots string `json:"robots"` + Googlebot string `json:"googlebot"` + Generator string `json:"generator"` + Viewport string `json:"viewport"` + Author string `json:"author"` + Publisher string `json:"publisher"` + Favicon string `json:"favicon"` +} + +func (s SocialTagsData) Empty() bool { + return (SocialTagsData{}) == s +} + +type SocialTags struct { + client *http.Client +} + +func NewSocialTags(client *http.Client) *SocialTags { + return &SocialTags{client: client} +} + +func (s *SocialTags) GetSocialTags(ctx context.Context, url string) (*SocialTagsData, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, err + } + resp, err := s.client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + // Parse HTML document + doc, err := goquery.NewDocumentFromReader(resp.Body) + if err != nil { + return nil, err + } + + // Extract social tags metadata + tags := &SocialTagsData{ + Title: doc.Find("head title").Text(), + Description: doc.Find("meta[name='description']").AttrOr("content", ""), + Keywords: doc.Find("meta[name='keywords']").AttrOr("content", ""), + CanonicalUrl: doc.Find("link[rel='canonical']").AttrOr("href", ""), + OgTitle: doc.Find("meta[property='og:title']").AttrOr("content", ""), + OgType: doc.Find("meta[property='og:type']").AttrOr("content", ""), + OgImage: doc.Find("meta[property='og:image']").AttrOr("content", ""), + OgUrl: doc.Find("meta[property='og:url']").AttrOr("content", ""), + OgDescription: doc.Find("meta[property='og:description']").AttrOr("content", ""), + OgSiteName: doc.Find("meta[property='og:site_name']").AttrOr("content", ""), + TwitterCard: doc.Find("meta[name='twitter:card']").AttrOr("content", ""), + TwitterSite: doc.Find("meta[name='twitter:site']").AttrOr("content", ""), + TwitterCreator: doc.Find("meta[name='twitter:creator']").AttrOr("content", ""), + TwitterTitle: doc.Find("meta[name='twitter:title']").AttrOr("content", ""), + TwitterDescription: doc.Find("meta[name='twitter:description']").AttrOr("content", ""), + TwitterImage: doc.Find("meta[name='twitter:image']").AttrOr("content", ""), + ThemeColor: doc.Find("meta[name='theme-color']").AttrOr("content", ""), + Robots: doc.Find("meta[name='robots']").AttrOr("content", ""), + Googlebot: doc.Find("meta[name='googlebot']").AttrOr("content", ""), + Generator: doc.Find("meta[name='generator']").AttrOr("content", ""), + Viewport: doc.Find("meta[name='viewport']").AttrOr("content", ""), + Author: doc.Find("meta[name='author']").AttrOr("content", ""), + Publisher: doc.Find("link[rel='publisher']").AttrOr("href", ""), + Favicon: doc.Find("link[rel='icon']").AttrOr("href", ""), + } + return tags, nil +} diff --git a/handlers/social_tags.go b/handlers/social_tags.go index 9c48393..c180221 100644 --- a/handlers/social_tags.go +++ b/handlers/social_tags.go @@ -1,120 +1,23 @@ package handlers import ( - "errors" "net/http" - "github.com/PuerkitoBio/goquery" + "github.com/xray-web/web-check-api/checks" ) -type SocialTags struct { - Title string `json:"title"` - Description string `json:"description"` - Keywords string `json:"keywords"` - CanonicalUrl string `json:"canonicalUrl"` - OgTitle string `json:"ogTitle"` - OgType string `json:"ogType"` - OgImage string `json:"ogImage"` - OgUrl string `json:"ogUrl"` - OgDescription string `json:"ogDescription"` - OgSiteName string `json:"ogSiteName"` - TwitterCard string `json:"twitterCard"` - TwitterSite string `json:"twitterSite"` - TwitterCreator string `json:"twitterCreator"` - TwitterTitle string `json:"twitterTitle"` - TwitterDescription string `json:"twitterDescription"` - TwitterImage string `json:"twitterImage"` - ThemeColor string `json:"themeColor"` - Robots string `json:"robots"` - Googlebot string `json:"googlebot"` - Generator string `json:"generator"` - Viewport string `json:"viewport"` - Author string `json:"author"` - Publisher string `json:"publisher"` - Favicon string `json:"favicon"` -} - -func isEmpty(tags *SocialTags) bool { - return tags.Title == "" && - tags.Description == "" && - tags.Keywords == "" && - tags.CanonicalUrl == "" && - tags.OgTitle == "" && - tags.OgType == "" && - tags.OgImage == "" && - tags.OgUrl == "" && - tags.OgDescription == "" && - tags.OgSiteName == "" && - tags.TwitterCard == "" && - tags.TwitterSite == "" && - tags.TwitterCreator == "" && - tags.TwitterTitle == "" && - tags.TwitterDescription == "" && - tags.TwitterImage == "" && - tags.ThemeColor == "" && - tags.Robots == "" && - tags.Googlebot == "" && - tags.Generator == "" && - tags.Viewport == "" && - tags.Author == "" && - tags.Publisher == "" && - tags.Favicon == "" -} - -func HandleGetSocialTags() http.Handler { +func HandleGetSocialTags(s *checks.SocialTags) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { rawURL, err := extractURL(r) if err != nil { JSONError(w, ErrMissingURLParameter, http.StatusBadRequest) return } - - // Fetch HTML content from the URL - resp, err := http.Get(rawURL.String()) + tags, err := s.GetSocialTags(r.Context(), rawURL.String()) if err != nil { + JSONError(w, err, http.StatusInternalServerError) return } - defer resp.Body.Close() - - // Parse HTML document - doc, err := goquery.NewDocumentFromReader(resp.Body) - if err != nil { - return - } - - // Extract social tags metadata - tags := &SocialTags{ - Title: doc.Find("head title").Text(), - Description: doc.Find("meta[name='description']").AttrOr("content", ""), - Keywords: doc.Find("meta[name='keywords']").AttrOr("content", ""), - CanonicalUrl: doc.Find("link[rel='canonical']").AttrOr("href", ""), - OgTitle: doc.Find("meta[property='og:title']").AttrOr("content", ""), - OgType: doc.Find("meta[property='og:type']").AttrOr("content", ""), - OgImage: doc.Find("meta[property='og:image']").AttrOr("content", ""), - OgUrl: doc.Find("meta[property='og:url']").AttrOr("content", ""), - OgDescription: doc.Find("meta[property='og:description']").AttrOr("content", ""), - OgSiteName: doc.Find("meta[property='og:site_name']").AttrOr("content", ""), - TwitterCard: doc.Find("meta[name='twitter:card']").AttrOr("content", ""), - TwitterSite: doc.Find("meta[name='twitter:site']").AttrOr("content", ""), - TwitterCreator: doc.Find("meta[name='twitter:creator']").AttrOr("content", ""), - TwitterTitle: doc.Find("meta[name='twitter:title']").AttrOr("content", ""), - TwitterDescription: doc.Find("meta[name='twitter:description']").AttrOr("content", ""), - TwitterImage: doc.Find("meta[name='twitter:image']").AttrOr("content", ""), - ThemeColor: doc.Find("meta[name='theme-color']").AttrOr("content", ""), - Robots: doc.Find("meta[name='robots']").AttrOr("content", ""), - Googlebot: doc.Find("meta[name='googlebot']").AttrOr("content", ""), - Generator: doc.Find("meta[name='generator']").AttrOr("content", ""), - Viewport: doc.Find("meta[name='viewport']").AttrOr("content", ""), - Author: doc.Find("meta[name='author']").AttrOr("content", ""), - Publisher: doc.Find("link[rel='publisher']").AttrOr("href", ""), - Favicon: doc.Find("link[rel='icon']").AttrOr("href", ""), - } - - if isEmpty(tags) { - JSONError(w, errors.New("no metadata found"), http.StatusBadRequest) - return - } - JSON(w, tags, http.StatusOK) }) } diff --git a/handlers/social_tags_test.go b/handlers/social_tags_test.go index 377b10d..98d65fd 100644 --- a/handlers/social_tags_test.go +++ b/handlers/social_tags_test.go @@ -7,82 +7,66 @@ import ( "testing" "github.com/stretchr/testify/assert" - "gopkg.in/h2non/gock.v1" + "github.com/xray-web/web-check-api/checks" + "github.com/xray-web/web-check-api/testutils" ) func TestHandleGetSocialTags(t *testing.T) { - // t.Parallel() - tests := []struct { - name string - urlParam string - mockResponse string - mockStatusCode int - expectedStatus int - expectedBody map[string]interface{} - }{ - { - name: "Missing URL parameter", - urlParam: "", - expectedStatus: http.StatusBadRequest, - expectedBody: map[string]interface{}{"error": "missing URL parameter"}, - }, - { - name: "Valid URL with social tags", - urlParam: "http://example.com", - mockResponse: `Example Domain`, - mockStatusCode: http.StatusOK, - expectedStatus: http.StatusOK, - expectedBody: map[string]interface{}{ - "title": "Example Domain", - "description": "Example description", - "keywords": "", - "canonicalUrl": "", - "ogTitle": "Example OG Title", - "ogType": "", - "ogImage": "", - "ogUrl": "", - "ogDescription": "", - "ogSiteName": "", - "twitterCard": "", - "twitterSite": "", - "twitterCreator": "", - "twitterTitle": "", - "twitterDescription": "", - "twitterImage": "", - "themeColor": "", - "robots": "", - "googlebot": "", - "generator": "", - "viewport": "", - "author": "", - "publisher": "", - "favicon": "", - }, - }, - } + t.Parallel() - for _, tc := range tests { - tc := tc - t.Run(tc.name, func(t *testing.T) { - // t.Parallel() - defer gock.Off() + t.Run("Missing URL parameter", func(t *testing.T) { + t.Parallel() + req := httptest.NewRequest("GET", "/social-tag?url=", nil) + rec := httptest.NewRecorder() - if tc.urlParam != "" { - gock.New(tc.urlParam). - Reply(tc.mockStatusCode). - BodyString(tc.mockResponse) - } + HandleGetSocialTags(checks.NewSocialTags(nil)).ServeHTTP(rec, req) - req := httptest.NewRequest("GET", "/social-tags?url="+tc.urlParam, nil) - rec := httptest.NewRecorder() - HandleGetSocialTags().ServeHTTP(rec, req) + assert.Equal(t, http.StatusBadRequest, rec.Code) + var response KV + err := json.Unmarshal(rec.Body.Bytes(), &response) + assert.NoError(t, err) + assert.Equal(t, KV{"error": "missing URL parameter"}, response) + }) - assert.Equal(t, tc.expectedStatus, rec.Code) + t.Run("Valid URL with social tags", func(t *testing.T) { + t.Parallel() + + req := httptest.NewRequest("GET", "/social-tags?url=example.com", nil) + rec := httptest.NewRecorder() + + HandleGetSocialTags(checks.NewSocialTags(testutils.MockClient(testutils.Response(http.StatusOK, []byte(`Example Domain`))))).ServeHTTP(rec, req) + + assert.Equal(t, http.StatusOK, rec.Code) + + var responseBody KV + err := json.Unmarshal(rec.Body.Bytes(), &responseBody) + assert.NoError(t, err) + assert.Equal(t, KV{ + "title": "Example Domain", + "description": "Example description", + "keywords": "", + "canonicalUrl": "", + "ogTitle": "Example OG Title", + "ogType": "", + "ogImage": "", + "ogUrl": "", + "ogDescription": "", + "ogSiteName": "", + "twitterCard": "", + "twitterSite": "", + "twitterCreator": "", + "twitterTitle": "", + "twitterDescription": "", + "twitterImage": "", + "themeColor": "", + "robots": "", + "googlebot": "", + "generator": "", + "viewport": "", + "author": "", + "publisher": "", + "favicon": "", + }, responseBody) + }) - var responseBody map[string]interface{} - err := json.Unmarshal(rec.Body.Bytes(), &responseBody) - assert.NoError(t, err) - assert.Equal(t, tc.expectedBody, responseBody) - }) - } } diff --git a/server/server.go b/server/server.go index c8a3669..612b4c9 100644 --- a/server/server.go +++ b/server/server.go @@ -46,7 +46,7 @@ func (s *Server) routes() { s.mux.Handle("GET /api/quality", handlers.HandleGetQuality()) s.mux.Handle("GET /api/rank", handlers.HandleGetRank(s.checks.Rank)) s.mux.Handle("GET /api/redirects", handlers.HandleGetRedirects()) - s.mux.Handle("GET /api/social-tags", handlers.HandleGetSocialTags()) + s.mux.Handle("GET /api/social-tags", handlers.HandleGetSocialTags(s.checks.SocialTags)) s.mux.Handle("GET /api/tls", handlers.HandleTLS()) s.mux.Handle("GET /api/trace-route", handlers.HandleTraceRoute()) }