Merge branch 'b-3.6.1' of ../kivitendo-erp_20220811
[kivitendo-erp.git] / t / db_helper / with_transaction.t
diff --git a/t/db_helper/with_transaction.t b/t/db_helper/with_transaction.t
new file mode 100644 (file)
index 0000000..cc3fc0e
--- /dev/null
@@ -0,0 +1,244 @@
+use Test::More tests => 17;
+use Test::Exception;
+
+use strict;
+
+use lib 't';
+use utf8;
+
+use Carp;
+use Data::Dumper;
+use Support::TestSetup;
+use SL::DB::Part;
+use SL::Dev::Part qw(new_part);
+
+Support::TestSetup::login();
+
+SL::DB::Manager::Part->delete_all(all => 1, cascade => 1);
+
+# silence the Test::Harness warn handler
+local $SIG{__WARN__} = sub {};
+
+# test simple transaction
+
+my $part = new_part();
+SL::DB->client->with_transaction(sub {
+  $part->save;
+  ok 1, 'part saved';
+  1;
+}) or do {
+  ok 0, 'error saving part';
+};
+
+# test failing transaction
+my $part2 = new_part(partnumber => $part->partnumber); # woops, duplicate partnumber
+SL::DB->client->with_transaction(sub {
+  $part2->save;
+  ok 0, 'part saved';
+  1;
+}) or do {
+  ok 1, 'saving part with duplicate partnumber generates graceful error';
+};
+
+# test transaction with run time exception
+dies_ok {
+  SL::DB->client->with_transaction(sub {
+    $part->method_that_does_not_exist;
+    ok 0, 'this should have died';
+    1;
+  }) or do {
+    ok 0, 'this should not get here';
+  };
+} 'method not found in transaction died as expect';
+
+# test transaction with hook error
+# TODO - not possible to test without locally adding hooks in run time
+
+# test if error gets correctly stored in db->error
+$part2 = new_part(partnumber => $part->partnumber); # woops, duplicate partnumber
+SL::DB->client->with_transaction(sub {
+  $part2->save;
+  ok 0, 'part saved';
+  1;
+}) or do {
+  like(SL::DB->client->error, qr/unique.constraint/i, 'error is in db->error');
+};
+
+# test stacked transactions
+# 1. test that it works
+SL::DB->client->with_transaction(sub {
+  $part->sellprice(1);
+  $part->save;
+
+  SL::DB->client->with_transaction(sub {
+    $part->sellprice(2);
+    $part->save;
+  }) or do {
+    ok 0, 'error saving part';
+  };
+
+  $part->sellprice(3);
+  $part->save;
+  1;
+}) or do {
+  ok 0, 'error saving part';
+};
+
+$part->load;
+is $part->sellprice, "3.00000", 'part saved';
+
+# 2. with a transaction rollback
+SL::DB->client->with_transaction(sub {
+  $part->sellprice(1);
+  $part2->save;
+  $part->save;
+
+  SL::DB->client->with_transaction(sub {
+    $part->sellprice(2);
+    $part->save;
+  }) or do {
+    ok 0, 'should not get here';
+  };
+
+  $part->sellprice(3);
+  $part->save;
+  ok 0, 'should not get here';
+  1;
+}) or do {
+  ok 1, 'sql error skips rest of the transaction';
+};
+
+
+SL::DB->client->with_transaction(sub {
+  $part->sellprice(1);
+  $part->save;
+
+  SL::DB->client->with_transaction(sub {
+    $part->sellprice(2);
+    $part->save;
+    $part2->save;
+  }) or do {
+    ok 0, 'should not get here';
+  };
+
+  $part->sellprice(3);
+  $part->save;
+  ok 0, 'should not get here';
+  1;
+}) or do {
+  ok 1, 'sql error in nested transaction rolls back';
+  like(SL::DB->client->error, qr/unique.constraint/i, 'error from nested transaction is in db->error');
+};
+
+$part->load;
+is $part->sellprice, "3.00000", 'saved part is not affected';
+
+
+
+SL::DB->client->with_transaction(sub {
+  $part->sellprice(1);
+  $part->save;
+
+  SL::DB->client->with_transaction(sub {
+    $part->sellprice(2);
+    $part->save;
+  }) or do {
+    ok 0, 'should not get here';
+  };
+
+  $part->sellprice(4);
+  $part->save;
+  $part2->save;
+  ok 0, 'should not get here';
+  1;
+}) or do {
+  ok 1, 'sql error after nested transaction rolls back';
+};
+
+$part->load;
+is $part->sellprice, "3.00000", 'saved part is not affected';
+
+eval {
+  SL::DB->client->with_transaction(sub {
+    $part->sellprice(1);
+    $part->not_existing_function();
+    $part->save;
+
+    SL::DB->client->with_transaction(sub {
+      $part->sellprice(2);
+      $part->save;
+    }) or do {
+      ok 0, 'should not get here';
+    };
+
+    $part->sellprice(4);
+    $part->save;
+    ok 0, 'should not get here';
+    1;
+  }) or do {
+    ok 0, 'should not get here';
+  };
+  1;
+} or do {
+  ok 1, 'runtime exception error before nested transaction rolls back';
+};
+
+$part->load;
+is $part->sellprice, "3.00000", 'saved part is not affected';
+
+eval {
+  SL::DB->client->with_transaction(sub {
+    $part->sellprice(1);
+    $part->save;
+
+    SL::DB->client->with_transaction(sub {
+      $part->sellprice(2);
+      $part->not_existing_function();
+      $part->save;
+    }) or do {
+      ok 0, 'should not get here';
+    };
+
+    $part->sellprice(4);
+    $part->save;
+    ok 0, 'should not get here';
+    1;
+  }) or do {
+    ok 0, 'should not get here';
+  };
+  1;
+} or do {
+  ok 1, 'runtime exception error in nested transaction rolls back';
+};
+
+$part->load;
+is $part->sellprice, "3.00000", 'saved part is not affected';
+
+
+eval {
+  SL::DB->client->with_transaction(sub {
+    $part->sellprice(1);
+    $part->save;
+
+    SL::DB->client->with_transaction(sub {
+      $part->sellprice(2);
+      $part->save;
+    }) or do {
+      ok 0, 'should not get here';
+    };
+
+    $part->sellprice(4);
+    $part->save;
+    $part->not_existing_function();
+    ok 0, 'should not get here';
+    1;
+  }) or do {
+    ok 0, 'should not get here';
+  };
+  1;
+} or do {
+  ok 1, 'runtime exception error after nested transaction rolls back';
+};
+
+$part->load;
+is $part->sellprice, "3.00000", 'saved part is not affected';