diff --git a/services/ical/service/ical/icalDescriptionGenerator.go b/services/ical/service/ical/icalDescriptionGenerator.go
index c967a19..4c2fc35 100644
--- a/services/ical/service/ical/icalDescriptionGenerator.go
+++ b/services/ical/service/ical/icalDescriptionGenerator.go
@@ -26,70 +26,61 @@ import (
_ "time/tzdata"
)
-// Generates user-agent specific description and adds it to the calendar event.
-func generateUserAgentSpecificDescription(vEvent *ics.VEvent, event model.Event, userAgent string) {
- // Generate description and ALTREP (alternative representation) based on user agent.
+const (
+ Thunderbird = "Thunderbird"
+ GoogleCalendar = "Google-Calendar-Importer"
+)
+
+// Adds a user-agent specific description and ALTREP to the calendar event.
+func addUserAgentSpecificDescription(vEvent *ics.VEvent, event model.Event, userAgent string) {
description, altrep := generateDescription(event, userAgent)
- if isThunderbird(userAgent) && altrep != "" {
- uri := &url.URL{
- Scheme: "data",
- Opaque: altrep,
- }
-
- vEvent.AddProperty(ics.ComponentPropertyDescription, description, ics.WithAlternativeRepresentation(uri))
+ if userAgentType := identifyUserAgent(userAgent); userAgentType == Thunderbird && altrep != "" {
+ vEvent.AddProperty(ics.ComponentPropertyDescription, description, ics.WithAlternativeRepresentation(buildDataURL(altrep)))
} else {
- // Default handling: Add plain DESCRIPTION property for other user agents.
- vEvent.AddProperty("DESCRIPTION", description)
+ vEvent.AddProperty(ics.ComponentPropertyDescription, description)
}
-
}
-// Generates description based on the event details and user agent.
+// Generates a description and ALTREP (alternative representation) based on the user agent.
func generateDescription(event model.Event, userAgent string) (string, string) {
plainDescription := buildPlainTextDescription(event)
- if isThunderbird(userAgent) {
- // Thunderbird-specific: Generate HTML and ALTREP format for Thunderbird users.
- htmlDescription := generateThunderbirdHTMLDescription(event)
- altrep := `text/html,` + url.PathEscape(htmlDescription)
+ switch identifyUserAgent(userAgent) {
+ case Thunderbird:
+ htmlDescription := generateHTMLDescriptionForThunderbird(event)
+ altrep := "text/html," + url.PathEscape(htmlDescription)
return plainDescription, altrep
+ case GoogleCalendar:
+ plainDescription += generateRoomLinksForGoogleCalendar(event.Rooms)
}
-
- // Google Calendar-specific handling.
- if isGoogleCalendar(userAgent) {
- plainDescription += generateGoogleCalendarDescription(event.Rooms)
- }
-
- // Default: Return plain text description without ALTREP for non-Thunderbird cases.
return plainDescription, ""
}
-// Builds the plain text description from the event details.
+// Builds a plain text description of the event details.
func buildPlainTextDescription(event model.Event) string {
- var description string
+ var description strings.Builder
if !functions.OnlyWhitespace(event.Prof) {
- description += "Profs: " + event.Prof + "\n"
+ description.WriteString("Profs: " + event.Prof + "\n")
}
if !functions.OnlyWhitespace(event.Course) {
- description += "Gruppen: " + event.Course + "\n"
+ description.WriteString("Gruppen: " + event.Course + "\n")
}
if !functions.OnlyWhitespace(event.EventType) {
- description += "Typ: " + event.EventType + event.Compulsory + "\n"
+ description.WriteString("Typ: " + event.EventType + event.Compulsory + "\n")
}
if !functions.OnlyWhitespace(event.Notes) {
- description += "Notizen: " + event.Notes + "\n"
+ description.WriteString("Notizen: " + event.Notes + "\n")
}
- return description
+ return description.String()
}
-// Helper function to generate HTML description for Thunderbird's ALTREP.
-func generateThunderbirdHTMLDescription(event model.Event) string {
+// Generates an HTML description for Thunderbird users.
+func generateHTMLDescriptionForThunderbird(event model.Event) string {
var htmlDescription strings.Builder
- // HTML-encoded description similar to plain text.
if !functions.OnlyWhitespace(event.Prof) {
htmlDescription.WriteString("Profs: " + event.Prof + "
")
}
@@ -100,41 +91,47 @@ func generateThunderbirdHTMLDescription(event model.Event) string {
htmlDescription.WriteString("Typ: " + event.EventType + event.Compulsory + "
")
}
- roomList := functions.SeperateRoomString(event.Rooms)
- htmlDescription.WriteString("Orte:
")
-
- for _, room := range roomList {
- mapRoomName := functions.MapRoom(room, true)
- _, bufferErr := htmlDescription.WriteString("" + room + "
")
-
- if bufferErr != nil {
- slog.Error("Error while writing to buffer", "error", bufferErr)
- return ""
+ if !functions.OnlyWhitespace(event.Rooms) {
+ htmlDescription.WriteString("Orte: ")
+ for _, room := range functions.SeperateRoomString(event.Rooms) {
+ roomLink := functions.MapRoom(room, true)
+ _, err := htmlDescription.WriteString("" + room + " ")
+ if err != nil {
+ slog.Error("Error while writing to HTML description", "error", err)
+ return ""
+ }
}
}
+
return htmlDescription.String()
}
-// Generates a room description with links for Google Calendar.
-func generateGoogleCalendarDescription(rooms string) string {
+// Generates room links formatted for Google Calendar.
+func generateRoomLinksForGoogleCalendar(rooms string) string {
var description strings.Builder
roomList := functions.SeperateRoomString(rooms)
- description.WriteString("Orte: \n ")
+ description.WriteString("Orte: \n")
for _, room := range roomList {
- mapRoomName := functions.MapRoom(room, true)
- description.WriteString(" " + mapRoomName + " \n")
+ roomLink := functions.MapRoom(room, true)
+ description.WriteString(" " + room + " \n")
}
return description.String()
}
-// Checks if the user agent is Thunderbird.
-func isThunderbird(userAgent string) bool {
- return strings.Contains(userAgent, "Thunderbird")
+// Identifies the user agent type (e.g., Thunderbird or Google Calendar).
+func identifyUserAgent(userAgent string) string {
+ if strings.Contains(userAgent, Thunderbird) {
+ return Thunderbird
+ }
+ if strings.Contains(userAgent, GoogleCalendar) {
+ return GoogleCalendar
+ }
+ return ""
}
-// Checks if the user agent is Google Calendar.
-func isGoogleCalendar(userAgent string) bool {
- return strings.Contains(userAgent, "Google-Calendar-Importer")
+// Constructs a data URL for the ALTREP representation.
+func buildDataURL(data string) *url.URL {
+ return &url.URL{Scheme: "data", Opaque: data}
}
diff --git a/services/ical/service/ical/icalDescriptionGenerator_test.go b/services/ical/service/ical/icalDescriptionGenerator_test.go
new file mode 100644
index 0000000..d5d14b9
--- /dev/null
+++ b/services/ical/service/ical/icalDescriptionGenerator_test.go
@@ -0,0 +1,324 @@
+package ical
+
+import (
+ "htwkalender/ical/model"
+ "net/url"
+ "reflect"
+ "testing"
+)
+
+func Test_buildDataURL(t *testing.T) {
+ type args struct {
+ data string
+ }
+ tests := []struct {
+ name string
+ args args
+ want *url.URL
+ }{
+ {
+ name: "Test 1",
+ args: args{
+ data: "text/calendar;charset=utf-8;base64,ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg",
+ },
+ want: &url.URL{
+ Scheme: "data",
+ Opaque: "text/calendar;charset=utf-8;base64,ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg",
+ },
+ },
+ {
+ name: "Test 2",
+ args: args{
+ data: "text/calendar;charset=utf-8;base64,ÄÜ'*A`+#Add\"$%&/()=?",
+ },
+ want: &url.URL{
+ Scheme: "data",
+ Opaque: "text/calendar;charset=utf-8;base64,ÄÜ'*A`+#Add\"$%&/()=?",
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := buildDataURL(tt.args.data); !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("buildDataURL() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func Test_identifyUserAgent(t *testing.T) {
+ type args struct {
+ userAgent string
+ }
+ tests := []struct {
+ name string
+ args args
+ want string
+ }{
+ {
+ name: "Test 1",
+ args: args{
+ userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:78.0) Gecko/20100101 Thunderbird/78.10.0",
+ },
+ want: "Thunderbird",
+ },
+ {
+ name: "Test 2",
+ args: args{
+ userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:78.0) Gecko/20100101 GoogleCalendar/78.10.0",
+ },
+ want: "",
+ },
+ {
+ name: "Test 3",
+ args: args{
+ userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:78.0) Gecko/20100101",
+ },
+ want: "",
+ },
+ {
+ name: "Test 4",
+ args: args{
+ userAgent: "Google-Calendar-Importer 1.0",
+ },
+ want: "Google-Calendar-Importer",
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := identifyUserAgent(tt.args.userAgent); got != tt.want {
+ t.Errorf("identifyUserAgent() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func Test_buildPlainTextDescription(t *testing.T) {
+ type args struct {
+ event model.Event
+ }
+ tests := []struct {
+ name string
+ args args
+ want string
+ }{
+ {
+ name: "All fields have valid values",
+ args: args{
+ event: model.Event{
+ Prof: "Dr. Smith",
+ Course: "Math 101",
+ EventType: "S",
+ Compulsory: "w",
+ Notes: "Bring textbook",
+ },
+ },
+ want: "Profs: Dr. Smith\nGruppen: Math 101\nTyp: Sw\nNotizen: Bring textbook\n",
+ },
+ {
+ name: "All fields are empty",
+ args: args{
+ event: model.Event{
+ Prof: "",
+ Course: "",
+ EventType: "",
+ Compulsory: "",
+ Notes: "",
+ },
+ },
+ want: "",
+ },
+ {
+ name: "Some fields are empty",
+ args: args{
+ event: model.Event{
+ Prof: "Dr. Smith",
+ Course: "",
+ EventType: "Seminar",
+ Compulsory: "",
+ Notes: "",
+ },
+ },
+ want: "Profs: Dr. Smith\nTyp: Seminar\n",
+ },
+ {
+ name: "Fields contain only whitespace",
+ args: args{
+ event: model.Event{
+ Prof: " ",
+ Course: " ",
+ EventType: " ",
+ Compulsory: "",
+ Notes: " ",
+ },
+ },
+ want: "",
+ },
+ {
+ name: "Mix of valid values, empty values, and whitespace",
+ args: args{
+ event: model.Event{
+ Prof: "Dr. Brown",
+ Course: " ",
+ EventType: "V",
+ Compulsory: "p",
+ Notes: "",
+ },
+ },
+ want: "Profs: Dr. Brown\nTyp: Vp\n",
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := buildPlainTextDescription(tt.args.event); got != tt.want {
+ t.Errorf("buildPlainTextDescription() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func Test_generateRoomLinksForGoogleCalendar(t *testing.T) {
+ type args struct {
+ rooms string
+ }
+ tests := []struct {
+ name string
+ args args
+ want string
+ }{
+ {
+ name: "Test 1",
+ args: args{
+ rooms: "A123, ZU423, C789",
+ },
+ want: "Orte: \n A123 \n ZU423 \n C789 \n",
+ },
+ {
+ name: "Test 2",
+ args: args{
+ rooms: "A123",
+ },
+ want: "Orte: \n A123 \n",
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := generateRoomLinksForGoogleCalendar(tt.args.rooms); got != tt.want {
+ t.Errorf("generateRoomLinksForGoogleCalendar() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func Test_generateHTMLDescriptionForThunderbird(t *testing.T) {
+ type args struct {
+ event model.Event
+ }
+ tests := []struct {
+ name string
+ args args
+ want string
+ }{
+ {
+ name: "All fields have valid values including rooms",
+ args: args{
+ event: model.Event{
+ Prof: "Dr. Smith",
+ Course: "Math 101",
+ EventType: "Lecture",
+ Compulsory: " (mandatory)",
+ Rooms: "ZU423, FE231",
+ },
+ },
+ want: "Profs: Dr. Smith
Gruppen: Math 101
Typ: Lecture (mandatory)
Orte: ZU423 FE231 ",
+ },
+ {
+ name: "All fields are empty",
+ args: args{
+ event: model.Event{
+ Prof: "",
+ Course: "",
+ EventType: "",
+ Compulsory: "",
+ Rooms: "",
+ },
+ },
+ want: "",
+ },
+ {
+ name: "Some fields are empty, rooms have valid values",
+ args: args{
+ event: model.Event{
+ Prof: "Dr. Smith",
+ Course: "",
+ EventType: "Seminar",
+ Compulsory: "",
+ Rooms: "TR_L1.06-B",
+ },
+ },
+ want: "Profs: Dr. Smith
Typ: Seminar
Orte: TR_L1.06-B ",
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := generateHTMLDescriptionForThunderbird(tt.args.event); got != tt.want {
+ t.Errorf("generateHTMLDescriptionForThunderbird() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func Test_generateDescription(t *testing.T) {
+ type args struct {
+ event model.Event
+ userAgent string
+ }
+ tests := []struct {
+ name string
+ args args
+ description string
+ altrep string
+ }{
+ {
+ name: "Test 1",
+ args: args{
+ event: model.Event{
+ Prof: "Dr. Smith",
+ Course: "Math 101",
+ EventType: "Lecture",
+ Compulsory: " (mandatory)",
+ Rooms: "ZU423, FE231",
+ },
+ userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:78.0) Gecko/20100101 Thunderbird/78.10.0",
+ },
+ description: "Profs: Dr. Smith\nGruppen: Math 101\nTyp: Lecture (mandatory)\n",
+ altrep: "text/html,Profs:%20Dr.%20Smith%3Cbr%3EGruppen:%20Math%20101%3Cbr%3ETyp:%20Lecture%20%28mandatory%29%3Cbr%3EOrte:%20%3Ca%20href=%22https:%2F%2Fmap.htwk-leipzig.de%2Froom%2FZU423%22%3EZU423%3C%2Fa%3E%20%3Ca%20href=%22https:%2F%2Fmap.htwk-leipzig.de%2Froom%2FFE231%22%3EFE231%3C%2Fa%3E%20",
+ },
+ {
+ name: "Test 2",
+ args: args{
+ event: model.Event{
+ Prof: "Dr. Smith",
+ Course: "Math 101",
+ EventType: "Lecture",
+ Compulsory: " (mandatory)",
+ Rooms: "ZU423, FE231",
+ },
+ userAgent: "Google-Calendar-Importer",
+ },
+ description: "Profs: Dr. Smith\nGruppen: Math 101\nTyp: Lecture (mandatory)\nOrte: \n ZU423 \n FE231 \n",
+ altrep: "",
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, got1 := generateDescription(tt.args.event, tt.args.userAgent)
+ if got != tt.description {
+ t.Errorf("generateDescription() got = %v, want %v", got, tt.description)
+ }
+ if got1 != tt.altrep {
+ t.Errorf("generateDescription() got1 = %v, want %v", got1, tt.altrep)
+ }
+ })
+ }
+}
diff --git a/services/ical/service/ical/icalFileGeneration.go b/services/ical/service/ical/icalFileGeneration.go
index fed15df..9674c06 100644
--- a/services/ical/service/ical/icalFileGeneration.go
+++ b/services/ical/service/ical/icalFileGeneration.go
@@ -30,6 +30,39 @@ import (
func GenerateIcalFeed(events model.Events, mapping map[string]model.FeedCollection, userAgent string) *ics.Calendar {
cal := ics.NewCalendarFor("HTWK Kalender")
+ setDefaultIcalParams(cal)
+
+ europeTime, _ := time.LoadLocation("Europe/Berlin")
+ internalClock := clock.RealClock{}
+
+ for _, event := range events {
+ mapEntry, mappingFound := mapping[event.UUID]
+
+ var eventHash = functions.HashString(time.Time(event.Start).String() + time.Time(event.End).String() + event.Course + event.Name + event.Rooms)
+
+ icalEvent := ics.NewEvent(eventHash + "@htwkalender.de")
+
+ icalEvent.SetDtStampTime(internalClock.Now().Local().In(europeTime))
+ icalEvent.SetStartAt(time.Time(event.Start).Local().In(europeTime))
+ icalEvent.SetEndAt(time.Time(event.End).Local().In(europeTime))
+
+ if mappingFound {
+ addPropertyIfNotEmpty(icalEvent, ics.ComponentPropertySummary, replaceNameIfUserDefined(&event, mapEntry))
+ addAlarmIfSpecified(icalEvent, event, mapEntry, internalClock)
+ } else {
+ addPropertyIfNotEmpty(icalEvent, ics.ComponentPropertySummary, event.Name)
+ }
+
+ addUserAgentSpecificDescription(icalEvent, event, userAgent)
+ addPropertyIfNotEmpty(icalEvent, ics.ComponentPropertyLocation, event.Rooms)
+
+ cal.AddVEvent(icalEvent)
+
+ }
+ return cal
+}
+
+func setDefaultIcalParams(cal *ics.Calendar) {
cal.SetMethod(ics.MethodPublish)
cal.SetProductId("-//HTWK Kalender//htwkalender.de//DE")
cal.SetTzid("Europe/Berlin")
@@ -116,35 +149,6 @@ func GenerateIcalFeed(events model.Events, mapping map[string]model.FeedCollecti
}
cal.AddVTimezone(vTimeZone)
-
- europeTime, _ := time.LoadLocation("Europe/Berlin")
- internalClock := clock.RealClock{}
-
- for _, event := range events {
- mapEntry, mappingFound := mapping[event.UUID]
-
- var eventHash = functions.HashString(time.Time(event.Start).String() + time.Time(event.End).String() + event.Course + event.Name + event.Rooms)
-
- icalEvent := ics.NewEvent(eventHash + "@htwkalender.de")
-
- icalEvent.SetDtStampTime(internalClock.Now().Local().In(europeTime))
- icalEvent.SetStartAt(time.Time(event.Start).Local().In(europeTime))
- icalEvent.SetEndAt(time.Time(event.End).Local().In(europeTime))
-
- if mappingFound {
- addPropertyIfNotEmpty(icalEvent, ics.ComponentPropertySummary, replaceNameIfUserDefined(&event, mapEntry))
- addAlarmIfSpecified(icalEvent, event, mapEntry, internalClock)
- } else {
- addPropertyIfNotEmpty(icalEvent, ics.ComponentPropertySummary, event.Name)
- }
-
- generateUserAgentSpecificDescription(icalEvent, event, userAgent)
- addPropertyIfNotEmpty(icalEvent, ics.ComponentPropertyLocation, event.Rooms)
-
- cal.AddVEvent(icalEvent)
-
- }
- return cal
}
// AddPropertyIfNotEmpty adds a property to the component if the value is not empty
@@ -173,6 +177,5 @@ func replaceNameIfUserDefined(event *model.Event, mapping model.FeedCollection)
if !functions.OnlyWhitespace(mapping.UserDefinedName) {
return names.ReplaceTemplateSubStrings(mapping.UserDefinedName, *event)
}
-
return event.Name
}
diff --git a/services/ical/service/ical/icalFileGeneration_test.go b/services/ical/service/ical/icalFileGeneration_test.go
new file mode 100644
index 0000000..71dfe96
--- /dev/null
+++ b/services/ical/service/ical/icalFileGeneration_test.go
@@ -0,0 +1,78 @@
+package ical
+
+import (
+ "htwkalender/ical/model"
+ "testing"
+)
+
+func Test_replaceNameIfUserDefined(t *testing.T) {
+ type args struct {
+ event *model.Event
+ mapping model.FeedCollection
+ }
+ tests := []struct {
+ name string
+ args args
+ want string
+ }{
+ {
+ name: "Custom Name Test",
+ args: args{
+ event: &model.Event{
+ Name: "Test",
+ },
+ mapping: model.FeedCollection{
+ Name: "Test",
+ UserDefinedName: "CustomTest",
+ },
+ },
+ want: "CustomTest",
+ },
+ {
+ name: "Empty Custom Name Test",
+ args: args{
+ event: &model.Event{
+ Name: "Test",
+ },
+ mapping: model.FeedCollection{
+ Name: "Test",
+ UserDefinedName: "",
+ },
+ },
+ want: "Test",
+ },
+ {
+ name: "Empty Name Test",
+ args: args{
+ event: &model.Event{
+ Name: "",
+ },
+ mapping: model.FeedCollection{
+ Name: "",
+ UserDefinedName: "CustomTest",
+ },
+ },
+ want: "CustomTest",
+ },
+ {
+ name: "Names dont match but check is done before",
+ args: args{
+ event: &model.Event{
+ Name: "Test",
+ },
+ mapping: model.FeedCollection{
+ Name: "Test2",
+ UserDefinedName: "CustomTest",
+ },
+ },
+ want: "CustomTest",
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := replaceNameIfUserDefined(tt.args.event, tt.args.mapping); got != tt.want {
+ t.Errorf("replaceNameIfUserDefined() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}