Merge branch 'master' into backend_setup

This commit is contained in:
Moriz Wahl 2021-07-13 16:26:38 +02:00
commit b6d51145df
54 changed files with 1900 additions and 765 deletions

View File

@ -261,7 +261,7 @@ jobs:
report_name: Coverage Frontend
type: lcov
result_path: ./coverage/lcov.info
min_coverage: 32
min_coverage: 46
token: ${{ github.token }}
##############################################################################

View File

@ -25,7 +25,7 @@ class AppRequestsController extends AppController
$this->loadComponent('GradidoNumber');
//$this->loadComponent('JsonRpcRequestClient');
//$this->Auth->allow(['add', 'edit']);
$this->Auth->allow(['index', 'sendCoins', 'createCoins', 'getBalance', 'listTransactions']);
$this->Auth->allow(['index', 'sendCoins', 'createCoins', 'getBalance', 'listTransactions', 'getDecayStartBlock']);
}
@ -333,16 +333,37 @@ class AppRequestsController extends AppController
$this->addAdminError('StateBalancesController', 'overview', $gdtEntries, $user['id'] ? $user['id'] : 0);
}
$limit = $count;
$offset = 0;
$skip_first_transaction = false;
if($page == 1) {
$limit--;
} else {
$offset = (( $page - 1 ) * $count) - 1;
}
if($offset && $orderDirection == 'ASC') {
// move cursor one step backwards to able to load one transaction previous last which will be shown for decay calculation
$offset--;
$limit++;
$skip_first_transaction = true;
} else if($orderDirection == 'DESC') {
$limit++;
$skip_first_transaction = true;
}
$stateUserTransactionsQuery = $stateUserTransactionsTable
->find()
->where(['state_user_id' => $user['id']])
->order(['balance_date' => $orderDirection])
->contain([])
->limit($count)
->page($page)
->limit($limit)
//->page($page)
->offset($offset)
;
$decay = true;
if($page > 1) {
$decay = false;
}
$transactions = [];
$transactions_from_db = $stateUserTransactionsQuery->toArray();
@ -351,7 +372,7 @@ class AppRequestsController extends AppController
$transactions_from_db = array_reverse($transactions_from_db);
}
$transactions = $transactionsTable->listTransactionsHumanReadable($transactions_from_db, $user, $decay);
$transactions = $transactionsTable->listTransactionsHumanReadable($transactions_from_db, $user, $decay, $skip_first_transaction);
if($orderDirection == 'DESC') {
$transactions = array_reverse($transactions);
@ -382,6 +403,16 @@ class AppRequestsController extends AppController
$this->set('body', $body);
}
public function getDecayStartBlock()
{
$transactionsTable = TableRegistry::getTableLocator()->get('Transactions');
$decayStartBlock = $transactionsTable->find()->where(['transaction_type_id' => 9]);
if(!$decayStartBlock->count()) {
return $this->returnJson(['state' => 'error', 'msg' => 'not found']);
}
return $this->returnJson(['state' => 'success', 'decay_start' => $decayStartBlock->first()->received]);
}
private function acquireAccessToken($session_id)
{

View File

@ -98,7 +98,12 @@ class StateBalancesTable extends AppTable
// if start date for decay is after enddate, we also just return input
if($decayStartDate === null || $decayStartDate >= $endDate) {
if($withInterval) {
return ['balance' => $startBalance, 'interval' => new \DateInterval('PT0S')];
return [
'balance' => $startBalance,
'interval' => new \DateInterval('PT0S'),
'start_date' => $startDate->getTimestamp(),
'end_date' => $startDate->getTimestamp()
];
} else {
return $startBalance;
}
@ -118,7 +123,12 @@ class StateBalancesTable extends AppTable
}
$decay = $state_balance->partDecay($endDate);
if($withInterval) {
return ['balance' => $decay, 'interval' => $interval];
return [
'balance' => $decay,
'interval' => $interval,
'start_date' => $state_balance->record_date->getTimestamp(),
'end_date' => $endDate->getTimestamp()
];
} else {
return $decay;
}

View File

@ -133,11 +133,12 @@ class TransactionsTable extends Table
}
public function listTransactionsHumanReadable($stateUserTransactions, array $user, $decay = true)
public function listTransactionsHumanReadable($stateUserTransactions, array $user, $decay = true, $skip_first_transaction = false)
{
$stateUsersTable = TableRegistry::getTableLocator()->get('StateUsers');
$stateBalancesTable = TableRegistry::getTableLocator()->get('StateBalances');
$transactionsTable = TableRegistry::getTableLocator()->get('Transactions');
$transaction_ids = [];
$involved_user_ids = [];
@ -162,11 +163,15 @@ class TransactionsTable extends Table
$state_balance = $stateBalancesTable->newEntity();
$final_transactions = [];
$decay_start_transaction = $transactionsTable->find()->where(['transaction_type_id' => 9]);
$decay_start_transaction_id = 0;
if($decay_start_transaction->count()) {
$decay_start_transaction_id = $decay_start_transaction->first()->id;
}
$decay_start_time = $stateBalancesTable->getDecayStartDateCached()->getTimestamp();
foreach($stateUserTransactions as $i => $su_transaction)
{
// sender or receiver when user has sended money
// group name if creation
// type: gesendet / empfangen / geschöpft
@ -185,19 +190,34 @@ class TransactionsTable extends Table
if($i > 0 ) {
$prev = $stateUserTransactions[$i-1];
}
if($prev && $decay == true)
if($prev)
{
if($prev->balance > 0) {
if($prev->balance > 0)
{
$current = $su_transaction;
$calculated_decay = $stateBalancesTable->calculateDecay($prev->balance, $prev->balance_date, $current->balance_date, true);
$balance = floatval($prev->balance - $calculated_decay['balance']);
if($balance > 100)
if($balance)
{
$final_transactions['decay'] = [
$final_transaction['decay'] = [
'balance' => $balance,
'decay_duration' => $calculated_decay['interval']->format('%a days, %H hours, %I minutes, %S seconds')
'decay_duration' => $calculated_decay['interval']->format('%a days, %H hours, %I minutes, %S seconds'),
'decay_start' => $calculated_decay['start_date'],
'decay_end' => $calculated_decay['end_date']
];
if($prev->transaction_id < $decay_start_transaction_id &&
$current->transaction_id > $decay_start_transaction_id) {
$final_transaction['decay']['decay_start_block'] = $decay_start_time;
}
// hint: use transaction id
/*if($calculated_decay['start_date'] < $decay_start_time && $calculated_decay['end_date'] > $decay_start_time) {
$final_transaction['decay']['decay_start_block'] = $decay_start_time;
} else {
echo "start block: " . $decay_start_time . "<br>";
echo "start date: " . $calculated_decay['start_date'] . "<br>";
echo "end date: " . $calculated_decay['end_date']. "<hr>";
}*/
}
}
}
@ -247,24 +267,28 @@ class TransactionsTable extends Table
$final_transaction['name'] = $otherUser->first_name . ' ' . $otherUser->last_name;
$final_transaction['email'] = $otherUser->email;
}
if($i > 0 || !$skip_first_transaction) {
$final_transactions[] = $final_transaction;
}
$final_transactions[] = $final_transaction;
if($i == $stateUserTransactionsCount-1 && $decay == true) {
if($i == $stateUserTransactionsCount-1 && $decay) {
$now = new FrozenTime();
$calculated_decay = $stateBalancesTable->calculateDecay(
$su_transaction->balance,
$su_transaction->balance_date, new FrozenTime(), true);
$su_transaction->balance_date, $now, true);
$decay_start_date = $stateBalancesTable->getDecayStartDateCached();
$duration = $su_transaction->balance_date->timeAgoInWords();
if($decay_start_date > $su_transaction->balance_date) {
$duration = $decay_start_date->timeAgoInWords();
}
$balance = floatval($su_transaction->balance - $calculated_decay['balance']);
if($balance > 100) {
if($balance) {
$final_transactions[] = [
'type' => 'decay',
'balance' => $balance,
'decay_duration' => $duration,
'decay_start' => $calculated_decay['start_date'],
'decay_end' => $calculated_decay['end_date'],
'memo' => ''
];
}

View File

@ -12,15 +12,19 @@ $body['gdtSum'] = $this->element('centToFloat', ['cent' => $body['gdtSum'], 'pre
foreach($body['transactions'] as $i => $transaction) {
$useCeil = false;
if($transaction['type'] == 'decay') {
$useCeil = true;
}
$body['transactions'][$i]['balance'] = $this->element('centToFloat', ['cent' => $transaction['balance'], 'precision' => 4, 'useCeil' => $useCeil]);
if(isset($transaction['creation_amount'])) {
$body['transactions'][$i]['creation_amount'] = $this->element('centToFloat', ['cent' => $transaction['creation_amount'], 'precision' => 4]);
}
if(isset($transaction['decay'])) {
$body['transactions'][$i]['decay']['balance'] = $this->element('centToFloat', ['cent' => $transaction['decay']['balance'], 'precision' => 4]);
if(!isset($transaction['type'])) {
$body = ['state' => 'error', 'msg' => 'transaction without type found', 'details' => $transaction];
} else {
if($transaction['type'] == 'decay') {
$useCeil = true;
}
$body['transactions'][$i]['balance'] = $this->element('centToFloat', ['cent' => $transaction['balance'], 'precision' => 4, 'useCeil' => $useCeil]);
if(isset($transaction['creation_amount'])) {
$body['transactions'][$i]['creation_amount'] = $this->element('centToFloat', ['cent' => $transaction['creation_amount'], 'precision' => 4]);
}
if(isset($transaction['decay'])) {
$body['transactions'][$i]['decay']['balance'] = $this->element('centToFloat', ['cent' => $transaction['decay']['balance'], 'precision' => 4]);
}
}
}

View File

@ -46,12 +46,12 @@ class StateUserTransactionsFixture extends BaseTestFixture
[8, 4, 4, 2, 0, '2021-04-14 00:00:00'],
[23, 1, 5, 2, 0, '2021-04-14 09:01:07'],
[24, 4, 5, 2, 0, '2021-04-14 09:01:07'],
[25, 4, 6, 2, 0, '2021-04-14 09:02:28'],
[26, 1, 6, 2, 0, '2021-04-14 09:02:28'],
[27, 4, 7, 2, 0, '2021-04-14 09:28:46'],
[28, 1, 7, 2, 0, '2021-04-14 09:28:46'],
[29, 4, 8, 2, 0, '2021-04-14 09:31:28'],
[30, 1, 8, 2, 0, '2021-04-14 09:31:28']
[25, 4, 7, 2, 0, '2021-04-14 09:02:28'],
[26, 1, 7, 2, 0, '2021-04-14 09:02:28'],
[27, 4, 8, 2, 0, '2021-04-14 09:28:46'],
[28, 1, 8, 2, 0, '2021-04-14 09:28:46'],
[29, 4, 9, 2, 0, '2021-04-14 09:31:28'],
[30, 1, 9, 2, 0, '2021-04-14 09:31:28']
];
$this->records = $this->sqlEntrysToRecords($sql, $this->fields);
parent::init();

View File

@ -43,9 +43,9 @@ class TransactionSendCoinsFixture extends BaseTestFixture
[2, 3, '0000000000000000000000000000000000000000000000000000000000000000', 1, 'e3369de3623ce8446d0424c4013e7a1d71a2671ae3d7bf1e798ebf0665d145f2', 4, 1000000, 6254699],
[3, 4, '0000000000000000000000000000000000000000000000000000000000000000', 1, 'e3369de3623ce8446d0424c4013e7a1d71a2671ae3d7bf1e798ebf0665d145f2', 4, 100000, 7027197],
[11, 5, '0000000000000000000000000000000000000000000000000000000000000000', 1, 'e3369de3623ce8446d0424c4013e7a1d71a2671ae3d7bf1e798ebf0665d145f2', 4, 100000, 6922113],
[12, 6, '0000000000000000000000000000000000000000000000000000000000000000', 4, 'f7f4a49a4ac10379f8b9ddcb731c4d9ec495e6edd16075f52672cd25e3179f0f', 1, 100000, 9212951],
[13, 7, '0000000000000000000000000000000000000000000000000000000000000000', 4, 'f7f4a49a4ac10379f8b9ddcb731c4d9ec495e6edd16075f52672cd25e3179f0f', 1, 100000, 9112627],
[14, 8, '0000000000000000000000000000000000000000000000000000000000000000', 4, 'f7f4a49a4ac10379f8b9ddcb731c4d9ec495e6edd16075f52672cd25e3179f0f', 1, 100000, 8912594]
[12, 7, '0000000000000000000000000000000000000000000000000000000000000000', 4, 'f7f4a49a4ac10379f8b9ddcb731c4d9ec495e6edd16075f52672cd25e3179f0f', 1, 100000, 9212951],
[13, 8, '0000000000000000000000000000000000000000000000000000000000000000', 4, 'f7f4a49a4ac10379f8b9ddcb731c4d9ec495e6edd16075f52672cd25e3179f0f', 1, 100000, 9112627],
[14, 9, '0000000000000000000000000000000000000000000000000000000000000000', 4, 'f7f4a49a4ac10379f8b9ddcb731c4d9ec495e6edd16075f52672cd25e3179f0f', 1, 100000, 8912594]
];
$this->records = $this->sqlEntrysToRecords($sql, $this->fields);

View File

@ -41,9 +41,9 @@ class TransactionSignaturesFixture extends BaseTestFixture
[3, 3, 'c70f124feaaea02194d22a5f597963ed3e430343122a0952877854766fe37a709f92b39510de2aae494ef11abe743cd59f08f971b1e0e36f4c333990453d8b0d', 'f7f4a49a4ac10379f8b9ddcb731c4d9ec495e6edd16075f52672cd25e3179f0f'],
[4, 4, 'a65b39e51ab6191c51d5629bbcefd30f85f801efbb14e1c635c519e97abe217a248820fa1fc6aef56227c9d888c1919bc92471d5d7ae3522c9c50fba9f0d8402', 'f7f4a49a4ac10379f8b9ddcb731c4d9ec495e6edd16075f52672cd25e3179f0f'],
[5, 5, 'a65b39e51ab6191c51d5629bbcefd30f85f801efbb14e1c635c519e97abe217a248820fa1fc6aef56227c9d888c1919bc92471d5d7ae3522c9c50fba9f0d8402', 'f7f4a49a4ac10379f8b9ddcb731c4d9ec495e6edd16075f52672cd25e3179f0f'],
[6, 6, 'c233726674bff9bfb8ccb98bf358c6bc701825d971ece915d3c3a3de98886d1d13ee2f773cd9fc4ccbe543ac17be0d780ebead23a0dbf4ec814f7bae2efb9c0e', 'e3369de3623ce8446d0424c4013e7a1d71a2671ae3d7bf1e798ebf0665d145f2'],
[7, 7, '83ab780535883ec53ee76d0f68db0e1596418c9e100c806a4d4655d4dedf589d54a6319a2795dabab301e212b52f0dafb2725b7583447f19e47cb417d188a107', 'e3369de3623ce8446d0424c4013e7a1d71a2671ae3d7bf1e798ebf0665d145f2'],
[8, 8, '83ab780535883ec53ee76d0f68db0e1596418c9e100c806a4d4655d4dedf589d54a6319a2795dabab301e212b52f0dafb2725b7583447f19e47cb417d188a107', 'e3369de3623ce8446d0424c4013e7a1d71a2671ae3d7bf1e798ebf0665d145f2']
[6, 7, 'c233726674bff9bfb8ccb98bf358c6bc701825d971ece915d3c3a3de98886d1d13ee2f773cd9fc4ccbe543ac17be0d780ebead23a0dbf4ec814f7bae2efb9c0e', 'e3369de3623ce8446d0424c4013e7a1d71a2671ae3d7bf1e798ebf0665d145f2'],
[7, 8, '83ab780535883ec53ee76d0f68db0e1596418c9e100c806a4d4655d4dedf589d54a6319a2795dabab301e212b52f0dafb2725b7583447f19e47cb417d188a107', 'e3369de3623ce8446d0424c4013e7a1d71a2671ae3d7bf1e798ebf0665d145f2'],
[8, 9, '83ab780535883ec53ee76d0f68db0e1596418c9e100c806a4d4655d4dedf589d54a6319a2795dabab301e212b52f0dafb2725b7583447f19e47cb417d188a107', 'e3369de3623ce8446d0424c4013e7a1d71a2671ae3d7bf1e798ebf0665d145f2']
];
$this->records = $this->sqlEntrysToRecords($sql, $this->fields);
parent::init();

View File

@ -44,9 +44,10 @@ class TransactionsFixture extends BaseTestFixture
[3, NULL, 2, '4e2235f208edaf5cbb285955732022a625cf1e100eb629c56896d2fbfb8b34e800000000000000000000000000000000', 'test', '2021-04-12 00:00:00', 1],
[4, NULL, 2, 'fc6e69696beb7c56ad7c511fc3999f954411427bec810184b70c092911deae1900000000000000000000000000000000', 'test time', '2021-04-14 00:00:00', 1],
[5, NULL, 2, 'a7149ebc0d6cd8c061906dafe05e13689b51642a41100d0ec7bb6cd2dcafdc1800000000000000000000000000000000', 'test time', '2021-04-14 09:01:07', 1],
[6, NULL, 2, '2e3c3ab3e42c06f2ecb12f61c970712467d8ad9ddfa16fa58dd76492e5924b7d00000000000000000000000000000000', 'test time 3', '2021-04-14 09:02:28', 1],
[7, NULL, 2, 'c2c6354d77ff371daeee25fce9c947748b53d3d6b8398a92bd681923cfd2057100000000000000000000000000000000', 'test login crash', '2021-04-14 09:28:46', 1],
[8, NULL, 2, '5a8cbf1aaac06b00b2951ff39983cb2ca9a1e6710d72c8e5067278dc679a823100000000000000000000000000000000', 'test login crash', '2021-04-14 09:31:28', 1]
[6, NULL, 9, '', '', '2021-04-14 09:02:00', 1],
[7, NULL, 2, '2e3c3ab3e42c06f2ecb12f61c970712467d8ad9ddfa16fa58dd76492e5924b7d00000000000000000000000000000000', 'test time 3', '2021-04-14 09:02:28', 1],
[8, NULL, 2, 'c2c6354d77ff371daeee25fce9c947748b53d3d6b8398a92bd681923cfd2057100000000000000000000000000000000', 'test login crash', '2021-04-14 09:28:46', 1],
[9, NULL, 2, '5a8cbf1aaac06b00b2951ff39983cb2ca9a1e6710d72c8e5067278dc679a823100000000000000000000000000000000', 'test login crash', '2021-04-14 09:31:28', 1]
];
$this->records = $this->sqlEntrysToRecords($sql, $this->fields);
parent::init();

View File

@ -57,8 +57,8 @@ class AppRequestControllerTest extends TestCase
$response = $this->getAndParseWithoutCompare('/api/get-balance/' . $session_id);
$this->assertEquals('success', $response->state);
$this->assertEquals(9100000, $response->balance);
$this->assertLessThan(9100000, $response->decay);
$this->assertEquals(9099652, $response->balance);
$this->assertLessThan(9099652, $response->decay);
}
@ -94,8 +94,8 @@ class AppRequestControllerTest extends TestCase
$response = $this->getAndParseWithoutCompare('/api/get-balance/' . $session_id);
$this->assertEquals('success', $response->state);
$this->assertEquals(10900000, $response->balance);
$this->assertLessThan(10900000, $response->decay);
$this->assertEquals(10899568, $response->balance);
$this->assertLessThan(10899568, $response->decay);
}
public function testGetBalanceInvalidSession()
@ -202,19 +202,16 @@ class AppRequestControllerTest extends TestCase
"email": "test3.yahoo.com"
},
{
"transaction_id": 6,
"transaction_id": 7,
"date": "2021-04-14T09:02:28+00:00",
"memo": "test time 3",
"balance": 100000,
"type": "receive",
"pubkey": "0000000000000000000000000000000000000000000000000000000000000000",
"name": "Samuel Schmied",
"email": "test3.yahoo.com"
},
{
"transaction_id": 7,
"date": "2021-04-14T09:28:46+00:00",
"memo": "test login crash",
"decay": {
"balance": 6,
"decay_duration": "0 days, 00 hours, 00 minutes, 28 seconds",
"decay_start": 1618390920,
"decay_end": 1618390948,
"decay_start_block": 1618390920
},
"balance": 100000,
"type": "receive",
"pubkey": "0000000000000000000000000000000000000000000000000000000000000000",
@ -223,22 +220,52 @@ class AppRequestControllerTest extends TestCase
},
{
"transaction_id": 8,
"date": "2021-04-14T09:31:28+00:00",
"date": "2021-04-14T09:28:46+00:00",
"memo": "test login crash",
"decay": {
"balance": 309,
"decay_duration": "0 days, 00 hours, 26 minutes, 18 seconds",
"decay_start": 1618390948,
"decay_end": 1618392526
},
"balance": 100000,
"type": "receive",
"pubkey": "0000000000000000000000000000000000000000000000000000000000000000",
"name": "Samuel Schmied",
"email": "test3.yahoo.com"
},
{
"transaction_id": 9,
"date": "2021-04-14T09:31:28+00:00",
"memo": "test login crash",
"decay": {
"balance": 33,
"decay_duration": "0 days, 00 hours, 02 minutes, 42 seconds",
"decay_start": 1618392526,
"decay_end": 1618392688
},
"balance": 100000,
"type": "receive",
"pubkey": "0000000000000000000000000000000000000000000000000000000000000000",
"name": "Samuel Schmied",
"email": "test3.yahoo.com"
},
{
"type": "decay",
"balance": 1345726,
"decay_duration": "on 14.04.21",
"decay_start": 1618392688,
"decay_end": 1625673853,
"memo": ""
}
],
"transactionExecutingCount": 0,
"count": 7,
"gdtSum": 180000,
"timeUsed": 0.7237420082092285,
"decay_date": "2021-06-22T08:54:43+00:00",
"balance": 9100000,
"decay": 9100000
"timeUsed": 0.44154810905456545,
"decay_date": "2021-07-07T16:04:13+00:00",
"balance": 9099652,
"decay": 7753926
}';
$this->getAndParse('/api/list-transactions/', json_decode($expectedResult, true));
}
@ -276,6 +303,19 @@ class AppRequestControllerTest extends TestCase
$expected[$field] = $json->$field;
}
}
// decay balance variy always
if(isset($expected['transactions'])) {
$dynamic_transaction_fields = ['decay_duration', 'balance', 'decay_end'];
foreach($expected['transactions'] as $i => $transaction) {
if(isset($transaction['type']) && $transaction['type'] == 'decay') {
foreach($dynamic_transaction_fields as $field) {
if(isset($transaction[$field])) {
$expected['transactions'][$i][$field] = $json->transactions[$i][$field];
}
}
}
}
}
$expected = json_encode($expected);
}

