Skip to content

Commit

Permalink
Merge pull request #14 from ditsuke/feat/daily-attendance
Browse files Browse the repository at this point in the history
  • Loading branch information
ditsuke committed Apr 8, 2023
2 parents 3f626f9 + ad0eaa2 commit 8fed2fd
Show file tree
Hide file tree
Showing 19 changed files with 674 additions and 312 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
145 changes: 128 additions & 17 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 @@ -16,11 +19,13 @@ import (
"gopkg.in/h2non/gock.v1"
)

// === Test setup helpers ===

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

// DummySetup is used when a test requires no setup.
Expand All @@ -37,7 +42,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 All @@ -48,6 +53,15 @@ func (c *TestCase[D, I]) sanityCheck(g *WithT) {
g.Expect(c.errMatcher).ToNot(BeNil(), "error matcher function must not be nil")
}

// === Test helpers ===

// toJSON converts a struct to a JSON string.
func toJSON[T any](t T, g *WithT) string {
s, err := json.Marshal(t)
g.Expect(err).ToNot(HaveOccurred(), "marshall json")
return string(s)
}

// @todo: implement test cases to test behavior when:
// - Amizone is not reachable
// - Amizone is reachable but login fails (invalid credentials, etc?)
Expand Down Expand Up @@ -450,14 +464,15 @@ func TestClient_GetWifiMacInfo(t *testing.T) {
errMatcher func(g *WithT, err error)
}{
{
name: "qkweq",
name: "amizone returns macs as usual",
client: loggedInClient,
setup: func(g *WithT) {
g.Expect(mock.GockRegisterWifiInfo()).ToNot(HaveOccurred())
},
infoMatcher: func(g *WithT, info *models.WifiMacInfo) {
g.Expect(info).ToNot(BeNil())
g.Expect(info.RegisteredAddresses).To(HaveLen(2))
g.Expect(toJSON(info, g)).To(MatchJSON(`{"RegisteredAddresses":["VQQt576k","/dUUGAyL"],"Slots":2,"FreeSlots":0}`))
},
errMatcher: func(g *WithT, err error) {
g.Expect(err).ToNot(HaveOccurred())
Expand Down Expand Up @@ -507,7 +522,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 +531,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 +544,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 +562,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 +579,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 +596,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 +612,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 +655,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 +666,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 +677,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 +700,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 +719,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 +736,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
}
]
Loading

0 comments on commit 8fed2fd

Please sign in to comment.