Services: Admin-Lehrer, Backend-Lehrer, Studio v2, Website, Klausur-Service, School-Service, Voice-Service, Geo-Service, BreakPilot Drive, Agent-Core Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
466 lines
9.3 KiB
Go
466 lines
9.3 KiB
Go
package services
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
func TestGradebookService_ValidateAttendanceStatus(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
status string
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "valid - present",
|
|
status: "present",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "valid - absent_excused",
|
|
status: "absent_excused",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "valid - absent_unexcused",
|
|
status: "absent_unexcused",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "valid - late",
|
|
status: "late",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "invalid status",
|
|
status: "invalid",
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "empty status",
|
|
status: "",
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := validateAttendanceStatus(tt.status)
|
|
if tt.wantErr {
|
|
assert.Error(t, err)
|
|
} else {
|
|
assert.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGradebookService_ValidateEntryType(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
entryType string
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "valid - note",
|
|
entryType: "note",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "valid - warning",
|
|
entryType: "warning",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "valid - praise",
|
|
entryType: "praise",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "valid - incident",
|
|
entryType: "incident",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "valid - homework",
|
|
entryType: "homework",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "invalid entry type",
|
|
entryType: "invalid",
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "empty entry type",
|
|
entryType: "",
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := validateEntryType(tt.entryType)
|
|
if tt.wantErr {
|
|
assert.Error(t, err)
|
|
} else {
|
|
assert.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGradebookService_ValidateAttendanceInput(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
studentID uuid.UUID
|
|
date time.Time
|
|
status string
|
|
periods int
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "valid attendance",
|
|
studentID: uuid.New(),
|
|
date: time.Now(),
|
|
status: "absent_excused",
|
|
periods: 2,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "zero periods",
|
|
studentID: uuid.New(),
|
|
date: time.Now(),
|
|
status: "absent_excused",
|
|
periods: 0,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "negative periods",
|
|
studentID: uuid.New(),
|
|
date: time.Now(),
|
|
status: "absent_excused",
|
|
periods: -1,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "too many periods",
|
|
studentID: uuid.New(),
|
|
date: time.Now(),
|
|
status: "absent_excused",
|
|
periods: 15,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "nil student ID",
|
|
studentID: uuid.Nil,
|
|
date: time.Now(),
|
|
status: "absent_excused",
|
|
periods: 2,
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := validateAttendanceInput(tt.studentID, tt.date, tt.status, tt.periods)
|
|
if tt.wantErr {
|
|
assert.Error(t, err)
|
|
} else {
|
|
assert.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGradebookService_ValidateGradebookEntry(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
classID uuid.UUID
|
|
entryType string
|
|
content string
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "valid entry",
|
|
classID: uuid.New(),
|
|
entryType: "note",
|
|
content: "Today we discussed the French Revolution",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "empty content",
|
|
classID: uuid.New(),
|
|
entryType: "note",
|
|
content: "",
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "invalid entry type",
|
|
classID: uuid.New(),
|
|
entryType: "invalid",
|
|
content: "Some content",
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "nil class ID",
|
|
classID: uuid.Nil,
|
|
entryType: "note",
|
|
content: "Some content",
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := validateGradebookEntry(tt.classID, tt.entryType, tt.content)
|
|
if tt.wantErr {
|
|
assert.Error(t, err)
|
|
} else {
|
|
assert.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGradebookService_CalculateAbsenceDays(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
attendances []attendanceRecord
|
|
expectedTotal int
|
|
expectedExcused int
|
|
}{
|
|
{
|
|
name: "no absences",
|
|
attendances: []attendanceRecord{
|
|
{Status: "present", Periods: 6},
|
|
{Status: "present", Periods: 6},
|
|
},
|
|
expectedTotal: 0,
|
|
expectedExcused: 0,
|
|
},
|
|
{
|
|
name: "excused absences only",
|
|
attendances: []attendanceRecord{
|
|
{Status: "absent_excused", Periods: 6},
|
|
{Status: "absent_excused", Periods: 6},
|
|
},
|
|
expectedTotal: 2,
|
|
expectedExcused: 2,
|
|
},
|
|
{
|
|
name: "unexcused absences only",
|
|
attendances: []attendanceRecord{
|
|
{Status: "absent_unexcused", Periods: 6},
|
|
},
|
|
expectedTotal: 1,
|
|
expectedExcused: 0,
|
|
},
|
|
{
|
|
name: "mixed absences",
|
|
attendances: []attendanceRecord{
|
|
{Status: "absent_excused", Periods: 6},
|
|
{Status: "absent_unexcused", Periods: 6},
|
|
{Status: "present", Periods: 6},
|
|
},
|
|
expectedTotal: 2,
|
|
expectedExcused: 1,
|
|
},
|
|
{
|
|
name: "late arrivals not counted",
|
|
attendances: []attendanceRecord{
|
|
{Status: "late", Periods: 1},
|
|
{Status: "late", Periods: 1},
|
|
},
|
|
expectedTotal: 0,
|
|
expectedExcused: 0,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
total, excused := calculateAbsenceDays(tt.attendances)
|
|
assert.Equal(t, tt.expectedTotal, total)
|
|
assert.Equal(t, tt.expectedExcused, excused)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGradebookService_DateRangeValidation(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
startDate time.Time
|
|
endDate time.Time
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "valid range - same day",
|
|
startDate: time.Date(2024, 1, 15, 0, 0, 0, 0, time.UTC),
|
|
endDate: time.Date(2024, 1, 15, 0, 0, 0, 0, time.UTC),
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "valid range - week",
|
|
startDate: time.Date(2024, 1, 15, 0, 0, 0, 0, time.UTC),
|
|
endDate: time.Date(2024, 1, 22, 0, 0, 0, 0, time.UTC),
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "invalid range - end before start",
|
|
startDate: time.Date(2024, 1, 22, 0, 0, 0, 0, time.UTC),
|
|
endDate: time.Date(2024, 1, 15, 0, 0, 0, 0, time.UTC),
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := validateDateRange(tt.startDate, tt.endDate)
|
|
if tt.wantErr {
|
|
assert.Error(t, err)
|
|
} else {
|
|
assert.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Helper types and functions
|
|
type attendanceRecord struct {
|
|
Status string
|
|
Periods int
|
|
}
|
|
|
|
func validateAttendanceStatus(status string) error {
|
|
validStatuses := map[string]bool{
|
|
"present": true,
|
|
"absent_excused": true,
|
|
"absent_unexcused": true,
|
|
"late": true,
|
|
}
|
|
if !validStatuses[status] {
|
|
return assert.AnError
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateEntryType(entryType string) error {
|
|
validTypes := map[string]bool{
|
|
"note": true,
|
|
"warning": true,
|
|
"praise": true,
|
|
"incident": true,
|
|
"homework": true,
|
|
}
|
|
if !validTypes[entryType] {
|
|
return assert.AnError
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateAttendanceInput(studentID uuid.UUID, date time.Time, status string, periods int) error {
|
|
if studentID == uuid.Nil {
|
|
return assert.AnError
|
|
}
|
|
if periods <= 0 || periods > 12 {
|
|
return assert.AnError
|
|
}
|
|
return validateAttendanceStatus(status)
|
|
}
|
|
|
|
func validateGradebookEntry(classID uuid.UUID, entryType, content string) error {
|
|
if classID == uuid.Nil {
|
|
return assert.AnError
|
|
}
|
|
if content == "" {
|
|
return assert.AnError
|
|
}
|
|
return validateEntryType(entryType)
|
|
}
|
|
|
|
func calculateAbsenceDays(attendances []attendanceRecord) (total, excused int) {
|
|
for _, a := range attendances {
|
|
if a.Status == "absent_excused" {
|
|
total++
|
|
excused++
|
|
} else if a.Status == "absent_unexcused" {
|
|
total++
|
|
}
|
|
}
|
|
return total, excused
|
|
}
|
|
|
|
func validateDateRange(start, end time.Time) error {
|
|
if end.Before(start) {
|
|
return assert.AnError
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func TestGradebookService_BulkAttendanceValidation(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
studentIDs []uuid.UUID
|
|
date time.Time
|
|
status string
|
|
wantErr bool
|
|
errCount int
|
|
}{
|
|
{
|
|
name: "all valid",
|
|
studentIDs: []uuid.UUID{uuid.New(), uuid.New(), uuid.New()},
|
|
date: time.Now(),
|
|
status: "present",
|
|
wantErr: false,
|
|
errCount: 0,
|
|
},
|
|
{
|
|
name: "empty list",
|
|
studentIDs: []uuid.UUID{},
|
|
date: time.Now(),
|
|
status: "present",
|
|
wantErr: true,
|
|
errCount: 1,
|
|
},
|
|
{
|
|
name: "contains nil UUID",
|
|
studentIDs: []uuid.UUID{uuid.New(), uuid.Nil, uuid.New()},
|
|
date: time.Now(),
|
|
status: "present",
|
|
wantErr: true,
|
|
errCount: 1,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
errs := validateBulkAttendance(tt.studentIDs, tt.date, tt.status)
|
|
if tt.wantErr {
|
|
assert.Len(t, errs, tt.errCount)
|
|
} else {
|
|
assert.Empty(t, errs)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func validateBulkAttendance(studentIDs []uuid.UUID, date time.Time, status string) []error {
|
|
var errs []error
|
|
if len(studentIDs) == 0 {
|
|
errs = append(errs, assert.AnError)
|
|
return errs
|
|
}
|
|
for _, id := range studentIDs {
|
|
if id == uuid.Nil {
|
|
errs = append(errs, assert.AnError)
|
|
}
|
|
}
|
|
if err := validateAttendanceStatus(status); err != nil {
|
|
errs = append(errs, err)
|
|
}
|
|
return errs
|
|
}
|