View File

@ -63,6 +63,8 @@ Assuming: session is valid
"type": "decay",
"balance": "14.74",
"decay_duration": "4 days, 2 hours ago",
"decay_start": 1618390948,
"decay_end": 1618392526,
"memo": ""
},
{
@ -73,7 +75,13 @@ Assuming: session is valid
"date": "2021-02-19T13:25:36+00:00",
"balance": 192.0,
"memo": "a piece of cake :)",
"pubkey": "038a6f93270dc57b91d76bf110ad3863fcb7d1b08e7692e793fcdb4467e5b6a7"
"pubkey": "038a6f93270dc57b91d76bf110ad3863fcb7d1b08e7692e793fcdb4467e5b6a7",
"decay": {
"balance": 309,
"decay_duration": "0 days, 00 hours, 26 minutes, 18 seconds",
"decay_start": 1618390948,
"decay_end": 1618392526
},
},
{
"name": "Gradido Akademie",

View File

@ -59,7 +59,6 @@
"sweetalert2": "^9.5.4",
"vee-validate": "^3.4.5",
"vue": "^2.6.11",
"vue-bootstrap-toasts": "^1.0.7",
"vue-bootstrap-typeahead": "^0.2.6",
"vue-chartjs": "^3.5.0",
"vue-cli-plugin-i18n": "^1.0.1",
@ -75,6 +74,7 @@
"vue-qrcode": "^0.3.5",
"vue-qrcode-reader": "^2.3.16",
"vue-router": "^3.0.6",
"vue-toasted": "^1.1.28",
"vue2-transitions": "^0.2.3",
"vuex": "^3.6.0",
"vuex-persistedstate": "^4.0.0-beta.3"

View File

@ -3,7 +3,6 @@
<div class="">
<particles-bg type="custom" :config="config" :bg="true" />
<component :is="$route.meta.requiresAuth ? 'DashboardLayout' : 'AuthLayoutGDD'" />
<Toasts></Toasts>
</div>
</div>
</template>

View File

@ -93,7 +93,7 @@ const loginAPI = {
update: {
'User.first_name': data.firstName,
'User.last_name': data.lastName,
'User.description': data.description,
// 'User.description': data.description,
},
}
return apiPost(CONFIG.LOGIN_API_URL + 'updateUserInfos', payload)

View File

@ -0,0 +1,85 @@
<template>
<div>
<span v-if="decaytyp === 'short'">
<small>{{ decay ? ' ' + decay.balance + ' ' + decayStartBlockTextShort : '' }}</small>
</span>
<div v-if="decaytyp === 'new'">
<b-list-group style="border: 0px">
<b-list-group-item style="border: 0px; background-color: #f1f1f1">
<div class="d-flex">
<div style="width: 40%" class="text-right pr-3 mr-2">
{{ $t('decay.calculation_decay') }}
<b-icon icon="droplet-half" height="12" class="mb-2" />
</div>
<div style="width: 60%"></div>
</div>
<div class="d-flex">
<div style="width: 40%" class="text-right pr-3 mr-2">
{{ $t('decay.last_transaction') }}
</div>
<div style="width: 60%">
<div v-if="decay.decay_start_block > 0">
<div class="display-4">{{ $t('decay.Starting_block_decay') }}</div>
<div>
{{ $t('decay.decay_introduced') }} :
{{ $d($moment.unix(decay.decay_start), 'long') }}
</div>
</div>
<div>
<span>{{ $d($moment.unix(decay.decay_start), 'long') }} Uhr</span>
</div>
</div>
</div>
<div class="d-flex">
<div style="width: 40%" class="text-right pr-3 mr-2">
{{ $t('decay.past_time') }}
</div>
<div style="width: 60%">
<div v-if="decay.decay_start_block > 0">{{ $t('decay.since_introduction') }}</div>
<span v-if="duration">
<b v-if="duration.years > 0">{{ duration.years }} {{ $t('decay.year') }},</b>
<b v-if="duration.months > 0">{{ duration.months }} {{ $t('decay.months') }},</b>
<b v-if="duration.days > 0">{{ duration.days }} {{ $t('decay.days') }},</b>
<b v-if="duration.hours > 0">{{ duration.hours }} {{ $t('decay.hours') }},</b>
<b v-if="duration.minutes > 0">{{ duration.minutes }} {{ $t('decay.minutes') }},</b>
<b v-if="duration.seconds > 0">{{ duration.seconds }} {{ $t('decay.seconds') }}</b>
</span>
</div>
</div>
</b-list-group-item>
</b-list-group>
</div>
</div>
</template>
<script>
export default {
name: 'DecayInformation',
props: {
decay: {
balance: '',
decay_duration: '',
decay_start: 0,
decay_end: 0,
decay_start_block: 0,
},
decaytyp: { type: String, default: '' },
},
computed: {
decayStartBlockTextShort() {
return this.decay.decay_start_block
? ' - Startblock Decay am: ' + this.$d(this.$moment.unix(this.decay.decay_start_block))
: ''
},
duration() {
return this.$moment.duration(
this.$moment
.unix(new Date(this.decay.decay_end))
.diff(this.$moment.unix(new Date(this.decay.decay_start))),
)._data
},
},
}
</script>

View File

@ -0,0 +1,71 @@
import { mount } from '@vue/test-utils'
import InputEmail from './InputEmail'
const localVue = global.localVue
describe('InputEmail', () => {
let wrapper
const propsData = {
name: 'input-field-name',
label: 'input-field-label',
placeholder: 'input-field-placeholder',
value: '',
}
const Wrapper = () => {
return mount(InputEmail, { localVue, propsData })
}
describe('mount', () => {
beforeEach(() => {
wrapper = Wrapper()
})
it('has an input field', () => {
expect(wrapper.find('input').exists()).toBeTruthy()
})
describe('properties', () => {
it('has the name "input-field-name"', () => {
expect(wrapper.find('input').attributes('name')).toEqual('input-field-name')
})
it('has the id "input-field-name-input-field"', () => {
expect(wrapper.find('input').attributes('id')).toEqual('input-field-name-input-field')
})
it('has the placeholder "input-field-placeholder"', () => {
expect(wrapper.find('input').attributes('placeholder')).toEqual('input-field-placeholder')
})
it('has the value ""', () => {
expect(wrapper.vm.currentValue).toEqual('')
})
it('has the label "input-field-label"', () => {
expect(wrapper.find('label').text()).toEqual('input-field-label')
})
it('has the label for "input-field-name-input-field"', () => {
expect(wrapper.find('label').attributes('for')).toEqual('input-field-name-input-field')
})
})
describe('input value changes', () => {
it('emits input with new value', async () => {
await wrapper.find('input').setValue('12')
expect(wrapper.emitted('input')).toBeTruthy()
expect(wrapper.emitted('input')).toEqual([['12']])
})
})
describe('value property changes', () => {
it('updates data model', async () => {
await wrapper.setProps({ value: 'user@example.org' })
expect(wrapper.vm.currentValue).toEqual('user@example.org')
})
})
})
})

View File

@ -0,0 +1,73 @@
<template>
<validation-provider
tag="div"
:rules="rules"
:name="name"
v-slot="{ errors, valid, validated, ariaInput, ariaMsg }"
>
<b-form-group :label="label" :label-for="labelFor">
<b-input-group>
<b-form-input
v-model="currentValue"
v-bind="ariaInput"
:id="labelFor"
:name="name"
:placeholder="placeholder"
type="email"
:state="validated ? valid : false"
trim
class="email-form-input"
></b-form-input>
<b-form-invalid-feedback v-bind="ariaMsg">
{{ errors[0] }}
</b-form-invalid-feedback>
</b-input-group>
</b-form-group>
</validation-provider>
</template>
<script>
export default {
name: 'InputEmail',
props: {
rules: {
default: () => {
return {
required: true,
email: true,
}
},
},
name: { type: String, default: 'Email' },
label: { type: String, default: 'Email' },
placeholder: { type: String, default: 'Email' },
value: { required: true, type: String },
},
data() {
return {
currentValue: '',
}
},
computed: {
labelFor() {
return this.name + '-input-field'
},
},
watch: {
currentValue() {
this.$emit('input', this.currentValue)
},
value() {
if (this.value !== this.currentValue) this.currentValue = this.value
},
},
}
</script>
<style>
.email-form-input {
border-right-style: solid !important;
border-right-width: 1px !important;
padding-right: 12px !important;
border-top-right-radius: 6px !important;
border-bottom-right-radius: 6px !important;
}
</style>

View File

@ -0,0 +1,98 @@
import { mount } from '@vue/test-utils'
import InputPassword from './InputPassword'
const localVue = global.localVue
describe('InputPassword', () => {
let wrapper
const propsData = {
name: 'input-field-name',
label: 'input-field-label',
placeholder: 'input-field-placeholder',
value: '',
}
const Wrapper = () => {
return mount(InputPassword, { localVue, propsData })
}
describe('mount', () => {
beforeEach(() => {
wrapper = Wrapper()
})
it('has an input field', () => {
expect(wrapper.find('input').exists()).toBeTruthy()
})
describe('properties', () => {
it('has the name "input-field-name"', () => {
expect(wrapper.find('input').attributes('name')).toEqual('input-field-name')
})
it('has the id "input-field-name-input-field"', () => {
expect(wrapper.find('input').attributes('id')).toEqual('input-field-name-input-field')
})
it('has the placeholder "input-field-placeholder"', () => {
expect(wrapper.find('input').attributes('placeholder')).toEqual('input-field-placeholder')
})
it('has the value ""', () => {
expect(wrapper.vm.currentValue).toEqual('')
})
it('has the label "input-field-label"', () => {
expect(wrapper.find('label').text()).toEqual('input-field-label')
})
it('has the label for "input-field-name-input-field"', () => {
expect(wrapper.find('label').attributes('for')).toEqual('input-field-name-input-field')
})
})
describe('input value changes', () => {
it('emits input with new value', async () => {
await wrapper.find('input').setValue('12')
expect(wrapper.emitted('input')).toBeTruthy()
expect(wrapper.emitted('input')).toEqual([['12']])
})
})
describe('password visibilty', () => {
it('has type password by default', () => {
expect(wrapper.find('input').attributes('type')).toEqual('password')
})
it('changes to type text when icon is clicked', async () => {
await wrapper.find('button').trigger('click')
expect(wrapper.find('input').attributes('type')).toEqual('text')
})
it('changes back to type password when icon is clicked twice', async () => {
await wrapper.find('button').trigger('click')
await wrapper.find('button').trigger('click')
expect(wrapper.find('input').attributes('type')).toEqual('password')
})
})
describe('password visibilty icon', () => {
it('is by default bi-eye-slash', () => {
expect(wrapper.find('svg').classes('bi-eye-slash')).toBe(true)
})
it('changes to bi-eye when clicked', async () => {
await wrapper.find('button').trigger('click')
expect(wrapper.find('svg').classes('bi-eye')).toBe(true)
})
it('changes back to bi-eye-slash when clicked twice', async () => {
await wrapper.find('button').trigger('click')
await wrapper.find('button').trigger('click')
expect(wrapper.find('svg').classes('bi-eye-slash')).toBe(true)
})
})
})
})

View File

@ -0,0 +1,81 @@
<template>
<validation-provider
tag="div"
:rules="rules"
:name="name"
:bails="!showAllErrors"
:immediate="immediate"
v-slot="{ errors, valid, validated, ariaInput, ariaMsg }"
>
<b-form-group :label="label" :label-for="labelFor">
<b-input-group>
<b-form-input
v-model="currentValue"
v-bind="ariaInput"
:id="labelFor"
:name="name"
:placeholder="placeholder"
:type="showPassword ? 'text' : 'password'"
:state="validated ? valid : false"
></b-form-input>
<b-input-group-append>
<b-button variant="outline-primary" @click="toggleShowPassword">
<b-icon :icon="showPassword ? 'eye' : 'eye-slash'" />
</b-button>
</b-input-group-append>
<b-form-invalid-feedback v-bind="ariaMsg">
<div v-if="showAllErrors">
<span v-for="error in errors" :key="error">
{{ error }}
<br />
</span>
</div>
<div v-else>
{{ errors[0] }}
</div>
</b-form-invalid-feedback>
</b-input-group>
</b-form-group>
</validation-provider>
</template>
<script>
export default {
name: 'InputPassword',
props: {
rules: {
default: () => {
return {
required: true,
}
},
},
name: { type: String, default: 'password' },
label: { type: String, default: 'Password' },
placeholder: { type: String, default: 'Password' },
value: { required: true, type: String },
showAllErrors: { type: Boolean, default: false },
immediate: { type: Boolean, default: false },
},
data() {
return {
currentValue: '',
showPassword: false,
}
},
computed: {
labelFor() {
return this.name + '-input-field'
},
},
methods: {
toggleShowPassword() {
this.showPassword = !this.showPassword
},
},
watch: {
currentValue() {
this.$emit('input', this.currentValue)
},
},
}
</script>

View File

@ -0,0 +1,64 @@
import { mount } from '@vue/test-utils'
import InputPasswordConfirmation from './InputPasswordConfirmation'
const localVue = global.localVue
// validation is tested in src/views/Pages/UserProfile/UserCard_FormUserPasswort.spec.js
describe('InputPasswordConfirmation', () => {
let wrapper
const propsData = {
value: {
password: '',
passwordRepeat: '',
},
}
const mocks = {
$t: jest.fn((t) => t),
}
const Wrapper = () => {
return mount(InputPasswordConfirmation, { localVue, propsData, mocks })
}
describe('mount', () => {
beforeEach(() => {
wrapper = Wrapper()
})
it('has two input fields', () => {
expect(wrapper.findAll('input')).toHaveLength(2)
})
describe('input values ', () => {
it('emits input with new value for first input field', async () => {
await wrapper.findAll('input').at(0).setValue('1234')
expect(wrapper.emitted('input')).toBeTruthy()
expect(wrapper.emitted('input')).toEqual([
[
{
password: '1234',
passwordRepeat: '',
},
],
])
})
it('emits input with new value for second input field', async () => {
await wrapper.findAll('input').at(1).setValue('1234')
expect(wrapper.emitted('input')).toBeTruthy()
expect(wrapper.emitted('input')).toEqual([
[
{
password: '',
passwordRepeat: '1234',
},
],
])
})
})
})
})

View File

@ -0,0 +1,68 @@
<template>
<div>
<b-row class="mb-2">
<b-col>
<input-password
:rules="{
required: true,
containsLowercaseCharacter: true,
containsUppercaseCharacter: true,
containsNumericCharacter: true,
atLeastEightCharactera: true,
}"
:label="$t('form.password_new')"
:showAllErrors="true"
:immediate="true"
:name="$t('form.password_new')"
:placeholder="$t('form.password_new')"
v-model="password"
></input-password>
</b-col>
</b-row>
<b-row class="mb-2">
<b-col>
<input-password
:rules="{ samePassword: value.password }"
:label="$t('form.password_new_repeat')"
:placeholder="$t('form.password_new_repeat')"
v-model="passwordRepeat"
></input-password>
</b-col>
</b-row>
</div>
</template>
<script>
import InputPassword from './InputPassword'
export default {
name: 'InputPasswordConfirm',
components: {
InputPassword,
},
props: {
value: {
type: Object,
required: true,
},
},
data() {
return {
password: '',
passwordRepeat: '',
}
},
computed: {
passwordObject() {
return { password: this.password, passwordRepeat: this.passwordRepeat }
},
},
watch: {
password() {
this.$emit('input', this.passwordObject)
},
passwordRepeat() {
this.$emit('input', this.passwordObject)
},
},
}
</script>

