Skip to content

Commit

Permalink
test(sdk-unit): Add tests for class schedule method
Browse files Browse the repository at this point in the history
  • Loading branch information
ditsuke committed Apr 8, 2023
1 parent d23760e commit bb02c03
Show file tree
Hide file tree
Showing 13 changed files with 233 additions and 39 deletions.
4 changes: 2 additions & 2 deletions amizone/amizone.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,13 +212,13 @@ func (a *Client) ClassSchedule(year int, month time.Month, date int) (models.Cla
response, err := a.doRequest(true, http.MethodGet, endpoint, nil)
if err != nil {
klog.Warningf("request (schedule): %s", err.Error())
return nil, errors.New(ErrFailedToVisitPage)
return nil, fmt.Errorf("%s: %s", ErrFailedToFetchPage, err.Error())
}

classSchedule, err := parse.ClassSchedule(response.Body)
if err != nil {
klog.Errorf("parse (schedule): %s", err.Error())
return nil, fmt.Errorf("%s: %w", ErrInternalFailure, err)
return nil, fmt.Errorf("%s: %w", ErrFailedToParsePage, err)
}
filteredSchedule := classSchedule.FilterByDate(timeFrom)

Expand Down
127 changes: 113 additions & 14 deletions amizone/amizone_test.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package amizone_test

import (
"encoding/json"
"fmt"
"net"
"net/http"
"net/http/cookiejar"
"net/url"
"strings"
"testing"
"time"

"github.com/ditsuke/go-amizone/amizone"
"github.com/ditsuke/go-amizone/amizone/internal/mock"
Expand All @@ -20,7 +23,7 @@ type Empty struct{}

// / DummyMatcher is a matcher for the Empty datatype that does exactly nothing,
// / for when the function to be tested returns nothing.
func DummyMatcher(_ Empty, _ *WithT) {
func DummyMatcher[T any](_ T, _ *WithT) {
}

// DummySetup is used when a test requires no setup.
Expand All @@ -37,7 +40,7 @@ type TestCase[D any, I any] struct {
client *amizone.Client
setup func(g *WithT)
input I
dataMatcher func(date D, g *WithT)
dataMatcher func(data D, g *WithT)
errMatcher func(err error, g *WithT)
}

Expand Down Expand Up @@ -507,7 +510,7 @@ func TestClient_RegisterWifiMac(t *testing.T) {
g.Expect(mock.GockRegisterWifiInfo()).ToNot(HaveOccurred())
},
input: RegisterMacArgs{A: net.HardwareAddr{}, O: false},
dataMatcher: DummyMatcher,
dataMatcher: DummyMatcher[Empty],
errMatcher: func(err error, g *WithT) {
g.Expect(err).To(HaveOccurred())
g.Expect(err.Error()).To(ContainSubstring(amizone.ErrInvalidMac))
Expand All @@ -516,7 +519,7 @@ func TestClient_RegisterWifiMac(t *testing.T) {
{
name: "client: logged in; mac: valid; free_slots: none; bypass: false",
client: loggedInClient,
dataMatcher: DummyMatcher,
dataMatcher: DummyMatcher[Empty],
errMatcher: func(err error, g *WithT) {
g.Expect(err).To(HaveOccurred())
g.Expect(err.Error()).To(ContainSubstring(amizone.ErrNoMacSlots))
Expand All @@ -529,7 +532,7 @@ func TestClient_RegisterWifiMac(t *testing.T) {
{
name: "client: logged in; mac: valid; free_slots: none; bypass: true",
client: loggedInClient,
dataMatcher: DummyMatcher,
dataMatcher: DummyMatcher[Empty],
errMatcher: NoError,
input: RegisterMacArgs{A: macNew, O: true},
setup: func(g *WithT) {
Expand All @@ -547,7 +550,7 @@ func TestClient_RegisterWifiMac(t *testing.T) {
name: "client: logged in; mac: valid; free slots: 1, bypass: false",
client: loggedInClient,
input: RegisterMacArgs{A: macNew, O: false},
dataMatcher: DummyMatcher,
dataMatcher: DummyMatcher[Empty],
errMatcher: NoError,
setup: func(g *WithT) {
g.Expect(mock.GockRegisterWifiInfoOneSlot()).ToNot(HaveOccurred())
Expand All @@ -564,7 +567,7 @@ func TestClient_RegisterWifiMac(t *testing.T) {
name: "client: logged in; mac: valid; free_slots: 1; bypass: true",
client: loggedInClient,
input: RegisterMacArgs{A: macNew, O: true},
dataMatcher: DummyMatcher,
dataMatcher: DummyMatcher[Empty],
errMatcher: NoError,
setup: func(g *WithT) {
g.Expect(mock.GockRegisterWifiInfoOneSlot()).ToNot(HaveOccurred())
Expand All @@ -581,7 +584,7 @@ func TestClient_RegisterWifiMac(t *testing.T) {
name: "client is logged in, mac already exists",
client: loggedInClient,
input: RegisterMacArgs{A: macStringtoMac(mock.ValidMac2, g), O: false},
dataMatcher: DummyMatcher,
dataMatcher: DummyMatcher[Empty],
errMatcher: NoError,
setup: func(g *WithT) {
g.Expect(mock.GockRegisterWifiInfo()).ToNot(HaveOccurred())
Expand All @@ -597,7 +600,7 @@ func TestClient_RegisterWifiMac(t *testing.T) {
g.Expect(mock.GockRegisterUnauthenticatedGet("/Home")).ToNot(HaveOccurred())
g.Expect(mock.GockRegisterUnauthenticatedGet("RegisterForWifi/mac/MacRegistration")).ToNot(HaveOccurred())
},
dataMatcher: DummyMatcher,
dataMatcher: DummyMatcher[Empty],
errMatcher: func(err error, g *WithT) {
g.Expect(err).To(HaveOccurred())
g.Expect(err.Error()).To(ContainSubstring(amizone.ErrFailedLogin))
Expand Down Expand Up @@ -640,7 +643,7 @@ func TestClient_RemoveWifiMac(t *testing.T) {
g.Expect(err).To(HaveOccurred())
g.Expect(err.Error()).To(ContainSubstring(amizone.ErrInvalidMac))
},
dataMatcher: DummyMatcher,
dataMatcher: DummyMatcher[Empty],
},
{
name: "amizone is unreachable",
Expand All @@ -651,7 +654,7 @@ func TestClient_RemoveWifiMac(t *testing.T) {
g.Expect(err).To(HaveOccurred())
g.Expect(err.Error()).To(ContainSubstring(amizone.ErrFailedToVisitPage))
},
dataMatcher: DummyMatcher,
dataMatcher: DummyMatcher[Empty],
},
{
name: "client is not logged in",
Expand All @@ -662,7 +665,7 @@ func TestClient_RemoveWifiMac(t *testing.T) {
g.Expect(err).To(HaveOccurred())
g.Expect(err.Error()).To(ContainSubstring(amizone.ErrFailedLogin))
},
dataMatcher: DummyMatcher,
dataMatcher: DummyMatcher[Empty],
},
{
name: "parser breaks when amizone changes something",
Expand All @@ -685,7 +688,7 @@ func TestClient_RemoveWifiMac(t *testing.T) {
g.Expect(err).To(HaveOccurred())
g.Expect(err.Error()).To(ContainSubstring(amizone.ErrFailedToParsePage))
},
dataMatcher: DummyMatcher,
dataMatcher: DummyMatcher[Empty],
},
{
name: "everything goes ok",
Expand All @@ -704,7 +707,7 @@ func TestClient_RemoveWifiMac(t *testing.T) {
},
input: RemoveWifiArgs{A: macStringtoMac(mock.ValidMac2, g)},
errMatcher: NoError,
dataMatcher: DummyMatcher,
dataMatcher: DummyMatcher[Empty],
},
}

Expand All @@ -721,6 +724,102 @@ func TestClient_RemoveWifiMac(t *testing.T) {
}
}

// Test out amizone.GetClassSchedule in the style of the other tests written
func TestClient_GetClassSchedule(t *testing.T) {
setupNetworking()
t.Cleanup(teardown)
g := NewWithT(t)

type GetClassScheduleArgs = struct {
year int
month time.Month
day int
}

loggedInClient := getLoggedInClient(g)
nonLoggedInClient := getNonLoggedInClient(g)

standardDate := GetClassScheduleArgs{year: 2023, month: time.April, day: 1}
standardDatePlusOne := GetClassScheduleArgs{year: 2023, month: time.April, day: 2}
fmtDate := func(args GetClassScheduleArgs) string {
return fmt.Sprintf("%02d-%02d-%02d", args.year, args.month, args.day)
}

testCases := []TestCase[models.ClassSchedule, GetClassScheduleArgs]{
{
name: "client is not logged in",
client: nonLoggedInClient,
input: standardDate,
errMatcher: func(err error, g *WithT) {
g.Expect(err).To(HaveOccurred())
g.Expect(err.Error()).To(ContainSubstring(amizone.ErrFailedLogin))
},
dataMatcher: DummyMatcher[models.ClassSchedule],
setup: DummySetup,
},
{
name: "amizone doesn't send back any events",
client: loggedInClient,
input: standardDate,
errMatcher: func(err error, g *WithT) {
g.Expect(err).ToNot(HaveOccurred())
},
dataMatcher: func(data models.ClassSchedule, g *WithT) {
g.Expect(data).To(BeEmpty())
},
setup: func(g *WithT) {
g.Expect(mock.GockRegisterCalendarEndpoint(fmtDate(standardDate), fmtDate(standardDatePlusOne), mock.DiaryEventsNone)).ToNot(HaveOccurred())
},
},
{
name: "amizone's response cannot be parsed (no longer json)",
client: loggedInClient,
input: standardDate,
errMatcher: func(err error, g *WithT) {
g.Expect(err).To(HaveOccurred())
g.Expect(err.Error()).To(ContainSubstring(amizone.ErrFailedToParsePage))
},
dataMatcher: DummyMatcher[models.ClassSchedule],
setup: func(g *WithT) {
g.Expect(mock.GockRegisterCalendarEndpoint(fmtDate(standardDate), fmtDate(standardDatePlusOne), mock.CoursesPage)).ToNot(HaveOccurred())
},
},
{
name: "amizone sends back response with events",
client: loggedInClient,
input: standardDate,
errMatcher: func(err error, g *WithT) {
g.Expect(err).ToNot(HaveOccurred())
},
dataMatcher: func(schedule models.ClassSchedule, g *WithT) {
g.Expect(schedule).To(HaveLen(3))
sb := strings.Builder{}
_ = json.NewEncoder(&sb).Encode(schedule)
g.Expect(sb.String()).To(MatchJSON(`[{"Course":{"Code":"IT414","Name":"SS"},"StartTime":"2023-04-01T12:15:00Z","EndTime":"2023-04-01T13:10:00Z","Faculty":"DRS[2434]","Room":"E1-309","Attended":2},{"Course":{"Code":"IT301","Name":"SE"},"StartTime":"2023-04-01T12:15:00Z","EndTime":"2023-04-01T13:10:00Z","Faculty":"DRG[2397],DSKD[2436]","Room":"E1-000","Attended":1},{"Course":{"Code":"CSE304","Name":"CC"},"StartTime":"2023-04-01T13:15:00Z","EndTime":"2023-04-01T14:10:00Z","Faculty":"DAG[307870]","Room":"E1-000","Attended":0}]`))
g.Expect(schedule[0].Attended).To(Equal(models.AttendanceStateAbsent))
g.Expect(schedule[1].Attended).To(Equal(models.AttendanceStatePresent))
g.Expect(schedule[2].Attended).To(Equal(models.AttendanceStatePending))
},
setup: func(g *WithT) {
g.Expect(mock.GockRegisterCalendarEndpoint(fmtDate(standardDate), fmtDate(standardDatePlusOne), mock.DiaryEventsSmallJSON)).ToNot(HaveOccurred())
},
},
}

for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
t.Cleanup(setupNetworking)
g := NewWithT(t)

testCase.sanityCheck(g)
testCase.setup(g)
classes, err := testCase.client.ClassSchedule(testCase.input.year, testCase.input.month, testCase.input.day)
testCase.errMatcher(err, g)
testCase.dataMatcher(classes, g)
})
}
}

// Test utilities

// setupNetworking tears down any existing network mocks and sets up gock anew to intercept network
Expand Down
7 changes: 7 additions & 0 deletions amizone/internal/mock/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,13 @@ func GockRegisterWifiInfoOneSlot() error {
return GockRegisterAuthenticatedGet("/RegisterForWifi/mac/MacRegistration", WifiPageOneSlot)
}

func GockRegisterCalendarEndpoint(start, end string, file File) error {
return GockRegisterAuthenticatedGetWithParams("/Calendar/home/GetDiaryEvents", map[string]string{
"start": start,
"end": end,
}, file)
}

// GockRegisterWifiRegistration() registers a gock route for the wifi registration page.
// The request must have the expected referrer, cookies and post data to be successful.
func GockRegisterWifiRegistration(payload url.Values) error {
Expand Down
20 changes: 11 additions & 9 deletions amizone/internal/mock/testdata.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@ func (f File) Open() (fs.File, error) {

// Constants for file paths in the filesystem embedded filesystem.
const (
DiaryEventsJSON File = "testdata/diary_events.json"
ExaminationSchedule File = "testdata/examination_schedule.html"
HomePageLoggedIn File = "testdata/home_page_logged_in.html"
LoginPage File = "testdata/login_page.html"
CoursesPage File = "testdata/my_courses.html"
CoursesPageSemWise File = "testdata/courses_semwise.html"
IDCardPage File = "testdata/id_card_page.html"
WifiPage File = "testdata/wifi_mac_registration.html"
WifiPageOneSlot File = "testdata/wifi_mac_registration_one_empty.html"
DiaryEventsNone File = "testdata/diary_events_none.json"
DiaryEventsJSON File = "testdata/diary_events.json"
DiaryEventsSmallJSON File = "testdata/diary_events_small.json"
ExaminationSchedule File = "testdata/examination_schedule.html"
HomePageLoggedIn File = "testdata/home_page_logged_in.html"
LoginPage File = "testdata/login_page.html"
CoursesPage File = "testdata/my_courses.html"
CoursesPageSemWise File = "testdata/courses_semwise.html"
IDCardPage File = "testdata/id_card_page.html"
WifiPage File = "testdata/wifi_mac_registration.html"
WifiPageOneSlot File = "testdata/wifi_mac_registration_one_empty.html"
)
2 changes: 1 addition & 1 deletion amizone/internal/mock/testdata/diary_events.json
Original file line number Diff line number Diff line change
Expand Up @@ -149,4 +149,4 @@
"url": "https://classurl.urlco",
"allDay": false
}
]
]
1 change: 1 addition & 0 deletions amizone/internal/mock/testdata/diary_events_none.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[]
47 changes: 47 additions & 0 deletions amizone/internal/mock/testdata/diary_events_small.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
[
{
"id": 43381795,
"title": "SS",
"start": "2023/04/01 12:15:00 PM",
"end": "2023/04/01 01:10:00 PM",
"color": "class-schedule-color",
"CourseCode": "IT414 ",
"sType": "C",
"className": "class-schedule-color",
"FacultyName": "DRS[2434]",
"RoomNo": "E1-309",
"AttndColor": "#f00",
"url": "",
"allDay": false
},
{
"id": 43386514,
"title": "SE",
"start": "2023/04/01 12:15:00 PM",
"end": "2023/04/01 01:10:00 PM",
"color": "class-schedule-color",
"CourseCode": "IT301 ",
"sType": "C",
"className": "class-schedule-color",
"FacultyName": "DRG[2397],DSKD[2436]",
"RoomNo": "E1-000",
"AttndColor": "#4FCC4F",
"url": "",
"allDay": false
},
{
"id": 43378789,
"title": "CC",
"start": "2023/04/01 01:15:00 PM",
"end": "2023/04/01 02:10:00 PM",
"color": "class-schedule-color",
"CourseCode": "CSE304",
"sType": "C",
"className": "class-schedule-color",
"FacultyName": "DAG[307870]",
"RoomNo": "E1-000",
"AttndColor": "#3a87ad",
"url": "",
"allDay": false
}
]
10 changes: 5 additions & 5 deletions amizone/internal/parse/class_schedule.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const (
func ClassSchedule(body io.Reader) (models.ClassSchedule, error) {
var diaryEvents models.AmizoneDiaryEvents
if err := json.NewDecoder(body).Decode(&diaryEvents); err != nil {
return nil, fmt.Errorf("%s: %w", ErrFailedToParse, err)
return nil, fmt.Errorf("JSON decode: %w", err)
}

var classSchedule models.ClassSchedule
Expand All @@ -40,13 +40,13 @@ func ClassSchedule(body io.Reader) (models.ClassSchedule, error) {

class := models.ScheduledClass{
Course: models.CourseRef{
Code: entry.CourseCode,
Name: entry.CourseName,
Code: cleanString(entry.CourseCode),
Name: cleanString(entry.CourseName),
},
StartTime: parseTime(entry.Start),
EndTime: parseTime(entry.End),
Faculty: entry.Faculty,
Room: entry.Room,
Faculty: cleanString(entry.Faculty),
Room: cleanString(entry.Room),
Attended: entry.AttendanceState(),
}

Expand Down
Loading

0 comments on commit bb02c03

Please sign in to comment.