Skip to content

Commit

Permalink
Merge pull request #44 from Blonteractor/feature-exam-result
Browse files Browse the repository at this point in the history
  • Loading branch information
ditsuke committed Jun 9, 2023
2 parents 6428fc4 + 5298dc3 commit 1c37670
Show file tree
Hide file tree
Showing 15 changed files with 2,365 additions and 421 deletions.
65 changes: 54 additions & 11 deletions amizone/amizone.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,18 @@ import (
const (
BaseURL = "https://" + internal.AmizoneDomain

loginRequestEndpoint = "/"
attendancePageEndpoint = "/Home"
scheduleEndpointTemplate = "/Calendar/home/GetDiaryEvents?start=%s&end=%s"
examScheduleEndpoint = "/Examination/ExamSchedule"
currentCoursesEndpoint = "/Academics/MyCourses"
coursesEndpoint = currentCoursesEndpoint + "/CourseListSemWise"
profileEndpoint = "/IDCard"
macBaseEndpoint = "/RegisterForWifi/mac"
getWifiMacsEndpoint = macBaseEndpoint + "/MacRegistration"
registerWifiMacsEndpoint = macBaseEndpoint + "/MacRegistrationSave"
loginRequestEndpoint = "/"
attendancePageEndpoint = "/Home"
scheduleEndpointTemplate = "/Calendar/home/GetDiaryEvents?start=%s&end=%s"
examScheduleEndpoint = "/Examination/ExamSchedule"
currentCoursesEndpoint = "/Academics/MyCourses"
coursesEndpoint = currentCoursesEndpoint + "/CourseListSemWise"
profileEndpoint = "/IDCard"
macBaseEndpoint = "/RegisterForWifi/mac"
currentExaminationResultEndpoint = "/Examination/Examination"
examinationResultEndpoint = currentExaminationResultEndpoint + "/ExaminationListSemWise"
getWifiMacsEndpoint = macBaseEndpoint + "/MacRegistration"
registerWifiMacsEndpoint = macBaseEndpoint + "/MacRegistrationSave"

// deleteWifiMacEndpoint is peculiar in that it requires the user's ID as a parameter.
// This _might_ open doors for an exploit (spoiler: indeed it does)
Expand Down Expand Up @@ -73,7 +75,7 @@ type Credentials struct {
}

// Client is the main struct for the amizone package, exposing the entire API surface
// for the portal as implemented here. The struct must always be initialised through a public
// for the portal as implemented here. The struct must always be initialized through a public
// constructor like NewClient()
type Client struct {
httpClient *http.Client
Expand Down Expand Up @@ -215,6 +217,47 @@ func (a *Client) GetAttendance() (models.AttendanceRecords, error) {
return models.AttendanceRecords(attendanceRecord), nil
}

// GetExaminationResult retrieves, parses and returns a ExaminationResultRecords from Amizone for their latest semester
// for which the result is available
func (a *Client) GetCurrentExaminationResult() (*models.ExamResultRecords, error) {
response, err := a.doRequest(true, http.MethodGet, currentExaminationResultEndpoint, nil)
if err != nil {
klog.Warningf("request (examination-result): %s", err.Error())
return nil, fmt.Errorf("%s: %s", ErrFailedToFetchPage, err.Error())
}

examinationResultRecords, err := parse.ExaminationResult(response.Body)
if err != nil {
klog.Errorf("parse (examination-result): %s", err.Error())
return nil, fmt.Errorf("%s: %w", ErrInternalFailure, err)
}

return examinationResultRecords, nil
}

// GetExaminationResult retrieves, parses and returns a ExaminationResultRecords from Amizone for the semester referred by
// semesterRef. Semester references should be retrieved through GetSemesters, which returns a list of valid
// semesters with names and references.
func (a *Client) GetExaminationResult(semesterRef string) (*models.ExamResultRecords, error) {
payload := url.Values{
"sem": []string{semesterRef},
}.Encode()

response, err := a.doRequest(true, http.MethodPost, examinationResultEndpoint, strings.NewReader(payload))
if err != nil {
klog.Warningf("request (examination-result): %s", err.Error())
return nil, fmt.Errorf("%s: %s", ErrFailedToFetchPage, err.Error())
}

examinationResultRecords, err := parse.ExaminationResult(response.Body)
if err != nil {
klog.Errorf("parse (examination-result): %s", err.Error())
return nil, fmt.Errorf("%s: %w", ErrInternalFailure, err)
}

return examinationResultRecords, nil
}

// GetClassSchedule retrieves, parses and returns class schedule data from Amizone.
// The date parameter is used to determine which schedule to retrieve, however as Amizone imposes arbitrary limits on the
// date range, as in scheduled for dates older than some months are not stored by Amizone, we have no way of knowing if a request will succeed.
Expand Down
159 changes: 159 additions & 0 deletions amizone/amizone_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,165 @@ func TestClient_GetCurrentCourses(t *testing.T) {
}
}

func TestClient_GetExaminationResult(t *testing.T) {
g := NewWithT(t)

setupNetworking()
t.Cleanup(teardown)

loggedInClient := createLoggedInClient(g)
nonLoggedInClient := createNonLoggedInClient(g)

testCases := []struct {
name string
client *amizone.Client
semesterRef string
setup func(g *WithT)
resultMatcher func(g *WithT, courses *models.ExamResultRecords)
errMatcher func(g *WithT, err error)
}{
{
name: "amizone client is logged in, we ask for the result of semester 1, return mock result page on expected POST",
client: loggedInClient,
semesterRef: "1",
setup: func(g *WithT) {
err := mock.GockRegisterExamResultRequest("1")
g.Expect(err).ToNot(HaveOccurred())
},
resultMatcher: func(g *WithT, result *models.ExamResultRecords) {
g.Expect(result.Overall).To(HaveLen(3))
g.Expect(result.CourseWise).To(HaveLen(8))
},
errMatcher: func(g *WithT, err error) {
g.Expect(err).ToNot(HaveOccurred())
},
},
{
name: "amizone client is logged in, we ask for the result of semester 2, return mock result page on expected POST",
client: loggedInClient,
semesterRef: "2",
setup: func(g *WithT) {
err := mock.GockRegisterExamResultRequest("2")
g.Expect(err).ToNot(HaveOccurred())
},
resultMatcher: func(g *WithT, result *models.ExamResultRecords) {
g.Expect(result.Overall).To(HaveLen(3))
g.Expect(result.CourseWise).To(HaveLen(8))
},
errMatcher: func(g *WithT, err error) {
g.Expect(err).ToNot(HaveOccurred())
},
},
{
name: "amizone client is not logged in, returns login page on request",
client: nonLoggedInClient,
semesterRef: "3",
setup: func(g *WithT) {
//err := mock.GockRegisterLoginPage()
//g.Expect(err).ToNot(HaveOccurred())
err := mock.GockRegisterUnauthenticatedGet("/")
g.Expect(err).ToNot(HaveOccurred())
mock.GockRegisterUnauthenticatedPost("/Examination/Examination/ExaminationListSemWise", url.Values{"sem": []string{"3"}}.Encode(), strings.NewReader("<no></no>"))
},
resultMatcher: func(g *WithT, result *models.ExamResultRecords) {
g.Expect(result).To(BeNil())
},
errMatcher: func(g *WithT, err error) {
g.Expect(err).To(HaveOccurred())
// TODO: verify against error string "not logged in" or something
g.Expect(err.Error()).ToNot(ContainSubstring(amizone.ErrFailedToVisitPage))
},
},
}

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

result, err := testCase.client.GetExaminationResult(testCase.semesterRef)
testCase.errMatcher(g, err)
testCase.resultMatcher(g, result)
})
}
}

