Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: added unit tests and github action #2

Merged
merged 1 commit into from
Nov 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: Go

on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]

jobs:

build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.20'

- name: Build
run: go build -v ./...

- name: Test
run: go test -v ./...
3 changes: 2 additions & 1 deletion cmd/bot/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ func handleLeaderboardError(leaderboard *aoc.Leaderboard, err error) *aoc.Leader
}

func initTracker(cfg *config.Config, storedLeaderboard *aoc.Leaderboard) *leaderboard.Tracker {
tracker := leaderboard.NewTracker(cfg, storedLeaderboard)
client := aoc.NewClient(cfg.SessionCookie)
tracker := leaderboard.NewTracker(cfg, storedLeaderboard, client)
if tracker == nil {
log.Fatal("tracker is nil")
}
Expand Down
5 changes: 5 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,13 @@ go 1.20

require (
github.com/bwmarrin/discordgo v0.27.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gorilla/websocket v1.4.2 // indirect
github.com/joho/godotenv v1.5.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/stretchr/testify v1.10.0 // indirect
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b // indirect
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
11 changes: 11 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
github.com/bwmarrin/discordgo v0.27.1 h1:ib9AIc/dom1E/fSIulrBwnez0CToJE113ZGt4HoliGY=
github.com/bwmarrin/discordgo v0.27.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b h1:7mWr3k41Qtv8XlltBkDkl8LoP3mpSgBW8BUoxtEdbXg=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
Expand All @@ -12,3 +20,6 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
20 changes: 13 additions & 7 deletions internal/aoc/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,42 +9,48 @@ import (

type Client struct {
SessionCookie string
HTTPClient *http.Client
}

// NewClient creates a new AOC client with the provided session cookie.
func NewClient(sessionCookie string) *Client {
return &Client{
SessionCookie: sessionCookie,
HTTPClient: http.DefaultClient,
}
}

// For testing purposes, SetHTTPClient allows you to set the HTTP client used by the client.
func (c *Client) SetHTTPClient(client *http.Client) {
c.HTTPClient = client
}

func (c *Client) GetLeaderboard(leaderboardID string) (*Leaderboard, error) {
url := fmt.Sprintf("https://adventofcode.com/2024/leaderboard/private/view/%s.json", leaderboardID)

req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, fmt.Errorf("error reading request: %w", err)
return nil, fmt.Errorf("error creating request: %w", err)
}

req.Header.Set("User-Agent", "github.com/PaytonWebber/aoc-discord-bot by [email protected]")

req.Header.Set("cookie", fmt.Sprintf("session=%s", c.SessionCookie))

client := &http.Client{}
resp, err := client.Do(req)
resp, err := c.HTTPClient.Do(req)
if err != nil {
return nil, fmt.Errorf("error reading response: %w", err)
return nil, fmt.Errorf("error making HTTP request: %w", err)
}

defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("error reading body: %w", err)
return nil, fmt.Errorf("error reading response body: %w", err)
}

var leaderboard Leaderboard
err = json.Unmarshal(body, &leaderboard)
if err != nil {
return nil, fmt.Errorf("error unmarshalling body: %w", err)
return nil, fmt.Errorf("error unmarshalling response body: %w", err)
}

return &leaderboard, nil
Expand Down
174 changes: 174 additions & 0 deletions internal/aoc/client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
package aoc

import (
"fmt"
"net/http"
"net/http/httptest"
"testing"
)

const mockLeaderboardJSON = `{
"event": "2024",
"owner_id": 12345,
"members": {
"67890": {
"id": 67890,
"last_star_ts": 1672444800,
"global_score": 100,
"local_score": 200,
"name": "Test User",
"completion_day_level": {
"1": {
"1": {
"get_star_ts": 1672444800,
"star_index": 1
},
"2": {
"get_star_ts": 1672444900,
"star_index": 2
}
}
},
"stars": 2
}
}
}`

func TestGetLeaderboardSuccess(t *testing.T) {
// Create a mock server to return a valid JSON response
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Validate the request
if r.Method != http.MethodGet {
t.Errorf("Expected GET request, got %s", r.Method)
}
expectedPath := "/2024/leaderboard/private/view/test-leaderboard.json"
if r.URL.Path != expectedPath {
t.Errorf("Expected URL path %s, got %s", expectedPath, r.URL.Path)
}
// Return mock JSON
w.Header().Set("Content-Type", "application/json")
fmt.Fprintln(w, mockLeaderboardJSON)
}))
defer mockServer.Close()

