feat:#49 refactoring and tests

This commit is contained in:
Elmar Kresse
2024-10-19 15:46:49 +02:00
parent d5b2eb4485
commit 20020e1e49
4 changed files with 488 additions and 86 deletions

View File

@ -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 + "<br>")
}
@ -100,41 +91,47 @@ func generateThunderbirdHTMLDescription(event model.Event) string {
htmlDescription.WriteString("Typ: " + event.EventType + event.Compulsory + "<br>")
}
roomList := functions.SeperateRoomString(event.Rooms)
htmlDescription.WriteString("Orte: <br>")
for _, room := range roomList {
mapRoomName := functions.MapRoom(room, true)
_, bufferErr := htmlDescription.WriteString("<a href=\"https://map.htwk-leipzig.de/room/" + mapRoomName + "\">" + room + "</a><br>")
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("<a href=\"https://map.htwk-leipzig.de/room/" + roomLink + "\">" + room + "</a> ")
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("<a href=\"https://map.htwk-leipzig.de/room/" + mapRoomName + "\"> " + mapRoomName + " </a>\n")
roomLink := functions.MapRoom(room, true)
description.WriteString("<a href=\"https://map.htwk-leipzig.de/room/" + roomLink + "\"> " + room + " </a>\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}
}

View File

@ -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<a href=\"https://map.htwk-leipzig.de/room/A123\"> A123 </a>\n<a href=\"https://map.htwk-leipzig.de/room/ZU423\"> ZU423 </a>\n<a href=\"https://map.htwk-leipzig.de/room/C789\"> C789 </a>\n",
},
{
name: "Test 2",
args: args{
rooms: "A123",
},
want: "Orte: \n<a href=\"https://map.htwk-leipzig.de/room/A123\"> A123 </a>\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<br>Gruppen: Math 101<br>Typ: Lecture (mandatory)<br>Orte: <a href=\"https://map.htwk-leipzig.de/room/ZU423\">ZU423</a> <a href=\"https://map.htwk-leipzig.de/room/FE231\">FE231</a> ",
},
{
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<br>Typ: Seminar<br>Orte: <a href=\"https://map.htwk-leipzig.de/room/TR_L106\">TR_L1.06-B</a> ",
},
}
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<a href=\"https://map.htwk-leipzig.de/room/ZU423\"> ZU423 </a>\n<a href=\"https://map.htwk-leipzig.de/room/FE231\"> FE231 </a>\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)
}
})
}
}

View File

@ -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
}

View File

@ -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)
}
})
}
}