func TestClient_GetCurrentExaminationResult(t *testing.T) {
g := NewWithT(t)

setupNetworking()
t.Cleanup(teardown)

loggedInClient := createLoggedInClient(g)
nonLoggedInClient := createNonLoggedInClient(g)

testCases := []struct {
name string
client *amizone.Client
setup func(g *WithT)
resultMatcher func(g *WithT, result *models.ExamResultRecords)
errMatcher func(g *WithT, err error)
}{
{
name: "amizone client is logged in and returns the (mock) exam result page",
client: loggedInClient,
setup: func(g *WithT) {
err := mock.GockRegisterExamResultPage()
g.Expect(err).ToNot(HaveOccurred())
},
resultMatcher: func(g *WithT, result *models.ExamResultRecords) {
g.Expect(result.Overall).To(HaveLen(3))
g.Expect(result.CourseWise).To(HaveLen(8))
},
errMatcher: func(g *WithT, err error) {
g.Expect(err).ToNot(HaveOccurred())
},
},
{
name: "amizone client is logged is and returns the (mock) sem-wise courses page",
client: loggedInClient,
setup: func(g *WithT) {
err := mock.GockRegisterExamResultPage()
g.Expect(err).ToNot(HaveOccurred())
},
resultMatcher: func(g *WithT, result *models.ExamResultRecords) {
g.Expect(result.Overall).To(HaveLen(3))
g.Expect(result.CourseWise).To(HaveLen(8))
},
errMatcher: func(g *WithT, err error) {
g.Expect(err).ToNot(HaveOccurred())
},
},
{
name: "amizone client is not logged in and returns the login page",
client: nonLoggedInClient,
setup: func(g *WithT) {
err := mock.GockRegisterUnauthenticatedGet("/")
g.Expect(err).ToNot(HaveOccurred())
},
resultMatcher: func(g *WithT, result *models.ExamResultRecords) {
g.Expect(result).To(BeNil())
},
errMatcher: func(g *WithT, err error) {
g.Expect(err).To(HaveOccurred())
},
},
}

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

result, err := testCase.client.GetCurrentExaminationResult()
testCase.errMatcher(g, err)
testCase.resultMatcher(g, result)
})
}
}

