1109 lines
		
	
	
		
			26 KiB
		
	
	
	
		
			Go
		
	
	
	
		
		
			
		
	
	
			1109 lines
		
	
	
		
			26 KiB
		
	
	
	
		
			Go
		
	
	
	
|  | package core_test | ||
|  | 
 | ||
|  | import ( | ||
|  | 	"bytes" | ||
|  | 	"context" | ||
|  | 	"encoding/json" | ||
|  | 	"errors" | ||
|  | 	"fmt" | ||
|  | 	"slices" | ||
|  | 	"strings" | ||
|  | 	"testing" | ||
|  | 
 | ||
|  | 	"github.com/pocketbase/pocketbase/core" | ||
|  | 	"github.com/pocketbase/pocketbase/tests" | ||
|  | 	"github.com/pocketbase/pocketbase/tools/filesystem" | ||
|  | 	"github.com/pocketbase/pocketbase/tools/list" | ||
|  | 	"github.com/pocketbase/pocketbase/tools/types" | ||
|  | ) | ||
|  | 
 | ||
|  | func TestFileFieldBaseMethods(t *testing.T) { | ||
|  | 	testFieldBaseMethods(t, core.FieldTypeFile) | ||
|  | } | ||
|  | 
 | ||
|  | func TestFileFieldColumnType(t *testing.T) { | ||
|  | 	app, _ := tests.NewTestApp() | ||
|  | 	defer app.Cleanup() | ||
|  | 
 | ||
|  | 	scenarios := []struct { | ||
|  | 		name     string | ||
|  | 		field    *core.FileField | ||
|  | 		expected string | ||
|  | 	}{ | ||
|  | 		{ | ||
|  | 			"single (zero)", | ||
|  | 			&core.FileField{}, | ||
|  | 			"TEXT DEFAULT '' NOT NULL", | ||
|  | 		}, | ||
|  | 		{ | ||
|  | 			"single", | ||
|  | 			&core.FileField{MaxSelect: 1}, | ||
|  | 			"TEXT DEFAULT '' NOT NULL", | ||
|  | 		}, | ||
|  | 		{ | ||
|  | 			"multiple", | ||
|  | 			&core.FileField{MaxSelect: 2}, | ||
|  | 			"JSON DEFAULT '[]' NOT NULL", | ||
|  | 		}, | ||
|  | 	} | ||
|  | 
 | ||
|  | 	for _, s := range scenarios { | ||
|  | 		t.Run(s.name, func(t *testing.T) { | ||
|  | 			if v := s.field.ColumnType(app); v != s.expected { | ||
|  | 				t.Fatalf("Expected\n%q\ngot\n%q", s.expected, v) | ||
|  | 			} | ||
|  | 		}) | ||
|  | 	} | ||
|  | } | ||
|  | 
 | ||
|  | func TestFileFieldIsMultiple(t *testing.T) { | ||
|  | 	scenarios := []struct { | ||
|  | 		name     string | ||
|  | 		field    *core.FileField | ||
|  | 		expected bool | ||
|  | 	}{ | ||
|  | 		{ | ||
|  | 			"zero", | ||
|  | 			&core.FileField{}, | ||
|  | 			false, | ||
|  | 		}, | ||
|  | 		{ | ||
|  | 			"single", | ||
|  | 			&core.FileField{MaxSelect: 1}, | ||
|  | 			false, | ||
|  | 		}, | ||
|  | 		{ | ||
|  | 			"multiple", | ||
|  | 			&core.FileField{MaxSelect: 2}, | ||
|  | 			true, | ||
|  | 		}, | ||
|  | 	} | ||
|  | 
 | ||
|  | 	for _, s := range scenarios { | ||
|  | 		t.Run(s.name, func(t *testing.T) { | ||
|  | 			if v := s.field.IsMultiple(); v != s.expected { | ||
|  | 				t.Fatalf("Expected %v, got %v", s.expected, v) | ||
|  | 			} | ||
|  | 		}) | ||
|  | 	} | ||
|  | } | ||
|  | 
 | ||
|  | func TestFileFieldPrepareValue(t *testing.T) { | ||
|  | 	app, _ := tests.NewTestApp() | ||
|  | 	defer app.Cleanup() | ||
|  | 
 | ||
|  | 	record := core.NewRecord(core.NewBaseCollection("test")) | ||
|  | 
 | ||
|  | 	f1, err := filesystem.NewFileFromBytes([]byte("test"), "test1.txt") | ||
|  | 	if err != nil { | ||
|  | 		t.Fatal(err) | ||
|  | 	} | ||
|  | 	f1Raw, err := json.Marshal(f1) | ||
|  | 	if err != nil { | ||
|  | 		t.Fatal(err) | ||
|  | 	} | ||
|  | 
 | ||
|  | 	scenarios := []struct { | ||
|  | 		raw      any | ||
|  | 		field    *core.FileField | ||
|  | 		expected string | ||
|  | 	}{ | ||
|  | 		// single
 | ||
|  | 		{nil, &core.FileField{MaxSelect: 1}, `""`}, | ||
|  | 		{"", &core.FileField{MaxSelect: 1}, `""`}, | ||
|  | 		{123, &core.FileField{MaxSelect: 1}, `"123"`}, | ||
|  | 		{"a", &core.FileField{MaxSelect: 1}, `"a"`}, | ||
|  | 		{`["a"]`, &core.FileField{MaxSelect: 1}, `"a"`}, | ||
|  | 		{*f1, &core.FileField{MaxSelect: 1}, string(f1Raw)}, | ||
|  | 		{f1, &core.FileField{MaxSelect: 1}, string(f1Raw)}, | ||
|  | 		{[]string{}, &core.FileField{MaxSelect: 1}, `""`}, | ||
|  | 		{[]string{"a", "b"}, &core.FileField{MaxSelect: 1}, `"b"`}, | ||
|  | 
 | ||
|  | 		// multiple
 | ||
|  | 		{nil, &core.FileField{MaxSelect: 2}, `[]`}, | ||
|  | 		{"", &core.FileField{MaxSelect: 2}, `[]`}, | ||
|  | 		{123, &core.FileField{MaxSelect: 2}, `["123"]`}, | ||
|  | 		{"a", &core.FileField{MaxSelect: 2}, `["a"]`}, | ||
|  | 		{`["a"]`, &core.FileField{MaxSelect: 2}, `["a"]`}, | ||
|  | 		{[]any{f1}, &core.FileField{MaxSelect: 2}, `[` + string(f1Raw) + `]`}, | ||
|  | 		{[]*filesystem.File{f1}, &core.FileField{MaxSelect: 2}, `[` + string(f1Raw) + `]`}, | ||
|  | 		{[]filesystem.File{*f1}, &core.FileField{MaxSelect: 2}, `[` + string(f1Raw) + `]`}, | ||
|  | 		{[]string{}, &core.FileField{MaxSelect: 2}, `[]`}, | ||
|  | 		{[]string{"a", "b", "c"}, &core.FileField{MaxSelect: 2}, `["a","b","c"]`}, | ||
|  | 	} | ||
|  | 
 | ||
|  | 	for i, s := range scenarios { | ||
|  | 		t.Run(fmt.Sprintf("%d_%#v_%v", i, s.raw, s.field.IsMultiple()), func(t *testing.T) { | ||
|  | 			v, err := s.field.PrepareValue(record, s.raw) | ||
|  | 			if err != nil { | ||
|  | 				t.Fatal(err) | ||
|  | 			} | ||
|  | 
 | ||
|  | 			vRaw, err := json.Marshal(v) | ||
|  | 			if err != nil { | ||
|  | 				t.Fatal(err) | ||
|  | 			} | ||
|  | 
 | ||
|  | 			if string(vRaw) != s.expected { | ||
|  | 				t.Fatalf("Expected %q, got %q", s.expected, vRaw) | ||
|  | 			} | ||
|  | 		}) | ||
|  | 	} | ||
|  | } | ||
|  | 
 | ||