client := NewClient("test-session-cookie")
// Inject mock HTTP client
client.SetHTTPClient(mockServer.Client())

// Override the request URL using a custom transport
client.HTTPClient.Transport = rewriteURLTransport("https://adventofcode.com", mockServer.URL)

// Call the method under test
leaderboard, err := client.GetLeaderboard("test-leaderboard")
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}

// Verify top-level fields
if leaderboard.Event != "2024" {
t.Errorf("Expected event '2024', got '%s'", leaderboard.Event)
}
if leaderboard.OwnerID != 12345 {
t.Errorf("Expected owner ID 12345, got %d", leaderboard.OwnerID)
}

// Verify member data
member, exists := leaderboard.Members["67890"]
if !exists {
t.Fatalf("Expected member with ID '67890' in leaderboard members")
}
if member.ID != 67890 {
t.Errorf("Expected member ID 67890, got %d", member.ID)
}
if member.Name != "Test User" {
t.Errorf("Expected member name 'Test User', got '%s'", member.Name)
}
if member.Stars != 2 {
t.Errorf("Expected stars 2, got %d", member.Stars)
}
if member.LocalScore != 200 {
t.Errorf("Expected local score 200, got %d", member.LocalScore)
}

// Verify completion day levels
day1, exists := member.CompletionDayLevels["1"]
if !exists {
t.Fatalf("Expected day '1' in completion_day_level")
}
if day1.Level1 == nil || day1.Level1.GetStarTs != 1672444800 {
t.Errorf("Expected Level1 get_star_ts 1672444800, got %v", day1.Level1)
}
if day1.Level2 == nil || day1.Level2.GetStarTs != 1672444900 {
t.Errorf("Expected Level2 get_star_ts 1672444900, got %v", day1.Level2)
}
}

func TestGetLeaderboardInvalidSession(t *testing.T) {
// Create a mock server that returns unauthorized response
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
}))
defer mockServer.Close()

client := NewClient("invalid-session-cookie")
client.SetHTTPClient(mockServer.Client())

// Override the request URL
client.HTTPClient.Transport = rewriteURLTransport("https://adventofcode.com", mockServer.URL)

_, err := client.GetLeaderboard("test-leaderboard")
if err == nil {
t.Fatalf("Expected an error due to unauthorized access, but got none")
}
expectedError := "error unmarshalling response body"
if err.Error()[:len(expectedError)] != expectedError {
t.Errorf("Expected error starting with '%s', got '%v'", expectedError, err)
}
}

func TestGetLeaderboardInvalidJSON(t *testing.T) {
// Create a mock server that returns invalid JSON
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "invalid json")
}))
defer mockServer.Close()

client := NewClient("test-session-cookie")
client.SetHTTPClient(mockServer.Client())

// Override the request URL
client.HTTPClient.Transport = rewriteURLTransport("https://adventofcode.com", mockServer.URL)

_, err := client.GetLeaderboard("test-leaderboard")
if err == nil {
t.Fatalf("Expected an error due to invalid JSON, but got none")
}
expectedError := "error unmarshalling response body"
if err.Error()[:len(expectedError)] != expectedError {
t.Errorf("Expected error starting with '%s', got '%v'", expectedError, err)
}
}

// rewriteURLTransport modifies the request URL to point to the mock server
func rewriteURLTransport(originalBase, mockBase string) http.RoundTripper {
return &urlRewritingTransport{
originalBase: originalBase,
mockBase: mockBase,
original: http.DefaultTransport,
}
}

type urlRewritingTransport struct {
originalBase string
mockBase string
original http.RoundTripper
}

func (t *urlRewritingTransport) RoundTrip(req *http.Request) (*http.Response, error) {
// Replace the original base URL with the mock server's URL
if req.URL.String()[:len(t.originalBase)] == t.originalBase {
req.URL.Scheme = "http"
req.URL.Host = t.mockBase[len("http://"):]
}
return t.original.RoundTrip(req)
}
Loading