package media import ( "strings" "testing" "github.com/tinode/chat/server/store/types" ) func TestMatchCORSOrigin(t *testing.T) { cases := []struct { allowed []string origin string expected string expectError bool errorMessage string }{ { allowed: []string{"https://example.com"}, origin: "https://example.com", expected: "https://example.com", }, { allowed: []string{"https://example2.com", "https://example.com"}, origin: "https://example.com", expected: "https://example.com", }, { allowed: []string{"*"}, origin: "https://example.com", expected: "https://example.com", }, { allowed: []string{""}, origin: "https://example.com", expected: "", }, { allowed: []string{}, origin: "", expected: "", }, { allowed: []string{"https://example.com"}, origin: "", expected: "", }, { allowed: []string{}, origin: "https://example.com", expected: "", }, { allowed: []string{"http://example.com"}, origin: "https://example.com", expected: "", }, { allowed: []string{"https://example.com"}, origin: "http://example.com", expected: "", }, { allowed: []string{"http://example.com:8000"}, origin: "http://example.com:8000", expected: "http://example.com:8000", }, { allowed: []string{"http://localhost:8090"}, origin: "http://localhost:8090", expected: "http://localhost:8090", }, { allowed: []string{"http://localhost:8090"}, origin: "http://localhost:8080", expected: "", }, { allowed: []string{"https://example.com"}, origin: "https://sub.example.com", expected: "", }, { allowed: []string{"https://*.example.com"}, origin: "https://sub.example.com", expected: "https://sub.example.com", }, { allowed: []string{"https://*.example.com"}, origin: "https://pre.sub.example.com", expected: "", }, { allowed: []string{"https://*.example.com", "https://*.sub.example.com"}, origin: "https://pre.sub.example.com", expected: "https://pre.sub.example.com", }, { allowed: []string{"https://*.*.example.com"}, origin: "https://pre.sub.example.com", expected: "https://pre.sub.example.com", }, { allowed: []string{"https://*.sub.example.com"}, origin: "https://pre.asd.example.com", expected: "", }, { allowed: []string{"https://pre.*.example.com"}, origin: "https://pre.sub.example.com", expected: "https://pre.sub.example.com", }, { allowed: []string{"https://*.*.*.example.com"}, origin: "https://www.pre.sub.example.com", expected: "https://www.pre.sub.example.com", }, { allowed: []string{"*"}, origin: "", expected: "*", // Should allow any origin, including empty }, // Error cases - these should fail during ParseCORSAllow { allowed: []string{"*", "https://example.com"}, origin: "https://example.com", expectError: true, errorMessage: "wildcard origin '*' must be the only entry", }, { allowed: []string{"not-a-valid-url"}, origin: "https://example.com", expectError: true, errorMessage: "invalid URI for request", }, { allowed: []string{"://invalid-url"}, origin: "https://example.com", expectError: true, errorMessage: "missing protocol scheme", }, { allowed: []string{"https://", "example.com"}, origin: "https://example.com", expectError: true, errorMessage: "invalid URI for request", }, // Valid cases that should not error { allowed: []string{"", "https://example.com"}, origin: "https://example.com", expectError: true, errorMessage: "empty allowed origin '' must be the only entry", // Empty string should make all origins disallowed }, { allowed: []string{"https://Example.com"}, origin: "https://example.com", expected: "", // Should not match due to case sensitivity }, { allowed: []string{"https://example.com/"}, origin: "https://example.com", expected: "", // Should not match due to trailing slash }, { allowed: []string{"https://example.com"}, origin: "not-a-valid-url", expected: "", // Should handle malformed origin gracefully }, { allowed: []string{"https://example.*.com"}, origin: "https://example.sub.com", expected: "https://example.sub.com", }, { allowed: []string{"https://*.com"}, origin: "https://example.com", expected: "https://example.com", }, { allowed: []string{"http://*.example.com"}, origin: "https://sub.example.com", expected: "", // Should not match due to protocol difference }, { allowed: []string{"https://example.com", "https://example.com"}, origin: "https://example.com", expected: "https://example.com", // Should still work with duplicates }, } for i, tc := range cases { allowedOrigins, err := ParseCORSAllow(tc.allowed) if tc.expectError { if err == nil { t.Errorf("Test case %d: Expected error but got none. Allowed: %v", i, tc.allowed) continue } if tc.errorMessage != "" && !containsSubstring(err.Error(), tc.errorMessage) { t.Errorf("Test case %d: Expected error containing '%s', got '%s'", i, tc.errorMessage, err.Error()) } // For error cases, we don't test the matching logic continue } if err != nil { t.Errorf("Test case %d: Unexpected error parsing allowed origins: %v. Allowed: %v", i, err, tc.allowed) continue } result := matchCORSOrigin(allowedOrigins, tc.origin) if result != tc.expected { t.Errorf("Test case %d: Match CORS origin got wrong result. Expected '%s', got '%s'. Allowed: %v, Origin: '%s'", i, tc.expected, result, tc.allowed, tc.origin) } } } // Helper function to check if a string contains a substring (case-insensitive) func containsSubstring(str, substr string) bool { return strings.Contains(strings.ToLower(str), strings.ToLower(substr)) } func TestExtensionByMimeType(t *testing.T) { cases := map[string]string{ "audio/x-m4a": ".m4a", "audio/x-m4a; charset=utf": ".m4a", "audio/mp4": ".m4a", "audio/aac": ".aac", "audio/mpeg": ".mp3", "image/jpeg": ".jpg", "image/jfif": ".jfif", "video/mp4": ".mp4", } for mimeType, want := range cases { if got := ExtensionByMimeType(mimeType); got != want { t.Fatalf("ExtensionByMimeType(%q)=%q, want %q", mimeType, got, want) } } } func TestMimeTypeForUploadPrefersContainerFilename(t *testing.T) { cases := []struct { name string detect string client string filename string want string }{ { name: "m4a misdetected as mp4", detect: "video/mp4", filename: "voice.m4a", want: "audio/x-m4a", }, { name: "xlsx misdetected as zip", detect: "application/zip", filename: "iOS任务拆解.xlsx", want: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", }, { name: "client content type fallback", detect: "application/octet-stream", client: "image/jfif", filename: "thumb", want: "image/jfif", }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { if got := MimeTypeForUpload(tc.detect, tc.client, tc.filename); got != tc.want { t.Fatalf("MimeTypeForUpload()=%q, want %q", got, tc.want) } }) } } func TestFileURLNamePrefersOriginalExtension(t *testing.T) { cases := []struct { name string def *types.FileDef want string }{ { name: "voice keeps m4a extension", def: &types.FileDef{ObjHeader: types.ObjHeader{Id: "file123"}, MimeType: "video/mp4", OriginalName: "voice.m4a"}, want: "file123.m4a", }, { name: "xlsx keeps xlsx extension", def: &types.FileDef{ObjHeader: types.ObjHeader{Id: "sheet123"}, MimeType: "application/zip", OriginalName: "iOS任务拆解.xlsx"}, want: "sheet123.xlsx", }, { name: "falls back to mime extension", def: &types.FileDef{ObjHeader: types.ObjHeader{Id: "image123"}, MimeType: "image/jpeg"}, want: "image123.jpg", }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { if got := FileURLName(tc.def); got != tc.want { t.Fatalf("FileURLName()=%q, want %q", got, tc.want) } }) } }