|  | func TestFileFieldDriverValue(t *testing.T) { | ||
|  | 	app, _ := tests.NewTestApp() | ||
|  | 	defer app.Cleanup() | ||
|  | 
 | ||
|  | 	f1, err := filesystem.NewFileFromBytes([]byte("test"), "test.txt") | ||
|  | 	if err != nil { | ||
|  | 		t.Fatal(err) | ||
|  | 	} | ||
|  | 
 | ||
|  | 	scenarios := []struct { | ||
|  | 		raw      any | ||
|  | 		field    *core.FileField | ||
|  | 		expected string | ||
|  | 	}{ | ||
|  | 		// single
 | ||
|  | 		{nil, &core.FileField{MaxSelect: 1}, `""`}, | ||
|  | 		{"", &core.FileField{MaxSelect: 1}, `""`}, | ||
|  | 		{123, &core.FileField{MaxSelect: 1}, `"123"`}, | ||
|  | 		{"a", &core.FileField{MaxSelect: 1}, `"a"`}, | ||
|  | 		{`["a"]`, &core.FileField{MaxSelect: 1}, `"a"`}, | ||
|  | 		{f1, &core.FileField{MaxSelect: 1}, `"` + f1.Name + `"`}, | ||
|  | 		{[]string{}, &core.FileField{MaxSelect: 1}, `""`}, | ||
|  | 		{[]string{"a", "b"}, &core.FileField{MaxSelect: 1}, `"b"`}, | ||
|  | 
 | ||
|  | 		// multiple
 | ||
|  | 		{nil, &core.FileField{MaxSelect: 2}, `[]`}, | ||
|  | 		{"", &core.FileField{MaxSelect: 2}, `[]`}, | ||
|  | 		{123, &core.FileField{MaxSelect: 2}, `["123"]`}, | ||
|  | 		{"a", &core.FileField{MaxSelect: 2}, `["a"]`}, | ||
|  | 		{`["a"]`, &core.FileField{MaxSelect: 2}, `["a"]`}, | ||
|  | 		{[]any{"a", f1}, &core.FileField{MaxSelect: 2}, `["a","` + f1.Name + `"]`}, | ||
|  | 		{[]string{}, &core.FileField{MaxSelect: 2}, `[]`}, | ||
|  | 		{[]string{"a", "b", "c"}, &core.FileField{MaxSelect: 2}, `["a","b","c"]`}, | ||
|  | 	} | ||
|  | 
 | ||
|  | 	for i, s := range scenarios { | ||
|  | 		t.Run(fmt.Sprintf("%d_%#v_%v", i, s.raw, s.field.IsMultiple()), func(t *testing.T) { | ||
|  | 			record := core.NewRecord(core.NewBaseCollection("test")) | ||
|  | 			record.SetRaw(s.field.GetName(), s.raw) | ||
|  | 
 | ||
|  | 			v, err := s.field.DriverValue(record) | ||
|  | 			if err != nil { | ||
|  | 				t.Fatal(err) | ||
|  | 			} | ||
|  | 
 | ||
|  | 			if s.field.IsMultiple() { | ||
|  | 				_, ok := v.(types.JSONArray[string]) | ||
|  | 				if !ok { | ||
|  | 					t.Fatalf("Expected types.JSONArray value, got %T", v) | ||
|  | 				} | ||
|  | 			} else { | ||
|  | 				_, ok := v.(string) | ||
|  | 				if !ok { | ||
|  | 					t.Fatalf("Expected string value, got %T", v) | ||
|  | 				} | ||
|  | 			} | ||
|  | 
 | ||
|  | 			vRaw, err := json.Marshal(v) | ||
|  | 			if err != nil { | ||
|  | 				t.Fatal(err) | ||
|  | 			} | ||
|  | 
 | ||
|  | 			if string(vRaw) != s.expected { | ||
|  | 				t.Fatalf("Expected %q, got %q", s.expected, vRaw) | ||
|  | 			} | ||
|  | 		}) | ||
|  | 	} | ||
|  | } | ||
|  | 
 | ||
