Skip to content

Commit 2a0578a

Browse files
Thomas StrombergThomas Stromberg
authored andcommitted
add timezone tests
1 parent 2305a83 commit 2a0578a

File tree

4 files changed

+116
-14
lines changed

4 files changed

+116
-14
lines changed

pkg/github/github.go

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,24 @@ import (
2626

2727
const platform = "github"
2828

29+
// profileTimezoneRegex extracts the UTC offset from GitHub's profile-timezone element.
30+
// Example: <profile-timezone data-hours-ahead-of-utc="-8.0">(UTC -08:00)</profile-timezone>.
31+
var profileTimezoneRegex = regexp.MustCompile(`<profile-timezone[^>]*data-hours-ahead-of-utc="([^"]*)"`)
32+
33+
// extractUTCOffset parses the UTC offset from GitHub profile HTML.
34+
// Returns nil if no timezone is found or the value is invalid.
35+
func extractUTCOffset(html string) *float64 {
36+
matches := profileTimezoneRegex.FindStringSubmatch(html)
37+
if len(matches) < 2 || matches[1] == "" {
38+
return nil
39+
}
40+
offset, err := strconv.ParseFloat(matches[1], 64)
41+
if err != nil {
42+
return nil
43+
}
44+
return &offset
45+
}
46+
2947
// Match returns true if the URL is a GitHub profile URL.
3048
func Match(urlStr string) bool {
3149
lower := strings.ToLower(urlStr)
@@ -182,8 +200,11 @@ func (c *Client) Fetch(ctx context.Context, urlStr string) (*profile.Profile, er
182200

183201
prof.SocialLinks = append(prof.SocialLinks, htmlLinks...)
184202

185-
// Extract README and organizations from HTML if available
203+
// Extract README, organizations, and UTC offset from HTML if available
186204
if htmlContent != "" {
205+
// Extract UTC offset from profile-timezone element
206+
prof.UTCOffset = extractUTCOffset(htmlContent)
207+
187208
// Extract organizations
188209
orgs := extractOrganizations(htmlContent)
189210
if len(orgs) > 0 {

pkg/github/github_test.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,85 @@ func TestDedupeLinks(t *testing.T) {
400400
}
401401
}
402402

403+
func TestExtractUTCOffset(t *testing.T) {
404+
tests := []struct {
405+
name string
406+
html string
407+
want *float64
408+
}{
409+
{
410+
name: "negative offset (PST)",
411+
html: `<profile-timezone class="color-fg-muted" data-hours-ahead-of-utc="-8.0">(UTC -08:00)</profile-timezone>`,
412+
want: ptr(-8.0),
413+
},
414+
{
415+
name: "negative offset (Hawaii)",
416+
html: `<profile-timezone class="color-fg-muted d-inline" data-hours-ahead-of-utc="-11.0">(UTC -11:00)</profile-timezone>`,
417+
want: ptr(-11.0),
418+
},
419+
{
420+
name: "positive offset (IST)",
421+
html: `<profile-timezone data-hours-ahead-of-utc="5.5">(UTC +05:30)</profile-timezone>`,
422+
want: ptr(5.5),
423+
},
424+
{
425+
name: "zero offset (UTC)",
426+
html: `<profile-timezone data-hours-ahead-of-utc="0">(UTC +00:00)</profile-timezone>`,
427+
want: ptr(0.0),
428+
},
429+
{
430+
name: "positive offset (CET)",
431+
html: `<profile-timezone data-hours-ahead-of-utc="1">(UTC +01:00)</profile-timezone>`,
432+
want: ptr(1.0),
433+
},
434+
{
435+
name: "fractional offset (Nepal)",
436+
html: `<profile-timezone data-hours-ahead-of-utc="5.75">(UTC +05:45)</profile-timezone>`,
437+
want: ptr(5.75),
438+
},
439+
{
440+
name: "no profile-timezone element",
441+
html: `<div class="profile-info">No timezone here</div>`,
442+
want: nil,
443+
},
444+
{
445+
name: "empty data-hours-ahead-of-utc",
446+
html: `<profile-timezone data-hours-ahead-of-utc="">(UTC)</profile-timezone>`,
447+
want: nil,
448+
},
449+
{
450+
name: "embedded in full page",
451+
html: `<!DOCTYPE html><html><body>
452+
<div class="sidebar">
453+
<profile-timezone class="color-fg-muted d-inline" data-hours-ahead-of-utc="-7.0">(UTC -07:00)</profile-timezone>
454+
</div>
455+
</body></html>`,
456+
want: ptr(-7.0),
457+
},
458+
}
459+
460+
for _, tt := range tests {
461+
t.Run(tt.name, func(t *testing.T) {
462+
got := extractUTCOffset(tt.html)
463+
if tt.want == nil {
464+
if got != nil {
465+
t.Errorf("extractUTCOffset() = %v, want nil", *got)
466+
}
467+
} else {
468+
if got == nil {
469+
t.Errorf("extractUTCOffset() = nil, want %v", *tt.want)
470+
} else if *got != *tt.want {
471+
t.Errorf("extractUTCOffset() = %v, want %v", *got, *tt.want)
472+
}
473+
}
474+
})
475+
}
476+
}
477+
478+
func ptr(f float64) *float64 {
479+
return &f
480+
}
481+
403482
func TestParseJSON_WithEmailInBlog(t *testing.T) {
404483
// Test case where blog field contains an email (which should be extracted)
405484
sampleJSON := `{

pkg/profile/profile.go

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,14 @@ type Profile struct {
4747
Error string `json:",omitempty"` // Error message if fetch failed (e.g., "login required")
4848

4949
// Core profile data
50-
Username string `json:",omitempty"` // Handle/username (without @ prefix)
51-
Name string `json:",omitempty"` // Display name
52-
Bio string `json:",omitempty"` // Profile bio/description
53-
Location string `json:",omitempty"` // Geographic location
54-
Website string `json:",omitempty"` // Personal website URL
55-
CreatedAt string `json:",omitempty"` // Account creation date (ISO timestamp)
56-
UpdatedAt string `json:",omitempty"` // Most recent activity or profile update (ISO timestamp)
50+
Username string `json:",omitempty"` // Handle/username (without @ prefix)
51+
Name string `json:",omitempty"` // Display name
52+
Bio string `json:",omitempty"` // Profile bio/description
53+
Location string `json:",omitempty"` // Geographic location
54+
Website string `json:",omitempty"` // Personal website URL
55+
CreatedAt string `json:",omitempty"` // Account creation date (ISO timestamp)
56+
UpdatedAt string `json:",omitempty"` // Most recent activity or profile update (ISO timestamp)
57+
UTCOffset *float64 `json:",omitempty"` // UTC offset in hours (e.g., -8 for PST, 5.5 for IST)
5758

5859
// Platform-specific fields
5960
Fields map[string]string `json:",omitempty"` // Additional platform-specific data (headline, employer, etc.)

pkg/sociopath/integration_test.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ func TestIntegrationLiveFetch(t *testing.T) {
8787
Platform: "github",
8888
URL: "https://github.com/tstromberg",
8989
Username: "tstromberg",
90-
Name: "Thomas Stromberg",
90+
Name: "Thomas |Ström`ber`g",
9191
CreatedAt: "2009-07-03T14:32:35Z",
9292
},
9393
},
@@ -224,7 +224,7 @@ func TestIntegrationLiveFetch(t *testing.T) {
224224
CreatedAt: "2022-11-03T00:00:00.000Z",
225225
},
226226
cmpOpts: []cmp.Option{
227-
cmpopts.IgnoreFields(profile.Profile{}, "Location", "Website", "UpdatedAt", "SocialLinks", "Fields", "Posts", "Unstructured", "IsGuess", "Confidence", "GuessMatch"),
227+
cmpopts.IgnoreFields(profile.Profile{}, "Location", "Website", "UpdatedAt", "SocialLinks", "Fields", "Posts", "Unstructured", "IsGuess", "Confidence", "GuessMatch", "UTCOffset"),
228228
},
229229
},
230230
{
@@ -635,7 +635,7 @@ func TestIntegrationLiveFetch(t *testing.T) {
635635
// Name, Bio, Location are empty when auth is broken
636636
},
637637
cmpOpts: []cmp.Option{
638-
cmpopts.IgnoreFields(profile.Profile{}, "Fields", "Name", "Bio", "Location", "Website", "CreatedAt", "UpdatedAt", "SocialLinks", "Posts", "Unstructured", "IsGuess", "Confidence", "GuessMatch"),
638+
cmpopts.IgnoreFields(profile.Profile{}, "Fields", "Name", "Bio", "Location", "Website", "CreatedAt", "UpdatedAt", "SocialLinks", "Posts", "Unstructured", "IsGuess", "Confidence", "GuessMatch", "UTCOffset"),
639639
},
640640
},
641641
{
@@ -660,7 +660,7 @@ func TestIntegrationLiveFetch(t *testing.T) {
660660
Username: "mattmoor",
661661
},
662662
cmpOpts: []cmp.Option{
663-
cmpopts.IgnoreFields(profile.Profile{}, "Fields", "Name", "Bio", "Location", "Website", "CreatedAt", "UpdatedAt", "SocialLinks", "Posts", "Unstructured", "IsGuess", "Confidence", "GuessMatch"),
663+
cmpopts.IgnoreFields(profile.Profile{}, "Fields", "Name", "Bio", "Location", "Website", "CreatedAt", "UpdatedAt", "SocialLinks", "Posts", "Unstructured", "IsGuess", "Confidence", "GuessMatch", "UTCOffset"),
664664
},
665665
},
666666
{
@@ -685,7 +685,7 @@ func TestIntegrationLiveFetch(t *testing.T) {
685685
Username: "austen-bryan-23485a19",
686686
},
687687
cmpOpts: []cmp.Option{
688-
cmpopts.IgnoreFields(profile.Profile{}, "Fields", "Name", "Bio", "Location", "Website", "CreatedAt", "UpdatedAt", "SocialLinks", "Posts", "Unstructured", "IsGuess", "Confidence", "GuessMatch"),
688+
cmpopts.IgnoreFields(profile.Profile{}, "Fields", "Name", "Bio", "Location", "Website", "CreatedAt", "UpdatedAt", "SocialLinks", "Posts", "Unstructured", "IsGuess", "Confidence", "GuessMatch", "UTCOffset"),
689689
},
690690
},
691691
}
@@ -695,7 +695,8 @@ func TestIntegrationLiveFetch(t *testing.T) {
695695
// Bio, Location, Website can be edited by users
696696
// Fields, SocialLinks contain varying platform-specific data
697697
// UpdatedAt, Posts, Unstructured change with activity
698-
cmpopts.IgnoreFields(profile.Profile{}, "Bio", "Location", "Website", "Fields", "SocialLinks", "UpdatedAt", "Posts", "Unstructured", "IsGuess", "Confidence", "GuessMatch"),
698+
// UTCOffset depends on user's timezone settings
699+
cmpopts.IgnoreFields(profile.Profile{}, "Bio", "Location", "Website", "Fields", "SocialLinks", "UpdatedAt", "Posts", "Unstructured", "IsGuess", "Confidence", "GuessMatch", "UTCOffset"),
699700
}
700701

701702
for _, tt := range tests {

0 commit comments

Comments
 (0)