mirror of
https://github.com/Xevion/banner.git
synced 2025-12-06 01:14:22 -06:00
Compare commits
2 Commits
95e760c549
...
e081e7f493
| Author | SHA1 | Date | |
|---|---|---|---|
| e081e7f493 | |||
| 5891bed728 |
@@ -20,7 +20,14 @@ tasks:
|
||||
test:
|
||||
desc: Run tests
|
||||
cmds:
|
||||
- go test ./...
|
||||
- go test ./tests/...
|
||||
env:
|
||||
ENVIRONMENT: test
|
||||
|
||||
test-coverage:
|
||||
desc: Run tests with coverage
|
||||
cmds:
|
||||
- go test -coverpkg=./internal/... -cover ./tests/...
|
||||
env:
|
||||
ENVIRONMENT: test
|
||||
|
||||
|
||||
@@ -18,8 +18,10 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
ICalTimestampFormatUtc = "20060102T150405Z"
|
||||
ICalTimestampFormatLocal = "20060102T150405"
|
||||
// ICalTimestampLayoutUtc is the formatting layout for timestamps in the UTC timezone.
|
||||
ICalTimestampLayoutUtc = "20060102T150405Z"
|
||||
// ICalTimestampLayoutLocal is the formatting layout for timestamps in the local timezone.
|
||||
ICalTimestampLayoutLocal = "20060102T150405"
|
||||
)
|
||||
|
||||
// CommandHandler is a function that handles a slash command interaction.
|
||||
@@ -27,13 +29,14 @@ type CommandHandler func(b *Bot, s *discordgo.Session, i *discordgo.InteractionC
|
||||
|
||||
var (
|
||||
// CommandDefinitions is a list of all the bot's command definitions.
|
||||
CommandDefinitions = []*discordgo.ApplicationCommand{TermCommandDefinition, TimeCommandDefinition, SearchCommandDefinition, IcsCommandDefinition}
|
||||
CommandDefinitions = []*discordgo.ApplicationCommand{TermCommandDefinition, TimeCommandDefinition, SearchCommandDefinition, IcsCommandDefinition, GCalCommandDefinition}
|
||||
// CommandHandlers is a map of command names to their handlers.
|
||||
CommandHandlers = map[string]CommandHandler{
|
||||
TimeCommandDefinition.Name: TimeCommandHandler,
|
||||
TermCommandDefinition.Name: TermCommandHandler,
|
||||
SearchCommandDefinition.Name: SearchCommandHandler,
|
||||
IcsCommandDefinition.Name: IcsCommandHandler,
|
||||
GCalCommandDefinition.Name: GCalCommandHandler,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -203,7 +206,7 @@ func SearchCommandHandler(b *Bot, s *discordgo.Session, i *discordgo.Interaction
|
||||
return err
|
||||
}
|
||||
|
||||
fetch_time := time.Now()
|
||||
fetchTime := time.Now()
|
||||
fields := []*discordgo.MessageEmbedField{}
|
||||
|
||||
for _, course := range courses.Data {
|
||||
@@ -252,7 +255,7 @@ func SearchCommandHandler(b *Bot, s *discordgo.Session, i *discordgo.Interaction
|
||||
Data: &discordgo.InteractionResponseData{
|
||||
Embeds: []*discordgo.MessageEmbed{
|
||||
{
|
||||
Footer: internal.GetFetchedFooter(b.Config, fetch_time),
|
||||
Footer: internal.GetFetchedFooter(b.Config, fetchTime),
|
||||
Description: fmt.Sprintf("%d Class%s", courses.TotalCount, internal.Plural(courses.TotalCount)),
|
||||
Fields: fields[:min(25, len(fields))],
|
||||
Color: color,
|
||||
@@ -322,7 +325,7 @@ func TermCommandHandler(b *Bot, s *discordgo.Session, i *discordgo.InteractionCr
|
||||
})
|
||||
}
|
||||
|
||||
fetch_time := time.Now()
|
||||
fetchTime := time.Now()
|
||||
|
||||
if len(fields) > 25 {
|
||||
log.Warn().Int("count", len(fields)).Msg("Too many fields in term command (trimmed)")
|
||||
@@ -333,7 +336,7 @@ func TermCommandHandler(b *Bot, s *discordgo.Session, i *discordgo.InteractionCr
|
||||
Data: &discordgo.InteractionResponseData{
|
||||
Embeds: []*discordgo.MessageEmbed{
|
||||
{
|
||||
Footer: internal.GetFetchedFooter(b.Config, fetch_time),
|
||||
Footer: internal.GetFetchedFooter(b.Config, fetchTime),
|
||||
Description: fmt.Sprintf("%d term%s (page %d)", len(termResult), internal.Plural(len(termResult)), pageNumber),
|
||||
Fields: fields[:min(25, len(fields))],
|
||||
},
|
||||
@@ -360,7 +363,7 @@ var TimeCommandDefinition = &discordgo.ApplicationCommand{
|
||||
|
||||
// TimeCommandHandler handles the /time command, which allows users to get the meeting times for a course.
|
||||
func TimeCommandHandler(b *Bot, s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
||||
fetch_time := time.Now()
|
||||
fetchTime := time.Now()
|
||||
crn := i.ApplicationCommandData().Options[0].IntValue()
|
||||
|
||||
// Fix static term
|
||||
@@ -393,7 +396,7 @@ func TimeCommandHandler(b *Bot, s *discordgo.Session, i *discordgo.InteractionCr
|
||||
Data: &discordgo.InteractionResponseData{
|
||||
Embeds: []*discordgo.MessageEmbed{
|
||||
{
|
||||
Footer: internal.GetFetchedFooter(b.Config, fetch_time),
|
||||
Footer: internal.GetFetchedFooter(b.Config, fetchTime),
|
||||
Description: "",
|
||||
Fields: []*discordgo.MessageEmbedField{
|
||||
{
|
||||
@@ -434,6 +437,93 @@ var IcsCommandDefinition = &discordgo.ApplicationCommand{
|
||||
},
|
||||
}
|
||||
|
||||
var GCalCommandDefinition = &discordgo.ApplicationCommand{
|
||||
Name: "gcal",
|
||||
Description: "Generate a link to create a Google Calendar event for a course",
|
||||
Options: []*discordgo.ApplicationCommandOption{
|
||||
{
|
||||
Type: discordgo.ApplicationCommandOptionInteger,
|
||||
Name: "crn",
|
||||
Description: "Course Reference Number",
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// GCalCommandHandler handles the /gcal command, which allows users to generate a link to create a Google Calendar event for a course.
|
||||
func GCalCommandHandler(b *Bot, s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
||||
// Parse all options
|
||||
options := internal.ParseOptions(i.ApplicationCommandData().Options)
|
||||
crn := options.GetInt("crn")
|
||||
|
||||
course, err := b.API.GetCourse(strconv.Itoa(int(crn)))
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error retrieving course data: %w", err)
|
||||
}
|
||||
|
||||
meetingTimes, err := b.API.GetCourseMeetingTime(202510, int(crn))
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error requesting meeting time: %w", err)
|
||||
}
|
||||
|
||||
if len(meetingTimes) == 0 {
|
||||
return fmt.Errorf("unexpected - no meeting time data found for course")
|
||||
}
|
||||
|
||||
// Check if the course has any meeting times
|
||||
meetingTime, exists := lo.Find(meetingTimes, func(mt models.MeetingTimeResponse) bool {
|
||||
switch mt.MeetingTime.MeetingType {
|
||||
case "ID", "OA":
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
if !exists {
|
||||
internal.RespondError(s, i.Interaction, "The course requested does not meet at a defined moment in time.", nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
startDay := meetingTime.StartDay()
|
||||
startTime := meetingTime.StartTime()
|
||||
endTime := meetingTime.EndTime()
|
||||
|
||||
// Create timestamps in UTC
|
||||
dtStart := time.Date(startDay.Year(), startDay.Month(), startDay.Day(), int(startTime.Hours), int(startTime.Minutes), 0, 0, b.Config.CentralTimeLocation)
|
||||
dtEnd := time.Date(startDay.Year(), startDay.Month(), startDay.Day(), int(endTime.Hours), int(endTime.Minutes), 0, 0, b.Config.CentralTimeLocation)
|
||||
|
||||
// Format times in UTC for Google Calendar
|
||||
startStr := dtStart.UTC().Format(ICalTimestampLayoutUtc)
|
||||
endStr := dtEnd.UTC().Format(ICalTimestampLayoutUtc)
|
||||
|
||||
// Generate RRULE for recurrence
|
||||
rrule := meetingTime.RRule()
|
||||
recurRule := fmt.Sprintf("FREQ=WEEKLY;BYDAY=%s;UNTIL=%s", rrule.ByDay, rrule.Until)
|
||||
|
||||
// Build calendar URL
|
||||
params := url.Values{}
|
||||
params.Add("action", "TEMPLATE")
|
||||
params.Add("text", fmt.Sprintf("%s %s - %s", course.Subject, course.CourseNumber, course.CourseTitle))
|
||||
params.Add("dates", fmt.Sprintf("%s/%s", startStr, endStr))
|
||||
params.Add("details", fmt.Sprintf("CRN: %s\nInstructor: %s\nDays: %s", course.CourseReferenceNumber, meetingTime.Faculty[0].DisplayName, internal.WeekdaysToString(meetingTime.Days())))
|
||||
params.Add("location", meetingTime.PlaceString())
|
||||
params.Add("trp", "true")
|
||||
params.Add("ctz", b.Config.CentralTimeLocation.String())
|
||||
params.Add("recur", "RRULE:"+recurRule)
|
||||
|
||||
calendarURL := "https://calendar.google.com/calendar/render?" + params.Encode()
|
||||
|
||||
err = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
||||
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
||||
Data: &discordgo.InteractionResponseData{
|
||||
Content: fmt.Sprintf("[Add to Google Calendar](<%s>)", calendarURL),
|
||||
AllowedMentions: &discordgo.MessageAllowedMentions{},
|
||||
},
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// IcsCommandHandler handles the /ics command, which allows users to generate an ICS file for a course.
|
||||
func IcsCommandHandler(b *Bot, s *discordgo.Session, i *discordgo.InteractionCreate) error {
|
||||
// Parse all options
|
||||
@@ -507,7 +597,7 @@ DTEND;TZID=America/Chicago:%s
|
||||
SUMMARY:%s
|
||||
DESCRIPTION:%s
|
||||
LOCATION:%s
|
||||
END:VEVENT`, now.Format(ICalTimestampFormatLocal), uid, dtStart.Format(ICalTimestampFormatLocal), rrule.ByDay, rrule.Until, dtEnd.Format(ICalTimestampFormatLocal), summary, strings.Replace(description, "\n", `\n`, -1), location)
|
||||
END:VEVENT`, now.Format(ICalTimestampLayoutLocal), uid, dtStart.Format(ICalTimestampLayoutLocal), rrule.ByDay, rrule.Until, dtEnd.Format(ICalTimestampLayoutLocal), summary, strings.Replace(description, "\n", `\n`, -1), location)
|
||||
|
||||
events = append(events, event)
|
||||
}
|
||||
|
||||
229
tests/config_test.go
Normal file
229
tests/config_test.go
Normal file
@@ -0,0 +1,229 @@
|
||||
package config_test
|
||||
|
||||
import (
|
||||
"banner/internal/config"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestGetCurrentTerm(t *testing.T) {
|
||||
// Initialize location for testing
|
||||
loc, _ := time.LoadLocation("America/Chicago")
|
||||
|
||||
// Use current year to avoid issues with global state
|
||||
currentYear := uint16(time.Now().Year())
|
||||
ranges := config.GetYearDayRange(loc, currentYear)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
date time.Time
|
||||
expectedCurrent *config.Term
|
||||
expectedNext *config.Term
|
||||
}{
|
||||
{
|
||||
name: "Spring term",
|
||||
date: time.Date(int(currentYear), 3, 15, 12, 0, 0, 0, loc),
|
||||
expectedCurrent: &config.Term{Year: currentYear, Season: config.Spring},
|
||||
expectedNext: &config.Term{Year: currentYear, Season: config.Summer},
|
||||
},
|
||||
{
|
||||
name: "Summer term",
|
||||
date: time.Date(int(currentYear), 6, 15, 12, 0, 0, 0, loc),
|
||||
expectedCurrent: &config.Term{Year: currentYear, Season: config.Summer},
|
||||
expectedNext: &config.Term{Year: currentYear, Season: config.Fall},
|
||||
},
|
||||
{
|
||||
name: "Fall term",
|
||||
date: time.Date(int(currentYear), 9, 15, 12, 0, 0, 0, loc),
|
||||
expectedCurrent: &config.Term{Year: currentYear + 1, Season: config.Fall},
|
||||
expectedNext: nil,
|
||||
},
|
||||
{
|
||||
name: "Between Spring and Summer",
|
||||
date: time.Date(int(currentYear), 5, 20, 12, 0, 0, 0, loc),
|
||||
expectedCurrent: nil,
|
||||
expectedNext: &config.Term{Year: currentYear, Season: config.Summer},
|
||||
},
|
||||
{
|
||||
name: "Between Summer and Fall",
|
||||
date: time.Date(int(currentYear), 8, 16, 12, 0, 0, 0, loc),
|
||||
expectedCurrent: nil,
|
||||
expectedNext: &config.Term{Year: currentYear + 1, Season: config.Fall},
|
||||
},
|
||||
{
|
||||
name: "Between Fall and Spring",
|
||||
date: time.Date(int(currentYear), 12, 15, 12, 0, 0, 0, loc),
|
||||
expectedCurrent: nil,
|
||||
expectedNext: &config.Term{Year: currentYear + 1, Season: config.Spring},
|
||||
},
|
||||
{
|
||||
name: "Early January before Spring",
|
||||
date: time.Date(int(currentYear), 1, 10, 12, 0, 0, 0, loc),
|
||||
expectedCurrent: nil,
|
||||
expectedNext: &config.Term{Year: currentYear, Season: config.Spring},
|
||||
},
|
||||
{
|
||||
name: "Spring start date",
|
||||
date: time.Date(int(currentYear), 1, 14, 0, 0, 0, 0, loc),
|
||||
expectedCurrent: &config.Term{Year: currentYear, Season: config.Spring},
|
||||
expectedNext: &config.Term{Year: currentYear, Season: config.Summer},
|
||||
},
|
||||
{
|
||||
name: "Summer start date",
|
||||
date: time.Date(int(currentYear), 5, 25, 0, 0, 0, 0, loc),
|
||||
expectedCurrent: &config.Term{Year: currentYear, Season: config.Summer},
|
||||
expectedNext: &config.Term{Year: currentYear, Season: config.Fall},
|
||||
},
|
||||
{
|
||||
name: "Fall start date",
|
||||
date: time.Date(int(currentYear), 8, 18, 0, 0, 0, 0, loc),
|
||||
expectedCurrent: &config.Term{Year: currentYear + 1, Season: config.Fall},
|
||||
expectedNext: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
current, next := config.GetCurrentTerm(ranges, tt.date)
|
||||
|
||||
if !termsEqual(current, tt.expectedCurrent) {
|
||||
t.Errorf("GetCurrentTerm() current = %v, want %v", current, tt.expectedCurrent)
|
||||
}
|
||||
|
||||
if !termsEqual(next, tt.expectedNext) {
|
||||
t.Errorf("GetCurrentTerm() next = %v, want %v", next, tt.expectedNext)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetYearDayRange(t *testing.T) {
|
||||
loc, _ := time.LoadLocation("America/Chicago")
|
||||
|
||||
ranges := config.GetYearDayRange(loc, 2024)
|
||||
|
||||
// Verify Spring range (Jan 14 to May 1)
|
||||
expectedSpringStart := time.Date(2024, 1, 14, 0, 0, 0, 0, loc).YearDay()
|
||||
expectedSpringEnd := time.Date(2024, 5, 1, 0, 0, 0, 0, loc).YearDay()
|
||||
|
||||
if ranges.Spring.Start != uint16(expectedSpringStart) {
|
||||
t.Errorf("Spring start = %d, want %d", ranges.Spring.Start, expectedSpringStart)
|
||||
}
|
||||
if ranges.Spring.End != uint16(expectedSpringEnd) {
|
||||
t.Errorf("Spring end = %d, want %d", ranges.Spring.End, expectedSpringEnd)
|
||||
}
|
||||
|
||||
// Verify Summer range (May 25 to Aug 15)
|
||||
expectedSummerStart := time.Date(2024, 5, 25, 0, 0, 0, 0, loc).YearDay()
|
||||
expectedSummerEnd := time.Date(2024, 8, 15, 0, 0, 0, 0, loc).YearDay()
|
||||
|
||||
if ranges.Summer.Start != uint16(expectedSummerStart) {
|
||||
t.Errorf("Summer start = %d, want %d", ranges.Summer.Start, expectedSummerStart)
|
||||
}
|
||||
if ranges.Summer.End != uint16(expectedSummerEnd) {
|
||||
t.Errorf("Summer end = %d, want %d", ranges.Summer.End, expectedSummerEnd)
|
||||
}
|
||||
|
||||
// Verify Fall range (Aug 18 to Dec 10)
|
||||
expectedFallStart := time.Date(2024, 8, 18, 0, 0, 0, 0, loc).YearDay()
|
||||
expectedFallEnd := time.Date(2024, 12, 10, 0, 0, 0, 0, loc).YearDay()
|
||||
|
||||
if ranges.Fall.Start != uint16(expectedFallStart) {
|
||||
t.Errorf("Fall start = %d, want %d", ranges.Fall.Start, expectedFallStart)
|
||||
}
|
||||
if ranges.Fall.End != uint16(expectedFallEnd) {
|
||||
t.Errorf("Fall end = %d, want %d", ranges.Fall.End, expectedFallEnd)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseTerm(t *testing.T) {
|
||||
tests := []struct {
|
||||
code string
|
||||
expected config.Term
|
||||
}{
|
||||
{"202410", config.Term{Year: 2024, Season: config.Fall}},
|
||||
{"202420", config.Term{Year: 2024, Season: config.Spring}},
|
||||
{"202430", config.Term{Year: 2024, Season: config.Summer}},
|
||||
{"202510", config.Term{Year: 2025, Season: config.Fall}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.code, func(t *testing.T) {
|
||||
result := config.ParseTerm(tt.code)
|
||||
if result != tt.expected {
|
||||
t.Errorf("ParseTerm(%s) = %v, want %v", tt.code, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTermToString(t *testing.T) {
|
||||
tests := []struct {
|
||||
term config.Term
|
||||
expected string
|
||||
}{
|
||||
{config.Term{Year: 2024, Season: config.Fall}, "202410"},
|
||||
{config.Term{Year: 2024, Season: config.Spring}, "202420"},
|
||||
{config.Term{Year: 2024, Season: config.Summer}, "202430"},
|
||||
{config.Term{Year: 2025, Season: config.Fall}, "202510"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.expected, func(t *testing.T) {
|
||||
result := tt.term.ToString()
|
||||
if result != tt.expected {
|
||||
t.Errorf("Term{Year: %d, Season: %d}.ToString() = %s, want %s",
|
||||
tt.term.Year, tt.term.Season, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultTerm(t *testing.T) {
|
||||
loc, _ := time.LoadLocation("America/Chicago")
|
||||
ranges := config.GetYearDayRange(loc, 2024)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
date time.Time
|
||||
expected config.Term
|
||||
}{
|
||||
{
|
||||
name: "During Spring term",
|
||||
date: time.Date(2024, 3, 15, 12, 0, 0, 0, loc),
|
||||
expected: config.Term{Year: 2024, Season: config.Spring},
|
||||
},
|
||||
{
|
||||
name: "Between terms - returns next term",
|
||||
date: time.Date(2024, 5, 20, 12, 0, 0, 0, loc),
|
||||
expected: config.Term{Year: 2024, Season: config.Summer},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
currentTerm, nextTerm := config.GetCurrentTerm(ranges, tt.date)
|
||||
var result config.Term
|
||||
if currentTerm == nil {
|
||||
result = *nextTerm
|
||||
} else {
|
||||
result = *currentTerm
|
||||
}
|
||||
|
||||
if result != tt.expected {
|
||||
t.Errorf("DefaultTerm() = %v, want %v", result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to compare terms, handling nil cases
|
||||
func termsEqual(a, b *config.Term) bool {
|
||||
if a == nil && b == nil {
|
||||
return true
|
||||
}
|
||||
if a == nil || b == nil {
|
||||
return false
|
||||
}
|
||||
return *a == *b
|
||||
}
|
||||
@@ -1,221 +0,0 @@
|
||||
package utils_test
|
||||
|
||||
import (
|
||||
"banner/internal/config"
|
||||
"banner/internal/utils"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestGetCurrentTerm(t *testing.T) {
|
||||
// Initialize config for testing
|
||||
config.CentralTimeLocation, _ = time.LoadLocation("America/Chicago")
|
||||
|
||||
// Use current year to avoid issues with global state
|
||||
currentYear := uint16(time.Now().Year())
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
date time.Time
|
||||
expectedCurrent *utils.Term
|
||||
expectedNext *utils.Term
|
||||
}{
|
||||
{
|
||||
name: "Spring term",
|
||||
date: time.Date(int(currentYear), 3, 15, 12, 0, 0, 0, config.CentralTimeLocation),
|
||||
expectedCurrent: &utils.Term{Year: currentYear, Season: utils.Spring},
|
||||
expectedNext: &utils.Term{Year: currentYear, Season: utils.Summer},
|
||||
},
|
||||
{
|
||||
name: "Summer term",
|
||||
date: time.Date(int(currentYear), 6, 15, 12, 0, 0, 0, config.CentralTimeLocation),
|
||||
expectedCurrent: &utils.Term{Year: currentYear, Season: utils.Summer},
|
||||
expectedNext: &utils.Term{Year: currentYear, Season: utils.Fall},
|
||||
},
|
||||
{
|
||||
name: "Fall term",
|
||||
date: time.Date(int(currentYear), 9, 15, 12, 0, 0, 0, config.CentralTimeLocation),
|
||||
expectedCurrent: &utils.Term{Year: currentYear + 1, Season: utils.Fall},
|
||||
expectedNext: nil,
|
||||
},
|
||||
{
|
||||
name: "Between Spring and Summer",
|
||||
date: time.Date(int(currentYear), 5, 20, 12, 0, 0, 0, config.CentralTimeLocation),
|
||||
expectedCurrent: nil,
|
||||
expectedNext: &utils.Term{Year: currentYear, Season: utils.Summer},
|
||||
},
|
||||
{
|
||||
name: "Between Summer and Fall",
|
||||
date: time.Date(int(currentYear), 8, 16, 12, 0, 0, 0, config.CentralTimeLocation),
|
||||
expectedCurrent: nil,
|
||||
expectedNext: &utils.Term{Year: currentYear + 1, Season: utils.Fall},
|
||||
},
|
||||
{
|
||||
name: "Between Fall and Spring",
|
||||
date: time.Date(int(currentYear), 12, 15, 12, 0, 0, 0, config.CentralTimeLocation),
|
||||
expectedCurrent: nil,
|
||||
expectedNext: &utils.Term{Year: currentYear + 1, Season: utils.Spring},
|
||||
},
|
||||
{
|
||||
name: "Early January before Spring",
|
||||
date: time.Date(int(currentYear), 1, 10, 12, 0, 0, 0, config.CentralTimeLocation),
|
||||
expectedCurrent: nil,
|
||||
expectedNext: &utils.Term{Year: currentYear, Season: utils.Spring},
|
||||
},
|
||||
{
|
||||
name: "Spring start date",
|
||||
date: time.Date(int(currentYear), 1, 14, 0, 0, 0, 0, config.CentralTimeLocation),
|
||||
expectedCurrent: &utils.Term{Year: currentYear, Season: utils.Spring},
|
||||
expectedNext: &utils.Term{Year: currentYear, Season: utils.Summer},
|
||||
},
|
||||
{
|
||||
name: "Summer start date",
|
||||
date: time.Date(int(currentYear), 5, 25, 0, 0, 0, 0, config.CentralTimeLocation),
|
||||
expectedCurrent: &utils.Term{Year: currentYear, Season: utils.Summer},
|
||||
expectedNext: &utils.Term{Year: currentYear, Season: utils.Fall},
|
||||
},
|
||||
{
|
||||
name: "Fall start date",
|
||||
date: time.Date(int(currentYear), 8, 18, 0, 0, 0, 0, config.CentralTimeLocation),
|
||||
expectedCurrent: &utils.Term{Year: currentYear + 1, Season: utils.Fall},
|
||||
expectedNext: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
current, next := utils.GetCurrentTerm(tt.date)
|
||||
|
||||
if !termsEqual(current, tt.expectedCurrent) {
|
||||
t.Errorf("GetCurrentTerm() current = %v, want %v", current, tt.expectedCurrent)
|
||||
}
|
||||
|
||||
if !termsEqual(next, tt.expectedNext) {
|
||||
t.Errorf("GetCurrentTerm() next = %v, want %v", next, tt.expectedNext)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetYearDayRange(t *testing.T) {
|
||||
config.CentralTimeLocation, _ = time.LoadLocation("America/Chicago")
|
||||
|
||||
spring, summer, fall := utils.GetYearDayRange(2024)
|
||||
|
||||
// Verify Spring range (Jan 14 to May 1)
|
||||
expectedSpringStart := time.Date(2024, 1, 14, 0, 0, 0, 0, config.CentralTimeLocation).YearDay()
|
||||
expectedSpringEnd := time.Date(2024, 5, 1, 0, 0, 0, 0, config.CentralTimeLocation).YearDay()
|
||||
|
||||
if spring.Start != uint16(expectedSpringStart) {
|
||||
t.Errorf("Spring start = %d, want %d", spring.Start, expectedSpringStart)
|
||||
}
|
||||
if spring.End != uint16(expectedSpringEnd) {
|
||||
t.Errorf("Spring end = %d, want %d", spring.End, expectedSpringEnd)
|
||||
}
|
||||
|
||||
// Verify Summer range (May 25 to Aug 15)
|
||||
expectedSummerStart := time.Date(2024, 5, 25, 0, 0, 0, 0, config.CentralTimeLocation).YearDay()
|
||||
expectedSummerEnd := time.Date(2024, 8, 15, 0, 0, 0, 0, config.CentralTimeLocation).YearDay()
|
||||
|
||||
if summer.Start != uint16(expectedSummerStart) {
|
||||
t.Errorf("Summer start = %d, want %d", summer.Start, expectedSummerStart)
|
||||
}
|
||||
if summer.End != uint16(expectedSummerEnd) {
|
||||
t.Errorf("Summer end = %d, want %d", summer.End, expectedSummerEnd)
|
||||
}
|
||||
|
||||
// Verify Fall range (Aug 18 to Dec 10)
|
||||
expectedFallStart := time.Date(2024, 8, 18, 0, 0, 0, 0, config.CentralTimeLocation).YearDay()
|
||||
expectedFallEnd := time.Date(2024, 12, 10, 0, 0, 0, 0, config.CentralTimeLocation).YearDay()
|
||||
|
||||
if fall.Start != uint16(expectedFallStart) {
|
||||
t.Errorf("Fall start = %d, want %d", fall.Start, expectedFallStart)
|
||||
}
|
||||
if fall.End != uint16(expectedFallEnd) {
|
||||
t.Errorf("Fall end = %d, want %d", fall.End, expectedFallEnd)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseTerm(t *testing.T) {
|
||||
tests := []struct {
|
||||
code string
|
||||
expected utils.Term
|
||||
}{
|
||||
{"202410", utils.Term{Year: 2024, Season: utils.Fall}},
|
||||
{"202420", utils.Term{Year: 2024, Season: utils.Spring}},
|
||||
{"202430", utils.Term{Year: 2024, Season: utils.Summer}},
|
||||
{"202510", utils.Term{Year: 2025, Season: utils.Fall}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.code, func(t *testing.T) {
|
||||
result := utils.ParseTerm(tt.code)
|
||||
if result != tt.expected {
|
||||
t.Errorf("ParseTerm(%s) = %v, want %v", tt.code, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTermToString(t *testing.T) {
|
||||
tests := []struct {
|
||||
term utils.Term
|
||||
expected string
|
||||
}{
|
||||
{utils.Term{Year: 2024, Season: utils.Fall}, "202410"},
|
||||
{utils.Term{Year: 2024, Season: utils.Spring}, "202420"},
|
||||
{utils.Term{Year: 2024, Season: utils.Summer}, "202430"},
|
||||
{utils.Term{Year: 2025, Season: utils.Fall}, "202510"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.expected, func(t *testing.T) {
|
||||
result := tt.term.ToString()
|
||||
if result != tt.expected {
|
||||
t.Errorf("Term{Year: %d, Season: %d}.ToString() = %s, want %s",
|
||||
tt.term.Year, tt.term.Season, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefault(t *testing.T) {
|
||||
config.CentralTimeLocation, _ = time.LoadLocation("America/Chicago")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
date time.Time
|
||||
expected utils.Term
|
||||
}{
|
||||
{
|
||||
name: "During Spring term",
|
||||
date: time.Date(2024, 3, 15, 12, 0, 0, 0, config.CentralTimeLocation),
|
||||
expected: utils.Term{Year: 2024, Season: utils.Spring},
|
||||
},
|
||||
{
|
||||
name: "Between terms - returns next term",
|
||||
date: time.Date(2024, 5, 20, 12, 0, 0, 0, config.CentralTimeLocation),
|
||||
expected: utils.Term{Year: 2024, Season: utils.Summer},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := utils.Default(tt.date)
|
||||
if result != tt.expected {
|
||||
t.Errorf("Default() = %v, want %v", result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to compare terms, handling nil cases
|
||||
func termsEqual(a, b *utils.Term) bool {
|
||||
if a == nil && b == nil {
|
||||
return true
|
||||
}
|
||||
if a == nil || b == nil {
|
||||
return false
|
||||
}
|
||||
return *a == *b
|
||||
}
|
||||
Reference in New Issue
Block a user