|  | func TestFileFieldValidateValue(t *testing.T) { | ||
|  | 	app, _ := tests.NewTestApp() | ||
|  | 	defer app.Cleanup() | ||
|  | 
 | ||
|  | 	collection := core.NewBaseCollection("test_collection") | ||
|  | 
 | ||
|  | 	f1, err := filesystem.NewFileFromBytes([]byte("test"), "test1.txt") | ||
|  | 	if err != nil { | ||
|  | 		t.Fatal(err) | ||
|  | 	} | ||
|  | 
 | ||
|  | 	f2, err := filesystem.NewFileFromBytes([]byte("test"), "test2.txt") | ||
|  | 	if err != nil { | ||
|  | 		t.Fatal(err) | ||
|  | 	} | ||
|  | 
 | ||
|  | 	f3, err := filesystem.NewFileFromBytes([]byte("test_abc"), "test3.txt") | ||
|  | 	if err != nil { | ||
|  | 		t.Fatal(err) | ||
|  | 	} | ||
|  | 
 | ||
|  | 	f4, err := filesystem.NewFileFromBytes(make([]byte, core.DefaultFileFieldMaxSize+1), "test4.txt") | ||
|  | 	if err != nil { | ||
|  | 		t.Fatal(err) | ||
|  | 	} | ||
|  | 
 | ||
|  | 	f5, err := filesystem.NewFileFromBytes(make([]byte, core.DefaultFileFieldMaxSize), "test5.txt") | ||
|  | 	if err != nil { | ||
|  | 		t.Fatal(err) | ||
|  | 	} | ||
|  | 
 | ||
|  | 	scenarios := []struct { | ||
|  | 		name        string | ||
|  | 		field       *core.FileField | ||
|  | 		record      func() *core.Record | ||
|  | 		expectError bool | ||
|  | 	}{ | ||
|  | 		// single
 | ||
|  | 		{ | ||
|  | 			"zero field value (not required)", | ||
|  | 			&core.FileField{Name: "test", MaxSize: 9999, MaxSelect: 1}, | ||
|  | 			func() *core.Record { | ||
|  | 				record := core.NewRecord(collection) | ||
|  | 				record.SetRaw("test", "") | ||
|  | 				return record | ||
|  | 			}, | ||
|  | 			false, | ||
|  | 		}, | ||
|  | 		{ | ||
|  | 			"zero field value (required)", | ||
|  | 			&core.FileField{Name: "test", MaxSize: 9999, MaxSelect: 1, Required: true}, | ||
|  | 			func() *core.Record { | ||
|  | 				record := core.NewRecord(collection) | ||
|  | 				record.SetRaw("test", "") | ||
|  | 				return record | ||
|  | 			}, | ||
|  | 			true, | ||
|  | 		}, | ||
|  | 		{ | ||
|  | 			"new plain filename", // new files must be *filesystem.File
 | ||
|  | 			&core.FileField{Name: "test", MaxSize: 9999, MaxSelect: 1}, | ||
|  | 			func() *core.Record { | ||
|  | 				record := core.NewRecord(collection) | ||
|  | 				record.SetRaw("test", "a") | ||
|  | 				return record | ||
|  | 			}, | ||
|  | 			true, | ||
|  | 		}, | ||
|  | 		{ | ||
|  | 			"new file", | ||
|  | 			&core.FileField{Name: "test", MaxSize: 9999, MaxSelect: 1}, | ||
|  | 			func() *core.Record { | ||
|  | 				record := core.NewRecord(collection) | ||
|  | 				record.SetRaw("test", f1) | ||
|  | 				return record | ||
|  | 			}, | ||
|  | 			false, | ||
|  | 		}, | ||
|  | 		{ | ||
|  | 			"new files > MaxSelect", | ||
|  | 			&core.FileField{Name: "test", MaxSize: 9999, MaxSelect: 1}, | ||
|  | 			func() *core.Record { | ||
|  | 				record := core.NewRecord(collection) | ||
|  | 				record.SetRaw("test", []any{f1, f2}) | ||
|  | 				return record | ||
|  | 			}, | ||
|  | 			true, | ||
|  | 		}, | ||
|  | 		{ | ||
|  | 			"new files <= MaxSelect", | ||
|  | 			&core.FileField{Name: "test", MaxSize: 9999, MaxSelect: 2}, | ||
|  | 			func() *core.Record { | ||
|  | 				record := core.NewRecord(collection) | ||
|  | 				record.SetRaw("test", []any{f1, f2}) | ||
|  | 				return record | ||
|  | 			}, | ||
|  | 			false, | ||
|  | 		}, | ||
|  | 		{ | ||
|  | 			"> default MaxSize", | ||
|  | 			&core.FileField{Name: "test"}, | ||
|  | 			func() *core.Record { | ||
|  | 				record := core.NewRecord(collection) | ||
|  | 				record.SetRaw("test", f4) | ||
|  | 				return record | ||
|  | 			}, | ||
|  | 			true, | ||
|  | 		}, | ||
|  | 		{ | ||
|  | 			"<= default MaxSize", | ||
|  | 			&core.FileField{Name: "test"}, | ||
|  | 			func() *core.Record { | ||
|  | 				record := core.NewRecord(collection) | ||
|  | 				record.SetRaw("test", f5) | ||
|  | 				return record | ||
|  | 			}, | ||
|  | 			false, | ||
|  | 		}, | ||
|  | 		{ | ||
|  | 			"> MaxSize", | ||
|  | 			&core.FileField{Name: "test", MaxSize: 4, MaxSelect: 3}, | ||
|  | 			func() *core.Record { | ||
|  | 				record := core.NewRecord(collection) | ||
|  | 				record.SetRaw("test", []any{f1, f2, f3}) // f3=8
 | ||
|  | 				return record | ||
|  | 			}, | ||
|  | 			true, | ||
|  | 		}, | ||
|  | 		{ | ||
|  | 			"<= MaxSize", | ||
|  | 			&core.FileField{Name: "test", MaxSize: 8, MaxSelect: 3}, | ||
|  | 			func() *core.Record { | ||
|  | 				record := core.NewRecord(collection) | ||
|  | 				record.SetRaw("test", []any{f1, f2, f3}) | ||
|  | 				return record | ||
|  | 			}, | ||
|  | 			false, | ||
|  | 		}, | ||
|  | 		{ | ||
|  | 			"non-matching MimeType", | ||
|  | 			&core.FileField{Name: "test", MaxSize: 999, MaxSelect: 3, MimeTypes: []string{"a", "b"}}, | ||
|  | 			func() *core.Record { | ||
|  | 				record := core.NewRecord(collection) | ||
|  | 				record.SetRaw("test", []any{f1, f2}) | ||
|  | 				return record | ||
|  | 			}, | ||
|  | 			true, | ||
|  | 		}, | ||
|  | 		{ | ||
|  | 			"matching MimeType", | ||
|  | 			&core.FileField{Name: "test", MaxSize: 999, MaxSelect: 3, MimeTypes: []string{"text/plain", "b"}}, | ||
|  | 			func() *core.Record { | ||
|  | 				record := core.NewRecord(collection) | ||
|  | 				record.SetRaw("test", []any{f1, f2}) | ||
|  | 				return record | ||
|  | 			}, | ||
|  | 			false, | ||
|  | 		}, | ||
|  | 		{ | ||
|  | 			"existing files > MaxSelect", | ||
|  | 			&core.FileField{Name: "file_many", MaxSize: 999, MaxSelect: 2}, | ||
|  | 			func() *core.Record { | ||
|  | 				record, _ := app.FindRecordById("demo1", "84nmscqy84lsi1t") // 5 files
 | ||
|  | 				return record | ||
|  | 			}, | ||
|  | 			true, | ||
|  | 		}, | ||
|  | 		{ | ||
|  | 			"existing files should ignore the MaxSize and Mimetypes checks", | ||
|  | 			&core.FileField{Name: "file_many", MaxSize: 1, MaxSelect: 5, MimeTypes: []string{"a", "b"}}, | ||
|  | 			func() *core.Record { | ||
|  | 				record, _ := app.FindRecordById("demo1", "84nmscqy84lsi1t") | ||
|  | 				return record | ||
|  | 			}, | ||
|  | 			false, | ||
|  | 		}, | ||
|  | 		{ | ||
|  | 			"existing + new file > MaxSelect (5+2)", | ||
|  | 			&core.FileField{Name: "file_many", MaxSize: 999, MaxSelect: 6}, | ||
|  | 			func() *core.Record { | ||
|  | 				record, _ := app.FindRecordById("demo1", "84nmscqy84lsi1t") | ||
|  | 				record.Set("file_many+", []any{f1, f2}) | ||
|  | 				return record | ||
|  | 			}, | ||
|  | 			true, | ||
|  | 		}, | ||
|  | 		{ | ||
|  | 			"existing + new file <= MaxSelect (5+2)", | ||
|  | 			&core.FileField{Name: "file_many", MaxSize: 999, MaxSelect: 7}, | ||
|  | 			func() *core.Record { | ||
|  | 				record, _ := app.FindRecordById("demo1", "84nmscqy84lsi1t") | ||
|  | 				record.Set("file_many+", []any{f1, f2}) | ||
|  | 				return record | ||
|  | 			}, | ||
|  | 			false, | ||
|  | 		}, | ||
|  | 		{ | ||
|  | 			"existing + new filename", | ||
|  | 			&core.FileField{Name: "file_many", MaxSize: 999, MaxSelect: 99}, | ||
|  | 			func() *core.Record { | ||
|  | 				record, _ := app.FindRecordById("demo1", "84nmscqy84lsi1t") | ||
|  | 				record.Set("file_many+", "test123.png") | ||
|  | 				return record | ||
|  | 			}, | ||
|  | 			true, | ||
|  | 		}, | ||
|  | 	} | ||
|  | 
 | ||
|  | 	for _, s := range scenarios { | ||
|  | 		t.Run(s.name, func(t *testing.T) { | ||
|  | 			err := s.field.ValidateValue(context.Background(), app, s.record()) | ||
|  | 
 | ||
|  | 			hasErr := err != nil | ||
|  | 			if hasErr != s.expectError { | ||
|  | 				t.Fatalf("Expected hasErr %v, got %v (%v)", s.expectError, hasErr, err) | ||
|  | 			} | ||
|  | 		}) | ||
|  | 	} | ||
|  | } | ||
|  | 
 | ||
