github.com/cloudreve/Cloudreve/v3@v3.0.0-20240224133659-3edb00a6484c/pkg/filesystem/file_test.go (about) 1 package filesystem 2 3 import ( 4 "context" 5 "errors" 6 "os" 7 "testing" 8 9 "github.com/DATA-DOG/go-sqlmock" 10 model "github.com/cloudreve/Cloudreve/v3/models" 11 "github.com/cloudreve/Cloudreve/v3/pkg/auth" 12 "github.com/cloudreve/Cloudreve/v3/pkg/cache" 13 "github.com/cloudreve/Cloudreve/v3/pkg/filesystem/fsctx" 14 "github.com/cloudreve/Cloudreve/v3/pkg/serializer" 15 "github.com/cloudreve/Cloudreve/v3/pkg/util" 16 "github.com/jinzhu/gorm" 17 "github.com/stretchr/testify/assert" 18 ) 19 20 func TestFileSystem_AddFile(t *testing.T) { 21 asserts := assert.New(t) 22 file := fsctx.FileStream{ 23 Size: 5, 24 Name: "1.png", 25 SavePath: "/Uploads/1_sad.png", 26 } 27 folder := model.Folder{ 28 Model: gorm.Model{ 29 ID: 1, 30 }, 31 } 32 fs := FileSystem{ 33 User: &model.User{ 34 Model: gorm.Model{ 35 ID: 1, 36 }, 37 Policy: model.Policy{ 38 Type: "cos", 39 Model: gorm.Model{ 40 ID: 1, 41 }, 42 }, 43 }, 44 Policy: &model.Policy{Type: "cos"}, 45 } 46 47 _, err := fs.AddFile(context.Background(), &folder, &file) 48 49 asserts.Error(err) 50 51 mock.ExpectBegin() 52 mock.ExpectExec("INSERT(.+)").WillReturnResult(sqlmock.NewResult(1, 1)) 53 mock.ExpectExec("UPDATE(.+)storage(.+)").WillReturnResult(sqlmock.NewResult(1, 1)) 54 mock.ExpectCommit() 55 56 f, err := fs.AddFile(context.Background(), &folder, &file) 57 58 asserts.NoError(err) 59 asserts.NoError(mock.ExpectationsWereMet()) 60 asserts.Equal("/Uploads/1_sad.png", f.SourceName) 61 62 // 前置钩子执行失败 63 { 64 hookExecuted := false 65 fs.Use("BeforeAddFile", func(ctx context.Context, fs *FileSystem, file fsctx.FileHeader) error { 66 hookExecuted = true 67 return errors.New("error") 68 }) 69 f, err := fs.AddFile(context.Background(), &folder, &file) 70 asserts.Error(err) 71 asserts.Nil(f) 72 asserts.True(hookExecuted) 73 } 74 75 // 后置钩子执行失败 76 { 77 hookExecuted := false 78 mock.ExpectBegin() 79 mock.ExpectExec("INSERT(.+)").WillReturnError(errors.New("error")) 80 mock.ExpectRollback() 81 fs.Hooks = map[string][]Hook{} 82 fs.Use("AfterValidateFailed", func(ctx context.Context, fs *FileSystem, file fsctx.FileHeader) error { 83 hookExecuted = true 84 return errors.New("error") 85 }) 86 f, err := fs.AddFile(context.Background(), &folder, &file) 87 asserts.Error(err) 88 asserts.Nil(f) 89 asserts.True(hookExecuted) 90 asserts.NoError(mock.ExpectationsWereMet()) 91 } 92 } 93 94 func TestFileSystem_GetContent(t *testing.T) { 95 asserts := assert.New(t) 96 ctx := context.Background() 97 fs := FileSystem{ 98 User: &model.User{ 99 Model: gorm.Model{ 100 ID: 1, 101 }, 102 Policy: model.Policy{ 103 Model: gorm.Model{ 104 ID: 1, 105 }, 106 }, 107 }, 108 } 109 110 // 文件不存在 111 rs, err := fs.GetContent(ctx, 1) 112 asserts.Equal(ErrObjectNotExist, err) 113 asserts.Nil(rs) 114 fs.CleanTargets() 115 116 // 未知存储策略 117 file, err := os.Create(util.RelativePath("TestFileSystem_GetContent.txt")) 118 asserts.NoError(err) 119 _ = file.Close() 120 121 cache.Deletes([]string{"1"}, "policy_") 122 mock.ExpectQuery("SELECT(.+)").WillReturnRows(sqlmock.NewRows([]string{"id", "source_name", "policy_id"}).AddRow(1, "TestFileSystem_GetContent.txt", 1)) 123 mock.ExpectQuery("SELECT(.+)poli(.+)").WillReturnRows(sqlmock.NewRows([]string{"id", "type"}).AddRow(1, "unknown")) 124 125 rs, err = fs.GetContent(ctx, 1) 126 asserts.Error(err) 127 asserts.NoError(mock.ExpectationsWereMet()) 128 fs.CleanTargets() 129 130 // 打开文件失败 131 cache.Deletes([]string{"1"}, "policy_") 132 mock.ExpectQuery("SELECT(.+)").WillReturnRows(sqlmock.NewRows([]string{"id", "source_name", "policy_id"}).AddRow(1, "TestFileSystem_GetContent2.txt", 1)) 133 mock.ExpectQuery("SELECT(.+)poli(.+)").WillReturnRows(sqlmock.NewRows([]string{"id", "type", "source_name"}).AddRow(1, "local", "not exist")) 134 135 rs, err = fs.GetContent(ctx, 1) 136 asserts.Equal(serializer.CodeIOFailed, err.(serializer.AppError).Code) 137 asserts.NoError(mock.ExpectationsWereMet()) 138 fs.CleanTargets() 139 140 // 打开成功 141 cache.Deletes([]string{"1"}, "policy_") 142 mock.ExpectQuery("SELECT(.+)").WillReturnRows(sqlmock.NewRows([]string{"id", "source_name", "policy_id", "source_name"}).AddRow(1, "TestFileSystem_GetContent.txt", 1, "TestFileSystem_GetContent.txt")) 143 mock.ExpectQuery("SELECT(.+)poli(.+)").WillReturnRows(sqlmock.NewRows([]string{"id", "type"}).AddRow(1, "local")) 144 145 rs, err = fs.GetContent(ctx, 1) 146 asserts.NoError(err) 147 asserts.NoError(mock.ExpectationsWereMet()) 148 } 149 150 func TestFileSystem_GetDownloadContent(t *testing.T) { 151 asserts := assert.New(t) 152 ctx := context.Background() 153 fs := FileSystem{ 154 User: &model.User{ 155 Model: gorm.Model{ 156 ID: 1, 157 }, 158 Policy: model.Policy{ 159 Model: gorm.Model{ 160 ID: 599, 161 }, 162 }, 163 }, 164 } 165 file, err := os.Create(util.RelativePath("TestFileSystem_GetDownloadContent.txt")) 166 asserts.NoError(err) 167 _ = file.Close() 168 169 cache.Deletes([]string{"599"}, "policy_") 170 mock.ExpectQuery("SELECT(.+)").WillReturnRows(sqlmock.NewRows([]string{"id", "name", "policy_id", "source_name"}).AddRow(1, "TestFileSystem_GetDownloadContent.txt", 599, "TestFileSystem_GetDownloadContent.txt")) 171 mock.ExpectQuery("SELECT(.+)poli(.+)").WillReturnRows(sqlmock.NewRows([]string{"id", "type"}).AddRow(1, "local")) 172 173 // 无限速 174 cache.Deletes([]string{"599"}, "policy_") 175 _, err = fs.GetDownloadContent(ctx, 1) 176 asserts.NoError(err) 177 asserts.NoError(mock.ExpectationsWereMet()) 178 fs.CleanTargets() 179 180 // 有限速 181 cache.Deletes([]string{"599"}, "policy_") 182 mock.ExpectQuery("SELECT(.+)").WillReturnRows(sqlmock.NewRows([]string{"id", "name", "policy_id", "source_name"}).AddRow(1, "TestFileSystem_GetDownloadContent.txt", 599, "TestFileSystem_GetDownloadContent.txt")) 183 mock.ExpectQuery("SELECT(.+)poli(.+)").WillReturnRows(sqlmock.NewRows([]string{"id", "type"}).AddRow(1, "local")) 184 185 fs.User.Group.SpeedLimit = 1 186 _, err = fs.GetDownloadContent(ctx, 1) 187 asserts.NoError(err) 188 asserts.NoError(mock.ExpectationsWereMet()) 189 } 190 191 func TestFileSystem_GroupFileByPolicy(t *testing.T) { 192 asserts := assert.New(t) 193 ctx := context.Background() 194 files := []model.File{ 195 model.File{ 196 PolicyID: 1, 197 Name: "1_1.txt", 198 }, 199 model.File{ 200 PolicyID: 2, 201 Name: "2_1.txt", 202 }, 203 model.File{ 204 PolicyID: 3, 205 Name: "3_1.txt", 206 }, 207 model.File{ 208 PolicyID: 2, 209 Name: "2_2.txt", 210 }, 211 model.File{ 212 PolicyID: 1, 213 Name: "1_2.txt", 214 }, 215 } 216 fs := FileSystem{} 217 policyGroup := fs.GroupFileByPolicy(ctx, files) 218 asserts.Equal(map[uint][]*model.File{ 219 1: {&files[0], &files[4]}, 220 2: {&files[1], &files[3]}, 221 3: {&files[2]}, 222 }, policyGroup) 223 } 224 225 func TestFileSystem_deleteGroupedFile(t *testing.T) { 226 asserts := assert.New(t) 227 ctx := context.Background() 228 fs := FileSystem{} 229 files := []model.File{ 230 { 231 PolicyID: 1, 232 Name: "1_1.txt", 233 SourceName: "1_1.txt", 234 Policy: model.Policy{Model: gorm.Model{ID: 1}, Type: "local"}, 235 }, 236 { 237 PolicyID: 2, 238 Name: "2_1.txt", 239 SourceName: "2_1.txt", 240 Policy: model.Policy{Model: gorm.Model{ID: 1}, Type: "local"}, 241 }, 242 { 243 PolicyID: 3, 244 Name: "3_1.txt", 245 SourceName: "3_1.txt", 246 Policy: model.Policy{Model: gorm.Model{ID: 1}, Type: "local"}, 247 }, 248 { 249 PolicyID: 2, 250 Name: "2_2.txt", 251 SourceName: "2_2.txt", 252 Policy: model.Policy{Model: gorm.Model{ID: 1}, Type: "local"}, 253 }, 254 { 255 PolicyID: 1, 256 Name: "1_2.txt", 257 SourceName: "1_2.txt", 258 Policy: model.Policy{Model: gorm.Model{ID: 1}, Type: "local"}, 259 }, 260 } 261 262 // 全部不存在 263 { 264 failed := fs.deleteGroupedFile(ctx, fs.GroupFileByPolicy(ctx, files)) 265 asserts.Equal(map[uint][]string{ 266 1: {}, 267 2: {}, 268 3: {}, 269 }, failed) 270 } 271 // 部分不存在 272 { 273 file, err := os.Create(util.RelativePath("1_1.txt")) 274 asserts.NoError(err) 275 _ = file.Close() 276 failed := fs.deleteGroupedFile(ctx, fs.GroupFileByPolicy(ctx, files)) 277 asserts.Equal(map[uint][]string{ 278 1: {}, 279 2: {}, 280 3: {}, 281 }, failed) 282 } 283 // 部分失败,包含整组未知存储策略导致的失败 284 { 285 file, err := os.Create(util.RelativePath("1_1.txt")) 286 asserts.NoError(err) 287 _ = file.Close() 288 289 files[1].Policy.Type = "unknown" 290 files[3].Policy.Type = "unknown" 291 failed := fs.deleteGroupedFile(ctx, fs.GroupFileByPolicy(ctx, files)) 292 asserts.Equal(map[uint][]string{ 293 1: {}, 294 2: {"2_1.txt", "2_2.txt"}, 295 3: {}, 296 }, failed) 297 } 298 // 包含上传会话文件 299 { 300 sessionID := "session" 301 cache.Set(UploadSessionCachePrefix+sessionID, serializer.UploadSession{Key: sessionID}, 0) 302 files[1].Policy.Type = "local" 303 files[3].Policy.Type = "local" 304 files[0].UploadSessionID = &sessionID 305 failed := fs.deleteGroupedFile(ctx, fs.GroupFileByPolicy(ctx, files)) 306 asserts.Equal(map[uint][]string{ 307 1: {}, 308 2: {}, 309 3: {}, 310 }, failed) 311 _, ok := cache.Get(UploadSessionCachePrefix + sessionID) 312 asserts.False(ok) 313 } 314 315 // 包含缩略图 316 { 317 files[0].MetadataSerialized = map[string]string{ 318 model.ThumbSidecarMetadataKey: "1", 319 } 320 failed := fs.deleteGroupedFile(ctx, fs.GroupFileByPolicy(ctx, files)) 321 asserts.Equal(map[uint][]string{ 322 1: {}, 323 2: {}, 324 3: {}, 325 }, failed) 326 } 327 } 328 329 func TestFileSystem_GetSource(t *testing.T) { 330 asserts := assert.New(t) 331 ctx := context.Background() 332 auth.General = auth.HMACAuth{SecretKey: []byte("123")} 333 334 // 正常 335 { 336 fs := FileSystem{ 337 User: &model.User{Model: gorm.Model{ID: 1}}, 338 } 339 // 清空缓存 340 err := cache.Deletes([]string{"siteURL"}, "setting_") 341 asserts.NoError(err) 342 // 查找文件 343 mock.ExpectQuery("SELECT(.+)"). 344 WithArgs(2, 1). 345 WillReturnRows( 346 sqlmock.NewRows([]string{"id", "policy_id", "source_name"}). 347 AddRow(2, 35, "1.txt"), 348 ) 349 // 查找上传策略 350 mock.ExpectQuery("SELECT(.+)"). 351 WillReturnRows( 352 sqlmock.NewRows([]string{"id", "type", "is_origin_link_enable"}). 353 AddRow(35, "local", true), 354 ) 355 356 sourceURL, err := fs.GetSource(ctx, 2) 357 asserts.NoError(mock.ExpectationsWereMet()) 358 asserts.NoError(err) 359 asserts.NotEmpty(sourceURL) 360 fs.CleanTargets() 361 } 362 363 // 文件不存在 364 { 365 fs := FileSystem{ 366 User: &model.User{Model: gorm.Model{ID: 1}}, 367 } 368 // 清空缓存 369 err := cache.Deletes([]string{"siteURL"}, "setting_") 370 asserts.NoError(err) 371 // 查找文件 372 mock.ExpectQuery("SELECT(.+)"). 373 WithArgs(2, 1). 374 WillReturnRows( 375 sqlmock.NewRows([]string{"id", "policy_id", "source_name"}), 376 ) 377 378 sourceURL, err := fs.GetSource(ctx, 2) 379 asserts.NoError(mock.ExpectationsWereMet()) 380 asserts.Error(err) 381 asserts.Equal(ErrObjectNotExist.Code, err.(serializer.AppError).Code) 382 asserts.Empty(sourceURL) 383 fs.CleanTargets() 384 } 385 386 // 未知上传策略 387 { 388 fs := FileSystem{ 389 User: &model.User{Model: gorm.Model{ID: 1}}, 390 } 391 // 清空缓存 392 err := cache.Deletes([]string{"siteURL"}, "setting_") 393 asserts.NoError(err) 394 // 查找文件 395 mock.ExpectQuery("SELECT(.+)"). 396 WithArgs(2, 1). 397 WillReturnRows( 398 sqlmock.NewRows([]string{"id", "policy_id", "source_name"}). 399 AddRow(2, 36, "1.txt"), 400 ) 401 // 查找上传策略 402 mock.ExpectQuery("SELECT(.+)"). 403 WillReturnRows( 404 sqlmock.NewRows([]string{"id", "type", "is_origin_link_enable"}). 405 AddRow(36, "?", true), 406 ) 407 408 sourceURL, err := fs.GetSource(ctx, 2) 409 asserts.NoError(mock.ExpectationsWereMet()) 410 asserts.Error(err) 411 asserts.Empty(sourceURL) 412 fs.CleanTargets() 413 } 414 415 // 不允许获取外链 416 { 417 fs := FileSystem{ 418 User: &model.User{Model: gorm.Model{ID: 1}}, 419 } 420 // 清空缓存 421 err := cache.Deletes([]string{"siteURL"}, "setting_") 422 asserts.NoError(err) 423 // 查找文件 424 mock.ExpectQuery("SELECT(.+)"). 425 WithArgs(2, 1). 426 WillReturnRows( 427 sqlmock.NewRows([]string{"id", "policy_id", "source_name"}). 428 AddRow(2, 37, "1.txt"), 429 ) 430 // 查找上传策略 431 mock.ExpectQuery("SELECT(.+)"). 432 WillReturnRows( 433 sqlmock.NewRows([]string{"id", "type", "is_origin_link_enable"}). 434 AddRow(37, "local", false), 435 ) 436 437 sourceURL, err := fs.GetSource(ctx, 2) 438 asserts.NoError(mock.ExpectationsWereMet()) 439 asserts.Error(err) 440 asserts.Equal(serializer.CodePolicyNotAllowed, err.(serializer.AppError).Code) 441 asserts.Empty(sourceURL) 442 fs.CleanTargets() 443 } 444 } 445 446 func TestFileSystem_GetDownloadURL(t *testing.T) { 447 asserts := assert.New(t) 448 ctx := context.Background() 449 fs := FileSystem{ 450 User: &model.User{Model: gorm.Model{ID: 1}}, 451 } 452 auth.General = auth.HMACAuth{SecretKey: []byte("123")} 453 454 // 正常 455 { 456 err := cache.Deletes([]string{"35"}, "policy_") 457 cache.Set("setting_download_timeout", "20", 0) 458 cache.Set("setting_siteURL", "https://cloudreve.org", 0) 459 asserts.NoError(err) 460 // 查找文件 461 mock.ExpectQuery("SELECT(.+)").WillReturnRows(sqlmock.NewRows([]string{"id", "name", "policy_id"}).AddRow(1, "1.txt", 35)) 462 // 查找上传策略 463 mock.ExpectQuery("SELECT(.+)"). 464 WillReturnRows( 465 sqlmock.NewRows([]string{"id", "type", "is_origin_link_enable"}). 466 AddRow(35, "local", true), 467 ) 468 // 相关设置 469 downloadURL, err := fs.GetDownloadURL(ctx, 1, "download_timeout") 470 asserts.NoError(mock.ExpectationsWereMet()) 471 asserts.NoError(err) 472 asserts.NotEmpty(downloadURL) 473 fs.CleanTargets() 474 } 475 476 // 文件不存在 477 { 478 err := cache.Deletes([]string{"siteURL"}, "setting_") 479 err = cache.Deletes([]string{"35"}, "policy_") 480 err = cache.Deletes([]string{"download_timeout"}, "setting_") 481 asserts.NoError(err) 482 // 查找文件 483 mock.ExpectQuery("SELECT(.+)").WillReturnRows(sqlmock.NewRows([]string{"id", "name", "policy_id"})) 484 485 downloadURL, err := fs.GetDownloadURL(ctx, 1, "download_timeout") 486 asserts.NoError(mock.ExpectationsWereMet()) 487 asserts.Error(err) 488 asserts.Empty(downloadURL) 489 fs.CleanTargets() 490 } 491 492 // 未知存储策略 493 { 494 err := cache.Deletes([]string{"siteURL"}, "setting_") 495 err = cache.Deletes([]string{"35"}, "policy_") 496 err = cache.Deletes([]string{"download_timeout"}, "setting_") 497 asserts.NoError(err) 498 // 查找文件 499 mock.ExpectQuery("SELECT(.+)").WillReturnRows(sqlmock.NewRows([]string{"id", "name", "policy_id"}).AddRow(1, "1.txt", 35)) 500 // 查找上传策略 501 mock.ExpectQuery("SELECT(.+)"). 502 WillReturnRows( 503 sqlmock.NewRows([]string{"id", "type", "is_origin_link_enable"}). 504 AddRow(35, "unknown", true), 505 ) 506 507 downloadURL, err := fs.GetDownloadURL(ctx, 1, "download_timeout") 508 asserts.NoError(mock.ExpectationsWereMet()) 509 asserts.Error(err) 510 asserts.Empty(downloadURL) 511 fs.CleanTargets() 512 } 513 } 514 515 func TestFileSystem_GetPhysicalFileContent(t *testing.T) { 516 asserts := assert.New(t) 517 ctx := context.Background() 518 fs := FileSystem{ 519 User: &model.User{}, 520 } 521 522 // 文件不存在 523 { 524 rs, err := fs.GetPhysicalFileContent(ctx, "not_exist.txt") 525 asserts.Error(err) 526 asserts.Nil(rs) 527 } 528 529 // 成功 530 { 531 testFile, err := os.Create(util.RelativePath("GetPhysicalFileContent.txt")) 532 asserts.NoError(err) 533 asserts.NoError(testFile.Close()) 534 535 rs, err := fs.GetPhysicalFileContent(ctx, "GetPhysicalFileContent.txt") 536 asserts.NoError(err) 537 asserts.NoError(rs.Close()) 538 asserts.NotNil(rs) 539 } 540 } 541 542 func TestFileSystem_Preview(t *testing.T) { 543 asserts := assert.New(t) 544 ctx := context.Background() 545 546 // 文件不存在 547 { 548 fs := FileSystem{ 549 User: &model.User{}, 550 } 551 mock.ExpectQuery("SELECT(.+)").WillReturnRows(sqlmock.NewRows([]string{"id"})) 552 resp, err := fs.Preview(ctx, 1, false) 553 asserts.NoError(mock.ExpectationsWereMet()) 554 asserts.Error(err) 555 asserts.Nil(resp) 556 } 557 558 // 直接返回文件内容,找不到文件 559 { 560 fs := FileSystem{ 561 User: &model.User{}, 562 } 563 fs.FileTarget = []model.File{ 564 { 565 SourceName: "tests/no.txt", 566 PolicyID: 1, 567 Policy: model.Policy{ 568 Model: gorm.Model{ID: 1}, 569 Type: "local", 570 }, 571 }, 572 } 573 resp, err := fs.Preview(ctx, 1, false) 574 asserts.Error(err) 575 asserts.Nil(resp) 576 } 577 578 // 直接返回文件内容 579 { 580 fs := FileSystem{ 581 User: &model.User{}, 582 } 583 fs.FileTarget = []model.File{ 584 { 585 SourceName: "tests/file1.txt", 586 PolicyID: 1, 587 Policy: model.Policy{ 588 Model: gorm.Model{ID: 1}, 589 Type: "local", 590 }, 591 }, 592 } 593 resp, err := fs.Preview(ctx, 1, false) 594 asserts.Error(err) 595 asserts.Nil(resp) 596 } 597 598 // 需要重定向,成功 599 { 600 fs := FileSystem{ 601 User: &model.User{}, 602 } 603 fs.FileTarget = []model.File{ 604 { 605 SourceName: "tests/file1.txt", 606 PolicyID: 1, 607 Policy: model.Policy{ 608 Model: gorm.Model{ID: 1}, 609 Type: "remote", 610 }, 611 }, 612 } 613 asserts.NoError(cache.Set("setting_preview_timeout", "233", 0)) 614 resp, err := fs.Preview(ctx, 1, false) 615 asserts.NoError(err) 616 asserts.NotNil(resp) 617 asserts.True(resp.Redirect) 618 } 619 620 // 文本文件,大小超出限制 621 { 622 fs := FileSystem{ 623 User: &model.User{}, 624 } 625 fs.FileTarget = []model.File{ 626 { 627 SourceName: "tests/file1.txt", 628 PolicyID: 1, 629 Policy: model.Policy{ 630 Model: gorm.Model{ID: 1}, 631 Type: "remote", 632 }, 633 Size: 11, 634 }, 635 } 636 asserts.NoError(cache.Set("setting_maxEditSize", "10", 0)) 637 resp, err := fs.Preview(ctx, 1, true) 638 asserts.Equal(ErrFileSizeTooBig, err) 639 asserts.Nil(resp) 640 } 641 } 642 643 func TestFileSystem_ResetFileIDIfNotExist(t *testing.T) { 644 asserts := assert.New(t) 645 ctx := context.WithValue(context.Background(), fsctx.LimitParentCtx, &model.Folder{Model: gorm.Model{ID: 1}}) 646 fs := FileSystem{ 647 FileTarget: []model.File{ 648 { 649 FolderID: 2, 650 }, 651 }, 652 } 653 asserts.Equal(ErrObjectNotExist, fs.resetFileIDIfNotExist(ctx, 1)) 654 } 655 656 func TestFileSystem_Search(t *testing.T) { 657 asserts := assert.New(t) 658 ctx := context.Background() 659 fs := &FileSystem{ 660 User: &model.User{}, 661 } 662 fs.User.ID = 1 663 664 mock.ExpectQuery("SELECT(.+)").WithArgs(1, "k1", "k2").WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(1)) 665 res, err := fs.Search(ctx, "k1", "k2") 666 asserts.NoError(mock.ExpectationsWereMet()) 667 asserts.NoError(err) 668 asserts.Len(res, 1) 669 }