From 20020e1e491b6dbaa8c4e3f6011849b6d70c40fa Mon Sep 17 00:00:00 2001 From: Elmar Kresse Date: Sat, 19 Oct 2024 15:46:49 +0200 Subject: [PATCH] feat:#49 refactoring and tests --- .../service/ical/icalDescriptionGenerator.go | 109 +++--- .../ical/icalDescriptionGenerator_test.go | 324 ++++++++++++++++++ .../ical/service/ical/icalFileGeneration.go | 63 ++-- .../service/ical/icalFileGeneration_test.go | 78 +++++ 4 files changed, 488 insertions(+), 86 deletions(-) create mode 100644 services/ical/service/ical/icalDescriptionGenerator_test.go create mode 100644 services/ical/service/ical/icalFileGeneration_test.go 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) + } + }) + } +}