|  | func TestFileFieldValidateSettings(t *testing.T) { | ||
|  | 	testDefaultFieldIdValidation(t, core.FieldTypeFile) | ||
|  | 	testDefaultFieldNameValidation(t, core.FieldTypeFile) | ||
|  | 
 | ||
|  | 	app, _ := tests.NewTestApp() | ||
|  | 	defer app.Cleanup() | ||
|  | 
 | ||
|  | 	scenarios := []struct { | ||
|  | 		name         string | ||
|  | 		field        func() *core.FileField | ||
|  | 		expectErrors []string | ||
|  | 	}{ | ||
|  | 		{ | ||
|  | 			"zero minimal", | ||
|  | 			func() *core.FileField { | ||
|  | 				return &core.FileField{ | ||
|  | 					Id:   "test", | ||
|  | 					Name: "test", | ||
|  | 				} | ||
|  | 			}, | ||
|  | 			[]string{}, | ||
|  | 		}, | ||
|  | 		{ | ||
|  | 			"0x0 thumb", | ||
|  | 			func() *core.FileField { | ||
|  | 				return &core.FileField{ | ||
|  | 					Id:        "test", | ||
|  | 					Name:      "test", | ||
|  | 					MaxSelect: 1, | ||
|  | 					Thumbs:    []string{"100x200", "0x0"}, | ||
|  | 				} | ||
|  | 			}, | ||
|  | 			[]string{"thumbs"}, | ||
|  | 		}, | ||
|  | 		{ | ||
|  | 			"0x0t thumb", | ||
|  | 			func() *core.FileField { | ||
|  | 				return &core.FileField{ | ||
|  | 					Id:        "test", | ||
|  | 					Name:      "test", | ||
|  | 					MaxSize:   1, | ||
|  | 					MaxSelect: 1, | ||
|  | 					Thumbs:    []string{"100x200", "0x0t"}, | ||
|  | 				} | ||
|  | 			}, | ||
|  | 			[]string{"thumbs"}, | ||
|  | 		}, | ||
|  | 		{ | ||
|  | 			"0x0b thumb", | ||
|  | 			func() *core.FileField { | ||
|  | 				return &core.FileField{ | ||
|  | 					Id:        "test", | ||
|  | 					Name:      "test", | ||
|  | 					MaxSize:   1, | ||
|  | 					MaxSelect: 1, | ||
|  | 					Thumbs:    []string{"100x200", "0x0b"}, | ||
|  | 				} | ||
|  | 			}, | ||
|  | 			[]string{"thumbs"}, | ||
|  | 		}, | ||
|  | 		{ | ||
|  | 			"0x0f thumb", | ||
|  | 			func() *core.FileField { | ||
|  | 				return &core.FileField{ | ||
|  | 					Id:        "test", | ||
|  | 					Name:      "test", | ||
|  | 					MaxSize:   1, | ||
|  | 					MaxSelect: 1, | ||
|  | 					Thumbs:    []string{"100x200", "0x0f"}, | ||
|  | 				} | ||
|  | 			}, | ||
|  | 			[]string{"thumbs"}, | ||
|  | 		}, | ||
|  | 		{ | ||
|  | 			"invalid format", | ||
|  | 			func() *core.FileField { | ||
|  | 				return &core.FileField{ | ||
|  | 					Id:        "test", | ||
|  | 					Name:      "test", | ||
|  | 					MaxSize:   1, | ||
|  | 					MaxSelect: 1, | ||
|  | 					Thumbs:    []string{"100x200", "100x"}, | ||
|  | 				} | ||
|  | 			}, | ||
|  | 			[]string{"thumbs"}, | ||
|  | 		}, | ||
|  | 		{ | ||
|  | 			"valid thumbs", | ||
|  | 			func() *core.FileField { | ||
|  | 				return &core.FileField{ | ||
|  | 					Id:        "test", | ||
|  | 					Name:      "test", | ||
|  | 					MaxSize:   1, | ||
|  | 					MaxSelect: 1, | ||
|  | 					Thumbs:    []string{"100x200", "100x40", "100x200"}, | ||
|  | 				} | ||
|  | 			}, | ||
|  | 			[]string{}, | ||
|  | 		}, | ||
|  | 	} | ||
|  | 
 | ||
|  | 	for _, s := range scenarios { | ||
|  | 		t.Run(s.name, func(t *testing.T) { | ||
|  | 			field := s.field() | ||
|  | 
 | ||
|  | 			collection := core.NewBaseCollection("test_collection") | ||
|  | 			collection.Fields.Add(field) | ||
|  | 
 | ||
|  | 			errs := field.ValidateSettings(context.Background(), app, collection) | ||
|  | 
 | ||
|  | 			tests.TestValidationErrors(t, errs, s.expectErrors) | ||
|  | 		}) | ||
|  | 	} | ||
|  | } | ||
|  | 
 | ||
