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/checks/social_tags_test.go b/checks/social_tags_test.go
new file mode 100644
index 0000000..d56f70f
--- /dev/null
+++ b/checks/social_tags_test.go
@@ -0,0 +1,65 @@
+package checks
+
+import (
+ "context"
+ "net/http"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/xray-web/web-check-api/testutils"
+)
+
+func TestSocialTagsEmpty(t *testing.T) {
+ t.Parallel()
+
+ t.Run("Empty", func(t *testing.T) {
+ t.Parallel()
+
+ s := SocialTagsData{}
+ assert.True(t, s.Empty())
+ })
+
+ t.Run("Not empty", func(t *testing.T) {
+ t.Parallel()
+
+ s := SocialTagsData{
+ Title: "Example Domain",
+ }
+ assert.False(t, s.Empty())
+ })
+}
+
+func TestNewSocialTags(t *testing.T) {
+ t.Parallel()
+
+ t.Run("No social tags", func(t *testing.T) {
+ t.Parallel()
+
+ client := testutils.MockClient(testutils.Response(http.StatusOK, []byte{}))
+ tags, err := NewSocialTags(client).GetSocialTags(context.TODO(), "http://example.com")
+ assert.NoError(t, err)
+ assert.True(t, tags.Empty())
+ })
+
+ t.Run("Social tags", func(t *testing.T) {
+ t.Parallel()
+
+ var html = []byte(`
+
+
+ Example Domain
+
+
+
+
+
+ `)
+ client := testutils.MockClient(testutils.Response(http.StatusOK, html))
+ tags, err := NewSocialTags(client).GetSocialTags(context.TODO(), "http://example.com")
+ assert.NoError(t, err)
+ assert.False(t, tags.Empty())
+ assert.Equal(t, "Example description", tags.Description)
+ assert.Equal(t, "Example Domain", tags.Title)
+ assert.Equal(t, "Example OG Title", tags.OgTitle)
+ })
+}
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())
}