View File

@ -6,7 +6,7 @@
<b-icon icon="chevron-left" variant="primary"></b-icon>
</b-button>
</b-col>
<b-col cols="2">
<b-col cols="3">
<p class="text-center pt-2">{{ currentPage }} / {{ totalPages }}</p>
</b-col>
<b-col>

View File

@ -18,7 +18,25 @@
"de": "Deutsch",
"en": "English"
},
"decay": "Vergänglichkeit",
"decay": {
"decay": "Vergänglichkeit",
"decay_since_last_transaction":"Vergänglichkeit seit der letzten Transaktion",
"calculation_decay":"Berechnung der Vergänglichkeit",
"Starting_block_decay":"Startblock Vergänglichkeit",
"decay_introduced":"Die Vergänglichkeit wurde Eingeführt am",
"last_transaction":"Letzte Transaktion",
"past_time":"Vergangene Zeit",
"since_introduction":"seit Einführung der Vergänglichkeit",
"year":"Jahre",
"months":"Monate",
"days":"Tage",
"hours":"Stunden",
"minutes":"Minuten",
"seconds":"Sekunden",
"received":"empfangen",
"sent":"gesendet",
"created":"geschöpft"
},
"form": {
"cancel": "Abbrechen",
"reset": "Zurücksetzen",
@ -66,6 +84,7 @@
},
"error": {
"error":"Fehler",
"no-account": "Leider konnten wir keinen Account finden mit diesen Daten!",
"change-password": "Fehler beim Ändern des Passworts"
},
"transaction":{
@ -140,6 +159,7 @@
},
"reset-password": {
"title": "Passwort zurücksetzen",
"text": "Jetzt kannst du ein neues Passwort speichern, mit dem du dich zukünftig in der Gradido-App anmelden kannst."
"text": "Jetzt kannst du ein neues Passwort speichern, mit dem du dich zukünftig in der Gradido-App anmelden kannst.",
"not-authenticated": "Leider konnten wir dich nicht authentifizieren. Bitte wende dich an den Support."
}
}

View File

@ -18,7 +18,25 @@
"de": "Deutsch",
"en": "English"
},
"decay": "Decay",
"decay": {
"decay": "Decay",
"decay_since_last_transaction":"Decay since the last transaction",
"calculation_decay": "Calculation of Decay",
"Starting_block_decay": "Starting Block Decay",
"decay_introduced": "Decay was Introduced on",
"last_transaction": "Last transaction:",
"past_time": "Past time",
"since_introduction": "Since the introduction of Decay",
"year": "Years",
"months": "Months",
"days": "Days",
"hours": "Hours",
"minutes": "Minutes",
"seconds": "Seconds",
"received":"received",
"sent":"sent",
"created":"created"
},
"form": {
"cancel":"Cancel",
"reset": "Reset",
@ -66,6 +84,7 @@
},
"error": {
"error":"Error",
"no-account": "Unfortunately we could not find an account to the given data!",
"change-password": "Error while changing password"
},
"transaction":{
@ -141,6 +160,7 @@
},
"reset-password": {
"title": "Reset Password",
"text": "Now you can save a new password to login to the Gradido-App in the future."
"text": "Now you can save a new password to login to the Gradido-App in the future.",
"not-authenticated": "Unfortunately we could not authenticate you. Please contact the support."
}
}

View File