|  | func TestFileFieldCalculateMaxBodySize(t *testing.T) { | ||
|  | 	testApp, _ := tests.NewTestApp() | ||
|  | 	defer testApp.Cleanup() | ||
|  | 
 | ||
|  | 	scenarios := []struct { | ||
|  | 		field    *core.FileField | ||
|  | 		expected int64 | ||
|  | 	}{ | ||
|  | 		{&core.FileField{}, core.DefaultFileFieldMaxSize}, | ||
|  | 		{&core.FileField{MaxSelect: 2}, 2 * core.DefaultFileFieldMaxSize}, | ||
|  | 		{&core.FileField{MaxSize: 10}, 10}, | ||
|  | 		{&core.FileField{MaxSize: 10, MaxSelect: 1}, 10}, | ||
|  | 		{&core.FileField{MaxSize: 10, MaxSelect: 2}, 20}, | ||
|  | 	} | ||
|  | 
 | ||
|  | 	for i, s := range scenarios { | ||
|  | 		t.Run(fmt.Sprintf("%d_%d_%d", i, s.field.MaxSelect, s.field.MaxSize), func(t *testing.T) { | ||
|  | 			result := s.field.CalculateMaxBodySize() | ||
|  | 
 | ||
|  | 			if result != s.expected { | ||
|  | 				t.Fatalf("Expected %d, got %d", s.expected, result) | ||
|  | 			} | ||
|  | 		}) | ||
|  | 	} | ||
|  | } | ||
|  | 
 | ||
|  | func TestFileFieldFindGetter(t *testing.T) { | ||
|  | 	app, _ := tests.NewTestApp() | ||
|  | 	defer app.Cleanup() | ||
|  | 
 | ||
|  | 	f1, err := filesystem.NewFileFromBytes([]byte("test"), "f1") | ||
|  | 	if err != nil { | ||
|  | 		t.Fatal(err) | ||
|  | 	} | ||
|  | 	f1.Name = "f1" | ||
|  | 
 | ||
|  | 	f2, err := filesystem.NewFileFromBytes([]byte("test"), "f2") | ||
|  | 	if err != nil { | ||
|  | 		t.Fatal(err) | ||
|  | 	} | ||
|  | 	f2.Name = "f2" | ||
|  | 
 | ||
|  | 	record, err := app.FindRecordById("demo3", "lcl9d87w22ml6jy") | ||
|  | 	if err != nil { | ||
|  | 		t.Fatal(err) | ||
|  | 	} | ||
|  | 	record.Set("files+", []any{f1, f2}) | ||
|  | 	record.Set("files-", "test_FLurQTgrY8.txt") | ||
|  | 
 | ||
|  | 	field, ok := record.Collection().Fields.GetByName("files").(*core.FileField) | ||
|  | 	if !ok { | ||
|  | 		t.Fatalf("Expected *core.FileField, got %T", record.Collection().Fields.GetByName("files")) | ||
|  | 	} | ||
|  | 
 | ||
|  | 	scenarios := []struct { | ||
|  | 		name      string | ||
|  | 		key       string | ||
|  | 		hasGetter bool | ||
|  | 		expected  string | ||
|  | 	}{ | ||
|  | 		{ | ||
|  | 			"no match", | ||
|  | 			"example", | ||
|  | 			false, | ||
|  | 			"", | ||
|  | 		}, | ||
|  | 		{ | ||
|  | 			"exact match", | ||
|  | 			field.GetName(), | ||
|  | 			true, | ||
|  | 			`["300_UhLKX91HVb.png",{"name":"f1","originalName":"f1","size":4},{"name":"f2","originalName":"f2","size":4}]`, | ||
|  | 		}, | ||
|  | 		{ | ||
|  | 			"uploaded", | ||
|  | 			field.GetName() + ":uploaded", | ||
|  | 			true, | ||
|  | 			`[{"name":"f1","originalName":"f1","size":4},{"name":"f2","originalName":"f2","size":4}]`, | ||
|  | 		}, | ||
|  | 	} | ||
|  | 
 | ||
|  | 	for _, s := range scenarios { | ||
|  | 		t.Run(s.name, func(t *testing.T) { | ||
|  | 			getter := field.FindGetter(s.key) | ||
|  | 
 | ||
|  | 			hasGetter := getter != nil | ||
|  | 			if hasGetter != s.hasGetter { | ||
|  | 				t.Fatalf("Expected hasGetter %v, got %v", s.hasGetter, hasGetter) | ||
|  | 			} | ||
|  | 
 | ||
|  | 			if !hasGetter { | ||
|  | 				return | ||
|  | 			} | ||
|  | 
 | ||
|  | 			v := getter(record) | ||
|  | 
 | ||
|  | 			raw, err := json.Marshal(v) | ||
|  | 			if err != nil { | ||
|  | 				t.Fatal(err) | ||
|  | 			} | ||
|  | 			rawStr := string(raw) | ||
|  | 
 | ||
|  | 			if rawStr != s.expected { | ||
|  | 				t.Fatalf("Expected\n%v\ngot\n%v", s.expected, rawStr) | ||
|  | 			} | ||
|  | 		}) | ||
|  | 	} | ||
|  | } | ||
|  | 
 | ||
