/***********************************************************************
* This file is part of iDempiere ERP Open Source *
* http://www.idempiere.org *
* *
* Copyright (C) Contributors *
* *
* This program is free software; you can redistribute it and/or *
* modify it under the terms of the GNU General Public License *
* as published by the Free Software Foundation; either version 2 *
* of the License, or (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, *
* MA 02110-1301, USA. *
* *
* Contributors: *
* - teo sarca *
* - hengsin *
**********************************************************************/
package org.idempiere.test.base;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.File;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Properties;
import org.adempiere.exceptions.DBException;
import org.compiere.dbPort.Convert;
import org.compiere.model.I_AD_UserPreference;
import org.compiere.model.MAcctSchema;
import org.compiere.model.MBPartner;
import org.compiere.model.MClient;
import org.compiere.model.MColumn;
import org.compiere.model.MMessage;
import org.compiere.model.MProduct;
import org.compiere.model.MProductCategory;
import org.compiere.model.MProductCategoryAcct;
import org.compiere.model.MProductionLine;
import org.compiere.model.MTest;
import org.compiere.model.POInfo;
import org.compiere.util.DB;
import org.compiere.util.Env;
import org.compiere.util.Ini;
import org.compiere.util.Trx;
import org.idempiere.test.AbstractTestCase;
import org.idempiere.test.DictionaryIDs;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.parallel.Isolated;
/**
* Tests for {@link org.compiere.model.PO} class.
* @author Teo Sarca, SC ARHIPAC SERVICE SRL
* @author hengsin
*
* Run Isolated because of migration script file management
*/
@Isolated
public class POTest extends AbstractTestCase
{
public static class MyTestPO extends MTest
{
private static final long serialVersionUID = -6861171283806782985L;
protected boolean failOnSave = false;
private MyTestPO m_parent = null;
private MyTestPO m_dependentRecord = null;
public static String getName(int Test_ID, String trxName)
{
String sql = "SELECT "+COLUMNNAME_Name+" FROM "+Table_Name
+" WHERE "+COLUMNNAME_Test_ID+"=?";
return DB.getSQLValueStringEx(trxName, sql, Test_ID);
}
public static boolean exists(int Test_ID, String trxName)
{
final String sql = "SELECT "+COLUMNNAME_Test_ID+" FROM "+Table_Name
+" WHERE "+COLUMNNAME_Test_ID+"=?";
int id = DB.getSQLValueEx(trxName, sql, Test_ID);
return id > 0 && id == Test_ID;
}
public MyTestPO(Properties ctx, boolean failOnSave, String trxName)
{
super(ctx, "Test_"+System.currentTimeMillis(), 10, trxName);
this.setDescription(""+getClass());
this.failOnSave = failOnSave;
}
public MyTestPO(Properties ctx, int id, String trxName)
{
super(ctx, id, trxName);
}
@Override
protected boolean afterSave(boolean newRecord, boolean success)
{
if (m_parent == null)
{
m_dependentRecord = new MyTestPO(getCtx(), false, get_TrxName());
m_dependentRecord.m_parent = this;
m_dependentRecord.setName("D_"+this.getName());
m_dependentRecord.saveEx();
}
if (this.failOnSave)
throw new RuntimeException("Never save this object [trxName="+get_TrxName()+", success="+success+"]");
return true;
}
public int getDependent_ID()
{
return (m_dependentRecord != null ? m_dependentRecord.get_ID() : -1);
}
};
/**
* Tests the following methods:
*
* - {@link org.compiere.model.PO#is_Changed()}
*
- {@link org.compiere.model.PO#is_ValueChanged(String)}
*
* Applies to following bugs:
*
* - [ 1704828 ] PO.is_Changed() and PO.is_ValueChanged are not consistent
*
*/
@Test
public void test_Changed()
{
String[] testStrings = new String[] {
"a",
"test",
};
// Create the test PO and save
MTest testPO = new MTest(Env.getCtx(), getClass().getName(), 1, getTrxName());
for (String str : testStrings)
{
testPO.setHelp(str);
testPO.saveEx();
String originalString = testPO.getHelp();
String info = "testString=[" + str + "]" + ", originalString=[" + originalString + "]";
// Initial asserts (nothing changed)
assertFalse(testPO.is_ValueChanged(MTest.COLUMNNAME_Help), info);
assertFalse(testPO.is_Changed(), info);
// Set the same name
testPO.setHelp(originalString);
assertFalse(testPO.is_ValueChanged(MTest.COLUMNNAME_Help), info);
assertFalse(testPO.is_Changed(), info);
// Set a new name
testPO.setHelp(originalString+"-changed");
assertTrue(testPO.is_ValueChanged(MTest.COLUMNNAME_Help), info);
assertTrue(testPO.is_Changed(), info);
// Set the original name back
testPO.setHelp(originalString);
assertFalse(testPO.is_ValueChanged(MTest.COLUMNNAME_Help), info);
assertFalse(testPO.is_Changed(), info);
}
// Finally, delete the testPO
testPO.delete(true, getTrxName());
}
/**
* BF [ 1990856 ] PO.set_Value* : truncate string more than needed
*/
@Test
public void testTruncatedStrings() {
//
// Creating a huge string for testing:
StringBuilder sb = new StringBuilder();
for (int i = 1; i <= 1000; i++) {
sb.append("0123456789");
}
String bigString = sb.toString();
//
// Create the test PO:
MTest testPO = new MTest(Env.getCtx(), getClass().getName(), 1, getTrxName());
//
// Getting Max Length:
POInfo info = POInfo.getPOInfo(Env.getCtx(), MTest.Table_ID);
int maxLength = info.getFieldLength(info.getColumnIndex(MTest.COLUMNNAME_Name));
//
// Test with a string that has less then maxLength
{
testPO.set_ValueOfColumn(MTest.COLUMNNAME_Name, bigString.substring(0, maxLength - 1));
String resultString = (String) testPO.get_Value(MTest.COLUMNNAME_Name);
assertEquals(maxLength - 1, resultString.length(), "String was not truncated correctly (1)");
//
testPO.setName(bigString.substring(0, maxLength - 1));
assertEquals(maxLength - 1, testPO.getName().length(), "String was not truncated correctly (2)");
}
//
// Test with a string that has maxLength
{
testPO.set_ValueOfColumn(MTest.COLUMNNAME_Name, bigString.substring(0, maxLength));
String resultString = (String) testPO.get_Value(MTest.COLUMNNAME_Name);
assertEquals(maxLength, resultString.length(), "String was not truncated correctly (3)");
//
testPO.setName(bigString.substring(0, maxLength));
assertEquals(maxLength, testPO.getName().length(), "String was not truncated correctly (4)");
}
//
// Test with a string that has more than maxLength
{
testPO.set_ValueOfColumn(MTest.COLUMNNAME_Name, bigString);
String resultString = (String) testPO.get_Value(MTest.COLUMNNAME_Name);
assertEquals(maxLength, resultString.length(), "String was not truncated correctly (5)");
//
testPO.setName(bigString);
assertEquals(maxLength, testPO.getName().length(), "String was not truncated correctly (6)");
}
}
/**
* Object should NOT be saved if afterSave fails EVEN if is outside transaction (trxName=null)
*/
@Test
public void testAfterSaveError()
{
//
// Test for new objects
{
MyTestPO test = new MyTestPO(Env.getCtx(), true, null);
assertFalse(test.save(), "Object should not be saved -- "+test);
assertFalse(test.get_ID() <= 0, "Object should not be saved -- "+test);
assertFalse(MyTestPO.exists(test.get_ID(), null), "Object should not be saved(2) -- "+test);
}
//
// Test for old objects
MyTestPO test = null;
try
{
test = new MyTestPO(Env.getCtx(), false, null);
assertTrue(test.save(), "Object *should* be saved -- "+test);
//
MyTestPO test2 = new MyTestPO(Env.getCtx(), test.get_ID(), null);
assertEquals(test2.get_ID(), test.get_ID(), "Object not found");
test2.failOnSave = true;
test2.setName(test2.getName()+"_2");
assertFalse(test2.save(), "Object should not be saved -- "+test2);
//
String name = MyTestPO.getName(test2.get_ID(), null);
assertEquals(test.getName(), name, "Object should not be modified(2) -- id="+test2);
}
finally
{
// cleanup
if (test != null)
{
if (test.getDependent_ID() > 0)
{
MyTestPO testDependent = new MyTestPO(Env.getCtx(), test.getDependent_ID(), null);
testDependent.deleteEx(true);
}
test.deleteEx(true);
}
}
}
/**
* If one object fails on after save we should not revert all transaction.
* BF [ 2849122 ] PO.AfterSave is not rollback on error
* https://sourceforge.net/p/adempiere/bugs/2073/
*/
@Test
public void testAfterSaveError_BF2849122()
{
assertNotNull(getTrxName(), "TrxName should not be null");
MyTestPO t1 = new MyTestPO(Env.getCtx(), false, getTrxName());
t1.saveEx();
assertTrue(MyTestPO.exists(t1.get_ID(), getTrxName()), "Object not found(1) - t1="+t1);
assertTrue(MyTestPO.exists(t1.getDependent_ID(), getTrxName()), "Object not found(1) - t1(dep)="+t1);
//
final MyTestPO t2 = new MyTestPO(Env.getCtx(), true, getTrxName());
try
{
t2.saveEx();
}
catch (Exception e){}
assertTrue(MyTestPO.exists(t1.get_ID(), getTrxName()), "Object not found(2) - t1="+t1);
assertTrue(MyTestPO.exists(t1.getDependent_ID(), getTrxName()), "Object not found(2) - t1(dep)="+t1);
assertFalse(MyTestPO.exists(t2.get_ID(), getTrxName()), "Object found(2) - t2="+t2);
assertFalse(MyTestPO.exists(t2.getDependent_ID(), getTrxName()), "Object found(2) - t2(dep)="+t2);
//
final MyTestPO t3 = new MyTestPO(Env.getCtx(), false, getTrxName());
t3.saveEx();
assertTrue(MyTestPO.exists(t1.get_ID(), getTrxName()), "Object not found(3) - t1="+t1);
assertTrue(MyTestPO.exists(t1.getDependent_ID(), getTrxName()), "Object not found(3) - t1(dep)="+t1);
assertFalse(MyTestPO.exists(t2.get_ID(), getTrxName()), "Object found(3) - t2="+t2);
assertFalse(MyTestPO.exists(t2.getDependent_ID(), getTrxName()), "Object found(3) - t2(dep)="+t2);
assertTrue(MyTestPO.exists(t3.get_ID(), getTrxName()), "Object not found(3) - t3="+t3);
assertTrue(MyTestPO.exists(t3.getDependent_ID(), getTrxName()), "Object not found(3) - t3(dep)="+t3);
}
@Test
public void testForUpdate()
{
MClient client = new MClient(Env.getCtx(), Env.getAD_Client_ID(Env.getCtx()), getTrxName());
assertDoesNotThrow(() -> {
boolean ok = DB.getDatabase().forUpdate(client, 5);
assertTrue(ok);
});
Trx trx2 = Trx.get(Trx.createTrxName(), true);
try {
trx2.start();
MClient client2 = new MClient(Env.getCtx(), Env.getAD_Client_ID(Env.getCtx()), trx2.getTrxName());
assertThrows(DBException.class, () -> {
DB.getDatabase().forUpdate(client2, 5);
});
} finally {
trx2.close();
}
String description = client.getDescription();
client.setDescription(client.getName()+".Description");
assertDoesNotThrow(() -> {
client.saveEx();
});
rollback();
client.load(getTrxName());
assertEquals(description, client.getDescription());
Trx trx3 = Trx.get(Trx.createTrxName(), true);
try {
trx3.start();
MClient client3 = new MClient(Env.getCtx(), Env.getAD_Client_ID(Env.getCtx()), trx3.getTrxName());
assertDoesNotThrow(() -> {
boolean ok = DB.getDatabase().forUpdate(client3, 5);
assertTrue(ok);
});
} finally {
trx3.close();
}
}
@Test
public void testOptimisticLocking()
{
int joeBlock = 118;
MBPartner bp1 = new MBPartner(Env.getCtx(), joeBlock, getTrxName());
MBPartner bp2 = new MBPartner(Env.getCtx(), joeBlock, getTrxName());
//normal update without optimistic locking
bp1.setDescription("bp1");
boolean updated = bp1.save();
assertTrue(updated);
bp2.setDescription("bp2");
updated = bp2.save();
assertTrue(updated);
//last update ok, description=bp2
bp1 = new MBPartner(Env.getCtx(), joeBlock, getTrxName());
bp2 = new MBPartner(Env.getCtx(), joeBlock, getTrxName());
assertEquals("bp2", bp1.getDescription());
assertEquals("bp2", bp2.getDescription());
//test update with default optimistic locking using updated timestamp
bp1.set_UseOptimisticLocking(true);
bp1.setDescription("bp1");
if (DB.isOracle()) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
}
updated = bp1.save();
assertTrue(updated);
bp2.set_UseOptimisticLocking(true);
bp2.setDescription("bp2.1");
updated = bp2.save();
assertFalse(updated);
//last update fail, description=bp1
bp1 = new MBPartner(Env.getCtx(), joeBlock, getTrxName());
bp2 = new MBPartner(Env.getCtx(), joeBlock, getTrxName());
assertEquals("bp1", bp1.getDescription());
assertEquals("bp1", bp2.getDescription());
//test update with custom optimistic locking columns
bp1.set_UseOptimisticLocking(true);
bp1.setDescription("bp1.1");
updated = bp1.save();
assertTrue(updated);
bp2.set_UseOptimisticLocking(true);
bp2.set_OptimisticLockingColumns(new String[] {"Name"});
bp2.setDescription("bp2");
updated = bp2.save();
assertTrue(updated);
//last update ok, description=bp2
bp1 = new MBPartner(Env.getCtx(), joeBlock, getTrxName());
bp2 = new MBPartner(Env.getCtx(), joeBlock, getTrxName());
assertEquals("bp2", bp1.getDescription());
assertEquals("bp2", bp2.getDescription());
//test update with custom multiple column optimistic locking
bp1.set_UseOptimisticLocking(true);
bp1.setDescription("bp1");
updated = bp1.save();
assertTrue(updated);
bp2.set_UseOptimisticLocking(true);
bp2.set_OptimisticLockingColumns(new String[] {"Name","Description"});
bp2.setDescription("bp2.1");
updated = bp2.save();
assertFalse(updated);
//last update fail, description=bp1
bp1 = new MBPartner(Env.getCtx(), joeBlock, getTrxName());
bp2 = new MBPartner(Env.getCtx(), joeBlock, getTrxName());
assertEquals("bp1", bp1.getDescription());
assertEquals("bp1", bp2.getDescription());
MMessage msg1 = new MMessage(Env.getCtx(), 0, getTrxName());
msg1.setValue("msg1 test");
msg1.setMsgText("msg1 test");
msg1.setMsgType(MMessage.MSGTYPE_Information);
msg1.saveEx();
//test normal delete
updated = msg1.delete(true);
assertTrue(updated);
msg1 = new MMessage(Env.getCtx(), 0, getTrxName());
msg1.setValue("msg1 test");
msg1.setMsgText("msg1 test");
msg1.setMsgType(MMessage.MSGTYPE_Information);
msg1.saveEx();
//test delete with default optimistic locking
MMessage msg2 = new MMessage(Env.getCtx(), msg1.getAD_Message_ID(), getTrxName());
msg1.setMsgText("msg 1.1 test");
if (DB.isOracle()) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
}
msg1.saveEx();
msg2.set_UseOptimisticLocking(true);
updated = msg2.delete(true);
assertFalse(updated);
//test delete with custom optimistic locking columns
msg2 = new MMessage(Env.getCtx(), msg1.getAD_Message_ID(), getTrxName());
assertEquals(msg1.getMsgText(), msg2.getMsgText());
msg1.setMsgText("msg1 test");
msg1.saveEx();
msg2.set_UseOptimisticLocking(true);
msg2.set_OptimisticLockingColumns(new String[] {"Value"});
updated = msg2.delete(true);
assertTrue(updated);
//test delete with multiple custom optimistic locking columns
msg1 = new MMessage(Env.getCtx(), 0, getTrxName());
msg1.setValue("msg1 test");
msg1.setMsgText("msg1 test");
msg1.setMsgType(MMessage.MSGTYPE_Information);
msg1.saveEx();
msg2 = new MMessage(Env.getCtx(), msg1.getAD_Message_ID(), getTrxName());
msg1.setMsgText("msg 1.1 test");
msg1.saveEx();
msg2.set_UseOptimisticLocking(true);
msg2.set_OptimisticLockingColumns(new String[] {"Value", "MsgText"});
updated = msg2.delete(true);
assertFalse(updated);
msg2 = new MMessage(Env.getCtx(), msg1.getAD_Message_ID(), getTrxName());
assertEquals(msg1.getMsgText(), msg2.getMsgText());
}
@Test
public void testVirtualColumnLoad() {
MTest testPo = new MTest(Env.getCtx(), getClass().getName(), 1, getTrxName());
testPo.save();
// asynchronous (default) virtual column loading
assertTrue(null == testPo.get_ValueOld(MTest.COLUMNNAME_TestVirtualQty));
BigDecimal expected = new BigDecimal("123.45");
assertEquals(expected, testPo.getTestVirtualQty().setScale(2, RoundingMode.HALF_UP), "Wrong value returned");
// synchronous virtual column loading
testPo = new MTest(Env.getCtx(), testPo.get_ID(), getTrxName(), MTest.COLUMNNAME_TestVirtualQty);
assertTrue(null != testPo.get_ValueOld(MTest.COLUMNNAME_TestVirtualQty));
assertEquals(expected, testPo.getTestVirtualQty().setScale(2, RoundingMode.HALF_UP), "Wrong value returned");
}
@Test
public void testLogMigrationScript() {
MClient client = MClient.get(Env.getCtx());
MAcctSchema as = client.getAcctSchema();
assertFalse(Env.isLogMigrationScript(MProduct.Table_Name), "Unexpected Log Migration Script default for MProduct");
Env.getCtx().setProperty(Ini.P_LOGMIGRATIONSCRIPT, "Y");
Env.setContext(Env.getCtx(), I_AD_UserPreference.COLUMNNAME_MigrationScriptComment, "testLogMigrationScript");
assertTrue(Env.isLogMigrationScript(MProduct.Table_Name), "Unexpected Log Migration Script Y/N value for MProduct");
MProductCategory lotLevel = new MProductCategory(Env.getCtx(), 0, null);
lotLevel.setName("testLogMigrationScript");
lotLevel.saveEx();
MProduct product = null;
try {
MProductCategoryAcct lotLevelAcct = MProductCategoryAcct.get(lotLevel.get_ID(), as.get_ID());
lotLevelAcct = new MProductCategoryAcct(Env.getCtx(), lotLevelAcct);
lotLevelAcct.setCostingLevel(MAcctSchema.COSTINGLEVEL_BatchLot);
lotLevelAcct.saveEx();
product = new MProduct(Env.getCtx(), 0, null);
product.setM_Product_Category_ID(lotLevel.get_ID());
product.setName("testLogMigrationScript");
product.setProductType(MProduct.PRODUCTTYPE_Item);
product.setIsStocked(true);
product.setIsSold(true);
product.setIsPurchased(true);
product.setC_UOM_ID(DictionaryIDs.C_UOM.EACH.id);
product.setC_TaxCategory_ID(DictionaryIDs.C_TaxCategory.STANDARD.id);
product.setM_AttributeSet_ID(DictionaryIDs.M_AttributeSet.FERTILIZER_LOT.id);
product.saveEx();
} finally {
rollback();
if (product != null) {
product.set_TrxName(null);
product.deleteEx(true);
}
lotLevel.deleteEx(true);
}
String fileName = Convert.getGeneratedMigrationScriptFileName();
String folderPg = Convert.getMigrationScriptFolder("postgresql");
String folderOr = Convert.getMigrationScriptFolder("oracle");
Convert.closeLogMigrationScript();
File file = new File(folderPg + fileName);
assertTrue(file.exists(), "Not found: " + folderPg + fileName);
file.delete();
file = new File(folderOr + fileName);
assertTrue(file.exists(), "Not found: " + folderOr + fileName);
file.delete();
}
@Test
public void testIsVirtualColumnMethods() {
//column sql with no prefix
MColumn column = MColumn.get(Env.getCtx(), MTest.Table_Name, MTest.COLUMNNAME_TestVirtualQty);
assertTrue(column.isVirtualColumn(), "MColumn.isVirtualColumn() not working as expected for ColumnSQL="+column.getColumnSQL());
assertTrue(column.isVirtualDBColumn(), "MColumn.isVirtualDBColumn() not working as expected for ColumnSQL="+column.getColumnSQL());
assertFalse(column.isVirtualUIColumn(), "MColumn.isVirtualUIColumn() not working as expected for ColumnSQL="+column.getColumnSQL());
assertFalse(column.isVirtualSearchColumn(), "MColumn.isVirtualSearchColumn() not working as expected for ColumnSQL="+column.getColumnSQL());
//column sql with @SQLFIND= prefix
column = MColumn.get(Env.getCtx(), MProductionLine.Table_Name, "ProductType");
assertTrue(column.isVirtualColumn(), "MColumn.isVirtualColumn() not working as expected for ColumnSQL="+column.getColumnSQL());
assertFalse(column.isVirtualDBColumn(), "MColumn.isVirtualDBColumn() not working as expected for ColumnSQL="+column.getColumnSQL());
assertFalse(column.isVirtualUIColumn(), "MColumn.isVirtualUIColumn() not working as expected for ColumnSQL="+column.getColumnSQL());
assertTrue(column.isVirtualSearchColumn(), "MColumn.isVirtualSearchColumn() not working as expected for ColumnSQL="+column.getColumnSQL());
//column sql with @SQL= prefix
column = MColumn.get(Env.getCtx(), MTest.Table_Name, MTest.COLUMNNAME_TestVirtualQty);
column = new MColumn(column);
column.setColumnSQL(MColumn.VIRTUAL_UI_COLUMN_PREFIX+column.getColumnSQL());
assertTrue(column.isVirtualColumn(), "MColumn.isVirtualColumn() not working as expected for ColumnSQL="+column.getColumnSQL());
assertFalse(column.isVirtualDBColumn(), "MColumn.isVirtualDBColumn() not working as expected for ColumnSQL="+column.getColumnSQL());
assertTrue(column.isVirtualUIColumn(), "MColumn.isVirtualUIColumn() not working as expected for ColumnSQL="+column.getColumnSQL());
assertFalse(column.isVirtualSearchColumn(), "MColumn.isVirtualSearchColumn() not working as expected for ColumnSQL="+column.getColumnSQL());
}
}