go-transaction-manager
794 строки · 19.9 Кб
1package manager
2
3import (
4"context"
5"errors"
6"sync"
7"testing"
8"time"
9
10"github.com/golang/mock/gomock"
11"github.com/stretchr/testify/assert"
12"github.com/stretchr/testify/require"
13
14trmcontext "github.com/avito-tech/go-transaction-manager/trm/v2/context"
15mock2 "github.com/avito-tech/go-transaction-manager/trm/v2/drivers/mock"
16mock_log "github.com/avito-tech/go-transaction-manager/trm/v2/manager/mock"
17"github.com/avito-tech/go-transaction-manager/trm/v2/settings"
18
19"github.com/avito-tech/go-transaction-manager/trm/v2"
20)
21
22func Test_transactionManager_Do(t *testing.T) {
23t.Parallel()
24
25type fields struct {
26factory trm.TrFactory
27settings trm.Settings
28log logger
29}
30
31type args struct {
32ctx context.Context
33settings trm.Settings
34nestedSettings trm.Settings
35}
36
37ctxManager := trmcontext.DefaultManager
38
39emptyFactory := func(ctx context.Context, _ trm.Settings) (context.Context, trm.Transaction, error) {
40return ctx, nil, nil
41}
42
43tests := map[string]struct {
44args args
45fields func(t *testing.T, ctrl *gomock.Controller, a args) fields
46wantErr assert.ErrorAssertionFunc
47wantEmptyCtx bool
48}{
49"PropagationRequired_success_commit": {
50args: args{
51ctx: context.Background(),
52settings: settings.Must(),
53nestedSettings: settings.Must(),
54},
55fields: func(_ *testing.T, ctrl *gomock.Controller, a args) fields {
56f := fields{
57factory: func(ctx context.Context, _ trm.Settings) (context.Context, trm.Transaction, error) {
58tx := mock2.NewMockTransaction(ctrl)
59
60tx.EXPECT().IsActive().Return(true)
61tx.EXPECT().Commit(gomock.Any()).Return(nil)
62
63return ctx, tx, nil
64},
65settings: a.settings,
66log: mock_log.NewMocklogger(ctrl),
67}
68
69return f
70},
71wantErr: assert.NoError,
72},
73"PropagationNested_success_commit": {
74args: args{
75ctx: context.Background(),
76settings: settings.Must(
77settings.WithPropagation(trm.PropagationNested),
78),
79nestedSettings: settings.Must(
80settings.WithPropagation(trm.PropagationNested),
81),
82},
83fields: func(_ *testing.T, ctrl *gomock.Controller, a args) fields {
84f := fields{
85factory: func(ctx context.Context, _ trm.Settings) (context.Context, trm.Transaction, error) {
86txSP := mock2.NewMocktransactionWithSP(ctrl)
87
88txSP.EXPECT().IsActive().Return(true).Times(2)
89txSP.EXPECT().Begin(gomock.Any(), a.settings).Return(ctx, txSP, nil)
90txSP.EXPECT().Commit(gomock.Any()).Return(nil).Times(2)
91
92return ctx, txSP, nil
93},
94settings: a.settings,
95log: mock_log.NewMocklogger(ctrl),
96}
97
98return f
99},
100wantErr: assert.NoError,
101},
102"PropagationsMandatory_success_commit": {
103args: args{
104ctx: ctxManager.SetByKey(
105context.Background(),
106settings.DefaultCtxKey,
107mock2.NewMockTransaction(nil),
108),
109settings: settings.Must(),
110nestedSettings: settings.Must(
111settings.WithPropagation(trm.PropagationsMandatory),
112),
113},
114fields: func(_ *testing.T, ctrl *gomock.Controller, a args) fields {
115f := fields{
116factory: func(ctx context.Context, _ trm.Settings) (context.Context, trm.Transaction, error) {
117txSP := mock2.NewMocktransactionWithSP(ctrl)
118
119txSP.EXPECT().IsActive().Return(true)
120txSP.EXPECT().Begin(gomock.Any(), a.settings).Return(txSP, nil)
121txSP.EXPECT().Commit(gomock.Any()).Return(nil).Times(2)
122
123return ctx, txSP, nil
124},
125settings: a.settings,
126log: mock_log.NewMocklogger(ctrl),
127}
128
129return f
130},
131wantErr: assert.NoError,
132},
133"PropagationsMandatory_error_without_open_transaction": {
134args: args{
135ctx: context.Background(),
136settings: settings.Must(
137settings.WithPropagation(trm.PropagationsMandatory),
138),
139},
140fields: func(_ *testing.T, ctrl *gomock.Controller, a args) fields {
141f := fields{
142factory: emptyFactory,
143settings: a.settings,
144log: mock_log.NewMocklogger(ctrl),
145}
146
147return f
148},
149wantErr: func(t assert.TestingT, err error, _ ...interface{}) bool {
150return assert.ErrorIs(t, err, trm.ErrPropagationMandatory)
151},
152},
153"PropagationNever_success": {
154args: args{
155ctx: context.Background(),
156settings: settings.Must(settings.WithPropagation(
157trm.PropagationNever,
158)),
159nestedSettings: settings.Must(settings.WithPropagation(
160trm.PropagationNever,
161)),
162},
163fields: func(_ *testing.T, ctrl *gomock.Controller, a args) fields {
164f := fields{
165factory: emptyFactory,
166settings: a.settings,
167log: mock_log.NewMocklogger(ctrl),
168}
169
170return f
171},
172wantEmptyCtx: true,
173wantErr: assert.NoError,
174},
175"PropagationNever_error_transaction_is_opened": {
176args: args{
177ctx: context.Background(),
178settings: settings.Must(),
179nestedSettings: settings.Must(settings.WithPropagation(
180trm.PropagationNever,
181)),
182},
183fields: func(_ *testing.T, ctrl *gomock.Controller, a args) fields {
184f := fields{
185factory: func(ctx context.Context, _ trm.Settings) (context.Context, trm.Transaction, error) {
186tx := mock2.NewMockTransaction(ctrl)
187
188tx.EXPECT().IsActive().Return(true)
189tx.EXPECT().Rollback(gomock.Any()).Return(nil)
190
191return ctx, tx, nil
192},
193settings: a.settings,
194log: mock_log.NewMocklogger(ctrl),
195}
196
197return f
198},
199wantErr: func(t assert.TestingT, err error, _ ...interface{}) bool {
200return assert.ErrorIs(t, err, trm.ErrPropagationNever)
201},
202},
203"PropagationNotSupported_success_commit": {
204args: args{
205ctx: context.Background(),
206settings: settings.Must(
207settings.WithPropagation(trm.PropagationNotSupported),
208),
209nestedSettings: settings.Must(
210settings.WithPropagation(trm.PropagationNotSupported),
211),
212},
213fields: func(_ *testing.T, ctrl *gomock.Controller, a args) fields {
214f := fields{
215factory: emptyFactory,
216settings: a.settings,
217log: mock_log.NewMocklogger(ctrl),
218}
219
220return f
221},
222wantErr: assert.NoError,
223wantEmptyCtx: true,
224},
225"PropagationNotSupported_nilling_ctx_success_commit": {
226args: args{
227ctx: context.Background(),
228settings: settings.Must(),
229nestedSettings: settings.Must(
230settings.WithPropagation(trm.PropagationNotSupported),
231),
232},
233fields: func(_ *testing.T, ctrl *gomock.Controller, a args) fields {
234f := fields{
235factory: func(ctx context.Context, _ trm.Settings) (context.Context, trm.Transaction, error) {
236txSP := mock2.NewMocktransactionWithSP(ctrl)
237
238txSP.EXPECT().IsActive().Return(true)
239txSP.EXPECT().Commit(gomock.Any()).Return(nil)
240
241return ctx, txSP, nil
242},
243settings: a.settings,
244log: mock_log.NewMocklogger(ctrl),
245}
246
247return f
248},
249wantErr: assert.NoError,
250wantEmptyCtx: true,
251},
252"PropagationRequiresNew": {
253args: args{
254ctx: context.Background(),
255settings: settings.Must(
256settings.WithPropagation(trm.PropagationRequiresNew),
257),
258nestedSettings: settings.Must(
259settings.WithPropagation(trm.PropagationRequiresNew),
260),
261},
262fields: func(_ *testing.T, ctrl *gomock.Controller, a args) fields {
263f := fields{
264factory: func() trm.TrFactory {
265txSP := mock2.NewMocktransactionWithSP(ctrl)
266
267txSP.EXPECT().IsActive().Return(true).Times(2)
268txSP.EXPECT().Commit(gomock.Any()).Return(nil).Times(2)
269
270return func(ctx context.Context, _ trm.Settings) (context.Context, trm.Transaction, error) {
271return ctx, txSP, nil
272}
273}(),
274settings: a.settings,
275log: mock_log.NewMocklogger(ctrl),
276}
277
278return f
279},
280wantErr: assert.NoError,
281},
282"PropagationSupports_nil_transaction": {
283args: args{
284ctx: context.Background(),
285settings: settings.Must(
286settings.WithPropagation(trm.PropagationSupports),
287),
288nestedSettings: settings.Must(
289settings.WithPropagation(trm.PropagationSupports),
290),
291},
292fields: func(_ *testing.T, ctrl *gomock.Controller, a args) fields {
293f := fields{
294factory: emptyFactory,
295settings: a.settings,
296log: mock_log.NewMocklogger(ctrl),
297}
298
299return f
300},
301wantErr: assert.NoError,
302wantEmptyCtx: true,
303},
304"PropagationSupports": {
305args: args{
306ctx: context.Background(),
307settings: settings.Must(
308settings.WithPropagation(trm.PropagationRequired),
309),
310nestedSettings: settings.Must(
311settings.WithPropagation(trm.PropagationSupports),
312),
313},
314fields: func(_ *testing.T, ctrl *gomock.Controller, a args) fields {
315f := fields{
316factory: func() func(ctx context.Context, _ trm.Settings) (context.Context, trm.Transaction, error) {
317txSP := mock2.NewMocktransactionWithSP(ctrl)
318
319txSP.EXPECT().IsActive().Return(true)
320txSP.EXPECT().Commit(gomock.Any()).Return(nil)
321
322return func(ctx context.Context, _ trm.Settings) (context.Context, trm.Transaction, error) {
323return ctx, txSP, nil
324}
325}(),
326settings: a.settings,
327log: mock_log.NewMocklogger(ctrl),
328}
329
330return f
331},
332wantErr: assert.NoError,
333},
334}
335for name, tt := range tests {
336tt := tt
337t.Run(name, func(t *testing.T) {
338t.Parallel()
339
340ctrl := gomock.NewController(t)
341defer ctrl.Finish()
342
343f := tt.fields(t, ctrl, tt.args)
344
345tr := Must(f.factory, WithLog(f.log), WithSettings(f.settings))
346
347err := tr.DoWithSettings(
348tt.args.ctx,
349tt.args.settings,
350func(ctx context.Context) error {
351return tr.DoWithSettings(
352ctx,
353tt.args.nestedSettings,
354func(ctx context.Context) error {
355if tt.wantEmptyCtx {
356require.Nil(t, ctxManager.Default(ctx))
357} else {
358require.NotNil(t, ctxManager.Default(ctx))
359}
360
361return nil
362},
363)
364},
365)
366
367tt.wantErr(t, err)
368})
369}
370}
371
372func Test_transactionManager_Do_Error(t *testing.T) {
373t.Parallel()
374
375type fields struct {
376factory trm.TrFactory
377settings trm.Settings
378log logger
379}
380
381type args struct {
382ctx context.Context
383settings trm.Settings
384}
385
386testErr := errors.New("error test")
387testCommitErr := errors.New("error Commit test")
388testRollbackErr := errors.New("error rollback test")
389defaultArgs := args{
390ctx: context.Background(),
391settings: settings.Must(),
392}
393
394tests := map[string]struct {
395args args
396fields func(t *testing.T, ctrl *gomock.Controller, a args) fields
397ret error
398wantErr assert.ErrorAssertionFunc
399}{
400//nolint:dupl
401"transaction_factory_&_rollback_error": {
402args: defaultArgs,
403fields: func(_ *testing.T, ctrl *gomock.Controller, a args) fields {
404return fields{
405factory: func(ctx context.Context, _ trm.Settings) (context.Context, trm.Transaction, error) {
406tx := mock2.NewMockTransaction(ctrl)
407
408tx.EXPECT().
409IsActive().
410Return(true)
411tx.EXPECT().
412Rollback(gomock.Any()).
413Return(testRollbackErr)
414
415return ctx, tx, nil
416},
417settings: a.settings,
418log: mock_log.NewMocklogger(ctrl),
419}
420},
421ret: testErr,
422wantErr: func(t assert.TestingT, err error, _ ...interface{}) bool {
423return assert.ErrorIs(t, err, testErr) &&
424assert.ErrorIs(t, err, trm.ErrRollback)
425},
426},
427"skip_rollback_with_error": {
428args: defaultArgs,
429fields: func(_ *testing.T, ctrl *gomock.Controller, a args) fields {
430return fields{
431factory: func(ctx context.Context, _ trm.Settings) (context.Context, trm.Transaction, error) {
432tx := mock2.NewMockTransaction(ctrl)
433
434tx.EXPECT().
435IsActive().
436Return(true)
437tx.EXPECT().
438Commit(gomock.Any())
439
440return ctx, tx, nil
441},
442settings: a.settings,
443log: mock_log.NewMocklogger(ctrl),
444}
445},
446ret: trm.Skippable(testErr),
447wantErr: func(t assert.TestingT, err error, _ ...interface{}) bool {
448return assert.ErrorIs(t, err, testErr) &&
449assert.True(t, trm.IsSkippable(err))
450},
451},
452"skip_rollback_with_commit_error": {
453args: defaultArgs,
454fields: func(_ *testing.T, ctrl *gomock.Controller, a args) fields {
455return fields{
456factory: func(ctx context.Context, _ trm.Settings) (context.Context, trm.Transaction, error) {
457tx := mock2.NewMockTransaction(ctrl)
458
459tx.EXPECT().
460IsActive().
461Return(true)
462tx.EXPECT().
463Commit(gomock.Any()).
464Return(testCommitErr)
465
466return ctx, tx, nil
467},
468settings: a.settings,
469log: mock_log.NewMocklogger(ctrl),
470}
471},
472ret: trm.Skippable(testErr),
473wantErr: func(t assert.TestingT, err error, _ ...interface{}) bool {
474return assert.ErrorIs(t, err, testErr) &&
475assert.ErrorIs(t, err, testCommitErr) &&
476assert.ErrorIs(t, err, trm.ErrCommit) &&
477assert.False(t, trm.IsSkippable(err))
478},
479},
480//nolint:dupl
481"commit_error": {
482args: defaultArgs,
483fields: func(_ *testing.T, ctrl *gomock.Controller, a args) fields {
484return fields{
485factory: func(ctx context.Context, _ trm.Settings) (context.Context, trm.Transaction, error) {
486tx := mock2.NewMockTransaction(ctrl)
487
488tx.EXPECT().
489IsActive().
490Return(true)
491tx.EXPECT().
492Commit(gomock.Any()).
493Return(testCommitErr)
494
495return ctx, tx, nil
496},
497settings: a.settings,
498log: mock_log.NewMocklogger(ctrl),
499}
500},
501ret: nil,
502wantErr: func(t assert.TestingT, err error, _ ...interface{}) bool {
503return assert.ErrorIs(t, err, testCommitErr) &&
504assert.ErrorIs(t, err, trm.ErrCommit)
505},
506},
507"PropagationNested_err_nested_begin": {
508args: args{
509ctx: context.Background(),
510settings: settings.Must(
511settings.WithPropagation(trm.PropagationNested),
512),
513},
514fields: func(_ *testing.T, ctrl *gomock.Controller, a args) fields {
515f := fields{
516factory: func(ctx context.Context, _ trm.Settings) (context.Context, trm.Transaction, error) {
517txSP := mock2.NewMocktransactionWithSP(ctrl)
518
519txSP.EXPECT().IsActive().Return(true).Times(1)
520txSP.EXPECT().Begin(gomock.Any(), a.settings).Return(ctx, nil, testErr)
521txSP.EXPECT().Rollback(gomock.Any()).Return(nil).Times(1)
522
523return ctx, txSP, nil
524},
525settings: a.settings,
526log: mock_log.NewMocklogger(ctrl),
527}
528
529return f
530},
531wantErr: func(t assert.TestingT, err error, _ ...interface{}) bool {
532return assert.ErrorIs(t, err, testErr) &&
533assert.ErrorIs(t, err, trm.ErrNestedBegin)
534},
535},
536}
537for name, tt := range tests {
538tt := tt
539t.Run(name, func(t *testing.T) {
540t.Parallel()
541
542ctrl := gomock.NewController(t)
543defer ctrl.Finish()
544
545f := tt.fields(t, ctrl, tt.args)
546
547tr := Must(f.factory, WithLog(f.log), WithSettings(f.settings))
548
549err := tr.DoWithSettings(
550tt.args.ctx,
551tt.args.settings,
552func(ctx context.Context) error {
553return tr.Do(
554ctx,
555func(_ context.Context) error {
556return tt.ret
557},
558)
559},
560)
561
562tt.wantErr(t, err)
563})
564}
565}
566
567func Test_transactionManager_Do_Panic(t *testing.T) {
568t.Parallel()
569
570ctrl := gomock.NewController(t)
571defer ctrl.Finish()
572
573testPanic := "panic"
574testRollbackErr := errors.New("rollback error")
575
576log := mock2.NewLog()
577factory := func(ctx context.Context, _ trm.Settings) (context.Context, trm.Transaction, error) {
578tx := mock2.NewMockTransaction(ctrl)
579
580tx.EXPECT().IsActive().Return(true)
581tx.EXPECT().Rollback(gomock.Any()).Return(testRollbackErr)
582
583return ctx, tx, nil
584}
585
586m := Must(factory, WithLog(log))
587ctxManager := trmcontext.DefaultManager
588
589defer func() {
590p := recover()
591
592assert.Equal(t, testPanic, p)
593assert.Equal(t, []string{"rollback error, panic"}, log.Logged)
594}()
595
596_ = m.Do(context.Background(), func(ctx context.Context) error {
597return m.Do(ctx, func(ctx context.Context) error {
598assert.NotNil(
599t,
600ctxManager.Default(ctx),
601)
602
603panic(testPanic)
604})
605})
606
607assert.NoError(t, errors.New("should not be here"))
608}
609
610//nolint:tparallel // there is not t.Cleanup in go 1.13 and less.
611func Test_transactionManager_Do_ClosedTransaction(t *testing.T) {
612t.Parallel()
613
614ctrl := gomock.NewController(t)
615defer ctrl.Finish()
616
617testErr := errors.New("test error")
618
619tests := map[string]struct {
620ret error
621wantErr require.ErrorAssertionFunc
622}{
623"without_error": {
624ret: nil,
625wantErr: func(t require.TestingT, err error, _ ...interface{}) {
626require.Equal(t, trm.ErrAlreadyClosed, err)
627},
628},
629"with_error": {
630ret: testErr,
631wantErr: func(t require.TestingT, err error, _ ...interface{}) {
632require.ErrorIs(t, err, testErr)
633require.ErrorIs(t, err, trm.ErrAlreadyClosed)
634},
635},
636}
637for name, tt := range tests {
638tt := tt
639t.Run(name, func(t *testing.T) {
640t.Parallel()
641
642tx := mock2.NewMockTransaction(ctrl)
643tx.EXPECT().IsActive().Return(false).MinTimes(2)
644
645factory := func(ctx context.Context, _ trm.Settings) (context.Context, trm.Transaction, error) {
646return ctx, tx, nil
647}
648
649m := Must(
650factory,
651WithSettings(settings.Must(settings.WithPropagation(trm.PropagationRequiresNew))),
652)
653
654err := m.Do(context.Background(), func(ctx context.Context) error {
655return m.Do(ctx, func(_ context.Context) error {
656return tt.ret
657})
658})
659
660tt.wantErr(t, err)
661})
662}
663}
664
665//nolint:tparallel // there is not t.Cleanup in go 1.13 and less.
666func Test_transactionManager_Do_Cancel(t *testing.T) {
667type fields struct {
668settings trm.Settings
669factory trm.TrFactory
670}
671
672t.Parallel()
673
674ctrl := gomock.NewController(t)
675defer ctrl.Finish()
676
677tests := map[string]struct {
678fields fields
679ctx func(ctx context.Context) (context.Context, context.CancelFunc)
680do func(t *testing.T, ctx context.Context)
681wantErr require.ErrorAssertionFunc
682}{
683"cancel": {
684fields: fields{
685factory: func(ctx context.Context, _ trm.Settings) (context.Context, trm.Transaction, error) {
686tr := mock2.NewMockTransaction(ctrl)
687tr.EXPECT().IsActive().Return(false)
688
689return ctx, tr, nil
690},
691settings: settings.Must(
692settings.WithCancelable(true),
693settings.WithPropagation(trm.PropagationRequiresNew),
694),
695},
696ctx: context.WithCancel,
697do: func(t *testing.T, ctx context.Context) {
698time.Sleep(time.Millisecond)
699
700assert.ErrorIs(t, ctx.Err(), context.Canceled)
701},
702wantErr: func(t require.TestingT, err error, _ ...interface{}) {
703assert.ErrorIs(t, err, context.Canceled)
704},
705},
706"timeout": {
707fields: fields{
708factory: func(ctx context.Context, _ trm.Settings) (context.Context, trm.Transaction, error) {
709tr := mock2.NewMockTransaction(ctrl)
710tr.EXPECT().IsActive().Return(false)
711
712return ctx, tr, nil
713},
714settings: settings.Must(
715settings.WithCancelable(true),
716settings.WithTimeout(time.Millisecond),
717settings.WithPropagation(trm.PropagationRequiresNew),
718),
719},
720ctx: func(ctx context.Context) (context.Context, context.CancelFunc) {
721return ctx, func() {}
722},
723do: func(t *testing.T, ctx context.Context) {
724select {
725case <-time.After(time.Second):
726case <-ctx.Done():
727}
728
729assert.ErrorIs(t, ctx.Err(), context.DeadlineExceeded)
730},
731wantErr: func(t require.TestingT, err error, _ ...interface{}) {
732assert.ErrorIs(t, err, context.DeadlineExceeded)
733},
734},
735}
736for name, tt := range tests {
737tt := tt
738t.Run(name, func(t *testing.T) {
739t.Parallel()
740
741m := Must(
742tt.fields.factory,
743WithSettings(tt.fields.settings),
744)
745
746var err error
747
748wg := sync.WaitGroup{}
749wg.Add(1)
750
751ctx, cancel := tt.ctx(context.Background())
752go func() {
753err = m.Do(ctx, func(ctx context.Context) error {
754return m.Do(ctx, func(ctx context.Context) error {
755tt.do(t, ctx)
756
757return nil
758})
759})
760
761wg.Done()
762}()
763
764cancel()
765
766wg.Wait()
767
768tt.wantErr(t, err)
769})
770}
771}
772
773func TestManager_WithOpts(t *testing.T) {
774t.Parallel()
775
776t.Run("set", func(t *testing.T) {
777t.Parallel()
778
779l := mock2.NewZeroLog()
780m := Must(nil, WithLog(l), WithSettings(s{}))
781
782assert.Equal(t, l, m.log)
783assert.Equal(t, s{}, m.settings)
784})
785
786t.Run("default", func(t *testing.T) {
787t.Parallel()
788
789m := Must(nil)
790
791assert.Equal(t, defaultLog, m.log)
792assert.Equal(t, settings.Must(), m.settings)
793})
794}
795