|  | func TestFileFieldFindSetter(t *testing.T) { | ||
|  | 	scenarios := []struct { | ||
|  | 		name      string | ||
|  | 		key       string | ||
|  | 		value     any | ||
|  | 		field     *core.FileField | ||
|  | 		hasSetter bool | ||
|  | 		expected  string | ||
|  | 	}{ | ||
|  | 		{ | ||
|  | 			"no match", | ||
|  | 			"example", | ||
|  | 			"b", | ||
|  | 			&core.FileField{Name: "test", MaxSelect: 1}, | ||
|  | 			false, | ||
|  | 			"", | ||
|  | 		}, | ||
|  | 		{ | ||
|  | 			"exact match (single)", | ||
|  | 			"test", | ||
|  | 			"b", | ||
|  | 			&core.FileField{Name: "test", MaxSelect: 1}, | ||
|  | 			true, | ||
|  | 			`"b"`, | ||
|  | 		}, | ||
|  | 		{ | ||
|  | 			"exact match (multiple)", | ||
|  | 			"test", | ||
|  | 			[]string{"a", "b", "b"}, | ||
|  | 			&core.FileField{Name: "test", MaxSelect: 2}, | ||
|  | 			true, | ||
|  | 			`["a","b"]`, | ||
|  | 		}, | ||
|  | 		{ | ||
|  | 			"append (single)", | ||
|  | 			"test+", | ||
|  | 			"b", | ||
|  | 			&core.FileField{Name: "test", MaxSelect: 1}, | ||
|  | 			true, | ||
|  | 			`"b"`, | ||
|  | 		}, | ||
|  | 		{ | ||
|  | 			"append (multiple)", | ||
|  | 			"test+", | ||
|  | 			[]string{"a"}, | ||
|  | 			&core.FileField{Name: "test", MaxSelect: 2}, | ||
|  | 			true, | ||
|  | 			`["c","d","a"]`, | ||
|  | 		}, | ||
|  | 		{ | ||
|  | 			"prepend (single)", | ||
|  | 			"+test", | ||
|  | 			"b", | ||
|  | 			&core.FileField{Name: "test", MaxSelect: 1}, | ||
|  | 			true, | ||
|  | 			`"d"`, // the last of the existing values
 | ||
|  | 		}, | ||
|  | 		{ | ||
|  | 			"prepend (multiple)", | ||
|  | 			"+test", | ||
|  | 			[]string{"a"}, | ||
|  | 			&core.FileField{Name: "test", MaxSelect: 2}, | ||
|  | 			true, | ||
|  | 			`["a","c","d"]`, | ||
|  | 		}, | ||
|  | 		{ | ||
|  | 			"subtract (single)", | ||
|  | 			"test-", | ||
|  | 			"d", | ||
|  | 			&core.FileField{Name: "test", MaxSelect: 1}, | ||
|  | 			true, | ||
|  | 			`"c"`, | ||
|  | 		}, | ||
|  | 		{ | ||
|  | 			"subtract (multiple)", | ||
|  | 			"test-", | ||
|  | 			[]string{"unknown", "c"}, | ||
|  | 			&core.FileField{Name: "test", MaxSelect: 2}, | ||
|  | 			true, | ||
|  | 			`["d"]`, | ||
|  | 		}, | ||
|  | 	} | ||
|  | 
 | ||
|  | 	for _, s := range scenarios { | ||
|  | 		t.Run(s.name, func(t *testing.T) { | ||
|  | 			collection := core.NewBaseCollection("test_collection") | ||
|  | 			collection.Fields.Add(s.field) | ||
|  | 
 | ||
|  | 			setter := s.field.FindSetter(s.key) | ||
|  | 
 | ||
|  | 			hasSetter := setter != nil | ||
|  | 			if hasSetter != s.hasSetter { | ||
|  | 				t.Fatalf("Expected hasSetter %v, got %v", s.hasSetter, hasSetter) | ||
|  | 			} | ||
|  | 
 | ||
|  | 			if !hasSetter { | ||
|  | 				return | ||
|  | 			} | ||
|  | 
 | ||
|  | 			record := core.NewRecord(collection) | ||
|  | 			record.SetRaw(s.field.GetName(), []string{"c", "d"}) | ||
|  | 
 | ||
|  | 			setter(record, s.value) | ||
|  | 
 | ||
|  | 			raw, err := json.Marshal(record.Get(s.field.GetName())) | ||
|  | 			if err != nil { | ||
|  | 				t.Fatal(err) | ||
|  | 			} | ||
|  | 			rawStr := string(raw) | ||
|  | 
 | ||
|  | 			if rawStr != s.expected { | ||
|  | 				t.Fatalf("Expected %q, got %q", s.expected, rawStr) | ||
|  | 			} | ||
|  | 		}) | ||
|  | 	} | ||
|  | } | ||
|  | 
 | ||