@ -2,22 +2,18 @@ import Vue from 'vue'
import DashboardPlugin from './plugins/dashboard-plugin'
import App from './App.vue'
import i18n from './i18n.js'
import { configure, extend } from 'vee-validate'
// eslint-disable-next-line camelcase
import { required, email, min, max, is_not } from 'vee-validate/dist/rules'
import { loadAllRules } from './validation-rules'
// store
import { store } from './store/store'
import loginAPI from './apis/loginAPI'
// router setup
import router from './routes/router'
// plugin setup
Vue.use(DashboardPlugin)
Vue.config.productionTip = false
loadAllRules(i18n)
router.beforeEach((to, from, next) => {
if (to.meta.requiresAuth && !store.state.sessionId) {
next({ path: '/login' })
@ -26,68 +22,6 @@ router.beforeEach((to, from, next) => {
}
})
configure({
defaultMessage: (field, values) => {
values._field_ = i18n.t(`fields.${field}`)
return i18n.t(`validations.messages.${values._rule_}`, values)
},
})
extend('email', {
...email,
message: (_, values) => i18n.t('validations.messages.email', values),
})
extend('required', {
...required,
message: (_, values) => i18n.t('validations.messages.required', values),
})
extend('min', {
...min,
message: (_, values) => i18n.t('validations.messages.min', values),
})
extend('max', {
...max,
message: (_, values) => i18n.t('validations.messages.max', values),
})
extend('gddSendAmount', {
validate(value, { min, max }) {
value = value.replace(',', '.')
return value.match(/^[0-9]+(\.[0-9]{0,2})?$/) && Number(value) >= min && Number(value) <= max
},
params: ['min', 'max'],
message: (_, values) => {
values.min = i18n.n(values.min, 'ungroupedDecimal')
values.max = i18n.n(values.max, 'ungroupedDecimal')
return i18n.t('form.validation.gddSendAmount', values)
},
})
extend('gddUsernameUnique', {
async validate(value) {
const result = await loginAPI.checkUsername(value)
return result.result.data.state === 'success'
},
message: (_, values) => i18n.t('form.validation.usernmae-unique', values),
})
extend('gddUsernameRgex', {
validate(value) {
return !!value.match(/^[a-zA-Z][-_a-zA-Z0-9]{2,}$/)
},
message: (_, values) => i18n.t('form.validation.usernmae-regex', values),
})
// eslint-disable-next-line camelcase
extend('is_not', {
// eslint-disable-next-line camelcase
...is_not,
message: (_, values) => i18n.t('form.validation.is-not', values),
})
/* eslint-disable no-new */
new Vue({
el: '#app',

View File

@ -1,12 +1,11 @@
import '@/polyfills'
import { configure, extend } from 'vee-validate'
import GlobalComponents from './globalComponents'
import GlobalDirectives from './globalDirectives'
import SideBar from '@/components/SidebarPlugin'
import PortalVue from 'portal-vue'
import VueBootstrapToasts from 'vue-bootstrap-toasts'
import Toasted from 'vue-toasted'
// vue-bootstrap
import { BootstrapVue, IconsPlugin } from 'bootstrap-vue'
@ -14,8 +13,6 @@ import { BootstrapVue, IconsPlugin } from 'bootstrap-vue'
// asset imports
import '@/assets/scss/argon.scss'
import '@/assets/vendor/nucleo/css/nucleo.css'
import * as rules from 'vee-validate/dist/rules'
import { messages } from 'vee-validate/dist/locale/en.json'
import VueQrcodeReader from 'vue-qrcode-reader'
import VueQrcode from 'vue-qrcode'
@ -28,13 +25,6 @@ import VueMoment from 'vue-moment'
import Loading from 'vue-loading-overlay'
import 'vue-loading-overlay/dist/vue-loading.css'
Object.keys(rules).forEach((rule) => {
extend(rule, {
...rules[rule], // copies rule configuration
message: messages[rule], // assign message
})
})
export default {
install(Vue) {
Vue.use(GlobalComponents)
@ -43,17 +33,20 @@ export default {
Vue.use(PortalVue)
Vue.use(BootstrapVue)
Vue.use(IconsPlugin)
Vue.use(VueBootstrapToasts)
Vue.use(VueMoment)
Vue.use(VueQrcodeReader)
Vue.use(VueQrcode)
Vue.use(FlatPickr)
Vue.use(Loading)
configure({
classes: {
valid: 'is-valid',
invalid: 'is-invalid',
dirty: ['is-dirty', 'is-dirty'], // multiple classes per flag!
Vue.use(Toasted, {
position: 'top-center',
duration: 5000,
fullWidth: true,
action: {
text: 'x',
onClick: (e, toastObject) => {
toastObject.goAway(0)
},
},
})
},

View File

@ -1,6 +1,6 @@
import { mutations, actions } from './store'
const { language, email, sessionId } = mutations
const { language, email, sessionId, username, firstName, lastName, description } = mutations
const { login, logout } = actions
describe('Vuex store', () => {
@ -28,51 +28,102 @@ describe('Vuex store', () => {
expect(state.sessionId).toEqual('1234')
})
})
describe('username', () => {
it('sets the state of username', () => {
const state = { username: null }
username(state, 'user')
expect(state.username).toEqual('user')
})
})
describe('firstName', () => {
it('sets the state of firstName', () => {
const state = { firstName: null }
firstName(state, 'Peter')
expect(state.firstName).toEqual('Peter')
})
})
describe('lastName', () => {
it('sets the state of lastName', () => {
const state = { lastName: null }
lastName(state, 'Lustig')
expect(state.lastName).toEqual('Lustig')
})
})
describe('description', () => {
it('sets the state of description', () => {
const state = { description: null }
description(state, 'Nickelbrille')
expect(state.description).toEqual('Nickelbrille')
})
})
})
describe('actions', () => {
describe('login', () => {
const commit = jest.fn()
const state = {}
const commitedData = {
sessionId: 1234,
user: {
email: 'someone@there.is',
language: 'en',
username: 'user',
first_name: 'Peter',
last_name: 'Lustig',
description: 'Nickelbrille',
},
}
it('calls three commits', () => {
login(
{ commit, state },
{ sessionId: 1234, user: { email: 'someone@there.is', language: 'en' } },
)
it('calls seven commits', () => {
login({ commit, state }, commitedData)
expect(commit).toHaveBeenCalledTimes(7)
})
it('commits sessionId', () => {
login(
{ commit, state },
{ sessionId: 1234, user: { email: 'someone@there.is', language: 'en' } },
)
login({ commit, state }, commitedData)
expect(commit).toHaveBeenNthCalledWith(1, 'sessionId', 1234)
})
it('commits email', () => {
login(
{ commit, state },
{ sessionId: 1234, user: { email: 'someone@there.is', language: 'en' } },
)
login({ commit, state }, commitedData)
expect(commit).toHaveBeenNthCalledWith(2, 'email', 'someone@there.is')
})
it('commits language', () => {
login(
{ commit, state },
{ sessionId: 1234, user: { email: 'someone@there.is', language: 'en' } },
)
login({ commit, state }, commitedData)
expect(commit).toHaveBeenNthCalledWith(3, 'language', 'en')
})
it('commits username', () => {
login({ commit, state }, commitedData)
expect(commit).toHaveBeenNthCalledWith(4, 'username', 'user')
})
it('commits firstName', () => {
login({ commit, state }, commitedData)
expect(commit).toHaveBeenNthCalledWith(5, 'firstName', 'Peter')
})
it('commits lastName', () => {
login({ commit, state }, commitedData)
expect(commit).toHaveBeenNthCalledWith(6, 'lastName', 'Lustig')
})
it('commits description', () => {
login({ commit, state }, commitedData)
expect(commit).toHaveBeenNthCalledWith(7, 'description', 'Nickelbrille')
})
})
describe('logout', () => {
const commit = jest.fn()
const state = {}
it('calls two commits', () => {
it('calls six commits', () => {
logout({ commit, state })
expect(commit).toHaveBeenCalledTimes(6)
})
@ -87,11 +138,36 @@ describe('Vuex store', () => {
expect(commit).toHaveBeenNthCalledWith(2, 'email', null)
})
// how can I get this working?
it.skip('calls sessionStorage.clear()', () => {
it('commits username', () => {
logout({ commit, state })
const spy = jest.spyOn(sessionStorage, 'clear')
expect(spy).toHaveBeenCalledTimes(1)
expect(commit).toHaveBeenNthCalledWith(3, 'username', '')
})
it('commits firstName', () => {
logout({ commit, state })
expect(commit).toHaveBeenNthCalledWith(4, 'firstName', '')
})
it('commits lastName', () => {
logout({ commit, state })
expect(commit).toHaveBeenNthCalledWith(5, 'lastName', '')
})
it('commits description', () => {
logout({ commit, state })
expect(commit).toHaveBeenNthCalledWith(6, 'description', '')
})
// how to get this working?
it.skip('calls sessionStorage.clear()', () => {
const clearStorageMock = jest.fn()
global.sessionStorage = jest.fn(() => {
return {
clear: clearStorageMock,
}
})
logout({ commit, state })
expect(clearStorageMock).toBeCalled()
})
})
})

View File

@ -0,0 +1,110 @@
import { configure, extend } from 'vee-validate'
// eslint-disable-next-line camelcase
import { required, email, min, max, is_not } from 'vee-validate/dist/rules'
import loginAPI from './apis/loginAPI'
export const loadAllRules = (i18nCallback) => {
configure({
defaultMessage: (field, values) => {
values._field_ = i18nCallback.t(`fields.${field}`)
return i18nCallback.t(`validations.messages.${values._rule_}`, values)
},
classes: {
valid: 'is-valid',
invalid: 'is-invalid',
dirty: ['is-dirty', 'is-dirty'], // multiple classes per flag!
},
})
extend('email', {
...email,
message: (_, values) => i18nCallback.t('validations.messages.email', values),
})
extend('required', {
...required,
message: (_, values) => i18nCallback.t('validations.messages.required', values),
})
extend('min', {
...min,
message: (_, values) => i18nCallback.t('validations.messages.min', values),
})
extend('max', {
...max,
message: (_, values) => i18nCallback.t('validations.messages.max', values),
})
extend('gddSendAmount', {
validate(value, { min, max }) {
value = value.replace(',', '.')
return value.match(/^[0-9]+(\.[0-9]{0,2})?$/) && Number(value) >= min && Number(value) <= max
},
params: ['min', 'max'],
message: (_, values) => {
values.min = i18nCallback.n(values.min, 'ungroupedDecimal')
values.max = i18nCallback.n(values.max, 'ungroupedDecimal')
return i18nCallback.t('form.validation.gddSendAmount', values)
},
})
extend('gddUsernameUnique', {
async validate(value) {
const result = await loginAPI.checkUsername(value)
return result.result.data.state === 'success'
},
message: (_, values) => i18nCallback.t('form.validation.usernmae-unique', values),
})
extend('gddUsernameRgex', {
validate(value) {
return !!value.match(/^[a-zA-Z][-_a-zA-Z0-9]{2,}$/)
},
message: (_, values) => i18nCallback.t('form.validation.usernmae-regex', values),
})
// eslint-disable-next-line camelcase
extend('is_not', {
// eslint-disable-next-line camelcase
...is_not,
message: (_, values) => i18nCallback.t('form.validation.is-not', values),
})
// Password validation
extend('containsLowercaseCharacter', {
validate(value) {
return !!value.match(/[a-z]+/)
},
message: (_, values) => i18nCallback.t('site.signup.lowercase', values),
})
extend('containsUppercaseCharacter', {
validate(value) {
return !!value.match(/[A-Z]+/)
},
message: (_, values) => i18nCallback.t('site.signup.uppercase', values),
})
extend('containsNumericCharacter', {
validate(value) {
return !!value.match(/[0-9]+/)
},
message: (_, values) => i18nCallback.t('site.signup.one_number', values),
})
extend('atLeastEightCharactera', {
validate(value) {
return !!value.match(/.{8,}/)
},
message: (_, values) => i18nCallback.t('site.signup.minimum', values),
})
extend('samePassword', {
validate(value, [pwd]) {
return value === pwd
},
message: (_, values) => i18nCallback.t('site.signup.dont_match', values),
})
}

View File

@ -34,7 +34,11 @@
</span>
<b-media-body class="ml-2">
<span class="avatar">
<vue-qrcode :value="$store.state.email" type="image/png"></vue-qrcode>
<vue-qrcode
v-if="$store.state.email"
:value="$store.state.email"
type="image/png"
></vue-qrcode>
</span>
</b-media-body>
</b-media>

View File

@ -1,34 +1,127 @@
import { shallowMount } from '@vue/test-utils'
import { mount } from '@vue/test-utils'
import AccountOverview from './AccountOverview'
import communityAPI from '../../apis/communityAPI.js'
jest.mock('../../apis/communityAPI.js')
const sendMock = jest.fn()
sendMock.mockReturnValue({ success: true })
communityAPI.send = sendMock
const localVue = global.localVue
describe('AccountOverview', () => {
let wrapper
const propsData = {
balance: 123.45,
transactionCount: 1,
}
const mocks = {
$t: jest.fn((t) => t),
$store: {
state: {
sessionId: 1,
},
},
$n: jest.fn((n) => String(n)),
}
const Wrapper = () => {
return shallowMount(AccountOverview, { localVue, mocks })
return mount(AccountOverview, { localVue, mocks, propsData })
}
describe('shallow Mount', () => {
describe('mount', () => {
beforeEach(() => {
wrapper = Wrapper()
})
it('has a status line', () => {
expect(wrapper.find('gdd-status-stub').exists()).toBeTruthy()
expect(wrapper.find('div.gdd-status').exists()).toBeTruthy()
})
it('has a send field', () => {
expect(wrapper.find('gdd-send-stub').exists()).toBeTruthy()
expect(wrapper.find('div.gdd-send').exists()).toBeTruthy()
})
it('has a transactions table', () => {
expect(wrapper.find('gdd-transaction-list-stub').exists()).toBeTruthy()
expect(wrapper.find('div.gdd-transaction-list').exists()).toBeTruthy()
})
describe('transaction form', () => {
it('steps forward in the dialog', async () => {
await wrapper.findComponent({ name: 'TransactionForm' }).vm.$emit('set-transaction', {
email: 'user@example.org',
amount: 23.45,
memo: 'Make the best of it!',
})
expect(wrapper.findComponent({ name: 'TransactionConfirmation' }).exists()).toBeTruthy()
})
})
describe('confirm transaction', () => {
beforeEach(() => {
wrapper.setData({
currentTransactionStep: 1,
transactionData: {
email: 'user@example.org',
amount: 23.45,
memo: 'Make the best of it!',
},
})
})
it('resets the transaction process when on-reset is emitted', async () => {
await wrapper.findComponent({ name: 'TransactionConfirmation' }).vm.$emit('on-reset')
expect(wrapper.findComponent({ name: 'TransactionForm' }).exists()).toBeTruthy()
expect(wrapper.vm.transactionData).toEqual({
email: '',
amount: 0,
memo: '',
})
})
describe('transaction is confirmed and server response is success', () => {
beforeEach(async () => {
jest.clearAllMocks()
await wrapper
.findComponent({ name: 'TransactionConfirmation' })
.vm.$emit('send-transaction')
})
it('calls the API when send-transaction is emitted', async () => {
expect(sendMock).toBeCalledWith(1, {
email: 'user@example.org',
amount: 23.45,
memo: 'Make the best of it!',
})
})
it('emits update-balance', () => {
expect(wrapper.emitted('update-balance')).toBeTruthy()
expect(wrapper.emitted('update-balance')).toEqual([[23.45]])
})
it('shows the succes page', () => {
expect(wrapper.find('div.card-body').text()).toContain('form.send_transaction_success')
})
})
describe('transaction is confirmed and server response is error', () => {
beforeEach(async () => {
jest.clearAllMocks()
sendMock.mockReturnValue({ success: false })
await wrapper
.findComponent({ name: 'TransactionConfirmation' })
.vm.$emit('send-transaction')
})
it('shows the error page', () => {
expect(wrapper.find('div.card-body').text()).toContain('form.send_transaction_error')
})
})
})
})
})

View File

@ -30,7 +30,7 @@
<gdd-transaction-list
v-if="showContext"
:transactions="transactions"
:page-size="5"
:pageSize="5"
:timestamp="timestamp"
:transaction-count="transactionCount"
@update-transactions="updateTransactions"
@ -69,7 +69,7 @@ export default {
data() {
return {
timestamp: Date.now(),
transactionData: EMPTY_TRANSACTION_DATA,
transactionData: { ...EMPTY_TRANSACTION_DATA },
error: false,
currentTransactionStep: 0,
loading: false,
@ -94,7 +94,7 @@ export default {
},
methods: {
setTransaction(data) {
this.transactionData = data
this.transactionData = { ...data }
this.currentTransactionStep = 1
},
async sendTransaction() {
@ -110,7 +110,7 @@ export default {
this.loading = false
},
onReset() {
this.transactionData = EMPTY_TRANSACTION_DATA
this.transactionData = { ...EMPTY_TRANSACTION_DATA }
this.currentTransactionStep = 0
},
updateTransactions(pagination) {

View File

@ -1,5 +1,6 @@
import { mount } from '@vue/test-utils'
import TransactionForm from './TransactionForm'
import flushPromises from 'flush-promises'
const localVue = global.localVue
@ -19,8 +20,12 @@ describe('GddSend', () => {
},
}
const propsData = {
balance: 100.0,
}
const Wrapper = () => {
return mount(TransactionForm, { localVue, mocks })
return mount(TransactionForm, { localVue, mocks, propsData })
}
describe('mount', () => {
@ -53,6 +58,18 @@ describe('GddSend', () => {
'E-Mail',
)
})
it('flushes an error message when no valid email is given', async () => {
await wrapper.find('#input-group-1').find('input').setValue('a')
await flushPromises()
expect(wrapper.find('span.errors').text()).toBe('validations.messages.email')
})
it('trims the email after blur', async () => {
await wrapper.find('#input-group-1').find('input').setValue(' valid@email.com ')
await flushPromises()
expect(wrapper.vm.form.email).toBe('valid@email.com')
})
})
describe('ammount field', () => {
@ -73,6 +90,24 @@ describe('GddSend', () => {
'0.01',
)
})
it('flushes an error message when no valid amount is given', async () => {
await wrapper.find('#input-group-2').find('input').setValue('a')
await flushPromises()
expect(wrapper.find('span.errors').text()).toBe('form.validation.gddSendAmount')
})
it('flushes an error message when amount is too high', async () => {
await wrapper.find('#input-group-2').find('input').setValue('123.34')
await flushPromises()
expect(wrapper.find('span.errors').text()).toBe('form.validation.gddSendAmount')
})
it('flushes no errors when amount is valid', async () => {
await wrapper.find('#input-group-2').find('input').setValue('87.34')
await flushPromises()
expect(wrapper.find('span.errors').exists()).toBeFalsy()
})
})
describe('message text box', () => {
@ -89,6 +124,18 @@ describe('GddSend', () => {
it('has a label form.memo', () => {
expect(wrapper.find('label.input-3').text()).toBe('form.memo')
})
it('flushes an error message when memo is less than 5 characters', async () => {
await wrapper.find('#input-group-3').find('textarea').setValue('a')
await flushPromises()
expect(wrapper.find('span.errors').text()).toBe('validations.messages.min')
})
it('flushes no error message when memo is valid', async () => {
await wrapper.find('#input-group-3').find('textarea').setValue('Long enough')
await flushPromises()
expect(wrapper.find('span.errors').exists()).toBeFalsy()
})
})
describe('cancel button', () => {
@ -100,11 +147,42 @@ describe('GddSend', () => {
expect(wrapper.find('button[type="reset"]').text()).toBe('form.reset')
})
it.skip('clears the email field on click', async () => {
wrapper.find('#input-group-1').find('input').setValue('someone@watches.tv')
wrapper.find('button[type="reset"]').trigger('click')
await wrapper.vm.$nextTick()
expect(wrapper.vm.form.email).toBeNull()
it('clears all fields on click', async () => {
await wrapper.find('#input-group-1').find('input').setValue('someone@watches.tv')
await wrapper.find('#input-group-2').find('input').setValue('87.23')
await wrapper.find('#input-group-3').find('textarea').setValue('Long enugh')
await flushPromises()
expect(wrapper.vm.form.email).toBe('someone@watches.tv')
expect(wrapper.vm.form.amount).toBe('87.23')
expect(wrapper.vm.form.memo).toBe('Long enugh')
await wrapper.find('button[type="reset"]').trigger('click')
await flushPromises()
expect(wrapper.vm.form.email).toBe('')
expect(wrapper.vm.form.amount).toBe('')
expect(wrapper.vm.form.memo).toBe('')
})
})
describe('submit', () => {
beforeEach(async () => {
await wrapper.find('#input-group-1').find('input').setValue('someone@watches.tv')
await wrapper.find('#input-group-2').find('input').setValue('87.23')
await wrapper.find('#input-group-3').find('textarea').setValue('Long enugh')
await wrapper.find('form').trigger('submit')
await flushPromises()
})
it('emits set-transaction', async () => {
expect(wrapper.emitted('set-transaction')).toBeTruthy()
expect(wrapper.emitted('set-transaction')).toEqual([
[
{
email: 'someone@watches.tv',
amount: 87.23,
memo: 'Long enugh',
},
],
])
})
})
})

View File

@ -168,7 +168,7 @@ export default {
},
methods: {
onSubmit() {
this.normalizeAmount()
this.normalizeAmount(true)
this.$emit('set-transaction', {
email: this.form.email,
amount: this.form.amountValue,
@ -181,10 +181,11 @@ export default {
this.form.amount = ''
this.form.memo = ''
},
setTransaction(data) {
this.form.email = data.email
this.form.amount = data.amount
},
/*
setTransaction(data) {
this.form.email = data.email
this.form.amount = data.amount
}, */
normalizeAmount(isValid) {
this.amountFocused = false
if (!isValid) return

View File

@ -1,5 +1,5 @@
<template>
<div>
<div class="gdd-status">
<b-row>
<b-col class="p-0">
<b-card class="p-0" style="background-color: #ebebeba3 !important">

View File

@ -7,6 +7,10 @@ const errorHandler = jest.fn()
localVue.config.errorHandler = errorHandler
const scrollToMock = jest.fn()
global.scrollTo = scrollToMock
describe('GddTransactionList', () => {
let wrapper
@ -199,7 +203,7 @@ describe('GddTransactionList', () => {
})
it('shows the name of the receiver', () => {
expect(transaction.findAll('div').at(3).text()).toBe('decay')
expect(transaction.findAll('div').at(3).text()).toBe('decay.decay_since_last_transaction')
})
})
})
@ -263,29 +267,30 @@ describe('GddTransactionList', () => {
})
it('emits update-transactions when next button is clicked', async () => {
paginationButtons.find('button.next-page').trigger('click')
await wrapper.vm.$nextTick()
await paginationButtons.find('button.next-page').trigger('click')
expect(wrapper.emitted('update-transactions')[1]).toEqual([{ firstPage: 2, items: 25 }])
})
it('shows text "2 / 2" when next button is clicked', async () => {
paginationButtons.find('button.next-page').trigger('click')
await wrapper.vm.$nextTick()
await paginationButtons.find('button.next-page').trigger('click')
expect(paginationButtons.find('p.text-center').text()).toBe('2 / 2')
})
it('has next-button disabled when next button is clicked', async () => {
paginationButtons.find('button.next-page').trigger('click')
await wrapper.vm.$nextTick()
await paginationButtons.find('button.next-page').trigger('click')
expect(paginationButtons.find('button.next-page').attributes('disabled')).toBe('disabled')
})
it('scrolls to top after loading next page', async () => {
await paginationButtons.find('button.next-page').trigger('click')
expect(scrollToMock).toBeCalled()
})
it('emits update-transactions when preivous button is clicked after next buton', async () => {
paginationButtons.find('button.next-page').trigger('click')
await wrapper.vm.$nextTick()
paginationButtons.find('button.previous-page').trigger('click')
await wrapper.vm.$nextTick()
await paginationButtons.find('button.next-page').trigger('click')
await paginationButtons.find('button.previous-page').trigger('click')
expect(wrapper.emitted('update-transactions')[2]).toEqual([{ firstPage: 1, items: 25 }])
expect(scrollToMock).toBeCalled()
})
})
})

View File

@ -2,69 +2,79 @@
<div class="gdd-transaction-list">
<b-list-group>
<b-list-group-item
v-for="item in transactions"
:key="item.id"
style="background-color: #ebebeba3 !important"
v-for="{ decay, transaction_id, type, date, balance, name, memo } in transactions"
:key="transaction_id"
:style="type === 'decay' ? 'background-color:#f1e0ae3d' : ''"
>
<div class="d-flex gdd-transaction-list-item" v-b-toggle="'a' + item.date + ''">
<!-- ROW Start -->
<div class="d-flex gdd-transaction-list-item" v-b-toggle="'a' + date + ''">
<!-- ICON -->
<div style="width: 8%">
<b-icon :icon="getProperties(item).icon" :class="getProperties(item).class" />
<b-icon :icon="getProperties(type).icon" :class="getProperties(type).class" />
</div>
<!-- Text Links -->
<div class="font1_2em pr-2 text-right" style="width: 32%">
<span>{{ getProperties(item).operator }}</span>
{{ $n(item.balance, 'decimal') }}
<span>{{ getProperties(type).operator }}</span>
<small v-if="type === 'decay'">{{ $n(balance, 'decimal') }}</small>
<span v-else>{{ $n(balance, 'decimal') }}</span>
<div v-if="decay">
<br />
<b-icon v-if="type != 'decay'" icon="droplet-half" height="15" class="mb-3" />
</div>
</div>
<!-- Text Rechts -->
<div class="font1_2em text-left pl-2" style="width: 55%">
{{ item.name ? item.name : $t('decay') }}
<div v-if="item.date" class="text-sm">{{ $d($moment(item.date), 'long') }}</div>
{{ name ? name : '' }}
<span v-if="type === 'decay'">
<small>{{ $t('decay.decay_since_last_transaction') }}</small>
</span>
<div v-if="date" class="text-sm">{{ $d($moment(date), 'long') }}</div>
<decay-information v-if="decay" decaytyp="short" :decay="decay" />
</div>
<div class="text-right" style="width: 5%">
<!-- Collaps Toggle Button -->
<div v-if="type != 'decay'" class="text-right" style="width: 5%">
<b-button class="btn-sm">
<b>i</b>
</b-button>
</div>
</div>
<b-collapse :id="'a' + item.date + ''" class="mt-2">
<b-card>
<b-list-group>
<b-list-group-item v-if="item.type === 'send'">
<b-badge class="mr-4" variant="primary" pill>{{ $t('form.receiver') }}</b-badge>
{{ item.name }}
</b-list-group-item>
<b-list-group-item v-else>
<b-badge class="mr-4" variant="primary" pill>{{ $t('form.sender') }}</b-badge>
{{ item.name }}
</b-list-group-item>
<b-list-group-item>
<b-badge class="mr-4" variant="primary" pill>type</b-badge>
{{ item.type }}
</b-list-group-item>
<b-list-group-item>
<b-badge class="mr-5" variant="primary" pill>id</b-badge>
{{ item.transaction_id }}
</b-list-group-item>
<b-list-group-item>
<b-badge class="mr-4" variant="primary" pill>{{ $t('form.date') }}</b-badge>
{{ item.date }}
</b-list-group-item>
<b-list-group-item>
<b-badge class="mr-4" variant="primary" pill>gdd</b-badge>
{{ item.balance }}
</b-list-group-item>
<b-list-group-item>
<b-badge class="mr-4" variant="primary" pill>{{ $t('form.memo') }}</b-badge>
{{ item.memo }}
</b-list-group-item>
</b-list-group>
<b-button v-b-toggle="'collapse-1-inner' + item.date" variant="secondary">
{{ $t('transaction.more') }}
</b-button>
<b-collapse :id="'collapse-1-inner' + item.date" class="mt-2">
<b-card>{{ item }}</b-card>
</b-collapse>
</b-card>
<!-- ROW End -->
<!-- Collaps Start -->
<b-collapse v-if="type != 'decay'" :id="'a' + date + ''">
<b-list-group v-if="type === 'receive' || type === 'send'">
<b-list-group-item style="border: 0px; background-color: #f1f1f1">
<div class="d-flex">
<div style="width: 40%" class="text-right pr-3 mr-2">
{{ type === 'receive' ? 'von:' : 'an:' }}
</div>
<div style="width: 60%">
{{ name }}
<b-avatar class="mr-3"></b-avatar>
</div>
</div>
<div class="d-flex">
<div style="width: 40%" class="text-right pr-3 mr-2">
{{ type === 'receive' ? 'Nachricht:' : 'Nachricht:' }}
</div>
<div style="width: 60%">
{{ memo }}
</div>
</div>
</b-list-group-item>
</b-list-group>
<b-list-group v-if="type === 'creation'">
<b-list-group-item style="border: 0px">
<div class="d-flex">
<div style="width: 40%" class="text-right pr-3 mr-2">Schöpfung</div>
<div style="width: 60%">Aus der Community</div>
</div>
</b-list-group-item>
</b-list-group>
<decay-information v-if="decay" decaytyp="new" :decay="decay" />
</b-collapse>
<!-- Collaps End -->
</b-list-group-item>
<pagination-buttons
v-if="showPagination && transactionCount > pageSize"
@ -84,6 +94,7 @@
<script>
import PaginationButtons from '../../../components/PaginationButtons'
import DecayInformation from '../../../components/DecayInformation'
const iconsByType = {
send: { icon: 'arrow-left-circle', classes: 'text-danger', operator: '-' },
@ -96,10 +107,12 @@ export default {
name: 'gdd-transaction-list',
components: {
PaginationButtons,
DecayInformation,
},
data() {
return {
currentPage: 1,
startDecay: 0,
}
},
props: {
@ -133,8 +146,8 @@ export default {
items: this.pageSize,
})
},
getProperties(item) {
const type = iconsByType[item.type]
getProperties(givenType) {
const type = iconsByType[givenType]
if (type)
return {
icon: type.icon,
@ -149,10 +162,12 @@ export default {
showNext() {
this.currentPage++
this.updateTransactions()
window.scrollTo(0, 0)
},
showPrevious() {
this.currentPage--
this.updateTransactions()
window.scrollTo(0, 0)
},
},
}

View File

@ -83,9 +83,7 @@ describe('ForgotPassword', () => {
})
it('displays an error', () => {
expect(form.find('#reset-pwd--live-feedback').text()).toEqual(
'The Email field must be a valid email',
)
expect(form.find('div.invalid-feedback').text()).toEqual('validations.messages.email')
})
it('does not call the API', () => {
@ -96,8 +94,7 @@ describe('ForgotPassword', () => {
describe('valid Email', () => {
beforeEach(async () => {
await form.find('input').setValue('user@example.org')
form.trigger('submit')
await wrapper.vm.$nextTick()
await form.trigger('submit')
await flushPromises()
})

View File

@ -19,27 +19,7 @@
<b-card-body class="p-4">
<validation-observer ref="observer" v-slot="{ handleSubmit }">
<b-form role="form" @submit.prevent="handleSubmit(onSubmit)">
<validation-provider
name="Email"
:rules="{ required: true, email: true }"
v-slot="validationContext"
>
<b-form-group class="mb-3" label="Email" label-for="input-reset-pwd">
<b-form-input
id="input-reset-pwd"
name="input-reset-pwd"
v-model="form.email"
placeholder="Email"
:state="getValidationState(validationContext)"
aria-describedby="reset-pwd--live-feedback"
></b-form-input>
<b-form-invalid-feedback id="reset-pwd--live-feedback">
{{ validationContext.errors[0] }}
</b-form-invalid-feedback>
</b-form-group>
</validation-provider>
<input-email v-model="form.email"></input-email>
<div class="text-center">
<b-button type="submit" variant="primary">
{{ $t('site.password.reset_now') }}
@ -59,9 +39,13 @@
</template>
<script>
import loginAPI from '../../apis/loginAPI.js'
import InputEmail from '../../components/Inputs/InputEmail'
export default {
name: 'password',
components: {
InputEmail,
},
data() {
return {
disable: 'disabled',
@ -71,9 +55,6 @@ export default {
}
},
methods: {
getValidationState({ dirty, validated, valid = null }) {
return dirty || validated ? valid : null
},
async onSubmit() {
await loginAPI.sendEmail(this.form.email)
// always give success to avoid email spying

View File

@ -1,10 +1,37 @@
import { mount, RouterLinkStub } from '@vue/test-utils'
import flushPromises from 'flush-promises'
import loginAPI from '../../apis/loginAPI'
import Login from './Login'
jest.mock('../../apis/loginAPI')
const localVue = global.localVue
const mockLoginCall = jest.fn()
mockLoginCall.mockReturnValue({
success: true,
result: {
data: {
session_id: 1,
user: {
name: 'Peter Lustig',
},
},
},
})
loginAPI.login = mockLoginCall
const toastErrorMock = jest.fn()
const mockStoreDispach = jest.fn()
const mockRouterPush = jest.fn()
const spinnerHideMock = jest.fn()
const spinnerMock = jest.fn(() => {
return {
hide: spinnerHideMock,
}
})
describe('Login', () => {
let wrapper
@ -13,6 +40,18 @@ describe('Login', () => {
locale: 'en',
},
$t: jest.fn((t) => t),
$store: {
dispatch: mockStoreDispach,
},
$loading: {
show: spinnerMock,
},
$router: {
push: mockRouterPush,
},
$toasted: {
error: toastErrorMock,
},
}
const stubs = {
@ -76,16 +115,76 @@ describe('Login', () => {
it('has a Submit button', () => {
expect(wrapper.find('button[type="submit"]').exists()).toBeTruthy()
})
it('shows a warning when no valid Email is entered', async () => {
wrapper.find('input[placeholder="Email"]').setValue('no_valid@Email')
await flushPromises()
await expect(wrapper.find('.invalid-feedback').text()).toEqual(
'The Email field must be a valid email',
)
})
})
// to do: test submit button
describe('submit', () => {
describe('no data', () => {
it('displays a message that Email is required', async () => {
await wrapper.find('form').trigger('submit')
await flushPromises()
expect(wrapper.findAll('div.invalid-feedback').at(0).text()).toBe(
'validations.messages.required',
)
})
it('displays a message that password is required', async () => {
await wrapper.find('form').trigger('submit')
await flushPromises()
expect(wrapper.findAll('div.invalid-feedback').at(1).text()).toBe(
'validations.messages.required',
)
})
})
describe('valid data', () => {
beforeEach(async () => {
jest.clearAllMocks()
await wrapper.find('input[placeholder="Email"]').setValue('user@example.org')
await wrapper.find('input[placeholder="form.password"]').setValue('1234')
await flushPromises()
await wrapper.find('form').trigger('submit')
await flushPromises()
})
it('calls the API with the given data', () => {
expect(mockLoginCall).toBeCalledWith('user@example.org', '1234')
})
it('creates a spinner', () => {
expect(spinnerMock).toBeCalled()
})
describe('login success', () => {
it('dispatches server response to store', () => {
expect(mockStoreDispach).toBeCalledWith('login', {
sessionId: 1,
user: { name: 'Peter Lustig' },
})
})
it('redirects to overview page', () => {
expect(mockRouterPush).toBeCalledWith('/overview')
})
it('hides the spinner', () => {
expect(spinnerHideMock).toBeCalled()
})
})
describe('login fails', () => {
beforeEach(() => {
mockLoginCall.mockReturnValue({ success: false })
})
it('hides the spinner', () => {
expect(spinnerHideMock).toBeCalled()
})
it('toasts an error message', () => {
expect(toastErrorMock).toBeCalledWith('error.no-account')
})
})
})
})
})
})

View File

@ -13,7 +13,6 @@
</div>
</b-container>
</div>
<!-- Page content -->
<b-container class="mt--8">
<b-row class="justify-content-center">
<b-col lg="5" md="7">
@ -22,76 +21,16 @@
<div class="text-center text-muted mb-4">
<small>{{ $t('login') }}</small>
</div>
<validation-observer ref="observer" v-slot="{ handleSubmit }">
<b-form @submit.stop.prevent="handleSubmit(onSubmit)">
<validation-provider
name="Email"
:rules="{ required: true, email: true }"
v-slot="validationContext"
>
<b-form-group class="mb-3" label="Email" label-for="login-email">
<b-form-input
id="login-email"
name="example-input-1"
v-model="form.email"
placeholder="Email"
:state="getValidationState(validationContext)"
aria-describedby="login-email-live-feedback"
></b-form-input>
<b-form-invalid-feedback id="login-email-live-feedback">
{{ validationContext.errors[0] }}
</b-form-invalid-feedback>
</b-form-group>
</validation-provider>
<validation-provider
<input-email v-model="form.email"></input-email>
<input-password
:label="$t('form.password')"
:placeholder="$t('form.password')"
:name="$t('form.password')"
:rules="{ required: true }"
v-slot="validationContext"
>
<b-form-group
class="mb-5"
id="example-input-group-1"
:label="$t('form.password')"
label-for="example-input-1"
>
<b-input-group>
<b-form-input
id="input-pwd"
name="input-pwd"
v-model="form.password"
:placeholder="$t('form.password')"
:type="passwordVisible ? 'text' : 'password'"
:state="getValidationState(validationContext)"
aria-describedby="input-2-live-feedback"
></b-form-input>
<b-input-group-append>
<b-button variant="outline-primary" @click="togglePasswordVisibility">
<b-icon :icon="passwordVisible ? 'eye' : 'eye-slash'" />
</b-button>
</b-input-group-append>
</b-input-group>
<b-form-invalid-feedback id="input-2-live-feedback">
{{ validationContext.errors[0] }}
</b-form-invalid-feedback>
</b-form-group>
</validation-provider>
<b-alert v-show="loginfail" show dismissible variant="warning">
<span class="alert-text bv-example-row">
<b-row>
<b-col class="col-9 text-left text-dark">
<strong>
Leider konnten wir keinen Account finden mit diesen Daten!
</strong>
</b-col>
</b-row>
</span>
</b-alert>
<div class="text-center">
v-model="form.password"
></input-password>
<div class="text-center mt-4">
<b-button type="submit" variant="primary">{{ $t('login') }}</b-button>
</div>
</b-form>
@ -118,32 +57,27 @@
<script>
import loginAPI from '../../apis/loginAPI'
import CONFIG from '../../config'
import InputPassword from '../../components/Inputs/InputPassword'
import InputEmail from '../../components/Inputs/InputEmail'
export default {
name: 'login',
components: {
InputPassword,
InputEmail,
},
data() {
return {
form: {
email: '',
password: '',
// rememberMe: false
},
loginfail: false,
allowRegister: CONFIG.ALLOW_REGISTER,
passwordVisible: false,
}
},
methods: {
getValidationState({ dirty, validated, valid = null }) {
return dirty || validated ? valid : null
},
togglePasswordVisibility() {
this.passwordVisible = !this.passwordVisible
},
async onSubmit() {
// error info ausschalten
this.loginfail = false
const loader = this.$loading.show({
container: this.$refs.submitButton,
})
@ -157,7 +91,7 @@ export default {
loader.hide()
} else {
loader.hide()
this.loginfail = true
this.$toasted.error(this.$t('error.no-account'))
}
},
},

View File

@ -84,7 +84,7 @@ describe('Register', () => {
wrapper.find('#registerEmail').setValue('no_valid@Email')
await flushPromises()
await expect(wrapper.find('#registerEmailLiveFeedback').text()).toEqual(
'The Email field must be a valid email',
'validations.messages.email',
)
})

View File

@ -3,6 +3,8 @@ import loginAPI from '../../apis/loginAPI'
import ResetPassword from './ResetPassword'
import flushPromises from 'flush-promises'
// validation is tested in src/views/Pages/UserProfile/UserCard_FormUserPasswort.spec.js
jest.mock('../../apis/loginAPI')
const localVue = global.localVue
@ -28,6 +30,7 @@ emailVerificationMock
.mockReturnValueOnce({ success: false, result: { message: 'error' } })
.mockReturnValueOnce({ success: false, result: { message: 'error' } })
.mockReturnValueOnce({ success: false, result: { message: 'error' } })
.mockReturnValueOnce({ success: false, result: { message: 'error' } })
.mockReturnValue(successResponseObject)
changePasswordMock
@ -50,7 +53,7 @@ describe('ResetPassword', () => {
optin: '123',
},
},
$toast: {
$toasted: {
error: toasterMock,
},
$router: {
@ -81,36 +84,39 @@ describe('ResetPassword', () => {
})
it('does not render the Reset Password form when not authenticated', () => {
expect(wrapper.find('div.resetpwd-form').exists()).toBeFalsy()
expect(wrapper.find('form').exists()).toBeFalsy()
})
it('toasts an error when no valid optin is given', () => {
expect(toasterMock).toHaveBeenCalledWith('error')
})
it('has a message suggesting to contact the support', () => {
expect(wrapper.find('div.header').text()).toContain('reset-password.title')
expect(wrapper.find('div.header').text()).toContain('reset-password.not-authenticated')
})
it('renders the Reset Password form when authenticated', async () => {
wrapper.setData({ authenticated: true })
await wrapper.vm.$nextTick()
await wrapper.setData({ authenticated: true })
expect(wrapper.find('div.resetpwd-form').exists()).toBeTruthy()
})
describe('Register header', () => {
it('has a welcome message', () => {
expect(wrapper.find('div.header').text()).toBe('reset-password.title reset-password.text')
expect(wrapper.find('div.header').text()).toContain('reset-password.title')
expect(wrapper.find('div.header').text()).toContain('reset-password.text')
})
})
/* there is no back button, why?
describe('links', () => {
it('has a link "Back"', () => {
expect(wrapper.findAllComponents(RouterLinkStub).at(0).text()).toEqual('back')
})
it('links to /login when clicking "Back"', () => {
expect(wrapper.findAllComponents(RouterLinkStub).at(0).props().to).toBe('/login')
expect(wrapper.findAllComponents(RouterLinkStub).at(0).props().to).toBe('/Login')
})
})
*/
describe('reset password form', () => {
it('has a register form', () => {
@ -121,10 +127,6 @@ describe('ResetPassword', () => {
expect(wrapper.findAll('input[type="password"]').length).toBe(2)
})
it('has no submit button when not completely filled', () => {
expect(wrapper.find('button[type="submit"]').exists()).toBe(false)
})
it('toggles the first input field to text when eye icon is clicked', async () => {
wrapper.findAll('button').at(0).trigger('click')
await wrapper.vm.$nextTick()
@ -140,23 +142,20 @@ describe('ResetPassword', () => {
describe('submit form', () => {
beforeEach(async () => {
wrapper.findAll('input').at(0).setValue('Aa123456')
wrapper.findAll('input').at(1).setValue('Aa123456')
await wrapper.vm.$nextTick()
await wrapper.findAll('input').at(0).setValue('Aa123456')
await wrapper.findAll('input').at(1).setValue('Aa123456')
await flushPromises()
wrapper.find('form').trigger('submit')
await wrapper.find('form').trigger('submit')
})
describe('server response with error', () => {
it('toasts an error message', async () => {
it('toasts an error message', () => {
expect(toasterMock).toHaveBeenCalledWith('error')
})
})
describe('server response with success', () => {
it('calls the API', async () => {
await wrapper.vm.$nextTick()
await flushPromises()
it('calls the API', () => {
expect(changePasswordMock).toHaveBeenCalledWith(1, 'user@example.org', 'Aa123456')
})

View File

@ -1,102 +1,34 @@
<template>
<div class="resetpwd-form" v-if="authenticated">
<!-- Header -->
<div class="header p-4">
<b-container class="container">
<div class="resetpwd-form">
<b-container>
<div class="header p-4" ref="header">
<div class="header-body text-center mb-7">
<b-row class="justify-content-center">
<b-col xl="5" lg="6" md="8" class="px-2">
<h1>{{ $t('reset-password.title') }}</h1>
<div class="pb-4">{{ $t('reset-password.text') }}</div>
<div class="pb-4" v-if="!pending">
<span v-if="authenticated">
{{ $t('reset-password.text') }}
</span>
<span v-else>
{{ $t('reset-password.not-authenticated') }}
</span>
</div>
</b-col>
</b-row>
</div>
</b-container>
</div>
<!-- Page content -->
</div>
</b-container>
<b-container class="mt--8 p-1">
<!-- Table -->
<b-row class="justify-content-center">
<b-row class="justify-content-center" v-if="authenticated">
<b-col lg="6" md="8">
<b-card no-body class="border-0" style="background-color: #ebebeba3 !important">
<b-card-body class="p-4">
<validation-observer ref="observer" v-slot="{ handleSubmit }">
<b-form role="form" @submit.prevent="handleSubmit(onSubmit)">
<validation-provider
:name="$t('form.password')"
:rules="{ required: true }"
v-slot="validationContext"
>
<b-form-group
class="mb-5"
:label="$t('form.password')"
label-for="resetPassword"
>
<b-input-group>
<b-form-input
id="resetPassword"
:name="$t('form.password')"
v-model="form.password"
:placeholder="$t('form.password')"
:type="passwordVisible ? 'text' : 'password'"
:state="getValidationState(validationContext)"
aria-describedby="resetPasswordLiveFeedback"
></b-form-input>
<b-input-group-append>
<b-button variant="outline-primary" @click="togglePasswordVisibility">
<b-icon :icon="passwordVisible ? 'eye' : 'eye-slash'" />
</b-button>
</b-input-group-append>
</b-input-group>
<b-form-invalid-feedback id="resetPasswordLiveFeedback">
{{ validationContext.errors[0] }}
</b-form-invalid-feedback>
</b-form-group>
</validation-provider>
<b-form-group
class="mb-5"
:label="$t('form.passwordRepeat')"
label-for="resetPasswordRepeat"
>
<b-input-group>
<b-form-input
id="resetPasswordRepeat"
:name="$t('form.passwordRepeat')"
v-model.lazy="form.passwordRepeat"
:placeholder="$t('form.passwordRepeat')"
:type="passwordVisibleRepeat ? 'text' : 'password'"
></b-form-input>
<b-input-group-append>
<b-button variant="outline-primary" @click="togglePasswordRepeatVisibility">
<b-icon :icon="passwordVisibleRepeat ? 'eye' : 'eye-slash'" />
</b-button>
</b-input-group-append>
</b-input-group>
</b-form-group>
<transition name="hint" appear>
<div v-if="passwordValidation.errors.length > 0 && !submitted" class="hints">
<ul>
<li v-for="error in passwordValidation.errors" :key="error">
<small>{{ error }}</small>
</li>
</ul>
</div>
<div class="matches" v-else-if="!samePasswords">
<p>
{{ $t('site.signup.dont_match') }}
<i class="ni ni-active-40" color="danger"></i>
</p>
</div>
</transition>
<div
class="text-center"
v-if="passwordsFilled && samePasswords && passwordValidation.valid"
>
<b-button type="submit" variant="secondary" class="mt-4">
<input-password-confirmation v-model="form" />
<div class="text-center">
<b-button type="submit" variant="primary" class="mt-4">
{{ $t('reset') }}
</b-button>
</div>
@ -106,38 +38,36 @@
</b-card>
</b-col>
</b-row>
<b-row>
<b-col class="text-center py-lg-4">
<router-link to="/Login" class="mt-3">{{ $t('back') }}</router-link>
</b-col>
</b-row>
</b-container>
</div>
</template>
<script>
import loginAPI from '../../apis/loginAPI'
import InputPasswordConfirmation from '../../components/Inputs/InputPasswordConfirmation'
export default {
name: 'reset',
name: 'ResetPassword',
components: {
InputPasswordConfirmation,
},
data() {
return {
form: {
password: '',
passwordRepeat: '',
},
password: '',
passwordVisible: false,
passwordVisibleRepeat: false,
submitted: false,
authenticated: false,
sessionId: null,
email: null,
pending: true,
}
},
methods: {
getValidationState({ dirty, validated, valid = null }) {
return dirty || validated ? valid : null
},
togglePasswordVisibility() {
this.passwordVisible = !this.passwordVisible
},
togglePasswordRepeatVisibility() {
this.passwordVisibleRepeat = !this.passwordVisibleRepeat
},
async onSubmit() {
const result = await loginAPI.changePassword(this.sessionId, this.email, this.form.password)
if (result.success) {
@ -150,12 +80,12 @@ export default {
*/
this.$router.push('/thx/reset')
} else {
this.$toast.error(result.result.message)
this.$toasted.error(result.result.message)
}
},
async authenticate() {
const loader = this.$loading.show({
container: this.$refs.submitButton,
container: this.$refs.header,
})
const optin = this.$route.params.optin
const result = await loginAPI.loginViaEmailVerificationCode(optin)
@ -164,40 +94,13 @@ export default {
this.sessionId = result.result.data.session_id
this.email = result.result.data.user.email
} else {
this.$toast.error(result.result.message)
this.$toasted.error(result.result.message)
}
loader.hide()
this.pending = false
},
},
computed: {
samePasswords() {
return this.form.password === this.form.passwordRepeat
},
passwordsFilled() {
return this.form.password !== '' && this.form.passwordRepeat !== ''
},
rules() {
return [
{ message: this.$t('site.signup.lowercase'), regex: /[a-z]+/ },
{ message: this.$t('site.signup.uppercase'), regex: /[A-Z]+/ },
{ message: this.$t('site.signup.minimum'), regex: /.{8,}/ },
{ message: this.$t('site.signup.one_number'), regex: /[0-9]+/ },
]
},
passwordValidation() {
const errors = []
for (const condition of this.rules) {
if (!condition.regex.test(this.form.password)) {
errors.push(condition.message)
}
}
if (errors.length === 0) {
return { valid: true, errors }
}
return { valid: false, errors }
},
},
async created() {
mounted() {
this.authenticate()
},
}

View File

@ -0,0 +1,169 @@
import { mount } from '@vue/test-utils'
import UserCardFormUserData from './UserCard_FormUserData'
import loginAPI from '../../../apis/loginAPI'
import flushPromises from 'flush-promises'
jest.mock('../../../apis/loginAPI')
const localVue = global.localVue
const mockAPIcall = jest.fn((args) => {
return { success: true }
})
const toastErrorMock = jest.fn()
const toastSuccessMock = jest.fn()
const storeCommitMock = jest.fn()
loginAPI.updateUserInfos = mockAPIcall
describe('UserCard_FormUsername', () => {
let wrapper
const mocks = {
$t: jest.fn((t) => t),
$store: {
state: {
sessionId: 1,
email: 'user@example.org',
firstName: 'Peter',
lastName: 'Lustig',
description: '',
},
commit: storeCommitMock,
},
$toasted: {
success: toastSuccessMock,
error: toastErrorMock,
},
}
const Wrapper = () => {
return mount(UserCardFormUserData, { localVue, mocks })
}
describe('mount', () => {
beforeEach(() => {
wrapper = Wrapper()
})
it('renders the component', () => {
expect(wrapper.find('div#userdata_form').exists()).toBeTruthy()
})
it('has an edit icon', () => {
expect(wrapper.find('svg.bi-pencil').exists()).toBeTruthy()
})
it('renders the first name', () => {
expect(wrapper.findAll('div.col').at(2).text()).toBe('Peter')
})
it('renders the last name', () => {
expect(wrapper.findAll('div.col').at(4).text()).toBe('Lustig')
})
it('renders the description', () => {
expect(wrapper.findAll('div.col').at(6).text()).toBe('')
})
describe('edit user data', () => {
beforeEach(async () => {
await wrapper.find('svg.bi-pencil').trigger('click')
})
it('shows an cancel icon', () => {
expect(wrapper.find('svg.bi-x-circle').exists()).toBeTruthy()
})
it('closes the input when cancel icon is clicked', async () => {
await wrapper.find('svg.bi-x-circle').trigger('click')
expect(wrapper.find('input').exists()).toBeFalsy()
})
it('does not change the userdate when cancel is clicked', async () => {
await wrapper.findAll('input').at(0).setValue('Petra')
await wrapper.findAll('input').at(1).setValue('Lustiger')
await wrapper.find('textarea').setValue('Keine Nickelbrille')
await wrapper.find('svg.bi-x-circle').trigger('click')
expect(wrapper.findAll('div.col').at(2).text()).toBe('Peter')
expect(wrapper.findAll('div.col').at(4).text()).toBe('Lustig')
expect(wrapper.findAll('div.col').at(6).text()).toBe('')
})
it('has a submit button', () => {
expect(wrapper.find('button[type="submit"]').exists()).toBeTruthy()
})
it('does not enable submit button when data is not changed', async () => {
await wrapper.find('form').trigger('keyup')
expect(wrapper.find('button[type="submit"]').attributes('disabled')).toBe('disabled')
})
describe('successfull submit', () => {
beforeEach(async () => {
jest.clearAllMocks()
await wrapper.findAll('input').at(0).setValue('Petra')
await wrapper.findAll('input').at(1).setValue('Lustiger')
await wrapper.find('textarea').setValue('Keine Nickelbrille')
await wrapper.find('form').trigger('keyup')
await wrapper.find('button[type="submit"]').trigger('click')
await flushPromises()
})
it('calls the loginAPI', () => {
expect(mockAPIcall).toBeCalledWith(1, 'user@example.org', {
firstName: 'Petra',
lastName: 'Lustiger',
description: 'Keine Nickelbrille',
})
})
it('commits firstname to store', () => {
expect(storeCommitMock).toBeCalledWith('firstName', 'Petra')
})
it('commits lastname to store', () => {
expect(storeCommitMock).toBeCalledWith('lastName', 'Lustiger')
})
it('commits description to store', () => {
expect(storeCommitMock).toBeCalledWith('description', 'Keine Nickelbrille')
})
it('toasts a success message', () => {
expect(toastSuccessMock).toBeCalledWith('site.profil.user-data.change-success')
})
it('has an edit button again', () => {
expect(wrapper.find('svg.bi-pencil').exists()).toBeTruthy()
})
})
describe('submit results in server error', () => {
beforeEach(async () => {
jest.clearAllMocks()
mockAPIcall.mockReturnValue({ success: false, result: { message: 'Error' } })
await wrapper.findAll('input').at(0).setValue('Petra')
await wrapper.findAll('input').at(1).setValue('Lustiger')
await wrapper.find('textarea').setValue('Keine Nickelbrille')
await wrapper.find('form').trigger('keyup')
await wrapper.find('button[type="submit"]').trigger('click')
await flushPromises()
})
it('calls the loginAPI', () => {
expect(mockAPIcall).toBeCalledWith(1, 'user@example.org', {
firstName: 'Petra',
lastName: 'Lustiger',
description: 'Keine Nickelbrille',
})
})
it('toasts an error message', () => {
expect(toastErrorMock).toBeCalledWith('Error')
})
})
})
})
})

View File

@ -135,9 +135,9 @@ export default {
this.$store.commit('lastName', this.form.lastName)
this.$store.commit('description', this.form.description)
this.showUserData = true
this.$toast.success(this.$t('site.profil.user-data.change-success'))
this.$toasted.success(this.$t('site.profil.user-data.change-success'))
} else {
this.$toast.error(result.result.message)
this.$toasted.error(result.result.message)
}
},
},

View File

@ -1,13 +1,15 @@
import { mount } from '@vue/test-utils'
import UserCardFormPasswort from './UserCard_FormUserPasswort'
import loginAPI from '../../../apis/loginAPI'
// import flushPromises from 'flush-promises'
import flushPromises from 'flush-promises'
jest.mock('../../../apis/loginAPI')
const localVue = global.localVue
const changePasswordProfileMock = jest.fn()
changePasswordProfileMock.mockReturnValue({ success: true })
loginAPI.changePasswordProfile = changePasswordProfileMock
const toastSuccessMock = jest.fn()
@ -24,7 +26,7 @@ describe('UserCardFormUserPasswort', () => {
email: 'user@example.org',
},
},
$toast: {
$toasted: {
success: toastSuccessMock,
error: toastErrorMock,
},
@ -59,8 +61,8 @@ describe('UserCardFormUserPasswort', () => {
let form
beforeEach(async () => {
wrapper.find('a').trigger('click')
await wrapper.vm.$nextTick()
await wrapper.find('a').trigger('click')
await flushPromises()
form = wrapper.find('form')
})
@ -69,12 +71,11 @@ describe('UserCardFormUserPasswort', () => {
})
it('has a cancel button', () => {
expect(form.find('svg.bi-x-circle').exists()).toBeTruthy()
expect(wrapper.find('svg.bi-x-circle').exists()).toBeTruthy()
})
it('closes the form when cancel button is clicked', async () => {
form.find('svg.bi-x-circle').trigger('click')
await wrapper.vm.$nextTick()
await wrapper.find('svg.bi-x-circle').trigger('click')
expect(wrapper.find('input').exists()).toBeFalsy()
})
@ -104,24 +105,103 @@ describe('UserCardFormUserPasswort', () => {
expect(form.find('button[type="submit"]').exists()).toBeTruthy()
})
/*
describe('submit', () => {
beforeEach(async () => {
await form.findAll('input').at(0).setValue('1234')
await form.findAll('input').at(1).setValue('Aa123456')
await form.findAll('input').at(2).setValue('Aa123456')
form.trigger('submit')
await wrapper.vm.$nextTick()
await flushPromises()
describe('validation', () => {
it('displays all password requirements', () => {
const feedbackArray = wrapper.findAll('div.invalid-feedback').at(1).findAll('span')
expect(feedbackArray).toHaveLength(5)
expect(feedbackArray.at(0).text()).toBe('validations.messages.required')
expect(feedbackArray.at(1).text()).toBe('site.signup.lowercase')
expect(feedbackArray.at(2).text()).toBe('site.signup.uppercase')
expect(feedbackArray.at(3).text()).toBe('site.signup.one_number')
expect(feedbackArray.at(4).text()).toBe('site.signup.minimum')
})
it('calls the API', async () => {
await wrapper.vm.$nextTick()
it('removes first message when a character is given', async () => {
await wrapper.findAll('input').at(1).setValue('@')
await flushPromises()
expect(changePasswordProfileMock).toHaveBeenCalledWith(1, 'user@example.org', '1234', 'Aa123456')
const feedbackArray = wrapper.findAll('div.invalid-feedback').at(1).findAll('span')
expect(feedbackArray).toHaveLength(4)
expect(feedbackArray.at(0).text()).toBe('site.signup.lowercase')
})
it('removes first and second message when a lowercase character is given', async () => {
await wrapper.findAll('input').at(1).setValue('a')
await flushPromises()
const feedbackArray = wrapper.findAll('div.invalid-feedback').at(1).findAll('span')
expect(feedbackArray).toHaveLength(3)
expect(feedbackArray.at(0).text()).toBe('site.signup.uppercase')
})
it('removes the first three messages when a lowercase and uppercase characters are given', async () => {
await wrapper.findAll('input').at(1).setValue('Aa')
await flushPromises()
const feedbackArray = wrapper.findAll('div.invalid-feedback').at(1).findAll('span')
expect(feedbackArray).toHaveLength(2)
expect(feedbackArray.at(0).text()).toBe('site.signup.one_number')
})
it('removes the first four messages when a lowercase, uppercase and numeric characters are given', async () => {
await wrapper.findAll('input').at(1).setValue('Aa1')
await flushPromises()
const feedbackArray = wrapper.findAll('div.invalid-feedback').at(1).findAll('span')
expect(feedbackArray).toHaveLength(1)
expect(feedbackArray.at(0).text()).toBe('site.signup.minimum')
})
it('removes all messages when all rules are fulfilled', async () => {
await wrapper.findAll('input').at(1).setValue('Aa123456')
await flushPromises()
const feedbackArray = wrapper.findAll('div.invalid-feedback').at(1).findAll('span')
expect(feedbackArray).toHaveLength(0)
})
})
describe('submit', () => {
describe('valid data', () => {
beforeEach(async () => {
await form.findAll('input').at(0).setValue('1234')
await form.findAll('input').at(1).setValue('Aa123456')
await form.findAll('input').at(2).setValue('Aa123456')
await form.trigger('submit')
await flushPromises()
})
it('calls the API', () => {
expect(changePasswordProfileMock).toHaveBeenCalledWith(
1,
'user@example.org',
'1234',
'Aa123456',
)
})
it('toasts a success message', () => {
expect(toastSuccessMock).toBeCalledWith('site.thx.reset')
})
it('cancels the edit process', () => {
expect(wrapper.find('input').exists()).toBeFalsy()
})
})
describe('server response is error', () => {
beforeEach(async () => {
changePasswordProfileMock.mockReturnValue({
success: false,
result: { message: 'error' },
})
await form.findAll('input').at(0).setValue('1234')
await form.findAll('input').at(1).setValue('Aa123456')
await form.findAll('input').at(2).setValue('Aa123456')
await form.trigger('submit')
await flushPromises()
})
it('toasts an error message', () => {
expect(toastErrorMock).toBeCalledWith('error')
})
})
})
*/
})
})
})

View File

@ -1,217 +1,94 @@
<template>
<b-card id="change_pwd" class="bg-transparent" style="background-color: #ebebeba3 !important">
<b-container>
<b-form @keyup.prevent="loadSubmitButton">
<div v-if="!editPassword">
<b-row class="mb-4 text-right">
<b-col class="text-right">
<a href="#change_pwd" v-if="!editPassword" @click="editPassword = !editPassword">
<a href="#change_pwd" @click="editPassword = !editPassword">
<span>{{ $t('form.change-password') }}</span>
<b-icon class="pointer ml-3" icon="pencil" />
</a>
<b-icon
v-else
@click="cancelEdit()"
class="pointer"
icon="x-circle"
variant="danger"
></b-icon>
</b-col>
</b-row>
<div v-if="editPassword">
<b-row class="mb-5">
<b-col class="col-12 col-lg-3 col-md-10 col-sm-10 text-md-left text-lg-right">
<small>{{ $t('form.password_old') }}</small>
</b-col>
<b-col class="col-md-9 col-sm-10">
<b-input-group>
<b-form-input
class="mb-0"
v-model="password"
name="Password"
:type="passwordVisibleOldPwd ? 'text' : 'password'"
prepend-icon="ni ni-lock-circle-open"
</div>
<div v-if="editPassword">
<b-row class="mb-4 text-right">
<b-col class="text-right">
<b-icon @click="cancelEdit()" class="pointer" icon="x-circle" variant="danger"></b-icon>
</b-col>
</b-row>
<validation-observer ref="observer" v-slot="{ handleSubmit }">
<b-form @submit.stop.prevent="handleSubmit(onSubmit)">
<b-row class="mb-2">
<b-col>
<input-password
:label="$t('form.password_old')"
:placeholder="$t('form.password_old')"
></b-form-input>
<b-input-group-append>
<b-button variant="outline-primary" @click="togglePasswordVisibilityOldPwd">
<b-icon :icon="passwordVisibleOldPwd ? 'eye' : 'eye-slash'" />
v-model="form.password"
></input-password>
</b-col>
</b-row>
<input-password-confirmation v-model="form.newPassword" />
<b-row class="text-right">
<b-col>
<div class="text-right">
<b-button type="submit" variant="primary" class="mt-4">
{{ $t('form.save') }}
</b-button>
</b-input-group-append>
</b-input-group>
</b-col>
</b-row>
<b-row class="mb-3">
<b-col class="col-12 col-lg-3 col-md-10 col-sm-10 text-md-left text-lg-right">
<small>{{ $t('form.password_new') }}</small>
</b-col>
<b-col class="col-md-9 col-sm-10">
<b-input-group>
<b-form-input
class="mb-0"
v-model="passwordNew"
name="Password"
:type="passwordVisibleNewPwd ? 'text' : 'password'"
prepend-icon="ni ni-lock-circle-open"
:placeholder="$t('form.password_new')"
></b-form-input>
<b-input-group-append>
<b-button variant="outline-primary" @click="togglePasswordVisibilityNewPwd">
<b-icon :icon="passwordVisibleNewPwd ? 'eye' : 'eye-slash'" />
</b-button>
</b-input-group-append>
</b-input-group>
</b-col>
</b-row>
<b-row class="mb-3">
<b-col class="col-12 col-lg-3 col-md-10 col-sm-10 text-md-left text-lg-right">
<small>{{ $t('form.password_new_repeat') }}</small>
</b-col>
<b-col class="col-md-9 col-sm-10">
<b-input-group>
<b-form-input
class="mb-0"
v-model="passwordNewRepeat"
name="Password"
:type="passwordVisibleNewPwdRepeat ? 'text' : 'password'"
prepend-icon="ni ni-lock-circle-open"
:placeholder="$t('form.password_new_repeat')"
></b-form-input>
<b-input-group-append>
<b-button variant="outline-primary" @click="togglePasswordVisibilityNewPwdRepeat">
<b-icon :icon="passwordVisibleNewPwdRepeat ? 'eye' : 'eye-slash'" />
</b-button>
</b-input-group-append>
</b-input-group>
</b-col>
</b-row>
<b-row>
<b-col></b-col>
<b-col class="col-12">
<transition name="hint" appear>
<div v-if="passwordValidation.errors.length > 0" class="hints">
<ul>
<li v-for="error in passwordValidation.errors" :key="error">
<small>{{ error }}</small>
</li>
</ul>
</div>
</transition>
</b-col>
</b-row>
<b-row class="text-right" v-if="editPassword">
<b-col>
<div class="text-right" ref="submitButton">
<b-button
:variant="loading ? 'default' : 'success'"
@click="onSubmit"
type="submit"
class="mt-4"
:disabled="loading"
>
{{ $t('form.save') }}
</b-button>
</div>
</b-col>
</b-row>
</div>
</b-form>
</b-col>
</b-row>
</b-form>
</validation-observer>
</div>
</b-container>
</b-card>
</template>
<script>
import loginAPI from '../../../apis/loginAPI'
import InputPassword from '../../../components/Inputs/InputPassword'
import InputPasswordConfirmation from '../../../components/Inputs/InputPasswordConfirmation'
export default {
name: 'FormUserPasswort',
components: {
InputPassword,
InputPasswordConfirmation,
},
data() {
return {
editPassword: false,
email: null,
password: '',
passwordNew: '',
passwordNewRepeat: '',
passwordVisibleOldPwd: false,
passwordVisibleNewPwd: false,
passwordVisibleNewPwdRepeat: false,
loading: true,
form: {
password: '',
newPassword: {
password: '',
passwordRepeat: '',
},
},
}
},
methods: {
cancelEdit() {
this.editPassword = false
this.password = ''
this.passwordNew = ''
this.passwordNewRepeat = ''
this.form.password = ''
this.form.passwordNew = ''
this.form.passwordNewRepeat = ''
},
togglePasswordVisibilityNewPwd() {
this.passwordVisibleNewPwd = !this.passwordVisibleNewPwd
},
togglePasswordVisibilityNewPwdRepeat() {
this.passwordVisibleNewPwdRepeat = !this.passwordVisibleNewPwdRepeat
},
togglePasswordVisibilityOldPwd() {
this.passwordVisibleOldPwd = !this.passwordVisibleOldPwd
},
loadSubmitButton() {
if (
this.password !== '' &&
this.passwordNew !== '' &&
this.passwordNewRepeat !== '' &&
this.passwordNew === this.passwordNewRepeat
) {
this.loading = false
} else {
this.loading = true
}
},
async onSubmit(event) {
event.preventDefault()
async onSubmit() {
const result = await loginAPI.changePasswordProfile(
this.$store.state.sessionId,
this.$store.state.email,
this.password,
this.passwordNew,
this.form.password,
this.form.newPassword.password,
)
if (result.success) {
this.$toast.success(this.$t('site.thx.reset'))
this.$toasted.success(this.$t('site.thx.reset'))
this.cancelEdit()
} else {
this.$toast.error(result.result.message)
this.$toasted.error(result.result.message)
}
},
},
computed: {
samePasswords() {
return this.password === this.passwordNew
},
rules() {
return [
{ message: this.$t('site.signup.lowercase'), regex: /[a-z]+/ },
{ message: this.$t('site.signup.uppercase'), regex: /[A-Z]+/ },
{ message: this.$t('site.signup.minimum'), regex: /.{8,}/ },
{ message: this.$t('site.signup.one_number'), regex: /[0-9]+/ },
]
},
passwordValidation() {
const errors = []
for (const condition of this.rules) {
if (!condition.regex.test(this.passwordNew)) {
errors.push(condition.message)
}
}
if (errors.length === 0) {
return { valid: true, errors }
}
return { valid: false, errors }
},
},
}
</script>
<style></style>

View File

@ -1,29 +1,28 @@
import { mount } from '@vue/test-utils'
import { extend } from 'vee-validate'
import UserCardFormUsername from './UserCard_FormUsername'
import loginAPI from '../../../apis/loginAPI'
import flushPromises from 'flush-promises'
import { extend } from 'vee-validate'
jest.mock('../../../apis/loginAPI')
extend('gddUsernameRgex', {
validate(value) {
return true
},
})
extend('gddUsernameUnique', {
validate(value) {
return true
},
})
const localVue = global.localVue
const mockAPIcall = jest.fn((args) => {
return { success: true }
})
// override this rule to avoid API call
extend('gddUsernameUnique', {
validate(value) {
return true
},
})
const toastErrorMock = jest.fn()
const toastSuccessMock = jest.fn()
const storeCommitMock = jest.fn()
loginAPI.changeUsernameProfile = mockAPIcall
describe('UserCard_FormUsername', () => {
@ -37,10 +36,11 @@ describe('UserCard_FormUsername', () => {
email: 'user@example.org',
username: '',
},
commit: jest.fn(),
commit: storeCommitMock,
},
$toast: {
success: jest.fn(),
$toasted: {
success: toastSuccessMock,
error: toastErrorMock,
},
}
@ -111,10 +111,43 @@ describe('UserCard_FormUsername', () => {
expect(wrapper.find('div.display-username').text()).toEqual('@username')
})
it('commits the username to the store', () => {
expect(storeCommitMock).toBeCalledWith('username', 'username')
})
it('toasts an success message', () => {
expect(toastSuccessMock).toBeCalledWith('site.profil.user-data.change-success')
})
it('has no edit button anymore', () => {
expect(wrapper.find('svg.bi-pencil').exists()).toBeFalsy()
})
})
describe('submit retruns error', () => {
beforeEach(async () => {
jest.clearAllMocks()
mockAPIcall.mockReturnValue({
success: false,
result: { message: 'Error' },
})
await wrapper.find('input[placeholder="Username"]').setValue('username')
await wrapper.find('form').trigger('submit')
await flushPromises()
})
it('calls the loginAPI', () => {
expect(mockAPIcall).toHaveBeenCalledWith(1, 'user@example.org', 'username')
})
it('toasts an error message', () => {
expect(toastErrorMock).toBeCalledWith('Error')
})
it('renders an empty username', () => {
expect(wrapper.find('div.display-username').text()).toEqual('@')
})
})
})
})
})

View File

@ -95,9 +95,9 @@ export default {
this.$store.commit('username', this.form.username)
this.username = this.form.username
this.showUsername = true
this.$toast.success(this.$t('site.profil.user-data.change-success'))
this.$toasted.success(this.$t('site.profil.user-data.change-success'))
} else {
this.$toast.error(result.result.message)
this.$toasted.error(result.result.message)
this.showUsername = true
this.username = this.$store.state.username
this.form.username = this.$store.state.username

View File

@ -0,0 +1,41 @@
import { mount } from '@vue/test-utils'
import UserProfileTransactionList from './UserProfileTransactionList'
const localVue = global.localVue
describe('UserProfileTransactionList', () => {
let wrapper
const mocks = {
$t: jest.fn((t) => t),
}
const Wrapper = () => {
return mount(UserProfileTransactionList, { localVue, mocks })
}
describe('mount', () => {
beforeEach(() => {
wrapper = Wrapper()
})
it('renders the transaction table', () => {
expect(wrapper.findComponent({ name: 'GddTransactionList' }).exists()).toBeTruthy()
})
it('emits update-transactions after creation', () => {
expect(wrapper.emitted('update-transactions')).toEqual(
expect.arrayContaining([expect.arrayContaining([{ firstPage: 1, items: 25 }])]),
)
})
it('emist update-transactions when update-transactions is called', () => {
wrapper
.findComponent({ name: 'GddTransactionList' })
.vm.$emit('update-transactions', { firstPage: 2, items: 25 })
expect(wrapper.emitted('update-transactions')).toEqual(
expect.arrayContaining([expect.arrayContaining([{ firstPage: 2, items: 25 }])]),
)
})
})
})

View File

@ -19,12 +19,13 @@
import GddTransactionList from './AccountOverview/GddTransactionList.vue'
export default {
name: 'UserProfileTransactionList',
components: {
GddTransactionList,
},
props: {
transactions: {
default: [],
default: () => [],
},
transactionCount: { type: Number, default: 0 },
},

View File

@ -1,10 +1,11 @@
import { createLocalVue } from '@vue/test-utils'
import { BootstrapVue, IconsPlugin } from 'bootstrap-vue'
import Vuex from 'vuex'
import { ValidationProvider, ValidationObserver, extend } from 'vee-validate'
import * as rules from 'vee-validate/dist/rules'
import { messages } from 'vee-validate/dist/locale/en.json'
import RegeneratorRuntime from 'regenerator-runtime'
import SideBar from '@/components/SidebarPlugin'
import VueQrcode from 'vue-qrcode'
@ -14,7 +15,7 @@ import VueMoment from 'vue-moment'
import clickOutside from '@/directives/click-ouside.js'
import { focus } from 'vue-focus'
global.localVue = createLocalVue()
import { loadAllRules } from '../src/validation-rules'
Object.keys(rules).forEach((rule) => {
extend(rule, {
@ -23,6 +24,15 @@ Object.keys(rules).forEach((rule) => {
})
})
const i18nMock = {
t: (identifier, values) => identifier,
n: (value, format) => value,
}
loadAllRules(i18nMock)
global.localVue = createLocalVue()
global.localVue.use(BootstrapVue)
global.localVue.use(Vuex)
global.localVue.use(IconsPlugin)

View File

@ -13268,11 +13268,6 @@ vm-browserify@^1.0.1:
resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0"
integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==
vue-bootstrap-toasts@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/vue-bootstrap-toasts/-/vue-bootstrap-toasts-1.0.7.tgz#111c38855941e8eb0538e21f41c173e2af67dd53"
integrity sha512-JhurJOAwdNcINQ/QlT701sx0r447YTGpvtxtmZNC9pwDvEqp2I0Pyv15jS4neWwYHkA1gXB42nBsDRcWcj1hlg==
vue-bootstrap-typeahead@^0.2.6:
version "0.2.6"
resolved "https://registry.yarnpkg.com/vue-bootstrap-typeahead/-/vue-bootstrap-typeahead-0.2.6.tgz#8c1999a00bf4bf9fc906bae3a462482482cbc297"
@ -13475,6 +13470,11 @@ vue-template-es2015-compiler@^1.6.0, vue-template-es2015-compiler@^1.9.0:
resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825"
integrity sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==
vue-toasted@^1.1.28:
version "1.1.28"
resolved "https://registry.yarnpkg.com/vue-toasted/-/vue-toasted-1.1.28.tgz#dbabb83acc89f7a9e8765815e491d79f0dc65c26"
integrity sha512-UUzr5LX51UbbiROSGZ49GOgSzFxaMHK6L00JV8fir/CYNJCpIIvNZ5YmS4Qc8Y2+Z/4VVYRpeQL2UO0G800Raw==
vue2-transitions@^0.2.3:
version "0.2.3"
resolved "https://registry.yarnpkg.com/vue2-transitions/-/vue2-transitions-0.2.3.tgz#69c9d75b1db05f231b80980c03459d68490ba27d"

View File

@ -123,13 +123,18 @@ Poco::JSON::Object* JsonUpdateUserInfos::handle(Poco::Dynamic::Var params)
}
}
else if ("User.description" == name) {
std::string str_val = validateString(value, "User.description", jsonErrorsArray);
std::string errorMessage = "User.description";
if (str_val.size() > 0 && str_val != user_model->getDescription()) {
if (!value.isString()) {
errorMessage += " isn't a string";
jsonErrorsArray.add(errorMessage);
}
std::string str_val = value.toString();
if (str_val != user_model->getDescription()) {
user_model->setDescription(str_val);
extractet_values++;
}
}
else if ("User.disabled" == name) {
bool disabled;