package classes_test import ( "strings" "testing" "time" "unicode/utf8" "git.golem15.com/jakub/sm-bm-plugin/classes" "git.golem15.com/jakub/sm-bm-plugin/models" ) func TestEscapeText(t *testing.T) { cases := map[string]string{ "zwykły tekst": "zwykły tekst", `a\b`: `a\\b`, "a;b,c": `a\;b\,c`, "pierwsza\ndruga": `pierwsza\ndruga`, "windows\r\nkoniec\rcr": `windows\nkonieccr`, `już \n nie jest znakiem`: `już \\n nie jest znakiem`, } for in, want := range cases { if got := classes.EscapeText(in); got != want { t.Errorf("EscapeText(%q) = %q, want %q", in, got, want) } } } // checkFolded checks that a folded line has physical lines of at most 75 // octets, each valid UTF-8, continuation lines starting with a space, and // that unfolding gives the input back. func checkFolded(t *testing.T, in, folded string) { t.Helper() parts := strings.Split(folded, "\r\n") for i, p := range parts { if len(p) > 75 { t.Errorf("physical line %d has %d octets: %q", i, len(p), p) } if !utf8.ValidString(p) { t.Errorf("physical line %d splits a UTF-8 sequence: %q", i, p) } if i > 0 && !strings.HasPrefix(p, " ") { t.Errorf("continuation line %d does not start with a space: %q", i, p) } } if unfolded := strings.ReplaceAll(folded, "\r\n ", ""); unfolded != in { t.Errorf("unfolded = %q, want %q", unfolded, in) } } func TestFoldLine(t *testing.T) { short := "SUMMARY:" + strings.Repeat("a", 67) // exactly 75 octets if got := classes.FoldLine(short); got != short { t.Errorf("a 75-octet line was folded: %q", got) } long := "SUMMARY:" + strings.Repeat("a", 68) got := classes.FoldLine(long) if got != "SUMMARY:"+strings.Repeat("a", 67)+"\r\n a" { t.Errorf("76-octet line folded as %q", got) } checkFolded(t, long, got) // Two-octet Polish letters at every offset, so a boundary falls inside a // character at some point; and a three-octet en dash. for pad := 0; pad < 4; pad++ { in := "DESCRIPTION:" + strings.Repeat("x", pad) + strings.Repeat("zażółć gęślą jaźń – ", 12) checkFolded(t, in, classes.FoldLine(in)) } // Four-octet characters. emoji := "SUMMARY:" + strings.Repeat("😀", 50) checkFolded(t, emoji, classes.FoldLine(emoji)) } func TestICSWriterUsesCRLFAndFolds(t *testing.T) { var w classes.ICS w.Begin("VCALENDAR") w.Text("X-WR-CALNAME", "Bezpieczeństwo międzynarodowe; plan, zjazdy\nwiele linii "+strings.Repeat("ąę", 40)) w.UTC("DTSTART", time.Date(2026, 10, 3, 8, 0, 0, 0, models.Location)) w.Date("DTSTART", time.Date(2026, 10, 24, 0, 0, 0, 0, models.Location)) w.End("VCALENDAR") out := string(w.Bytes()) if !strings.HasSuffix(out, "END:VCALENDAR\r\n") { t.Errorf("does not end with END:VCALENDAR CRLF: %q", out) } if strings.Contains(strings.ReplaceAll(out, "\r\n", ""), "\n") || strings.Contains(strings.ReplaceAll(out, "\r\n", ""), "\r") { t.Errorf("bare LF or CR in %q", out) } for _, line := range strings.Split(strings.TrimSuffix(out, "\r\n"), "\r\n") { if len(line) > 75 { t.Errorf("line of %d octets: %q", len(line), line) } } unfolded := strings.ReplaceAll(out, "\r\n ", "") for _, want := range []string{ `X-WR-CALNAME:Bezpieczeństwo międzynarodowe\; plan\, zjazdy\nwiele linii ąę`, "DTSTART:20261003T060000Z\r\n", "DTSTART;VALUE=DATE:20261024\r\n", } { if !strings.Contains(unfolded, want) { t.Errorf("output lacks %q:\n%s", want, unfolded) } } } func TestFormatUTC(t *testing.T) { // Saturday before the DST change (+02:00), Sunday after it (+01:00). sat := time.Date(2026, 10, 24, 8, 0, 0, 0, models.Location) sun := time.Date(2026, 10, 25, 8, 0, 0, 0, models.Location) if got := classes.FormatUTC(sat); got != "20261024T060000Z" { t.Errorf("Saturday = %s", got) } if got := classes.FormatUTC(sun); got != "20261025T070000Z" { t.Errorf("Sunday = %s", got) } }