github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/web/jobs/jobs_test.go (about) 1 package jobs 2 3 import ( 4 "net/http/httptest" 5 "strings" 6 "testing" 7 "time" 8 9 "github.com/cozy/cozy-stack/model/instance" 10 "github.com/cozy/cozy-stack/model/job" 11 "github.com/cozy/cozy-stack/pkg/config/config" 12 "github.com/cozy/cozy-stack/pkg/consts" 13 "github.com/cozy/cozy-stack/pkg/emailer" 14 "github.com/cozy/cozy-stack/pkg/mail" 15 "github.com/cozy/cozy-stack/tests/testutils" 16 "github.com/cozy/cozy-stack/web/errors" 17 "github.com/cozy/cozy-stack/web/middlewares" 18 "github.com/gavv/httpexpect/v2" 19 "github.com/labstack/echo/v4" 20 "github.com/stretchr/testify/assert" 21 "github.com/stretchr/testify/mock" 22 "github.com/stretchr/testify/require" 23 ) 24 25 func setupRouter(t *testing.T, inst *instance.Instance, emailerSvc emailer.Emailer) *httptest.Server { 26 t.Helper() 27 28 handler := echo.New() 29 handler.HTTPErrorHandler = errors.ErrorHandler 30 group := handler.Group("/jobs", func(next echo.HandlerFunc) echo.HandlerFunc { 31 return func(context echo.Context) error { 32 context.Set("instance", inst) 33 34 tok := middlewares.GetRequestToken(context) 35 // Forcing the token parsing to have the "claims" parameter in the 36 // context (in production, it is done via 37 // middlewares.CheckInstanceBlocked) 38 _, err := middlewares.ParseJWT(context, inst, tok) 39 if err != nil { 40 return err 41 } 42 43 return next(context) 44 } 45 }) 46 47 NewHTTPHandler(emailerSvc).Register(group) 48 ts := httptest.NewServer(handler) 49 t.Cleanup(ts.Close) 50 51 return ts 52 } 53 54 func TestJobs(t *testing.T) { 55 if testing.Short() { 56 t.Skip("an instance is required for this test: test skipped due to the use of --short flag") 57 } 58 59 config.UseTestFile(t) 60 testutils.NeedCouchdb(t) 61 setup := testutils.NewSetup(t, t.Name()) 62 63 job.AddWorker(&job.WorkerConfig{ 64 WorkerType: "print", 65 Concurrency: 4, 66 WorkerFunc: func(ctx *job.TaskContext) error { 67 var msg string 68 if err := ctx.UnmarshalMessage(&msg); err != nil { 69 return err 70 } 71 72 t.Log(msg) 73 74 return nil 75 }, 76 }) 77 78 testInstance := setup.GetTestInstance() 79 80 scope := strings.Join([]string{ 81 consts.Jobs + ":ALL:print:worker", 82 consts.Triggers + ":ALL:print:worker", 83 }, " ") 84 token, _ := testInstance.MakeJWT(consts.CLIAudience, "CLI", scope, 85 "", time.Now()) 86 87 emailerSvc := emailer.NewMock(t) 88 ts := setupRouter(t, testInstance, emailerSvc) 89 ts.Config.Handler.(*echo.Echo).HTTPErrorHandler = errors.ErrorHandler 90 t.Cleanup(ts.Close) 91 92 t.Run("GetQueue", func(t *testing.T) { 93 e := testutils.CreateTestClient(t, ts.URL) 94 95 e.GET("/jobs/queue/print"). 96 WithHeader("Authorization", "Bearer "+token). 97 Expect().Status(200). 98 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 99 Object().Value("data").Array(). 100 Length().IsEqual(0) 101 }) 102 103 t.Run("CreateJob", func(t *testing.T) { 104 e := testutils.CreateTestClient(t, ts.URL) 105 106 obj := e.POST("/jobs/queue/print"). 107 WithHeader("Authorization", "Bearer "+token). 108 WithHeader("Content-Type", "application/json"). 109 WithBytes([]byte(`{ 110 "data": { 111 "attributes": { "arguments": "foobar" } 112 } 113 }`)). 114 Expect().Status(202). 115 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 116 Object() 117 118 attrs := obj.Path("$.data.attributes").Object() 119 attrs.HasValue("worker", "print") 120 attrs.NotContainsKey("manual_execution") 121 }) 122 123 t.Run("CreateJobWithTimeout", func(t *testing.T) { 124 e := testutils.CreateTestClient(t, ts.URL) 125 126 obj := e.POST("/jobs/queue/print"). 127 WithHeader("Authorization", "Bearer "+token). 128 WithHeader("Content-Type", "application/json"). 129 WithBytes([]byte(`{ 130 "data": { 131 "attributes": { 132 "arguments": "foobar", 133 "options": { "timeout": 42 } 134 } 135 } 136 }`)). 137 Expect().Status(202). 138 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 139 Object() 140 141 jobID := obj.Path("$.data.id").String().NotEmpty().Raw() 142 job, err := job.Get(testInstance, jobID) 143 require.NoError(t, err) 144 require.NotNil(t, job.Options) 145 assert.Equal(t, 42*time.Second, job.Options.Timeout) 146 }) 147 148 t.Run("CreateManualJob", func(t *testing.T) { 149 e := testutils.CreateTestClient(t, ts.URL) 150 151 obj := e.POST("/jobs/queue/print"). 152 WithHeader("Authorization", "Bearer "+token). 153 WithHeader("Content-Type", "application/json"). 154 WithBytes([]byte(`{ 155 "data": { 156 "attributes": { 157 "arguments": "foobar", 158 "manual": true 159 } 160 } 161 }`)). 162 Expect().Status(202). 163 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 164 Object() 165 166 attrs := obj.Path("$.data.attributes").Object() 167 attrs.HasValue("worker", "print") 168 attrs.HasValue("manual_execution", true) 169 }) 170 171 t.Run("CreateJobForReservedWorker", func(t *testing.T) { 172 e := testutils.CreateTestClient(t, ts.URL) 173 174 e.POST("/jobs/queue/trash-files"). 175 WithHeader("Authorization", "Bearer "+token). 176 WithHeader("Content-Type", "application/json"). 177 WithBytes([]byte(`{"data": {"attributes": {"arguments": "foobar"}}}`)). 178 Expect().Status(403) 179 }) 180 181 t.Run("CreateJobNotExist", func(t *testing.T) { 182 e := testutils.CreateTestClient(t, ts.URL) 183 184 tokenNone, _ := testInstance.MakeJWT(consts.CLIAudience, "CLI", 185 consts.Jobs+":ALL:none:worker", 186 "", time.Now()) 187 188 e.POST("/jobs/queue/none"). // invalid 189 WithHeader("Authorization", "Bearer "+tokenNone). 190 WithHeader("Content-Type", "application/json"). 191 WithBytes([]byte(`{"data": {"attributes": {"arguments": "foobar"}}}`)). 192 Expect().Status(404) 193 }) 194 195 t.Run("AddGetAndDeleteTriggerAt", func(t *testing.T) { 196 var triggerID string 197 at := time.Now().Add(1100 * time.Millisecond).Format(time.RFC3339) 198 199 t.Run("AddSuccess", func(t *testing.T) { 200 e := testutils.CreateTestClient(t, ts.URL) 201 202 obj := e.POST("/jobs/triggers"). 203 WithHeader("Authorization", "Bearer "+token). 204 WithHeader("Content-Type", "application/json"). 205 WithBytes([]byte(`{ 206 "data": { 207 "attributes": { 208 "type": "@at", 209 "arguments": "` + at + `", 210 "worker": "print", 211 "message": "foo" 212 } 213 } 214 }`)). 215 Expect().Status(201). 216 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 217 Object() 218 219 data := obj.Value("data").Object() 220 triggerID = data.Value("id").String().NotEmpty().Raw() 221 data.HasValue("type", consts.Triggers) 222 223 attrs := data.Value("attributes").Object() 224 attrs.HasValue("arguments", at) 225 attrs.HasValue("worker", "print") 226 }) 227 228 t.Run("AddFailure", func(t *testing.T) { 229 e := testutils.CreateTestClient(t, ts.URL) 230 231 e.POST("/jobs/triggers"). 232 WithHeader("Authorization", "Bearer "+token). 233 WithHeader("Content-Type", "application/json"). 234 WithBytes([]byte(`{ 235 "data": { 236 "attributes": { 237 "type": "@at", 238 "arguments": "garbage", 239 "worker": "print", 240 "message": "foo" 241 } 242 } 243 }`)). 244 Expect().Status(400) 245 }) 246 247 t.Run("GetSuccess", func(t *testing.T) { 248 e := testutils.CreateTestClient(t, ts.URL) 249 250 e.GET("/jobs/triggers/"+triggerID). 251 WithHeader("Authorization", "Bearer "+token). 252 Expect().Status(200) 253 }) 254 255 t.Run("DeleteSuccess", func(t *testing.T) { 256 e := testutils.CreateTestClient(t, ts.URL) 257 258 e.DELETE("/jobs/triggers/"+triggerID). 259 WithHeader("Authorization", "Bearer "+token). 260 Expect().Status(204) 261 }) 262 263 t.Run("GetNotFound", func(t *testing.T) { 264 e := testutils.CreateTestClient(t, ts.URL) 265 266 e.GET("/jobs/triggers/"+triggerID). 267 WithHeader("Authorization", "Bearer "+token). 268 Expect().Status(404) 269 }) 270 }) 271 272 t.Run("AddGetAndDeleteTriggerIn", func(t *testing.T) { 273 var triggerID string 274 275 t.Run("AddSuccess", func(t *testing.T) { 276 e := testutils.CreateTestClient(t, ts.URL) 277 278 obj := e.POST("/jobs/triggers"). 279 WithHeader("Authorization", "Bearer "+token). 280 WithHeader("Content-Type", "application/json"). 281 WithBytes([]byte(`{ 282 "data": { 283 "attributes": { 284 "type": "@in", 285 "arguments": "1s", 286 "worker": "print", 287 "message": "foo" 288 } 289 } 290 }`)). 291 Expect().Status(201). 292 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 293 Object() 294 295 data := obj.Value("data").Object() 296 triggerID = data.Value("id").String().NotEmpty().Raw() 297 data.HasValue("type", consts.Triggers) 298 299 attrs := data.Value("attributes").Object() 300 attrs.HasValue("type", "@in") 301 attrs.HasValue("arguments", "1s") 302 attrs.HasValue("worker", "print") 303 }) 304 305 t.Run("AddFailure", func(t *testing.T) { 306 e := testutils.CreateTestClient(t, ts.URL) 307 308 e.POST("/jobs/triggers"). 309 WithHeader("Authorization", "Bearer "+token). 310 WithHeader("Content-Type", "application/json"). 311 WithBytes([]byte(`{ 312 "data": { 313 "attributes": { 314 "type": "@in", 315 "arguments": "garbage", 316 "worker": "print", 317 "message": "foo" 318 } 319 } 320 }`)). 321 Expect().Status(400) 322 }) 323 324 t.Run("GetSuccess", func(t *testing.T) { 325 e := testutils.CreateTestClient(t, ts.URL) 326 327 e.GET("/jobs/triggers/"+triggerID). 328 WithHeader("Authorization", "Bearer "+token). 329 Expect().Status(200) 330 }) 331 332 t.Run("DeleteSuccess", func(t *testing.T) { 333 e := testutils.CreateTestClient(t, ts.URL) 334 335 e.DELETE("/jobs/triggers/"+triggerID). 336 WithHeader("Authorization", "Bearer "+token). 337 Expect().Status(204) 338 }) 339 340 t.Run("GetNotFound", func(t *testing.T) { 341 e := testutils.CreateTestClient(t, ts.URL) 342 343 e.GET("/jobs/triggers/"+triggerID). 344 WithHeader("Authorization", "Bearer "+token). 345 Expect().Status(404) 346 }) 347 }) 348 349 t.Run("AddGetUpdateAndDeleteTriggerCron", func(t *testing.T) { 350 var triggerID string 351 352 t.Run("AddSuccess", func(t *testing.T) { 353 e := testutils.CreateTestClient(t, ts.URL) 354 355 obj := e.POST("/jobs/triggers"). 356 WithHeader("Authorization", "Bearer "+token). 357 WithHeader("Content-Type", "application/json"). 358 WithBytes([]byte(`{ 359 "data": { 360 "attributes": { 361 "type": "@cron", 362 "arguments": "0 0 0 * * 0", 363 "worker": "print", 364 "message": "foo" 365 } 366 } 367 }`)). 368 Expect().Status(201). 369 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 370 Object() 371 372 data := obj.Value("data").Object() 373 triggerID = data.Value("id").String().NotEmpty().Raw() 374 data.HasValue("type", consts.Triggers) 375 376 attrs := data.Value("attributes").Object() 377 attrs.HasValue("type", "@cron") 378 attrs.HasValue("arguments", "0 0 0 * * 0") 379 attrs.HasValue("worker", "print") 380 }) 381 382 t.Run("PatchArgumentsSuccess", func(t *testing.T) { 383 e := testutils.CreateTestClient(t, ts.URL) 384 385 e.PATCH("/jobs/triggers/"+triggerID). 386 WithHeader("Authorization", "Bearer "+token). 387 WithHeader("Content-Type", "application/json"). 388 WithBytes([]byte(`{ 389 "data": { 390 "attributes": { 391 "arguments": "0 0 0 * * 1" 392 } 393 } 394 }`)). 395 Expect().Status(200) 396 }) 397 398 t.Run("PatchMessageSuccess", func(t *testing.T) { 399 e := testutils.CreateTestClient(t, ts.URL) 400 401 e.PATCH("/jobs/triggers/"+triggerID). 402 WithHeader("Authorization", "Bearer "+token). 403 WithHeader("Content-Type", "application/json"). 404 WithBytes([]byte(`{ 405 "data": { 406 "attributes": { 407 "message": { 408 "folder_to_save": "123" 409 } 410 } 411 } 412 }`)). 413 Expect().Status(200) 414 }) 415 416 t.Run("GetSuccess", func(t *testing.T) { 417 e := testutils.CreateTestClient(t, ts.URL) 418 419 obj := e.GET("/jobs/triggers/"+triggerID). 420 WithHeader("Authorization", "Bearer "+token). 421 Expect().Status(200). 422 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 423 Object() 424 425 data := obj.Value("data").Object() 426 triggerID = data.Value("id").String().NotEmpty().Raw() 427 data.HasValue("type", consts.Triggers) 428 429 attrs := data.Value("attributes").Object() 430 attrs.HasValue("type", "@cron") 431 attrs.HasValue("arguments", "0 0 0 * * 1") 432 attrs.HasValue("worker", "print") 433 }) 434 435 t.Run("DeleteSuccess", func(t *testing.T) { 436 e := testutils.CreateTestClient(t, ts.URL) 437 438 e.DELETE("/jobs/triggers/"+triggerID). 439 WithHeader("Authorization", "Bearer "+token). 440 Expect().Status(204) 441 }) 442 }) 443 444 t.Run("AddTriggerWithMetadata", func(t *testing.T) { 445 var triggerID string 446 447 at := time.Now().Add(1100 * time.Millisecond).Format(time.RFC3339) 448 449 t.Run("AddSuccess", func(t *testing.T) { 450 e := testutils.CreateTestClient(t, ts.URL) 451 452 obj := e.POST("/jobs/triggers"). 453 WithHeader("Authorization", "Bearer "+token). 454 WithHeader("Content-Type", "application/json"). 455 WithBytes([]byte(`{ 456 "data": { 457 "attributes": { 458 "type": "@webhook", 459 "arguments": "` + at + `", 460 "worker": "print", 461 "message": "foo" 462 } 463 } 464 }`)). 465 Expect().Status(201). 466 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 467 Object() 468 469 data := obj.Value("data").Object() 470 triggerID = data.Value("id").String().NotEmpty().Raw() 471 data.HasValue("type", consts.Triggers) 472 data.Path("$.links.webhook").IsEqual("https://" + testInstance.Domain + "/jobs/webhooks/" + triggerID) 473 474 attrs := data.Value("attributes").Object() 475 attrs.HasValue("type", "@webhook") 476 attrs.HasValue("arguments", at) 477 attrs.HasValue("worker", "print") 478 479 metas := attrs.Value("cozyMetadata").Object() 480 metas.HasValue("doctypeVersion", "1") 481 metas.HasValue("metadataVersion", 1) 482 metas.HasValue("createdByApp", "CLI") 483 metas.Value("createdAt").String().AsDateTime(time.RFC3339) 484 metas.Value("updatedAt").String().AsDateTime(time.RFC3339) 485 }) 486 487 t.Run("GetSuccess", func(t *testing.T) { 488 e := testutils.CreateTestClient(t, ts.URL) 489 490 e.GET("/jobs/triggers/"+triggerID). 491 WithHeader("Authorization", "Bearer "+token). 492 Expect().Status(200) 493 }) 494 495 t.Run("DeleteSuccess", func(t *testing.T) { 496 e := testutils.CreateTestClient(t, ts.URL) 497 498 e.DELETE("/jobs/triggers/"+triggerID). 499 WithHeader("Authorization", "Bearer "+token). 500 Expect().Status(204) 501 }) 502 }) 503 504 t.Run("GetAllJobs", func(t *testing.T) { 505 tokenTriggers, _ := testInstance.MakeJWT(consts.CLIAudience, "CLI", consts.Triggers, "", time.Now()) 506 507 t.Run("GetNoJobs", func(t *testing.T) { 508 e := testutils.CreateTestClient(t, ts.URL) 509 510 e.GET("/jobs/triggers"). 511 WithHeader("Authorization", "Bearer "+tokenTriggers). 512 Expect().Status(200). 513 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 514 Object(). 515 Value("data").Array().IsEmpty() 516 }) 517 518 t.Run("CreateAJob", func(t *testing.T) { 519 e := testutils.CreateTestClient(t, ts.URL) 520 521 e.POST("/jobs/triggers"). 522 WithHeader("Authorization", "Bearer "+tokenTriggers). 523 WithHeader("Content-Type", "application/json"). 524 // worker_arguments is deprecated but should still works 525 // we are using it here to check that it still works 526 WithBytes([]byte(`{ 527 "data": { 528 "attributes": { 529 "type": "@in", 530 "arguments": "10s", 531 "worker": "print", 532 "worker_arguments": "foo" 533 } 534 } 535 }`)). 536 Expect().Status(201) 537 }) 538 539 t.Run("GetAllJobs", func(t *testing.T) { 540 e := testutils.CreateTestClient(t, ts.URL) 541 542 obj := e.GET("/jobs/triggers"). 543 WithHeader("Authorization", "Bearer "+tokenTriggers). 544 Expect().Status(200). 545 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 546 Object() 547 548 obj.Value("data").Array().Length().IsEqual(1) 549 elem := obj.Value("data").Array().Value(0).Object() 550 elem.HasValue("type", consts.Triggers) 551 attrs := elem.Value("attributes").Object() 552 attrs.HasValue("type", "@in") 553 attrs.HasValue("arguments", "10s") 554 attrs.HasValue("worker", "print") 555 }) 556 557 t.Run("WithWorkerQueryAndResult", func(t *testing.T) { 558 e := testutils.CreateTestClient(t, ts.URL) 559 560 obj := e.GET("/jobs/triggers"). 561 WithQuery("Worker", "print"). 562 WithHeader("Authorization", "Bearer "+tokenTriggers). 563 Expect().Status(200). 564 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 565 Object() 566 567 obj.Value("data").Array().Length().IsEqual(1) 568 elem := obj.Value("data").Array().Value(0).Object() 569 elem.HasValue("type", consts.Triggers) 570 attrs := elem.Value("attributes").Object() 571 attrs.HasValue("type", "@in") 572 attrs.HasValue("arguments", "10s") 573 attrs.HasValue("worker", "print") 574 }) 575 576 t.Run("WithWorkerQueryAndNoResults", func(t *testing.T) { 577 e := testutils.CreateTestClient(t, ts.URL) 578 579 e.GET("/jobs/triggers"). 580 WithQuery("Worker", "nojobforme"). // no matching job 581 WithHeader("Authorization", "Bearer "+tokenTriggers). 582 Expect().Status(200). 583 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 584 Object().Value("data"). 585 Array().IsEmpty() 586 }) 587 588 t.Run("WithTypeQuery", func(t *testing.T) { 589 e := testutils.CreateTestClient(t, ts.URL) 590 591 obj := e.GET("/jobs/triggers"). 592 WithQuery("Type", "@in"). 593 WithHeader("Authorization", "Bearer "+tokenTriggers). 594 Expect().Status(200). 595 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 596 Object() 597 598 obj.Value("data").Array().Length().IsEqual(1) 599 elem := obj.Value("data").Array().Value(0).Object() 600 elem.HasValue("type", consts.Triggers) 601 attrs := elem.Value("attributes").Object() 602 attrs.HasValue("type", "@in") 603 attrs.HasValue("arguments", "10s") 604 attrs.HasValue("worker", "print") 605 }) 606 }) 607 608 t.Run("ClientJobs", func(t *testing.T) { 609 var triggerID string 610 var jobID string 611 612 scope := consts.Jobs + " " + consts.Triggers 613 token, _ := testInstance.MakeJWT(consts.CLIAudience, "CLI", scope, "", time.Now()) 614 615 t.Run("CreateAClientJob", func(t *testing.T) { 616 e := testutils.CreateTestClient(t, ts.URL) 617 618 obj := e.POST("/jobs/triggers"). 619 WithHeader("Authorization", "Bearer "+token). 620 WithHeader("Content-Type", "application/json"). 621 WithBytes([]byte(`{ 622 "data": { 623 "attributes": { 624 "type": "@client", 625 "message": "foobar" 626 } 627 } 628 }`)). 629 Expect().Status(201). 630 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 631 Object() 632 633 triggerID = obj.Path("$.data.id").String().NotEmpty().Raw() 634 635 attrs := obj.Path("$.data.attributes").Object() 636 attrs.HasValue("type", "@client") 637 attrs.HasValue("worker", "client") 638 }) 639 640 t.Run("LaunchAClientJob", func(t *testing.T) { 641 e := testutils.CreateTestClient(t, ts.URL) 642 643 obj := e.POST("/jobs/triggers/"+triggerID+"/launch"). 644 WithHeader("Authorization", "Bearer "+token). 645 Expect().Status(201). 646 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 647 Object() 648 649 jobID = obj.Path("$.data.id").String().NotEmpty().Raw() 650 651 obj.Path("$.data.type").IsEqual(consts.Jobs) 652 attrs := obj.Path("$.data.attributes").Object() 653 attrs.HasValue("worker", "client") 654 attrs.HasValue("state", job.Running) 655 attrs.Value("queued_at").String().AsDateTime(time.RFC3339) 656 attrs.Value("started_at").String().AsDateTime(time.RFC3339) 657 }) 658 659 t.Run("PatchAClientJob", func(t *testing.T) { 660 e := testutils.CreateTestClient(t, ts.URL) 661 662 obj := e.PATCH("/jobs/"+jobID). 663 WithHeader("Authorization", "Bearer "+token). 664 WithHeader("Content-Type", "application/json"). 665 WithBytes([]byte(`{ 666 "data": { 667 "attributes": { 668 "state": "errored", 669 "error": "LOGIN_FAILED" 670 } 671 } 672 }`)). 673 Expect().Status(200). 674 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 675 Object() 676 677 obj.Path("$.data.type").IsEqual(consts.Jobs) 678 attrs := obj.Path("$.data.attributes").Object() 679 attrs.HasValue("worker", "client") 680 attrs.HasValue("state", job.Errored) 681 attrs.HasValue("error", "LOGIN_FAILED") 682 attrs.Value("queued_at").String().AsDateTime(time.RFC3339) 683 attrs.Value("started_at").String().AsDateTime(time.RFC3339) 684 attrs.Value("finished_at").String().AsDateTime(time.RFC3339) 685 }) 686 }) 687 688 t.Run("SendCampaignEmail", func(t *testing.T) { 689 e := testutils.CreateTestClient(t, ts.URL) 690 691 t.Run("WithoutPermissions", func(t *testing.T) { 692 e.POST("/jobs/campaign-emails"). 693 WithHeader("Authorization", "Bearer "+token). 694 WithHeader("Content-Type", "application/json"). 695 WithBytes([]byte(`{ 696 "data": { 697 "attributes": { 698 "arguments": { 699 "subject": "Some subject", 700 "parts": [ 701 { "body": "Some content", "type": "text/plain" } 702 ] 703 } 704 } 705 } 706 }`)).Expect().Status(403) 707 708 emailerSvc.AssertNumberOfCalls(t, "SendCampaignEmail", 0) 709 }) 710 711 t.Run("WithProperArguments", func(t *testing.T) { 712 emailerSvc. 713 On("SendCampaignEmail", testInstance, mock.Anything). 714 Return(nil). 715 Once() 716 717 scope := strings.Join([]string{ 718 consts.Jobs + ":ALL:sendmail:worker", 719 }, " ") 720 token, _ := testInstance.MakeJWT(consts.CLIAudience, "CLI", scope, 721 "", time.Now()) 722 723 e.POST("/jobs/campaign-emails"). 724 WithHeader("Authorization", "Bearer "+token). 725 WithHeader("Content-Type", "application/json"). 726 WithBytes([]byte(`{ 727 "data": { 728 "attributes": { 729 "arguments": { 730 "subject": "Some subject", 731 "parts": [ 732 { "body": "Some content", "type": "text/plain" } 733 ] 734 } 735 } 736 } 737 }`)).Expect().Status(204) 738 739 emailerSvc.AssertCalled(t, "SendCampaignEmail", testInstance, &emailer.CampaignEmailCmd{ 740 Subject: "Some subject", 741 Parts: []mail.Part{ 742 {Body: "Some content", Type: "text/plain"}, 743 }, 744 }) 745 }) 746 747 t.Run("WithMissingSubject", func(t *testing.T) { 748 emailerSvc. 749 On("SendCampaignEmail", testInstance, mock.Anything). 750 Return(emailer.ErrMissingSubject). 751 Once() 752 753 scope := strings.Join([]string{ 754 consts.Jobs + ":ALL:sendmail:worker", 755 }, " ") 756 token, _ := testInstance.MakeJWT(consts.CLIAudience, "CLI", scope, 757 "", time.Now()) 758 759 e.POST("/jobs/campaign-emails"). 760 WithHeader("Authorization", "Bearer "+token). 761 WithHeader("Content-Type", "application/json"). 762 WithBytes([]byte(`{ 763 "data": { 764 "attributes": { 765 "arguments": { 766 "parts": [ 767 { "body": "Some content", "type": "text/plain" } 768 ] 769 } 770 } 771 } 772 }`)).Expect().Status(400) 773 774 emailerSvc.AssertCalled(t, "SendCampaignEmail", testInstance, &emailer.CampaignEmailCmd{ 775 Parts: []mail.Part{ 776 {Body: "Some content", Type: "text/plain"}, 777 }, 778 }) 779 }) 780 }) 781 }