|  | func TestFileFieldIntercept(t *testing.T) { | ||
|  | 	testApp, _ := tests.NewTestApp() | ||
|  | 	defer testApp.Cleanup() | ||
|  | 
 | ||
|  | 	demo1, err := testApp.FindCollectionByNameOrId("demo1") | ||
|  | 	if err != nil { | ||
|  | 		t.Fatal(err) | ||
|  | 	} | ||
|  | 	demo1.Fields.GetByName("text").(*core.TextField).Required = true // trigger validation error
 | ||
|  | 
 | ||
|  | 	f1, err := filesystem.NewFileFromBytes([]byte("test"), "new1.txt") | ||
|  | 	if err != nil { | ||
|  | 		t.Fatal(err) | ||
|  | 	} | ||
|  | 
 | ||
|  | 	f2, err := filesystem.NewFileFromBytes([]byte("test"), "new2.txt") | ||
|  | 	if err != nil { | ||
|  | 		t.Fatal(err) | ||
|  | 	} | ||
|  | 
 | ||
|  | 	f3, err := filesystem.NewFileFromBytes([]byte("test"), "new3.txt") | ||
|  | 	if err != nil { | ||
|  | 		t.Fatal(err) | ||
|  | 	} | ||
|  | 
 | ||
|  | 	f4, err := filesystem.NewFileFromBytes([]byte("test"), "new4.txt") | ||
|  | 	if err != nil { | ||
|  | 		t.Fatal(err) | ||
|  | 	} | ||
|  | 
 | ||
|  | 	record := core.NewRecord(demo1) | ||
|  | 
 | ||
|  | 	ok := t.Run("1. create - with validation error", func(t *testing.T) { | ||
|  | 		record.Set("file_many", []any{f1, f2}) | ||
|  | 
 | ||
|  | 		err := testApp.Save(record) | ||
|  | 
 | ||
|  | 		tests.TestValidationErrors(t, err, []string{"text"}) | ||
|  | 
 | ||
|  | 		value, _ := record.GetRaw("file_many").([]any) | ||
|  | 		if len(value) != 2 { | ||
|  | 			t.Fatalf("Expected the file field value to be unchanged, got %v", value) | ||
|  | 		} | ||
|  | 	}) | ||
|  | 	if !ok { | ||
|  | 		return | ||
|  | 	} | ||
|  | 
 | ||
|  | 	ok = t.Run("2. create - fixing the validation error", func(t *testing.T) { | ||
|  | 		record.Set("text", "abc") | ||
|  | 
 | ||
|  | 		err := testApp.Save(record) | ||
|  | 		if err != nil { | ||
|  | 			t.Fatalf("Expected save to succeed, got %v", err) | ||
|  | 		} | ||
|  | 
 | ||
|  | 		expectedKeys := []string{f1.Name, f2.Name} | ||
|  | 
 | ||
|  | 		raw := record.GetRaw("file_many") | ||
|  | 
 | ||
|  | 		// ensure that the value was replaced with the file names
 | ||
|  | 		value := list.ToUniqueStringSlice(raw) | ||
|  | 		if len(value) != len(expectedKeys) { | ||
|  | 			t.Fatalf("Expected the file field to be updated with the %d file names, got\n%v", len(expectedKeys), raw) | ||
|  | 		} | ||
|  | 		for _, name := range expectedKeys { | ||
|  | 			if !slices.Contains(value, name) { | ||
|  | 				t.Fatalf("Missing file %q in %v", name, value) | ||
|  | 			} | ||
|  | 		} | ||
|  | 
 | ||
|  | 		checkRecordFiles(t, testApp, record, expectedKeys) | ||
|  | 	}) | ||
|  | 	if !ok { | ||
|  | 		return | ||
|  | 	} | ||
|  | 
 | ||
|  | 	ok = t.Run("3. update - validation error", func(t *testing.T) { | ||
|  | 		record.Set("text", "") | ||
|  | 		record.Set("file_many+", f3) | ||
|  | 		record.Set("file_many-", f2.Name) | ||
|  | 
 | ||
|  | 		err := testApp.Save(record) | ||
|  | 
 | ||
|  | 		tests.TestValidationErrors(t, err, []string{"text"}) | ||
|  | 
 | ||
|  | 		raw, _ := json.Marshal(record.GetRaw("file_many")) | ||
|  | 		expectedRaw, _ := json.Marshal([]any{f1.Name, f3}) | ||
|  | 		if !bytes.Equal(expectedRaw, raw) { | ||
|  | 			t.Fatalf("Expected file field value\n%s\ngot\n%s", expectedRaw, raw) | ||
|  | 		} | ||
|  | 
 | ||
|  | 		checkRecordFiles(t, testApp, record, []string{f1.Name, f2.Name}) | ||
|  | 	}) | ||
|  | 	if !ok { | ||
|  | 		return | ||
|  | 	} | ||
|  | 
 | ||
|  | 	ok = t.Run("4. update - fixing the validation error", func(t *testing.T) { | ||
|  | 		record.Set("text", "abc2") | ||
|  | 
 | ||
|  | 		err := testApp.Save(record) | ||
|  | 		if err != nil { | ||
|  | 			t.Fatalf("Expected save to succeed, got %v", err) | ||
|  | 		} | ||
|  | 
 | ||
|  | 		raw, _ := json.Marshal(record.GetRaw("file_many")) | ||
|  | 		expectedRaw, _ := json.Marshal([]any{f1.Name, f3.Name}) | ||
|  | 		if !bytes.Equal(expectedRaw, raw) { | ||
|  | 			t.Fatalf("Expected file field value\n%s\ngot\n%s", expectedRaw, raw) | ||
|  | 		} | ||
|  | 
 | ||
|  | 		checkRecordFiles(t, testApp, record, []string{f1.Name, f3.Name}) | ||
|  | 	}) | ||
|  | 	if !ok { | ||
|  | 		return | ||
|  | 	} | ||
|  | 
 | ||
|  | 	t.Run("5. update - second time update", func(t *testing.T) { | ||
|  | 		record.Set("file_many-", f1.Name) | ||
|  | 		record.Set("file_many+", f4) | ||
|  | 
 | ||
|  | 		err := testApp.Save(record) | ||
|  | 		if err != nil { | ||
|  | 			t.Fatalf("Expected save to succeed, got %v", err) | ||
|  | 		} | ||
|  | 
 | ||
|  | 		raw, _ := json.Marshal(record.GetRaw("file_many")) | ||
|  | 		expectedRaw, _ := json.Marshal([]any{f3.Name, f4.Name}) | ||
|  | 		if !bytes.Equal(expectedRaw, raw) { | ||
|  | 			t.Fatalf("Expected file field value\n%s\ngot\n%s", expectedRaw, raw) | ||
|  | 		} | ||
|  | 
 | ||
|  | 		checkRecordFiles(t, testApp, record, []string{f3.Name, f4.Name}) | ||
|  | 	}) | ||
|  | } | ||
|  | 
 | ||