func TestClient_GetProfile(t *testing.T) {
g := NewWithT(t)

Expand Down
20 changes: 20 additions & 0 deletions amizone/internal/mock/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,26 @@ func GockRegisterProfilePage() error {
return GockRegisterAuthenticatedGet("/IDCard", IDCardPage)
}

func GockRegisterExamResultPage() error {
return GockRegisterAuthenticatedGet("/Examination/Examination", ExaminationResultPage)
}

func GockRegisterExamResultRequest(semesterRef string) error {
return GockRegisterAuthenticatedPost("/Examination/Examination/ExaminationListSemWise",
func(r1 *http.Request, r2 *gock.Request) (bool, error) {
r, err := io.ReadAll(r1.Body)
if err != nil {
return false, fmt.Errorf("error checking request body: %s", err.Error())
}
if string(r) == (url.Values{"sem": []string{semesterRef}}.Encode()) {
return true, nil
}
return false, nil
},
ExaminationResultPage,
)
}

func GockRegisterSemWiseCoursesPage() error {
return GockRegisterAuthenticatedGet("/Academics/MyCourses", CoursesPageSemWise)
}
Expand Down
1 change: 1 addition & 0 deletions amizone/internal/mock/testdata.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const (
WifiPage File = "testdata/wifi_mac_registration.html"
WifiPageOneSlotPopulated File = "testdata/wifi_mac_registration_one_empty.html"
FacultyPage File = "testdata/faculty_page.html"
ExaminationResultPage File = "testdata/examination_result.html"
)

type ExpectedJSON string
Expand Down
Loading

0 comments on commit 1c37670

Please sign in to comment.