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>
440 lines
8.9 KiB
Go
440 lines
8.9 KiB
Go
package services
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/breakpilot/school-service/internal/models"
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// MockPool implements a mock for pgxpool.Pool for testing
|
|
// In production tests, use a real test database or testcontainers
|
|
|
|
func TestClassService_CreateSchoolYear(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
teacherID uuid.UUID
|
|
yearName string
|
|
startDate time.Time
|
|
endDate time.Time
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "valid school year",
|
|
teacherID: uuid.New(),
|
|
yearName: "2024/2025",
|
|
startDate: time.Date(2024, 8, 1, 0, 0, 0, 0, time.UTC),
|
|
endDate: time.Date(2025, 7, 31, 0, 0, 0, 0, time.UTC),
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "empty year name",
|
|
teacherID: uuid.New(),
|
|
yearName: "",
|
|
startDate: time.Date(2024, 8, 1, 0, 0, 0, 0, time.UTC),
|
|
endDate: time.Date(2025, 7, 31, 0, 0, 0, 0, time.UTC),
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "end date before start date",
|
|
teacherID: uuid.New(),
|
|
yearName: "2024/2025",
|
|
startDate: time.Date(2025, 8, 1, 0, 0, 0, 0, time.UTC),
|
|
endDate: time.Date(2024, 7, 31, 0, 0, 0, 0, time.UTC),
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Validate input
|
|
err := validateSchoolYearInput(tt.yearName, tt.startDate, tt.endDate)
|
|
if tt.wantErr {
|
|
assert.Error(t, err)
|
|
} else {
|
|
assert.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestClassService_CreateClass(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
teacherID uuid.UUID
|
|
schoolYearID uuid.UUID
|
|
className string
|
|
gradeLevel int
|
|
schoolType string
|
|
federalState string
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "valid class",
|
|
teacherID: uuid.New(),
|
|
schoolYearID: uuid.New(),
|
|
className: "7a",
|
|
gradeLevel: 7,
|
|
schoolType: "gymnasium",
|
|
federalState: "niedersachsen",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "empty class name",
|
|
teacherID: uuid.New(),
|
|
schoolYearID: uuid.New(),
|
|
className: "",
|
|
gradeLevel: 7,
|
|
schoolType: "gymnasium",
|
|
federalState: "niedersachsen",
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "invalid grade level - too low",
|
|
teacherID: uuid.New(),
|
|
schoolYearID: uuid.New(),
|
|
className: "0a",
|
|
gradeLevel: 0,
|
|
schoolType: "gymnasium",
|
|
federalState: "niedersachsen",
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "invalid grade level - too high",
|
|
teacherID: uuid.New(),
|
|
schoolYearID: uuid.New(),
|
|
className: "14a",
|
|
gradeLevel: 14,
|
|
schoolType: "gymnasium",
|
|
federalState: "niedersachsen",
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "valid grundschule",
|
|
teacherID: uuid.New(),
|
|
schoolYearID: uuid.New(),
|
|
className: "3b",
|
|
gradeLevel: 3,
|
|
schoolType: "grundschule",
|
|
federalState: "bayern",
|
|
wantErr: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := validateClassInput(tt.className, tt.gradeLevel, tt.schoolType)
|
|
if tt.wantErr {
|
|
assert.Error(t, err)
|
|
} else {
|
|
assert.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestClassService_CreateStudent(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
classID uuid.UUID
|
|
firstName string
|
|
lastName string
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "valid student",
|
|
classID: uuid.New(),
|
|
firstName: "Max",
|
|
lastName: "Mustermann",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "empty first name",
|
|
classID: uuid.New(),
|
|
firstName: "",
|
|
lastName: "Mustermann",
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "empty last name",
|
|
classID: uuid.New(),
|
|
firstName: "Max",
|
|
lastName: "",
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := validateStudentInput(tt.firstName, tt.lastName)
|
|
if tt.wantErr {
|
|
assert.Error(t, err)
|
|
} else {
|
|
assert.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestClassService_CreateSubject(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
teacherID uuid.UUID
|
|
subjectName string
|
|
shortName string
|
|
isMainSubject bool
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "valid subject - Mathematik",
|
|
teacherID: uuid.New(),
|
|
subjectName: "Mathematik",
|
|
shortName: "Ma",
|
|
isMainSubject: true,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "valid subject - Sport",
|
|
teacherID: uuid.New(),
|
|
subjectName: "Sport",
|
|
shortName: "Sp",
|
|
isMainSubject: false,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "empty subject name",
|
|
teacherID: uuid.New(),
|
|
subjectName: "",
|
|
shortName: "Ma",
|
|
isMainSubject: true,
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := validateSubjectInput(tt.subjectName)
|
|
if tt.wantErr {
|
|
assert.Error(t, err)
|
|
} else {
|
|
assert.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseCSVStudents(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
csvData string
|
|
expected int
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "valid CSV with header",
|
|
csvData: "Vorname,Nachname\nMax,Mustermann\nAnna,Schmidt",
|
|
expected: 2,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "valid CSV without header",
|
|
csvData: "Max,Mustermann\nAnna,Schmidt",
|
|
expected: 2,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "empty CSV",
|
|
csvData: "",
|
|
expected: 0,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "CSV with only header",
|
|
csvData: "Vorname,Nachname",
|
|
expected: 0,
|
|
wantErr: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
students, err := parseCSVStudents(tt.csvData)
|
|
if tt.wantErr {
|
|
assert.Error(t, err)
|
|
} else {
|
|
assert.NoError(t, err)
|
|
assert.Len(t, students, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Validation helper functions (these should be in the actual service)
|
|
func validateSchoolYearInput(name string, start, end time.Time) error {
|
|
if name == "" {
|
|
return assert.AnError
|
|
}
|
|
if end.Before(start) {
|
|
return assert.AnError
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateClassInput(name string, gradeLevel int, schoolType string) error {
|
|
if name == "" {
|
|
return assert.AnError
|
|
}
|
|
if gradeLevel < 1 || gradeLevel > 13 {
|
|
return assert.AnError
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateStudentInput(firstName, lastName string) error {
|
|
if firstName == "" || lastName == "" {
|
|
return assert.AnError
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateSubjectInput(name string) error {
|
|
if name == "" {
|
|
return assert.AnError
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type csvStudent struct {
|
|
FirstName string
|
|
LastName string
|
|
}
|
|
|
|
func parseCSVStudents(csvData string) ([]csvStudent, error) {
|
|
if csvData == "" {
|
|
return nil, assert.AnError
|
|
}
|
|
|
|
lines := splitLines(csvData)
|
|
if len(lines) == 0 {
|
|
return nil, assert.AnError
|
|
}
|
|
|
|
var students []csvStudent
|
|
startIdx := 0
|
|
|
|
// Check if first line is header
|
|
firstLine := lines[0]
|
|
if isHeader(firstLine) {
|
|
startIdx = 1
|
|
}
|
|
|
|
for i := startIdx; i < len(lines); i++ {
|
|
line := lines[i]
|
|
if line == "" {
|
|
continue
|
|
}
|
|
parts := splitCSV(line)
|
|
if len(parts) >= 2 {
|
|
students = append(students, csvStudent{
|
|
FirstName: parts[0],
|
|
LastName: parts[1],
|
|
})
|
|
}
|
|
}
|
|
|
|
return students, nil
|
|
}
|
|
|
|
func splitLines(s string) []string {
|
|
var lines []string
|
|
current := ""
|
|
for _, c := range s {
|
|
if c == '\n' {
|
|
lines = append(lines, current)
|
|
current = ""
|
|
} else {
|
|
current += string(c)
|
|
}
|
|
}
|
|
if current != "" {
|
|
lines = append(lines, current)
|
|
}
|
|
return lines
|
|
}
|
|
|
|
func splitCSV(s string) []string {
|
|
var parts []string
|
|
current := ""
|
|
for _, c := range s {
|
|
if c == ',' {
|
|
parts = append(parts, current)
|
|
current = ""
|
|
} else {
|
|
current += string(c)
|
|
}
|
|
}
|
|
if current != "" {
|
|
parts = append(parts, current)
|
|
}
|
|
return parts
|
|
}
|
|
|
|
func isHeader(line string) bool {
|
|
lower := ""
|
|
for _, c := range line {
|
|
if c >= 'A' && c <= 'Z' {
|
|
lower += string(c + 32)
|
|
} else {
|
|
lower += string(c)
|
|
}
|
|
}
|
|
return contains(lower, "vorname") || contains(lower, "nachname") || contains(lower, "first") || contains(lower, "last")
|
|
}
|
|
|
|
func contains(s, substr string) bool {
|
|
for i := 0; i <= len(s)-len(substr); i++ {
|
|
if s[i:i+len(substr)] == substr {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Integration test helper - requires real database
|
|
func setupTestDB(t *testing.T) *pgxpool.Pool {
|
|
t.Helper()
|
|
// Skip if no test database available
|
|
t.Skip("Integration test requires database connection")
|
|
return nil
|
|
}
|
|
|
|
func TestClassService_Integration(t *testing.T) {
|
|
pool := setupTestDB(t)
|
|
if pool == nil {
|
|
return
|
|
}
|
|
defer pool.Close()
|
|
|
|
service := NewClassService(pool)
|
|
ctx := context.Background()
|
|
teacherID := uuid.New()
|
|
|
|
// Test CreateSchoolYear
|
|
t.Run("CreateSchoolYear_Integration", func(t *testing.T) {
|
|
req := &models.CreateSchoolYearRequest{
|
|
Name: "2024/2025",
|
|
StartDate: "2024-08-01",
|
|
EndDate: "2025-07-31",
|
|
}
|
|
year, err := service.CreateSchoolYear(ctx, teacherID.String(), req)
|
|
require.NoError(t, err)
|
|
assert.NotEqual(t, uuid.Nil, year.ID)
|
|
assert.Equal(t, "2024/2025", year.Name)
|
|
})
|
|
}
|