|  | func TestFileFieldInterceptTx(t *testing.T) { | ||
|  | 	testApp, _ := tests.NewTestApp() | ||
|  | 	defer testApp.Cleanup() | ||
|  | 
 | ||
|  | 	demo1, err := testApp.FindCollectionByNameOrId("demo1") | ||
|  | 	if err != nil { | ||
|  | 		t.Fatal(err) | ||
|  | 	} | ||
|  | 	demo1.Fields.GetByName("text").(*core.TextField).Required = true // trigger validation error
 | ||
|  | 
 | ||
|  | 	f1, err := filesystem.NewFileFromBytes([]byte("test"), "new1.txt") | ||
|  | 	if err != nil { | ||
|  | 		t.Fatal(err) | ||
|  | 	} | ||
|  | 
 | ||
|  | 	f2, err := filesystem.NewFileFromBytes([]byte("test"), "new2.txt") | ||
|  | 	if err != nil { | ||
|  | 		t.Fatal(err) | ||
|  | 	} | ||
|  | 
 | ||
|  | 	f3, err := filesystem.NewFileFromBytes([]byte("test"), "new3.txt") | ||
|  | 	if err != nil { | ||
|  | 		t.Fatal(err) | ||
|  | 	} | ||
|  | 
 | ||
|  | 	f4, err := filesystem.NewFileFromBytes([]byte("test"), "new4.txt") | ||
|  | 	if err != nil { | ||
|  | 		t.Fatal(err) | ||
|  | 	} | ||
|  | 
 | ||
|  | 	var record *core.Record | ||
|  | 
 | ||
|  | 	tx := func(succeed bool) func(txApp core.App) error { | ||
|  | 		var txErr error | ||
|  | 		if !succeed { | ||
|  | 			txErr = errors.New("tx error") | ||
|  | 		} | ||
|  | 
 | ||
|  | 		return func(txApp core.App) error { | ||
|  | 			record = core.NewRecord(demo1) | ||
|  | 			ok := t.Run(fmt.Sprintf("[tx_%v] create with validation error", succeed), func(t *testing.T) { | ||
|  | 				record.Set("text", "") | ||
|  | 				record.Set("file_many", []any{f1, f2}) | ||
|  | 
 | ||
|  | 				err := txApp.Save(record) | ||
|  | 				tests.TestValidationErrors(t, err, []string{"text"}) | ||
|  | 
 | ||
|  | 				checkRecordFiles(t, txApp, record, []string{}) // no uploaded files
 | ||
|  | 			}) | ||
|  | 			if !ok { | ||
|  | 				return txErr | ||
|  | 			} | ||
|  | 
 | ||
|  | 			// ---
 | ||
|  | 
 | ||
|  | 			ok = t.Run(fmt.Sprintf("[tx_%v] create with fixed validation error", succeed), func(t *testing.T) { | ||
|  | 				record.Set("text", "abc") | ||
|  | 
 | ||
|  | 				err = txApp.Save(record) | ||
|  | 				if err != nil { | ||
|  | 					t.Fatalf("Expected save to succeed, got %v", err) | ||
|  | 				} | ||
|  | 
 | ||
|  | 				checkRecordFiles(t, txApp, record, []string{f1.Name, f2.Name}) | ||
|  | 			}) | ||
|  | 			if !ok { | ||
|  | 				return txErr | ||
|  | 			} | ||
|  | 
 | ||
|  | 			// ---
 | ||
|  | 
 | ||
|  | 			ok = t.Run(fmt.Sprintf("[tx_%v] update with validation error", succeed), func(t *testing.T) { | ||
|  | 				record.Set("text", "") | ||
|  | 				record.Set("file_many+", f3) | ||
|  | 				record.Set("file_many-", f2.Name) | ||
|  | 
 | ||
|  | 				err = txApp.Save(record) | ||
|  | 				tests.TestValidationErrors(t, err, []string{"text"}) | ||
|  | 
 | ||
|  | 				raw, _ := json.Marshal(record.GetRaw("file_many")) | ||
|  | 				expectedRaw, _ := json.Marshal([]any{f1.Name, f3}) | ||
|  | 				if !bytes.Equal(expectedRaw, raw) { | ||
|  | 					t.Fatalf("Expected file field value\n%s\ngot\n%s", expectedRaw, raw) | ||
|  | 				} | ||
|  | 
 | ||
|  | 				checkRecordFiles(t, txApp, record, []string{f1.Name, f2.Name}) // no file changes
 | ||
|  | 			}) | ||
|  | 			if !ok { | ||
|  | 				return txErr | ||
|  | 			} | ||
|  | 
 | ||
|  | 			// ---
 | ||
|  | 
 | ||
|  | 			ok = t.Run(fmt.Sprintf("[tx_%v] update with fixed validation error", succeed), func(t *testing.T) { | ||
|  | 				record.Set("text", "abc2") | ||
|  | 
 | ||
|  | 				err = txApp.Save(record) | ||
|  | 				if err != nil { | ||
|  | 					t.Fatalf("Expected save to succeed, got %v", err) | ||
|  | 				} | ||
|  | 
 | ||
|  | 				raw, _ := json.Marshal(record.GetRaw("file_many")) | ||
|  | 				expectedRaw, _ := json.Marshal([]any{f1.Name, f3.Name}) | ||
|  | 				if !bytes.Equal(expectedRaw, raw) { | ||
|  | 					t.Fatalf("Expected file field value\n%s\ngot\n%s", expectedRaw, raw) | ||
|  | 				} | ||
|  | 
 | ||
|  | 				checkRecordFiles(t, txApp, record, []string{f1.Name, f3.Name, f2.Name}) // f2 shouldn't be deleted yet
 | ||
|  | 			}) | ||
|  | 			if !ok { | ||
|  | 				return txErr | ||
|  | 			} | ||
|  | 
 | ||
|  | 			// ---
 | ||
|  | 
 | ||
|  | 			ok = t.Run(fmt.Sprintf("[tx_%v] second time update", succeed), func(t *testing.T) { | ||
|  | 				record.Set("file_many-", f1.Name) | ||
|  | 				record.Set("file_many+", f4) | ||
|  | 
 | ||
|  | 				err := txApp.Save(record) | ||
|  | 				if err != nil { | ||
|  | 					t.Fatalf("Expected save to succeed, got %v", err) | ||
|  | 				} | ||
|  | 
 | ||
|  | 				raw, _ := json.Marshal(record.GetRaw("file_many")) | ||
|  | 				expectedRaw, _ := json.Marshal([]any{f3.Name, f4.Name}) | ||
|  | 				if !bytes.Equal(expectedRaw, raw) { | ||
|  | 					t.Fatalf("Expected file field value\n%s\ngot\n%s", expectedRaw, raw) | ||
|  | 				} | ||
|  | 
 | ||
|  | 				checkRecordFiles(t, txApp, record, []string{f3.Name, f4.Name, f1.Name, f2.Name}) // f1 and f2 shouldn't be deleted yet
 | ||
|  | 			}) | ||
|  | 			if !ok { | ||
|  | 				return txErr | ||
|  | 			} | ||
|  | 
 | ||
|  | 			// ---
 | ||
|  | 
 | ||
|  | 			return txErr | ||
|  | 		} | ||
|  | 	} | ||
|  | 
 | ||
|  | 	// failed transaction
 | ||
|  | 	txErr := testApp.RunInTransaction(tx(false)) | ||
|  | 	if txErr == nil { | ||
|  | 		t.Fatal("Expected transaction error") | ||
|  | 	} | ||
|  | 	// there shouldn't be any fails associated with the record id
 | ||
|  | 	checkRecordFiles(t, testApp, record, []string{}) | ||
|  | 
 | ||
|  | 	txErr = testApp.RunInTransaction(tx(true)) | ||
|  | 	if txErr != nil { | ||
|  | 		t.Fatalf("Expected transaction to succeed, got %v", txErr) | ||
|  | 	} | ||
|  | 	// only the last updated files should remain
 | ||
|  | 	checkRecordFiles(t, testApp, record, []string{f3.Name, f4.Name}) | ||
|  | } | ||
|  | 
 | ||
|  | // -------------------------------------------------------------------
 | ||
|  | 
 | ||
|  | func checkRecordFiles(t *testing.T, testApp core.App, record *core.Record, expectedKeys []string) { | ||
|  | 	fsys, err := testApp.NewFilesystem() | ||
|  | 	if err != nil { | ||
|  | 		t.Fatal(err) | ||
|  | 	} | ||
|  | 	defer fsys.Close() | ||
|  | 
 | ||
|  | 	objects, err := fsys.List(record.BaseFilesPath() + "/") | ||
|  | 	if err != nil { | ||
|  | 		t.Fatal(err) | ||
|  | 	} | ||
|  | 	objectKeys := make([]string, 0, len(objects)) | ||
|  | 	for _, obj := range objects { | ||
|  | 		// exclude thumbs
 | ||
|  | 		if !strings.Contains(obj.Key, "/thumbs_") { | ||
|  | 			objectKeys = append(objectKeys, obj.Key) | ||
|  | 		} | ||
|  | 	} | ||
|  | 
 | ||
|  | 	if len(objectKeys) != len(expectedKeys) { | ||
|  | 		t.Fatalf("Expected files:\n%v\ngot\n%v", expectedKeys, objectKeys) | ||
|  | 	} | ||
|  | 	for _, key := range expectedKeys { | ||
|  | 		fullKey := record.BaseFilesPath() + "/" + key | ||
|  | 		if !slices.Contains(objectKeys, fullKey) { | ||
|  | 			t.Fatalf("Missing expected file key\n%q\nin\n%v", fullKey, objectKeys) | ||
|  | 		} | ||
|  | 	} | ||
|  | } |