github.com/condensat/bank-core@v0.1.0/database/query/accountoperation_test.go (about) 1 // Copyright 2020 Condensat Tech. All rights reserved. 2 // Use of this source code is governed by a MIT 3 // license that can be found in the LICENSE file. 4 5 package query 6 7 import ( 8 "reflect" 9 "testing" 10 "time" 11 12 "github.com/condensat/bank-core/database" 13 "github.com/condensat/bank-core/database/model" 14 "github.com/condensat/bank-core/database/query/tests" 15 16 "github.com/jinzhu/gorm" 17 ) 18 19 func TestAppendAccountOperation(t *testing.T) { 20 const databaseName = "TestAppendAccountOperation" 21 t.Parallel() 22 23 db := tests.Setup(databaseName, AccountOperationModel()) 24 defer tests.Teardown(db, databaseName) 25 26 data := createTestAccountOperationData(db) 27 refAccountOperation := createOperation(data.Accounts[0].ID, 1.0, 1.0) 28 29 first := lastLinkedOperation(createLinkedOperations(db, data.Accounts[0].ID, 1, 1.0)) 30 nextAccountOperation := createOperation(first.AccountID, -0.5, 0.5) 31 32 refInvalidAccountID := cloneOperation(refAccountOperation) 33 refInvalidAccountID.AccountID = 0 34 35 refNotExistingAccountID := cloneOperation(refAccountOperation) 36 refNotExistingAccountID.AccountID = 42 37 38 refInvalidPreCheck := cloneOperation(refAccountOperation) 39 *refInvalidPreCheck.Balance = 0.0 40 41 refCurrencyDisabled := createOperation(data.Accounts[1].ID, 1.0, 1.0) 42 refAccountDisabled := createOperation(data.Accounts[2].ID, 1.0, 1.0) 43 44 type args struct { 45 db database.Context 46 operation model.AccountOperation 47 } 48 tests := []struct { 49 name string 50 args args 51 want model.AccountOperation 52 wantErr bool 53 }{ 54 {"Default", args{}, model.AccountOperation{}, true}, 55 {"NilDB", args{nil, refAccountOperation}, model.AccountOperation{}, true}, 56 {"InvalidAccountID", args{db, refInvalidAccountID}, model.AccountOperation{}, true}, 57 {"NotExistingAccountID", args{db, refNotExistingAccountID}, model.AccountOperation{}, true}, 58 {"InvalidPreCheck", args{db, refInvalidPreCheck}, model.AccountOperation{}, true}, 59 60 {"Valid", args{db, refAccountOperation}, refAccountOperation, false}, 61 {"Next", args{db, nextAccountOperation}, nextAccountOperation, false}, 62 63 {"CurrencyDisabled", args{db, refCurrencyDisabled}, model.AccountOperation{}, true}, 64 {"AccountOperation", args{db, refAccountDisabled}, model.AccountOperation{}, true}, 65 } 66 for _, tt := range tests { 67 tt := tt // capture range variable 68 t.Run(tt.name, func(t *testing.T) { 69 got, err := AppendAccountOperation(tt.args.db, tt.args.operation) 70 if (err != nil) != tt.wantErr { 71 t.Errorf("AppendAccountOperation() error = %v, wantErr %v", err, tt.wantErr) 72 return 73 } 74 75 tt.want.ID = got.ID 76 if !reflect.DeepEqual(got, tt.want) { 77 t.Errorf("AppendAccountOperation() = %v, want %v", got, tt.want) 78 } 79 }) 80 } 81 } 82 83 func TestGetPreviousAccountOperation(t *testing.T) { 84 const databaseName = "TestGetPreviousAccountOperation" 85 t.Parallel() 86 87 db := tests.Setup(databaseName, AccountOperationModel()) 88 defer tests.Teardown(db, databaseName) 89 90 data := createTestAccountStateData(db) 91 var ops []model.AccountOperation 92 var prev []model.AccountOperation 93 for i := 0; i < len(data.Accounts); i++ { 94 linked := createLinkedOperations(db, data.Accounts[i].ID, i+1, 1.0) 95 96 ops = append(ops, linked[len(linked)-1]) 97 prev = append(prev, linked[len(linked)-2]) 98 } 99 type args struct { 100 accountID model.AccountID 101 operationID model.AccountOperationID 102 } 103 tests := []struct { 104 name string 105 args args 106 want model.AccountOperation 107 wantErr bool 108 }{ 109 {"Default", args{}, model.AccountOperation{}, true}, 110 {"InvalidAccountID", args{0, ops[0].ID}, model.AccountOperation{}, true}, 111 {"InvalidOperationID", args{ops[0].AccountID, 0}, model.AccountOperation{}, true}, 112 113 {"op1", args{ops[0].AccountID, ops[0].ID}, prev[0], false}, 114 {"op2", args{ops[1].AccountID, ops[1].ID}, prev[1], false}, 115 {"op3", args{ops[2].AccountID, ops[2].ID}, prev[2], false}, 116 {"op4", args{ops[3].AccountID, ops[3].ID}, prev[3], false}, 117 } 118 for _, tt := range tests { 119 tt := tt // capture range variable 120 t.Run(tt.name, func(t *testing.T) { 121 got, err := GetPreviousAccountOperation(db, tt.args.accountID, tt.args.operationID) 122 if (err != nil) != tt.wantErr { 123 t.Errorf("GetPreviousAccountOperation() error = %v, wantErr %v", err, tt.wantErr) 124 return 125 } 126 if !reflect.DeepEqual(got, tt.want) { 127 t.Errorf("GetPreviousAccountOperation() = %v, want %v", got, tt.want) 128 } 129 }) 130 } 131 } 132 133 func TestGetNextAccountOperation(t *testing.T) { 134 const databaseName = "TestGetNextAccountOperation" 135 t.Parallel() 136 137 db := tests.Setup(databaseName, AccountOperationModel()) 138 defer tests.Teardown(db, databaseName) 139 140 data := createTestAccountStateData(db) 141 var ops []model.AccountOperation 142 var next []model.AccountOperation 143 for i := 0; i < len(data.Accounts); i++ { 144 linked := createLinkedOperations(db, data.Accounts[i].ID, i+1, 1.0) 145 146 ops = append(ops, linked[len(linked)-2]) 147 next = append(next, linked[len(linked)-1]) 148 } 149 150 type args struct { 151 accountID model.AccountID 152 operationID model.AccountOperationID 153 } 154 tests := []struct { 155 name string 156 args args 157 want model.AccountOperation 158 wantErr bool 159 }{ 160 {"Default", args{}, model.AccountOperation{}, true}, 161 {"InvalidAccountID", args{0, ops[0].ID}, model.AccountOperation{}, true}, 162 {"InvalidOperationID", args{ops[0].AccountID, 0}, model.AccountOperation{}, true}, 163 164 {"op1", args{ops[0].AccountID, ops[0].ID}, next[0], false}, 165 {"op2", args{ops[1].AccountID, ops[1].ID}, next[1], false}, 166 {"op3", args{ops[2].AccountID, ops[2].ID}, next[2], false}, 167 {"op4", args{ops[3].AccountID, ops[3].ID}, next[3], false}, 168 } 169 for _, tt := range tests { 170 tt := tt // capture range variable 171 t.Run(tt.name, func(t *testing.T) { 172 got, err := GetNextAccountOperation(db, tt.args.accountID, tt.args.operationID) 173 if (err != nil) != tt.wantErr { 174 t.Errorf("GetNextAccountOperation() error = %v, wantErr %v", err, tt.wantErr) 175 return 176 } 177 if !reflect.DeepEqual(got, tt.want) { 178 t.Errorf("GetNextAccountOperation() = %v, want %v", got, tt.want) 179 } 180 }) 181 } 182 } 183 184 func TestGetLastAccountOperation(t *testing.T) { 185 const databaseName = "TestGetLastAccountOperation" 186 t.Parallel() 187 188 db := tests.Setup(databaseName, AccountOperationModel()) 189 defer tests.Teardown(db, databaseName) 190 191 data := createTestAccountStateData(db) 192 var ops []model.AccountOperation 193 for i := 0; i < len(data.Accounts); i++ { 194 ops = append(ops, lastLinkedOperation(createLinkedOperations(db, data.Accounts[i].ID, i+1, 1.0))) 195 } 196 197 type args struct { 198 accountID model.AccountID 199 } 200 tests := []struct { 201 name string 202 args args 203 wantID model.AccountOperationID 204 wantErr bool 205 }{ 206 {"Default", args{}, 0, true}, 207 {"InvalidAccountID", args{0}, 0, true}, 208 209 {"op1", args{ops[0].AccountID}, ops[0].ID, false}, 210 {"op2", args{ops[1].AccountID}, ops[1].ID, false}, 211 {"op3", args{ops[2].AccountID}, ops[2].ID, false}, 212 {"op4", args{ops[3].AccountID}, ops[3].ID, false}, 213 } 214 for _, tt := range tests { 215 tt := tt // capture range variable 216 t.Run(tt.name, func(t *testing.T) { 217 got, err := GetLastAccountOperation(db, tt.args.accountID) 218 if (err != nil) != tt.wantErr { 219 t.Errorf("GetLastAccountOperation() error = %v, wantErr %v", err, tt.wantErr) 220 return 221 } 222 if got.ID != tt.wantID { 223 t.Errorf("GetLastAccountOperation() ID = %v, wantID %v", got.ID, tt.wantID) 224 } 225 }) 226 } 227 } 228 229 func TestGeAccountHistory(t *testing.T) { 230 const databaseName = "TestGeAccountHistory" 231 t.Parallel() 232 233 db := tests.Setup(databaseName, AccountOperationModel()) 234 defer tests.Teardown(db, databaseName) 235 236 data := createTestAccountStateData(db) 237 var ops [][]model.AccountOperation 238 for i := 0; i < len(data.Accounts); i++ { 239 ops = append(ops, createLinkedOperations(db, data.Accounts[i].ID, i+1, 1.0)) 240 } 241 242 type args struct { 243 accountID model.AccountID 244 } 245 tests := []struct { 246 name string 247 args args 248 want []model.AccountOperation 249 wantErr bool 250 }{ 251 {"Default", args{}, nil, true}, 252 {"InvalidAccountID", args{0}, nil, true}, 253 254 {"op1", args{lastLinkedOperation(ops[0]).AccountID}, ops[0], false}, 255 {"op2", args{lastLinkedOperation(ops[1]).AccountID}, ops[1], false}, 256 {"op3", args{lastLinkedOperation(ops[2]).AccountID}, ops[2], false}, 257 {"op4", args{lastLinkedOperation(ops[3]).AccountID}, ops[3], false}, 258 } 259 for _, tt := range tests { 260 tt := tt // capture range variable 261 t.Run(tt.name, func(t *testing.T) { 262 got, err := GeAccountHistory(db, tt.args.accountID) 263 if (err != nil) != tt.wantErr { 264 t.Errorf("GeAccountHistory() error = %v, wantErr %v", err, tt.wantErr) 265 return 266 } 267 if !reflect.DeepEqual(got, tt.want) { 268 t.Errorf("GeAccountHistory() = %v, want %v", got, tt.want) 269 } 270 }) 271 } 272 } 273 274 func TestGeAccountHistoryWithPrevNext(t *testing.T) { 275 const databaseName = "TestGeAccountHistoryWithPrevNext" 276 t.Parallel() 277 278 db := tests.Setup(databaseName, AccountOperationModel()) 279 defer tests.Teardown(db, databaseName) 280 281 data := createTestAccountStateData(db) 282 var ops [][]model.AccountOperation 283 for i := 0; i < len(data.Accounts); i++ { 284 ops = append(ops, createLinkedOperations(db, data.Accounts[i].ID, i+1, 1.0)) 285 } 286 287 type args struct { 288 accountID model.AccountID 289 } 290 tests := []struct { 291 name string 292 args args 293 want []AccountOperationPrevNext 294 wantErr bool 295 }{ 296 {"Default", args{}, nil, true}, 297 {"InvalidAccountID", args{0}, nil, true}, 298 299 {"op1", args{lastLinkedOperation(ops[0]).AccountID}, createAccountOperationPrevNextList(ops[0]), false}, 300 {"op2", args{lastLinkedOperation(ops[1]).AccountID}, createAccountOperationPrevNextList(ops[1]), false}, 301 {"op3", args{lastLinkedOperation(ops[2]).AccountID}, createAccountOperationPrevNextList(ops[2]), false}, 302 {"op4", args{lastLinkedOperation(ops[3]).AccountID}, createAccountOperationPrevNextList(ops[3]), false}, 303 } 304 for _, tt := range tests { 305 tt := tt // capture range variable 306 t.Run(tt.name, func(t *testing.T) { 307 got, err := GeAccountHistoryWithPrevNext(db, tt.args.accountID) 308 if (err != nil) != tt.wantErr { 309 t.Errorf("GeAccountHistoryWithPrevNext() error = %v, wantErr %v", err, tt.wantErr) 310 return 311 } 312 if !reflect.DeepEqual(got, tt.want) { 313 t.Errorf("GeAccountHistoryWithPrevNext() = %+v, want %+v", got, tt.want) 314 } 315 }) 316 } 317 } 318 319 func TestGeAccountHistoryRange(t *testing.T) { 320 const databaseName = "TestGeAccountHistoryRange" 321 t.Parallel() 322 323 db := tests.Setup(databaseName, AccountOperationModel()) 324 defer tests.Teardown(db, databaseName) 325 326 data := createTestAccountStateData(db) 327 var ops [][]model.AccountOperation 328 for i := 0; i < len(data.Accounts); i++ { 329 ops = append(ops, createLinkedOperations(db, data.Accounts[i].ID, i+1, 1.0)) 330 } 331 332 to := time.Now() 333 from := to.Add(-10 * time.Second) 334 335 afterTo := to.Add(time.Minute) 336 afterFrom := from.Add(time.Minute) 337 338 beforeTo := to.Add(-time.Minute) 339 beforeFrom := from.Add(-time.Minute) 340 341 type args struct { 342 db database.Context 343 accountID model.AccountID 344 from time.Time 345 to time.Time 346 } 347 tests := []struct { 348 name string 349 args args 350 want []model.AccountOperation 351 wantErr bool 352 }{ 353 {"Default", args{}, nil, true}, 354 {"NilDB", args{nil, lastLinkedOperation(ops[0]).AccountID, time.Time{}, time.Time{}}, nil, true}, 355 {"InvalidAccountID", args{db, 0, from, to}, nil, true}, 356 357 {"DefaultRangeOp1", args{db, lastLinkedOperation(ops[0]).AccountID, time.Time{}, time.Time{}}, nil, false}, 358 {"DefaultRangeOp2", args{db, lastLinkedOperation(ops[1]).AccountID, time.Time{}, time.Time{}}, nil, false}, 359 {"DefaultRangeOp3", args{db, lastLinkedOperation(ops[2]).AccountID, time.Time{}, time.Time{}}, nil, false}, 360 {"DefaultRangeOp4", args{db, lastLinkedOperation(ops[3]).AccountID, time.Time{}, time.Time{}}, nil, false}, 361 362 {"Rangeop1", args{db, lastLinkedOperation(ops[0]).AccountID, from, to}, ops[0], false}, 363 {"Rangeop2", args{db, lastLinkedOperation(ops[1]).AccountID, from, to}, ops[1], false}, 364 {"Rangeop3", args{db, lastLinkedOperation(ops[2]).AccountID, from, to}, ops[2], false}, 365 {"Rangeop4", args{db, lastLinkedOperation(ops[3]).AccountID, from, to}, ops[3], false}, 366 367 {"InvertRangeOp1", args{db, lastLinkedOperation(ops[0]).AccountID, to, from}, ops[0], false}, 368 {"InvertRangeOp2", args{db, lastLinkedOperation(ops[1]).AccountID, to, from}, ops[1], false}, 369 {"InvertRangeOp3", args{db, lastLinkedOperation(ops[2]).AccountID, to, from}, ops[2], false}, 370 {"InvertRangeOp4", args{db, lastLinkedOperation(ops[3]).AccountID, to, from}, ops[3], false}, 371 372 {"BeforeRangeOp1", args{db, lastLinkedOperation(ops[0]).AccountID, beforeFrom, beforeTo}, nil, false}, 373 {"BeforeRangeOp2", args{db, lastLinkedOperation(ops[1]).AccountID, beforeFrom, beforeTo}, nil, false}, 374 {"BeforeRangeOp3", args{db, lastLinkedOperation(ops[2]).AccountID, beforeFrom, beforeTo}, nil, false}, 375 {"BeforeRangeOp4", args{db, lastLinkedOperation(ops[3]).AccountID, beforeFrom, beforeTo}, nil, false}, 376 377 {"AfterRangeOp1", args{db, lastLinkedOperation(ops[0]).AccountID, afterFrom, afterTo}, nil, false}, 378 {"AfterRangeOp2", args{db, lastLinkedOperation(ops[1]).AccountID, afterFrom, afterTo}, nil, false}, 379 {"AfterRangeOp3", args{db, lastLinkedOperation(ops[2]).AccountID, afterFrom, afterTo}, nil, false}, 380 {"AfterRangeOp4", args{db, lastLinkedOperation(ops[3]).AccountID, afterFrom, afterTo}, nil, false}, 381 } 382 for _, tt := range tests { 383 tt := tt // capture range variable 384 t.Run(tt.name, func(t *testing.T) { 385 got, err := GeAccountHistoryRange(tt.args.db, tt.args.accountID, tt.args.from, tt.args.to) 386 if (err != nil) != tt.wantErr { 387 t.Errorf("GeAccountHistoryRange() error = %v, wantErr %v", err, tt.wantErr) 388 return 389 } 390 if !reflect.DeepEqual(got, tt.want) { 391 t.Errorf("GeAccountHistoryRange() = %v, want %v", got, tt.want) 392 } 393 }) 394 } 395 } 396 397 func TestFindAccountOperationByReference(t *testing.T) { 398 const databaseName = "TestFindAccountOperationByReference" 399 t.Parallel() 400 401 db := tests.Setup(databaseName, AccountOperationModel()) 402 defer tests.Teardown(db, databaseName) 403 404 data := createTestAccountOperationData(db) 405 ref1, _ := AppendAccountOperation(db, model.NewAccountOperation(0, 406 data.Accounts[0].ID, 407 model.SynchroneousTypeSync, 408 model.OperationTypeDeposit, 409 1337, 410 time.Now(), 411 0.1337, 42.0, 412 0.0, 0.0, 413 )) 414 ref2, _ := AppendAccountOperation(db, model.NewAccountOperation(0, 415 data.Accounts[0].ID, 416 model.SynchroneousTypeAsyncStart, 417 model.OperationTypeTransfer, 418 1338, 419 time.Now(), 420 0.1337, 42.0, 421 0.0, 0.0, 422 )) 423 424 type args struct { 425 synchroneousType model.SynchroneousType 426 operationType model.OperationType 427 referenceID model.RefID 428 } 429 tests := []struct { 430 name string 431 args args 432 want model.AccountOperation 433 wantErr bool 434 }{ 435 {"default", args{}, model.AccountOperation{}, true}, 436 437 {"invalid_sync", args{model.SynchroneousTypeInvalid, model.OperationTypeInit, 42}, model.AccountOperation{}, true}, 438 {"invalid_type", args{model.SynchroneousTypeSync, model.OperationTypeInvalid, 42}, model.AccountOperation{}, true}, 439 {"invalid_ref", args{model.SynchroneousTypeSync, model.OperationTypeInit, 0}, model.AccountOperation{}, true}, 440 441 {"valid1", args{ref1.SynchroneousType, ref1.OperationType, ref1.ReferenceID}, ref1, false}, 442 {"valid2", args{ref2.SynchroneousType, ref2.OperationType, ref2.ReferenceID}, ref2, false}, 443 } 444 for _, tt := range tests { 445 tt := tt // capture range variable 446 t.Run(tt.name, func(t *testing.T) { 447 got, err := FindAccountOperationByReference(db, tt.args.synchroneousType, tt.args.operationType, tt.args.referenceID) 448 if (err != nil) != tt.wantErr { 449 t.Errorf("FindAccountOperationByReference() error = %v, wantErr %v", err, tt.wantErr) 450 return 451 } 452 if !reflect.DeepEqual(got, tt.want) { 453 t.Errorf("FindAccountOperationByReference() = %v, want %v", got, tt.want) 454 } 455 }) 456 } 457 } 458 459 func createOperation(account model.AccountID, amount, balance model.Float) model.AccountOperation { 460 return model.NewAccountOperation(0, account, model.SynchroneousTypeSync, model.OperationTypeDeposit, 0, time.Now(), amount, balance, 0.0, 0.0) 461 } 462 463 func cloneOperation(operation model.AccountOperation) model.AccountOperation { 464 return createOperation(operation.AccountID, *operation.Amount, *operation.Balance) 465 } 466 467 func createLinkedOperations(db database.Context, account model.AccountID, count int, amount model.Float) []model.AccountOperation { 468 list, _ := GeAccountHistory(db, account) 469 var balance model.Float 470 for i := 0; i < count; i++ { 471 balance += amount 472 last := storeOperation(db, createOperation(account, amount, balance)) 473 if !last.IsValid() { 474 panic("Invalid AccountOperation") 475 } 476 list = append(list, last) 477 } 478 479 return list 480 } 481 482 func lastLinkedOperation(list []model.AccountOperation) model.AccountOperation { 483 if len(list) == 0 { 484 panic("empty list") 485 } 486 487 return list[len(list)-1] 488 } 489 490 func createAccountOperationPrevNext(op model.AccountOperation, previous, next model.AccountOperationID) AccountOperationPrevNext { 491 return AccountOperationPrevNext{ 492 AccountOperation: op, 493 Previous: previous, 494 Next: next, 495 } 496 } 497 498 func createAccountOperationPrevNextList(ops []model.AccountOperation) []AccountOperationPrevNext { 499 var list []AccountOperationPrevNext 500 for i, op := range ops { 501 prev := model.AccountOperationID(0) 502 if i > 0 { 503 prev = ops[i-1].ID 504 } 505 next := model.AccountOperationID(0) 506 if i < len(ops)-1 { 507 next = ops[i+1].ID 508 } 509 list = append(list, createAccountOperationPrevNext(op, prev, next)) 510 511 } 512 return list 513 } 514 515 func storeOperation(db database.Context, operation model.AccountOperation) model.AccountOperation { 516 gdb := db.DB().(*gorm.DB) 517 if gdb == nil { 518 return model.AccountOperation{} 519 } 520 521 err := gdb.Create(&operation).Error 522 if err != nil { 523 return model.AccountOperation{} 524 } 525 526 return operation 527 } 528 529 type AccountOperationTestData struct { 530 AccountStateTestData 531 AccountStates []model.AccountState 532 } 533 534 func createTestAccountOperationData(db database.Context) AccountOperationTestData { 535 var data AccountOperationTestData 536 data.AccountStateTestData = createTestAccountStateData(db) 537 538 // Disable 2nd currency 539 *data.Currencies[1].Available = FlagCurencyDisable 540 _, _ = AddOrUpdateCurrency(db, data.Currencies[1]) 541 542 accountState1, _ := AddOrUpdateAccountState(db, model.AccountState{AccountID: data.Accounts[0].ID, State: model.AccountStatusNormal}) 543 accountState2, _ := AddOrUpdateAccountState(db, model.AccountState{AccountID: data.Accounts[1].ID, State: model.AccountStatusNormal}) 544 accountState3, _ := AddOrUpdateAccountState(db, model.AccountState{AccountID: data.Accounts[2].ID, State: model.AccountStatusDisabled}) // disable 3rd account 545 accountState4, _ := AddOrUpdateAccountState(db, model.AccountState{AccountID: data.Accounts[3].ID, State: model.AccountStatusNormal}) 546 547 data.AccountStates = append(data.AccountStates, accountState1) 548 data.AccountStates = append(data.AccountStates, accountState2) 549 data.AccountStates = append(data.AccountStates, accountState3) 550 data.AccountStates = append(data.AccountStates, accountState4) 551 552 return data 553 }