1// Copyright 2018 The Gitea Authors. All rights reserved.
2// Copyright 2024 The Forgejo Authors. All rights reserved.
3// SPDX-License-Identifier: MIT
4
5package util_test
6
7import (
8 "bytes"
9 "crypto/rand"
10 "regexp"
11 "strings"
12 "testing"
13
14 "forgejo.org/modules/test"
15 "forgejo.org/modules/util"
16
17 "github.com/stretchr/testify/assert"
18 "github.com/stretchr/testify/require"
19)
20
21func TestURLJoin(t *testing.T) {
22 type test struct {
23 Expected string
24 Base string
25 Elements []string
26 }
27 newTest := func(expected, base string, elements ...string) test {
28 return test{Expected: expected, Base: base, Elements: elements}
29 }
30 for _, test := range []test{
31 newTest("https://try.gitea.io/a/b/c",
32 "https://try.gitea.io", "a/b", "c"),
33 newTest("https://try.gitea.io/a/b/c",
34 "https://try.gitea.io/", "/a/b/", "/c/"),
35 newTest("https://try.gitea.io/a/c",
36 "https://try.gitea.io/", "/a/./b/", "../c/"),
37 newTest("a/b/c",
38 "a", "b/c/"),
39 newTest("a/b/d",
40 "a/", "b/c/", "/../d/"),
41 newTest("https://try.gitea.io/a/b/c#d",
42 "https://try.gitea.io", "a/b", "c#d"),
43 newTest("/a/b/d",
44 "/a/", "b/c/", "/../d/"),
45 newTest("/a/b/c",
46 "/a", "b/c/"),
47 newTest("/a/b/c#hash",
48 "/a", "b/c#hash"),
49 } {
50 assert.Equal(t, test.Expected, util.URLJoin(test.Base, test.Elements...))
51 }
52}
53
54func TestIsEmptyString(t *testing.T) {
55 cases := []struct {
56 s string
57 expected bool
58 }{
59 {"", true},
60 {" ", true},
61 {" ", true},
62 {" a", false},
63 }
64
65 for _, v := range cases {
66 assert.Equal(t, v.expected, util.IsEmptyString(v.s))
67 }
68}
69
70func Test_NormalizeEOL(t *testing.T) {
71 data1 := []string{
72 "",
73 "This text starts with empty lines",
74 "another",
75 "",
76 "",
77 "",
78 "Some other empty lines in the middle",
79 "more.",
80 "And more.",
81 "Ends with empty lines too.",
82 "",
83 "",
84 "",
85 }
86
87 data2 := []string{
88 "This text does not start with empty lines",
89 "another",
90 "",
91 "",
92 "",
93 "Some other empty lines in the middle",
94 "more.",
95 "And more.",
96 "Ends without EOLtoo.",
97 }
98
99 buildEOLData := func(data []string, eol string) []byte {
100 return []byte(strings.Join(data, eol))
101 }
102
103 dos := buildEOLData(data1, "\r\n")
104 unix := buildEOLData(data1, "\n")
105 mac := buildEOLData(data1, "\r")
106
107 assert.Equal(t, unix, util.NormalizeEOL(dos))
108 assert.Equal(t, unix, util.NormalizeEOL(mac))
109 assert.Equal(t, unix, util.NormalizeEOL(unix))
110
111 dos = buildEOLData(data2, "\r\n")
112 unix = buildEOLData(data2, "\n")
113 mac = buildEOLData(data2, "\r")
114
115 assert.Equal(t, unix, util.NormalizeEOL(dos))
116 assert.Equal(t, unix, util.NormalizeEOL(mac))
117 assert.Equal(t, unix, util.NormalizeEOL(unix))
118
119 assert.Equal(t, []byte("one liner"), util.NormalizeEOL([]byte("one liner")))
120 assert.Equal(t, []byte("\n"), util.NormalizeEOL([]byte("\n")))
121 assert.Equal(t, []byte("\ntwo liner"), util.NormalizeEOL([]byte("\ntwo liner")))
122 assert.Equal(t, []byte("two liner\n"), util.NormalizeEOL([]byte("two liner\n")))
123 assert.Equal(t, []byte{}, util.NormalizeEOL([]byte{}))
124
125 assert.Equal(t, []byte("mix\nand\nmatch\n."), util.NormalizeEOL([]byte("mix\r\nand\rmatch\n.")))
126}
127
128func Test_RandomInt(t *testing.T) {
129 randInt, err := util.CryptoRandomInt(255)
130 assert.GreaterOrEqual(t, randInt, int64(0))
131 assert.LessOrEqual(t, randInt, int64(255))
132 require.NoError(t, err)
133}
134
135func Test_RandomString(t *testing.T) {
136 str1, err := util.CryptoRandomString(32)
137 require.NoError(t, err)
138 matches, err := regexp.MatchString(`^[a-zA-Z0-9]{32}$`, str1)
139 require.NoError(t, err)
140 assert.True(t, matches)
141
142 str2, err := util.CryptoRandomString(32)
143 require.NoError(t, err)
144 matches, err = regexp.MatchString(`^[a-zA-Z0-9]{32}$`, str1)
145 require.NoError(t, err)
146 assert.True(t, matches)
147
148 assert.NotEqual(t, str1, str2)
149
150 str3, err := util.CryptoRandomString(256)
151 require.NoError(t, err)
152 matches, err = regexp.MatchString(`^[a-zA-Z0-9]{256}$`, str3)
153 require.NoError(t, err)
154 assert.True(t, matches)
155
156 str4, err := util.CryptoRandomString(256)
157 require.NoError(t, err)
158 matches, err = regexp.MatchString(`^[a-zA-Z0-9]{256}$`, str4)
159 require.NoError(t, err)
160 assert.True(t, matches)
161
162 assert.NotEqual(t, str3, str4)
163}
164
165func Test_RandomBytes(t *testing.T) {
166 bytes1 := util.CryptoRandomBytes(32)
167 bytes2 := util.CryptoRandomBytes(32)
168
169 assert.Len(t, bytes1, 32)
170 assert.Len(t, bytes2, 32)
171 assert.NotEqual(t, bytes1, bytes2)
172
173 bytes3 := util.CryptoRandomBytes(256)
174 bytes4 := util.CryptoRandomBytes(256)
175
176 assert.Len(t, bytes3, 256)
177 assert.Len(t, bytes4, 256)
178 assert.NotEqual(t, bytes3, bytes4)
179}
180
181// Test case for any function which accepts and returns a single string.
182type StringTest struct {
183 in, out string
184}
185
186var upperTests = []StringTest{
187 {"", ""},
188 {"ONLYUPPER", "ONLYUPPER"},
189 {"abc", "ABC"},
190 {"AbC123", "ABC123"},
191 {"azAZ09_", "AZAZ09_"},
192 {"longStrinGwitHmixofsmaLLandcAps", "LONGSTRINGWITHMIXOFSMALLANDCAPS"},
193 {"long\u0250string\u0250with\u0250nonascii\u2C6Fchars", "LONG\u0250STRING\u0250WITH\u0250NONASCII\u2C6FCHARS"},
194 {"\u0250\u0250\u0250\u0250\u0250", "\u0250\u0250\u0250\u0250\u0250"},
195 {"a\u0080\U0010FFFF", "A\u0080\U0010FFFF"},
196 {"lél", "LéL"},
197}
198
199func TestToUpperASCII(t *testing.T) {
200 for _, tc := range upperTests {
201 assert.Equal(t, util.ToUpperASCII(tc.in), tc.out)
202 }
203}
204
205func BenchmarkToUpper(b *testing.B) {
206 for _, tc := range upperTests {
207 b.Run(tc.in, func(b *testing.B) {
208 for i := 0; i < b.N; i++ {
209 util.ToUpperASCII(tc.in)
210 }
211 })
212 }
213}
214
215func TestToTitleCase(t *testing.T) {
216 assert.Equal(t, `Foo Bar Baz`, util.ToTitleCase(`foo bar baz`))
217 assert.Equal(t, `Foo Bar Baz`, util.ToTitleCase(`FOO BAR BAZ`))
218}
219
220func TestToPointer(t *testing.T) {
221 assert.Equal(t, "abc", *util.ToPointer("abc"))
222 assert.Equal(t, 123, *util.ToPointer(123))
223 abc := "abc"
224 assert.NotSame(t, &abc, util.ToPointer(abc))
225 val123 := 123
226 assert.NotSame(t, &val123, util.ToPointer(val123))
227}
228
229func TestReserveLineBreakForTextarea(t *testing.T) {
230 assert.Equal(t, "test\ndata", util.ReserveLineBreakForTextarea("test\r\ndata"))
231 assert.Equal(t, "test\ndata\n", util.ReserveLineBreakForTextarea("test\r\ndata\r\n"))
232}
233
234const (
235 testPublicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAOhB7/zzhC+HXDdGOdLwJln5NYwm6UNXx3chmQSVTG4\n"
236 testPrivateKey = `-----BEGIN OPENSSH PRIVATE KEY-----
237b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtz
238c2gtZWQyNTUxOQAAACADoQe/884Qvh1w3RjnS8CZZ+TWMJulDV8d3IZkElUxuAAA
239AIggISIjICEiIwAAAAtzc2gtZWQyNTUxOQAAACADoQe/884Qvh1w3RjnS8CZZ+TW
240MJulDV8d3IZkElUxuAAAAEAAAQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0e
241HwOhB7/zzhC+HXDdGOdLwJln5NYwm6UNXx3chmQSVTG4AAAAAAECAwQF
242-----END OPENSSH PRIVATE KEY-----` + "\n"
243)
244
245func TestGeneratingEd25519Keypair(t *testing.T) {
246 defer test.MockProtect(&rand.Reader)()
247
248 // Only 32 bytes needs to be provided to generate a ed25519 keypair.
249 // And another 32 bytes are required, which is included as random value
250 // in the OpenSSH format.
251 b := make([]byte, 64)
252 for i := 0; i < 64; i++ {
253 b[i] = byte(i)
254 }
255 rand.Reader = bytes.NewReader(b)
256
257 publicKey, privateKey, err := util.GenerateSSHKeypair()
258 require.NoError(t, err)
259 assert.Equal(t, testPublicKey, string(publicKey))
260 assert.Equal(t, testPrivateKey, string(privateKey))
261}
262
263func TestOptionalArg(t *testing.T) {
264 foo := func(other any, optArg ...int) int {
265 return util.OptionalArg(optArg)
266 }
267 bar := func(other any, optArg ...int) int {
268 return util.OptionalArg(optArg, 42)
269 }
270 assert.Equal(t, 0, foo(nil))
271 assert.Equal(t, 100, foo(nil, 100))
272 assert.Equal(t, 42, bar(nil))
273 assert.Equal(t, 100, bar(nil, 100))
274}