|
9 | 9 | import static org.junit.Assert.assertFalse; |
10 | 10 | import static org.junit.Assert.assertNotEquals; |
11 | 11 | import static org.junit.Assert.assertTrue; |
| 12 | +import static org.mockito.Mockito.mock; |
| 13 | +import static org.mockito.Mockito.when; |
12 | 14 |
|
| 15 | +import com.dotcms.contenttype.business.ContentTypeAPI; |
| 16 | +import com.dotcms.contenttype.business.FieldAPI; |
13 | 17 | import com.dotcms.contenttype.model.field.ColumnField; |
| 18 | +import com.dotcms.contenttype.model.field.DataTypes; |
14 | 19 | import com.dotcms.contenttype.model.field.Field; |
| 20 | +import com.dotcms.contenttype.model.field.FieldBuilder; |
15 | 21 | import com.dotcms.contenttype.model.field.RelationshipField; |
16 | 22 | import com.dotcms.contenttype.model.field.RowField; |
| 23 | +import com.dotcms.contenttype.model.field.TextField; |
17 | 24 | import com.dotcms.contenttype.model.type.BaseContentType; |
18 | 25 | import com.dotcms.contenttype.model.type.ContentType; |
19 | 26 | import com.dotcms.datagen.ContentTypeDataGen; |
|
40 | 47 | import com.dotmarketing.portlets.templates.model.Template; |
41 | 48 | import com.dotmarketing.portlets.workflows.business.SystemWorkflowConstants; |
42 | 49 | import com.dotmarketing.util.Config; |
| 50 | +import com.dotmarketing.util.UUIDGenerator; |
43 | 51 | import com.dotmarketing.util.UUIDUtil; |
44 | 52 | import com.dotmarketing.util.WebKeys; |
| 53 | +import com.liferay.portal.model.User; |
45 | 54 | import com.liferay.util.FileUtil; |
46 | 55 | import com.tngtech.java.junit.dataprovider.DataProvider; |
47 | 56 | import com.tngtech.java.junit.dataprovider.DataProviderRunner; |
@@ -77,6 +86,16 @@ private static void prepareIfNecessary() throws Exception { |
77 | 86 |
|
78 | 87 | private final DeterministicIdentifierAPIImpl defaultGenerator = new DeterministicIdentifierAPIImpl(); |
79 | 88 |
|
| 89 | + /** |
| 90 | + * Expected deterministic id seed for a field: {@code variable:typeName:dataType} when the field |
| 91 | + * has a data type, {@code variable:typeName} otherwise. |
| 92 | + */ |
| 93 | + private static String expectedFieldSeed(final Field field) { |
| 94 | + return null != field.dataType() |
| 95 | + ? String.format("%s:%s:%s", field.variable(), field.typeName(), field.dataType().value) |
| 96 | + : String.format("%s:%s", field.variable(), field.typeName()); |
| 97 | + } |
| 98 | + |
80 | 99 |
|
81 | 100 | /** |
82 | 101 | * Given Scenario: We have a fileAsset with missing physical file |
@@ -334,7 +353,7 @@ public void Test_Generate_Content_Type_Identifier(final ContentTypeTestCase test |
334 | 353 | final String fieldIdentifier2 = defaultGenerator.generateDeterministicIdBestEffort(field, field::variable); |
335 | 354 | //Test it is idempotent |
336 | 355 | assertEquals(fieldIdentifier1, fieldIdentifier2); |
337 | | - final String expected = String.format("%s:%s", field.variable(), field.typeName()); |
| 356 | + final String expected = expectedFieldSeed(field); |
338 | 357 | assertEquals(expected, defaultGenerator.resolveName(field, field::variable)); |
339 | 358 | } |
340 | 359 |
|
@@ -588,7 +607,7 @@ public void test_resolveName_seedShouldContainFieldType(){ |
588 | 607 |
|
589 | 608 | //verify the seed contains the field type |
590 | 609 | for(final Field field : contentType.fields()){ |
591 | | - final String expected = String.format("%s:%s", field.variable(), field.typeName()); |
| 610 | + final String expected = expectedFieldSeed(field); |
592 | 611 | assertEquals(expected, defaultGenerator.resolveName(field, field::variable)); |
593 | 612 | } |
594 | 613 |
|
@@ -667,4 +686,177 @@ public void Test_Generate_Folder_Identifier() throws DotDataException { |
667 | 686 | } |
668 | 687 | } |
669 | 688 |
|
| 689 | + /** |
| 690 | + * Method to test: {@link DeterministicIdentifierAPIImpl#generateDeterministicIdBestEffort(Field, Supplier)} |
| 691 | + * Given Scenario: A Text field is created with data type TEXT, then deleted, then re-created with the |
| 692 | + * same variable name but data type INTEGER (the scenario behind issue #36636). |
| 693 | + * ExpectedResult: The re-created field must get a DIFFERENT deterministic id (the data type is part of |
| 694 | + * the seed) and its data type must be INTEGER. |
| 695 | + */ |
| 696 | + @Test |
| 697 | + public void Test_Delete_And_Recreate_Field_With_Different_DataType_Generates_New_Id() throws Exception { |
| 698 | + prepareIfNecessary(); |
| 699 | + final boolean generateConsistentIdentifiers = Config |
| 700 | + .getBooleanProperty(GENERATE_DETERMINISTIC_IDENTIFIERS, true); |
| 701 | + try { |
| 702 | + Config.setProperty(GENERATE_DETERMINISTIC_IDENTIFIERS, true); |
| 703 | + |
| 704 | + final User systemUser = APILocator.systemUser(); |
| 705 | + final FieldAPI fieldAPI = APILocator.getContentTypeFieldAPI(); |
| 706 | + final String fieldVarName = "myField" + System.currentTimeMillis(); |
| 707 | + |
| 708 | + final Field textField = new FieldDataGen() |
| 709 | + .type(TextField.class) |
| 710 | + .name(fieldVarName) |
| 711 | + .velocityVarName(fieldVarName) |
| 712 | + .dataType(DataTypes.TEXT) |
| 713 | + .next(); |
| 714 | + |
| 715 | + final ContentType contentType = new ContentTypeDataGen() |
| 716 | + .workflowId(SystemWorkflowConstants.SYSTEM_WORKFLOW_ID) |
| 717 | + .baseContentType(BaseContentType.CONTENT) |
| 718 | + .field(textField) |
| 719 | + .nextPersisted(); |
| 720 | + try { |
| 721 | + final Field originalField = fieldAPI |
| 722 | + .byContentTypeIdAndVar(contentType.id(), fieldVarName); |
| 723 | + assertEquals(DataTypes.TEXT, originalField.dataType()); |
| 724 | + |
| 725 | + fieldAPI.delete(originalField); |
| 726 | + |
| 727 | + final Field recreatedField = fieldAPI.save(FieldBuilder.builder(TextField.class) |
| 728 | + .name(fieldVarName) |
| 729 | + .variable(fieldVarName) |
| 730 | + .contentTypeId(contentType.id()) |
| 731 | + .dataType(DataTypes.INTEGER) |
| 732 | + .build(), systemUser); |
| 733 | + |
| 734 | + assertEquals(DataTypes.INTEGER, recreatedField.dataType()); |
| 735 | + assertNotEquals( |
| 736 | + "A field re-created with the same variable but a different data type must get a different deterministic id", |
| 737 | + originalField.id(), recreatedField.id()); |
| 738 | + } finally { |
| 739 | + ContentTypeDataGen.remove(contentType); |
| 740 | + } |
| 741 | + } finally { |
| 742 | + Config.setProperty(GENERATE_DETERMINISTIC_IDENTIFIERS, generateConsistentIdentifiers); |
| 743 | + } |
| 744 | + } |
| 745 | + |
| 746 | + /** |
| 747 | + * Method to test: {@link com.dotcms.contenttype.business.ContentTypeAPI#save(ContentType, List)} |
| 748 | + * Given Scenario: Replicates the push-publish receiver flow for issue #36636. A Content Type holds a |
| 749 | + * Text field with data type TEXT; an incoming save carries the same field variable re-created under a |
| 750 | + * DIFFERENT id (as the sender's bundle does once the data type is part of the deterministic id seed) |
| 751 | + * with data type INTEGER. |
| 752 | + * ExpectedResult: The old field is removed and the data-type change is applied, never silently dropped. |
| 753 | + */ |
| 754 | + @Test |
| 755 | + public void Test_ContentType_Save_Applies_DataType_Change_When_Field_Id_Differs() throws Exception { |
| 756 | + prepareIfNecessary(); |
| 757 | + final boolean generateConsistentIdentifiers = Config |
| 758 | + .getBooleanProperty(GENERATE_DETERMINISTIC_IDENTIFIERS, true); |
| 759 | + try { |
| 760 | + Config.setProperty(GENERATE_DETERMINISTIC_IDENTIFIERS, true); |
| 761 | + |
| 762 | + final User systemUser = APILocator.systemUser(); |
| 763 | + final FieldAPI fieldAPI = APILocator.getContentTypeFieldAPI(); |
| 764 | + final ContentTypeAPI contentTypeAPI = APILocator.getContentTypeAPI(systemUser); |
| 765 | + final String fieldVarName = "myField" + System.currentTimeMillis(); |
| 766 | + |
| 767 | + final Field textField = new FieldDataGen() |
| 768 | + .type(TextField.class) |
| 769 | + .name(fieldVarName) |
| 770 | + .velocityVarName(fieldVarName) |
| 771 | + .dataType(DataTypes.TEXT) |
| 772 | + .next(); |
| 773 | + |
| 774 | + final ContentType contentType = new ContentTypeDataGen() |
| 775 | + .workflowId(SystemWorkflowConstants.SYSTEM_WORKFLOW_ID) |
| 776 | + .baseContentType(BaseContentType.CONTENT) |
| 777 | + .field(textField) |
| 778 | + .nextPersisted(); |
| 779 | + try { |
| 780 | + final Field originalField = fieldAPI |
| 781 | + .byContentTypeIdAndVar(contentType.id(), fieldVarName); |
| 782 | + assertEquals(DataTypes.TEXT, originalField.dataType()); |
| 783 | + |
| 784 | + // the incoming (pushed) field: same variable, different id, different data type |
| 785 | + final Field recreatedField = FieldBuilder.builder(TextField.class) |
| 786 | + .name(fieldVarName) |
| 787 | + .variable(fieldVarName) |
| 788 | + .contentTypeId(contentType.id()) |
| 789 | + .id(UUIDGenerator.generateUuid()) |
| 790 | + .dataType(DataTypes.INTEGER) |
| 791 | + .build(); |
| 792 | + |
| 793 | + final List<Field> newFields = contentType.fields().stream() |
| 794 | + .map(field -> fieldVarName.equalsIgnoreCase(field.variable()) |
| 795 | + ? recreatedField : field) |
| 796 | + .collect(Collectors.toList()); |
| 797 | + |
| 798 | + contentTypeAPI.save(contentType, newFields); |
| 799 | + |
| 800 | + final Field savedField = fieldAPI |
| 801 | + .byContentTypeIdAndVar(contentType.id(), fieldVarName); |
| 802 | + assertEquals(DataTypes.INTEGER, savedField.dataType()); |
| 803 | + assertEquals(recreatedField.id(), savedField.id()); |
| 804 | + assertNotEquals(originalField.id(), savedField.id()); |
| 805 | + } finally { |
| 806 | + ContentTypeDataGen.remove(contentType); |
| 807 | + } |
| 808 | + } finally { |
| 809 | + Config.setProperty(GENERATE_DETERMINISTIC_IDENTIFIERS, generateConsistentIdentifiers); |
| 810 | + } |
| 811 | + } |
| 812 | + |
| 813 | + /** |
| 814 | + * Method to test: {@link DeterministicIdentifierAPIImpl#resolveName(Field, Supplier)} |
| 815 | + * Given Scenario: Two fields share the same variable and field type but differ on data type; a third |
| 816 | + * field has no data type at all. |
| 817 | + * ExpectedResult: The seed must include the data type when the field has one (so the resulting |
| 818 | + * deterministic ids differ) and must fall back to the legacy {@code variable:typeName} format when the |
| 819 | + * data type is absent. |
| 820 | + */ |
| 821 | + @Test |
| 822 | + public void Test_ResolveName_Seed_Includes_DataType_When_Present() { |
| 823 | + final String fieldVarName = "seedField"; |
| 824 | + |
| 825 | + final Field textDataTypeField = FieldBuilder.builder(TextField.class) |
| 826 | + .name(fieldVarName) |
| 827 | + .variable(fieldVarName) |
| 828 | + .contentTypeId("fakeContentTypeId") |
| 829 | + .dataType(DataTypes.TEXT) |
| 830 | + .build(); |
| 831 | + |
| 832 | + final Field integerDataTypeField = FieldBuilder.builder(TextField.class) |
| 833 | + .name(fieldVarName) |
| 834 | + .variable(fieldVarName) |
| 835 | + .contentTypeId("fakeContentTypeId") |
| 836 | + .dataType(DataTypes.INTEGER) |
| 837 | + .build(); |
| 838 | + |
| 839 | + final String textSeed = defaultGenerator |
| 840 | + .resolveName(textDataTypeField, textDataTypeField::variable); |
| 841 | + final String integerSeed = defaultGenerator |
| 842 | + .resolveName(integerDataTypeField, integerDataTypeField::variable); |
| 843 | + |
| 844 | + assertEquals(String.format("%s:%s:%s", fieldVarName, textDataTypeField.typeName(), |
| 845 | + DataTypes.TEXT.value), textSeed); |
| 846 | + assertEquals(String.format("%s:%s:%s", fieldVarName, integerDataTypeField.typeName(), |
| 847 | + DataTypes.INTEGER.value), integerSeed); |
| 848 | + assertNotEquals( |
| 849 | + "Fields with the same variable but different data types must produce different seeds", |
| 850 | + textSeed, integerSeed); |
| 851 | + |
| 852 | + // a field without a data type keeps the legacy variable:typeName seed |
| 853 | + final Field noDataTypeField = mock(Field.class); |
| 854 | + when(noDataTypeField.variable()).thenReturn(fieldVarName); |
| 855 | + when(noDataTypeField.typeName()).thenReturn(textDataTypeField.typeName()); |
| 856 | + when(noDataTypeField.dataType()).thenReturn(null); |
| 857 | + |
| 858 | + assertEquals(String.format("%s:%s", fieldVarName, textDataTypeField.typeName()), |
| 859 | + defaultGenerator.resolveName(noDataTypeField, noDataTypeField::variable)); |
| 860 | + } |
| 861 | + |
670 | 862 | } |
0 commit comments