Repository: Accenture/sfmc-devtools (js)

Issue:
[FEATURE] Support Sender Profile / CSCLSROZ-173
Support

- https://developer.salesforce.com/docs/atlas.en-us.mc-apis.meta/mc-apis/senderprofile.htm

- https://developer.salesforce.com/docs/atlas.en-us.mc-apis.meta/mc-apis/sendclassification.htm

- https://developer.salesforce.com/docs/atlas.en-us.mc-apis.meta/mc-apis/deliveryprofile.htm

Pull request:
Feature/54 support sender profile csclsroz 173
# PR details

## What changes did you make? (Give an overview)

- closes #54 

## Further details (optional)

...

## Checklist

- [x] I have performed a self-review of my own code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] test scripts updated
- [ ] Wiki updated (if applicable)


Patch:
diff --git a/lib/MetadataTypeDefinitions.js b/lib/MetadataTypeDefinitions.js
index d34eeeef..7b26654c 100644
--- a/lib/MetadataTypeDefinitions.js
+++ b/lib/MetadataTypeDefinitions.js
@@ -29,6 +29,7 @@ import query from './metadataTypes/definitions/Query.definition.js';
 import role from './metadataTypes/definitions/Role.definition.js';
 import script from './metadataTypes/definitions/Script.definition.js';
 import sendClassification from './metadataTypes/definitions/SendClassification.definition.js';
+import senderProfile from './metadataTypes/definitions/SenderProfile.definition.js';
 import transactionalEmail from './metadataTypes/definitions/TransactionalEmail.definition.js';
 import transactionalPush from './metadataTypes/definitions/TransactionalPush.definition.js';
 import transactionalSMS from './metadataTypes/definitions/TransactionalSMS.definition.js';
@@ -69,6 +70,7 @@ export default {
     role,
     script,
     sendClassification,
+    senderProfile,
     transactionalEmail,
     transactionalPush,
     transactionalSMS,
diff --git a/lib/MetadataTypeInfo.js b/lib/MetadataTypeInfo.js
index 17ae9d19..b0be83ea 100644
--- a/lib/MetadataTypeInfo.js
+++ b/lib/MetadataTypeInfo.js
@@ -29,6 +29,7 @@ import query from './metadataTypes/Query.js';
 import role from './metadataTypes/Role.js';
 import script from './metadataTypes/Script.js';
 import sendClassification from './metadataTypes/SendClassification.js';
+import senderProfile from './metadataTypes/SenderProfile.js';
 import transactionalEmail from './metadataTypes/TransactionalEmail.js';
 import transactionalPush from './metadataTypes/TransactionalPush.js';
 import transactionalSMS from './metadataTypes/TransactionalSMS.js';
@@ -69,6 +70,7 @@ export default {
     role,
     script,
     sendClassification,
+    senderProfile,
     transactionalEmail,
     transactionalPush,
     transactionalSMS,
diff --git a/lib/metadataTypes/SenderProfile.js b/lib/metadataTypes/SenderProfile.js
new file mode 100644
index 00000000..41ebfedd
--- /dev/null
+++ b/lib/metadataTypes/SenderProfile.js
@@ -0,0 +1,115 @@
+'use strict';
+
+import MetadataType from './MetadataType.js';
+import { Util } from '../util/util.js';
+
+/**
+ * @typedef {import('../../types/mcdev.d.js').BuObject} BuObject
+ * @typedef {import('../../types/mcdev.d.js').CodeExtract} CodeExtract
+ * @typedef {import('../../types/mcdev.d.js').CodeExtractItem} CodeExtractItem
+ * @typedef {import('../../types/mcdev.d.js').MetadataTypeItem} MetadataTypeItem
+ * @typedef {import('../../types/mcdev.d.js').MetadataTypeItemDiff} MetadataTypeItemDiff
+ * @typedef {import('../../types/mcdev.d.js').MetadataTypeItemObj} MetadataTypeItemObj
+ * @typedef {import('../../types/mcdev.d.js').MetadataTypeMap} MetadataTypeMap
+ * @typedef {import('../../types/mcdev.d.js').MetadataTypeMapObj} MetadataTypeMapObj
+ * @typedef {import('../../types/mcdev.d.js').SoapRequestParams} SoapRequestParams
+ * @typedef {import('../../types/mcdev.d.js').TemplateMap} TemplateMap
+ */
+
+/**
+ * SenderProfile MetadataType
+ *
+ * @augments MetadataType
+ */
+class SenderProfile extends MetadataType {
+    /**
+     * Retrieves SOAP based metadata of metadata type into local filesystem. executes callback with retrieved metadata
+     *
+     * @param {string} retrieveDir Directory where retrieved metadata directory will be saved
+     * @param {void} [_] unused parameter
+     * @param {void} [__] unused parameter
+     * @param {void} [___] unused parameter
+     * @param {string} [key] customer key of single item to retrieve
+     * @returns {Promise.<MetadataTypeMapObj>} Promise of metadata
+     */
+    static retrieve(retrieveDir, _, __, ___, key) {
+        /** @type {SoapRequestParams} */
+        let requestParams = null;
+        if (key) {
+            requestParams = {
+                filter: {
+                    leftOperand: 'CustomerKey',
+                    operator: 'equals',
+                    rightOperand: key,
+                },
+            };
+        }
+        return super.retrieveSOAP(retrieveDir, null, requestParams);
+    }
+    /**
+     * Updates a single item
+     *
+     * @param {MetadataTypeItem} metadataItem a single item
+     * @returns {Promise} Promise
+     */
+    static update(metadataItem) {
+        return super.updateSOAP(metadataItem);
+    }
+
+    /**
+     * Creates a single item
+     *
+     * @param {MetadataTypeItem} metadataItem a single item
+     * @returns {Promise} Promise
+     */
+    static create(metadataItem) {
+        return super.createSOAP(metadataItem);
+    }
+
+    /**
+     * Delete a metadata item from the specified business unit
+     *
+     * @param {string} customerKey Identifier of data extension
+     * @returns {Promise.<boolean>} deletion success status
+     */
+    static deleteByKey(customerKey) {
+        return super.deleteByKeySOAP(customerKey);
+    }
+
+    /**
+     * prepares a single item for deployment
+     *
+     * @param {MetadataTypeItem} metadata a single query activity
+     * @returns {Promise.<MetadataTypeItem>} Promise
+     */
+    static async preDeployTasks(metadata) {
+        if (
+            metadata.UseDefaultRMMRules &&
+            (metadata.AutoForwardToEmailAddress !== '' || metadata.AutoForwardToName !== '')
+        ) {
+            Util.logger.warn(
+                ` - ${this.definition.type} ${metadata[this.definition.nameField]} (${
+                    metadata[this.definition.keyField]
+                }): AutoForwardToEmailAddress and AutoForwardToName will be ignored because UseDefaultRMMRules is set to true; setting UseDefaultRMMRules to false`
+            );
+            metadata.UseDefaultRMMRules = false;
+        } else if (
+            !metadata.UseDefaultRMMRules &&
+            (metadata.AutoForwardToEmailAddress === '' || metadata.AutoForwardToName === '')
+        ) {
+            Util.logger.warn(
+                ` - ${this.definition.type} ${metadata[this.definition.nameField]} (${
+                    metadata[this.definition.keyField]
+                }): AutoForwardToEmailAddress and AutoForwardToName are required when UseDefaultRMMRules is set to false; setting UseDefaultRMMRules to true`
+            );
+            metadata.UseDefaultRMMRules = true;
+        }
+        return metadata;
+    }
+}
+
+// Assign definition & cache to static attributes
+import MetadataTypeDefinitions from '../MetadataTypeDefinitions.js';
+SenderProfile.definition = MetadataTypeDefinitions.senderProfile;
+
+export default SenderProfile;
diff --git a/lib/metadataTypes/definitions/SenderProfile.definition.js b/lib/metadataTypes/definitions/SenderProfile.definition.js
new file mode 100644
index 00000000..53957a1e
--- /dev/null
+++ b/lib/metadataTypes/definitions/SenderProfile.definition.js
@@ -0,0 +1,184 @@
+export default {
+    bodyIteratorField: 'Results',
+    dependencies: [],
+    filter: {},
+    hasExtended: false,
+    idField: 'ObjectID',
+    keyIsFixed: false,
+    keyField: 'CustomerKey',
+    nameField: 'Name',
+    createdDateField: 'CreatedDate',
+    createdNameField: null,
+    lastmodDateField: 'ModifiedDate',
+    lastmodNameField: null,
+    restPagination: false,
+    type: 'senderProfile',
+    typeDescription: 'Specifies the From information for a send in a central location',
+    typeRetrieveByDefault: false,
+    typeName: 'Sender Profile',
+    fields: {
+        'Client.ID': {
+            isCreateable: false,
+            isUpdateable: false,
+            retrieving: false,
+            template: false,
+        },
+        CreatedDate: {
+            isCreateable: false,
+            isUpdateable: false,
+            retrieving: true,
+            template: false,
+        },
+        ModifiedDate: {
+            isCreateable: false,
+            isUpdateable: false,
+            retrieving: true,
+            template: false,
+        },
+        CustomerKey: {
+            isCreateable: true,
+            isUpdateable: true,
+            retrieving: true,
+            template: true,
+        },
+        ObjectID: {
+            isCreateable: false,
+            isUpdateable: true,
+            retrieving: true,
+            template: false,
+        },
+        Name: {
+            isCreateable: true,
+            isUpdateable: true,
+            retrieving: true,
+            template: true,
+        },
+        Description: {
+            isCreateable: true,
+            isUpdateable: true,
+            retrieving: true,
+            template: true,
+        },
+        PartnerKey: {
+            isCreateable: false,
+            isUpdateable: false,
+            retrieving: false,
+            template: false,
+        },
+        PartnerProperties: {
+            isCreateable: false,
+            isUpdateable: false,
+            retrieving: false,
+            template: false,
+        },
+
+        AutoForwardToEmailAddress: {
+            isCreateable: true,
+            isUpdateable: true,
+            retrieving: true,
+            template: true,
+        },
+        AutoForwardToName: {
+            isCreateable: true,
+            isUpdateable: true,
+            retrieving: true,
+            template: true,
+        },
+        AutoForwardTriggeredSend: {
+            // not supported
+            isCreateable: false,
+            isUpdateable: false,
+            retrieving: false,
+            template: false,
+        },
+        AutoReply: {
+            isCreateable: true,
+            isUpdateable: true,
+            retrieving: true,
+            template: true,
+        },
+        AutoReplyTriggeredSend: {
+            // not supported
+            isCreateable: false,
+            isUpdateable: false,
+            retrieving: false,
+            template: false,
+        },
+        DataRetentionPeriodLength: {
+            isCreateable: true,
+            isUpdateable: true,
+            retrieving: true,
+            template: true,
+        },
+        DataRetentionPeriodUnitOfMeasure: {
+            isCreateable: true,
+            isUpdateable: true,
+            retrieving: true,
+            template: true,
+        },
+        DirectForward: {
+            isCreateable: true,
+            isUpdateable: true,
+            retrieving: true,
+            template: true,
+        },
+        FromAddress: {
+            isCreateable: true,
+            isUpdateable: true,
+            retrieving: true,
+            template: true,
+        },
+        FromName: {
+            isCreateable: true,
+            isUpdateable: true,
+            retrieving: true,
+            template: true,
+        },
+        ID: {
+            // not supported
+            isCreateable: false,
+            isUpdateable: false,
+            retrieving: false,
+            template: false,
+        },
+        ReplyManagementRuleSet: {
+            // not supported
+            isCreateable: false,
+            isUpdateable: false,
+            retrieving: false,
+            template: false,
+        },
+        ReplyToAddress: {
+            // not supported
+            isCreateable: false,
+            isUpdateable: false,
+            retrieving: false,
+            template: false,
+        },
+        ReplyToDisplayName: {
+            // not supported
+            isCreateable: false,
+            isUpdateable: false,
+            retrieving: false,
+            template: false,
+        },
+        SenderHeaderEmailAddress: {
+            isCreateable: true,
+            isUpdateable: true,
+            retrieving: true,
+            template: true,
+        },
+        SenderHeaderName: {
+            isCreateable: true,
+            isUpdateable: true,
+            retrieving: true,
+            template: true,
+        },
+        UseDefaultRMMRules: {
+            isCreateable: true,
+            isUpdateable: true,
+            retrieving: true,
+            template: true,
+        },
+    },
+};


Test patch:
diff --git a/test/mockRoot/deploy/testInstance/testBU/senderProfile/testExisting_senderProfile.senderProfile-meta.json b/test/mockRoot/deploy/testInstance/testBU/senderProfile/testExisting_senderProfile.senderProfile-meta.json
new file mode 100644
index 00000000..949919fb
--- /dev/null
+++ b/test/mockRoot/deploy/testInstance/testBU/senderProfile/testExisting_senderProfile.senderProfile-meta.json
@@ -0,0 +1,14 @@
+{
+    "CustomerKey": "testExisting_senderProfile",
+    "Name": "testExisting_senderProfile",
+    "Description": "updated via deploy",
+    "FromName": "Jörn Berkefeld",
+    "FromAddress": "joern.berkefeld@accenture.com",
+    "UseDefaultRMMRules": false,
+    "AutoForwardToEmailAddress": "joern.berkefeld@accenture.com",
+    "AutoForwardToName": "Jörn Berkefeld",
+    "DirectForward": false,
+    "AutoReply": false,
+    "SenderHeaderEmailAddress": "",
+    "SenderHeaderName": ""
+}
diff --git a/test/mockRoot/deploy/testInstance/testBU/senderProfile/testNew_senderProfile.senderProfile-meta.json b/test/mockRoot/deploy/testInstance/testBU/senderProfile/testNew_senderProfile.senderProfile-meta.json
new file mode 100644
index 00000000..46f07378
--- /dev/null
+++ b/test/mockRoot/deploy/testInstance/testBU/senderProfile/testNew_senderProfile.senderProfile-meta.json
@@ -0,0 +1,14 @@
+{
+    "CustomerKey": "testNew_senderProfile",
+    "Name": "testNew_senderProfile",
+    "Description": "created on deploy",
+    "FromName": "Jörn Berkefeld",
+    "FromAddress": "joern.berkefeld@accenture.com",
+    "UseDefaultRMMRules": false,
+    "AutoForwardToEmailAddress": "joern.berkefeld@accenture.com",
+    "AutoForwardToName": "Jörn Berkefeld",
+    "DirectForward": false,
+    "AutoReply": false,
+    "SenderHeaderEmailAddress": "",
+    "SenderHeaderName": ""
+}
diff --git a/test/resources/9999999/senderProfile/build-expected.json b/test/resources/9999999/senderProfile/build-expected.json
new file mode 100644
index 00000000..b4e1838f
--- /dev/null
+++ b/test/resources/9999999/senderProfile/build-expected.json
@@ -0,0 +1,14 @@
+{
+    "AutoForwardToEmailAddress": "",
+    "AutoForwardToName": "",
+    "AutoReply": false,
+    "CustomerKey": "testTemplated_senderProfile",
+    "Description": "Send from joern.berkefeld@accenture.com",
+    "DirectForward": false,
+    "FromAddress": "joern.berkefeld@accenture.com",
+    "FromName": "Jörn Berkefeld",
+    "Name": "testTemplated_senderProfile",
+    "SenderHeaderEmailAddress": "",
+    "SenderHeaderName": "",
+    "UseDefaultRMMRules": true
+}
diff --git a/test/resources/9999999/senderProfile/create-response.xml b/test/resources/9999999/senderProfile/create-response.xml
new file mode 100644
index 00000000..24c20420
--- /dev/null
+++ b/test/resources/9999999/senderProfile/create-response.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="utf-8"?>
+<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+    xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing"
+    xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
+    xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
+    <soap:Header>
+        <wsa:Action>CreateResponse</wsa:Action>
+        <wsa:MessageID>urn:uuid:da5fb2ee-2ca9-4f56-a9e8-2b98d43379f7</wsa:MessageID>
+        <wsa:RelatesTo>urn:uuid:3cbd2577-f0e6-4593-8657-e1f48d94624c</wsa:RelatesTo>
+        <wsa:To>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</wsa:To>
+        <wsse:Security>
+            <wsu:Timestamp wsu:Id="Timestamp-8768684a-59c7-44f4-977e-3a4e44db441d">
+                <wsu:Created>2024-04-12T12:53:28Z</wsu:Created>
+                <wsu:Expires>2024-04-12T12:58:28Z</wsu:Expires>
+            </wsu:Timestamp>
+        </wsse:Security>
+    </soap:Header>
+    <soap:Body>
+        <CreateResponse xmlns="http://exacttarget.com/wsdl/partnerAPI">
+            <Results>
+                <StatusCode>OK</StatusCode>
+                <StatusMessage>SenderProfile created</StatusMessage>
+                <OrdinalID>0</OrdinalID>
+                <NewID>0</NewID>
+                <NewObjectID>70accba4-cbf8-ee11-a5c8-5cba2c6fc270</NewObjectID>
+                <Object xsi:type="SenderProfile">
+                    <PartnerKey xsi:nil="true" />
+                    <ObjectID>70accba4-cbf8-ee11-a5c8-5cba2c6fc270</ObjectID>
+                    <CustomerKey>testNew_senderProfile</CustomerKey>
+                    <Name>testNew_senderProfile</Name>
+                    <Description>created on deploy</Description>
+                    <FromName>Jörn Berkefeld</FromName>
+                    <FromAddress>joern.berkefeld@accenture.com</FromAddress>
+                    <UseDefaultRMMRules>false</UseDefaultRMMRules>
+                    <AutoForwardToEmailAddress>joern.berkefeld@accenture.com</AutoForwardToEmailAddress>
+                    <AutoForwardToName>Jörn Berkefeld</AutoForwardToName>
+                    <DirectForward>false</DirectForward>
+                    <AutoReply>false</AutoReply>
+                    <SenderHeaderEmailAddress />
+                    <SenderHeaderName />
+                </Object>
+            </Results>
+            <RequestID>b6be1480-01a4-4e48-9f47-06bf51a301c1</RequestID>
+            <OverallStatus>OK</OverallStatus>
+        </CreateResponse>
+    </soap:Body>
+</soap:Envelope>
diff --git a/test/resources/9999999/senderProfile/delete-response.xml b/test/resources/9999999/senderProfile/delete-response.xml
new file mode 100644
index 00000000..2be19837
--- /dev/null
+++ b/test/resources/9999999/senderProfile/delete-response.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+    xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing"
+    xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
+    xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
+    <soap:Header>
+        <wsa:Action>DeleteResponse</wsa:Action>
+        <wsa:MessageID>urn:uuid:9d089538-9132-421c-b905-827dc760447b</wsa:MessageID>
+        <wsa:RelatesTo>urn:uuid:2dbd051c-b714-4302-bb66-53d0916620ed</wsa:RelatesTo>
+        <wsa:To>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</wsa:To>
+        <wsse:Security>
+            <wsu:Timestamp wsu:Id="Timestamp-c7ba7ffe-ac41-4cb7-a0a1-471df321d8a6">
+                <wsu:Created>2024-04-12T13:05:55Z</wsu:Created>
+                <wsu:Expires>2024-04-12T13:10:55Z</wsu:Expires>
+            </wsu:Timestamp>
+        </wsse:Security>
+    </soap:Header>
+    <soap:Body>
+        <DeleteResponse xmlns="http://exacttarget.com/wsdl/partnerAPI">
+            <Results>
+                <StatusCode>OK</StatusCode>
+                <StatusMessage>SenderProfile deleted</StatusMessage>
+                <OrdinalID>0</OrdinalID>
+                <Object xsi:type="SenderProfile">
+                    <PartnerKey xsi:nil="true" />
+                    <ObjectID xsi:nil="true" />
+                    <CustomerKey>testExisting_senderProfile</CustomerKey>
+                </Object>
+            </Results>
+            <RequestID>0387ba17-c330-4239-a2dd-c0e15f968476</RequestID>
+            <OverallStatus>OK</OverallStatus>
+        </DeleteResponse>
+    </soap:Body>
+</soap:Envelope>
diff --git a/test/resources/9999999/senderProfile/get-expected.json b/test/resources/9999999/senderProfile/get-expected.json
new file mode 100644
index 00000000..e92fcba4
--- /dev/null
+++ b/test/resources/9999999/senderProfile/get-expected.json
@@ -0,0 +1,16 @@
+{
+    "CreatedDate": "2021-08-03T11:14:00",
+    "ModifiedDate": "2021-08-03T11:14:00",
+    "CustomerKey": "testExisting_senderProfile",
+    "Name": "testExisting_senderProfile",
+    "Description": "Send from joern.berkefeld@accenture.com",
+    "FromName": "Jörn Berkefeld",
+    "FromAddress": "joern.berkefeld@accenture.com",
+    "UseDefaultRMMRules": true,
+    "AutoForwardToEmailAddress": "",
+    "AutoForwardToName": "",
+    "DirectForward": false,
+    "AutoReply": false,
+    "SenderHeaderEmailAddress": "",
+    "SenderHeaderName": ""
+}
diff --git a/test/resources/9999999/senderProfile/patch-expected.json b/test/resources/9999999/senderProfile/patch-expected.json
new file mode 100644
index 00000000..c82bbdec
--- /dev/null
+++ b/test/resources/9999999/senderProfile/patch-expected.json
@@ -0,0 +1,14 @@
+{
+    "AutoForwardToEmailAddress": "joern.berkefeld@accenture.com",
+    "AutoForwardToName": "Jörn Berkefeld",
+    "AutoReply": false,
+    "CustomerKey": "testExisting_senderProfile",
+    "Description": "updated via deploy",
+    "DirectForward": false,
+    "FromAddress": "joern.berkefeld@accenture.com",
+    "FromName": "Jörn Berkefeld",
+    "Name": "testExisting_senderProfile",
+    "SenderHeaderEmailAddress": "",
+    "SenderHeaderName": "",
+    "UseDefaultRMMRules": false
+}
diff --git a/test/resources/9999999/senderProfile/post-expected.json b/test/resources/9999999/senderProfile/post-expected.json
new file mode 100644
index 00000000..8286574c
--- /dev/null
+++ b/test/resources/9999999/senderProfile/post-expected.json
@@ -0,0 +1,14 @@
+{
+    "AutoForwardToEmailAddress": "joern.berkefeld@accenture.com",
+    "AutoForwardToName": "Jörn Berkefeld",
+    "AutoReply": false,
+    "CustomerKey": "testNew_senderProfile",
+    "Description": "created on deploy",
+    "DirectForward": false,
+    "FromAddress": "joern.berkefeld@accenture.com",
+    "FromName": "Jörn Berkefeld",
+    "Name": "testNew_senderProfile",
+    "SenderHeaderEmailAddress": "",
+    "SenderHeaderName": "",
+    "UseDefaultRMMRules": false
+}
diff --git a/test/resources/9999999/senderProfile/retrieve-response.xml b/test/resources/9999999/senderProfile/retrieve-response.xml
new file mode 100644
index 00000000..8ed9de26
--- /dev/null
+++ b/test/resources/9999999/senderProfile/retrieve-response.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+    xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing"
+    xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
+    xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
+    <soap:Header>
+        <wsa:Action>RetrieveResponse</wsa:Action>
+        <wsa:MessageID>urn:uuid:cbecb68a-7bdf-417e-8fce-bef6c72febbd</wsa:MessageID>
+        <wsa:RelatesTo>urn:uuid:2e88f8d2-82e7-4e2d-b780-b885a2205a0f</wsa:RelatesTo>
+        <wsa:To>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</wsa:To>
+        <wsse:Security>
+            <wsu:Timestamp wsu:Id="Timestamp-ed420673-a71e-47b4-a2f4-4d4399b7e6a9">
+                <wsu:Created>2024-04-12T12:20:37Z</wsu:Created>
+                <wsu:Expires>2024-04-12T12:25:37Z</wsu:Expires>
+            </wsu:Timestamp>
+        </wsse:Security>
+    </soap:Header>
+    <soap:Body>
+        <RetrieveResponseMsg xmlns="http://exacttarget.com/wsdl/partnerAPI">
+            <OverallStatus>OK</OverallStatus>
+            <RequestID>4c6f37a1-7ab4-47a1-b341-f9912462ee97</RequestID>
+            <Results xsi:type="SenderProfile">
+                <PartnerKey xsi:nil="true" />
+                <CreatedDate>2021-08-03T11:14:00</CreatedDate>
+                <ModifiedDate>2021-08-03T11:14:00</ModifiedDate>
+                <ObjectID>a75d452b-7ef4-eb11-b82d-48df37d1da95</ObjectID>
+                <CustomerKey>testExisting_senderProfile</CustomerKey>
+                <Name>testExisting_senderProfile</Name>
+                <Description>Send from joern.berkefeld@accenture.com</Description>
+                <FromName>Jörn Berkefeld</FromName>
+                <FromAddress>joern.berkefeld@accenture.com</FromAddress>
+                <UseDefaultRMMRules>true</UseDefaultRMMRules>
+                <AutoForwardToEmailAddress />
+                <AutoForwardToName />
+                <DirectForward>false</DirectForward>
+                <AutoReply>false</AutoReply>
+                <SenderHeaderEmailAddress />
+                <SenderHeaderName />
+            </Results>
+        </RetrieveResponseMsg>
+    </soap:Body>
+</soap:Envelope>
diff --git a/test/resources/9999999/senderProfile/template-expected.json b/test/resources/9999999/senderProfile/template-expected.json
new file mode 100644
index 00000000..2dee8a07
--- /dev/null
+++ b/test/resources/9999999/senderProfile/template-expected.json
@@ -0,0 +1,14 @@
+{
+    "AutoForwardToEmailAddress": "",
+    "AutoForwardToName": "",
+    "AutoReply": false,
+    "CustomerKey": "{{{prefix}}}senderProfile",
+    "Description": "Send from joern.berkefeld@accenture.com",
+    "DirectForward": false,
+    "FromAddress": "joern.berkefeld@accenture.com",
+    "FromName": "Jörn Berkefeld",
+    "Name": "{{{prefix}}}senderProfile",
+    "SenderHeaderEmailAddress": "",
+    "SenderHeaderName": "",
+    "UseDefaultRMMRules": true
+}
diff --git a/test/resources/9999999/senderProfile/update-response.xml b/test/resources/9999999/senderProfile/update-response.xml
new file mode 100644
index 00000000..f2a4fa26
--- /dev/null
+++ b/test/resources/9999999/senderProfile/update-response.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+    xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing"
+    xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
+    xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
+    <soap:Header>
+        <wsa:Action>UpdateResponse</wsa:Action>
+        <wsa:MessageID>urn:uuid:4ba5314f-13c0-4fcf-a193-f6160531acd6</wsa:MessageID>
+        <wsa:RelatesTo>urn:uuid:4f56d229-63e7-4d8b-9bfe-1ace0c596056</wsa:RelatesTo>
+        <wsa:To>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</wsa:To>
+        <wsse:Security>
+            <wsu:Timestamp wsu:Id="Timestamp-224bd293-15f1-40fb-8a6f-f4ed6e2d6b16">
+                <wsu:Created>2022-04-19T20:03:41Z</wsu:Created>
+                <wsu:Expires>2024-04-12T12:58:29Z</wsu:Expires>
+            </wsu:Timestamp>
+        </wsse:Security>
+    </soap:Header>
+    <soap:Body>
+        <UpdateResponse xmlns="http://exacttarget.com/wsdl/partnerAPI">
+            <Results>
+                <StatusCode>OK</StatusCode>
+                <StatusMessage>SenderProfile updated</StatusMessage>
+                <OrdinalID>0</OrdinalID>
+                <Object xsi:type="SenderProfile">
+                    <PartnerKey xsi:nil="true" />
+                    <ObjectID xsi:nil="true" />
+                    <CustomerKey>testExisting_senderProfile</CustomerKey>
+                    <Name>testExisting_senderProfile</Name>
+                    <Description>updated via deploy</Description>
+                    <FromName>Jörn Berkefeld</FromName>
+                    <FromAddress>joern.berkefeld@accenture.com</FromAddress>
+                    <UseDefaultRMMRules>false</UseDefaultRMMRules>
+                    <AutoForwardToEmailAddress>joern.berkefeld@accenture.com</AutoForwardToEmailAddress>
+                    <AutoForwardToName>Jörn Berkefeld</AutoForwardToName>
+                    <DirectForward>false</DirectForward>
+                    <AutoReply>false</AutoReply>
+                    <SenderHeaderEmailAddress />
+                    <SenderHeaderName />
+                </Object>
+            </Results>
+            <RequestID>ef6e2933-b767-47ea-a1c4-d3dc44674a56</RequestID>
+            <OverallStatus>OK</OverallStatus>
+        </UpdateResponse>
+    </soap:Body>
+</soap:Envelope>
diff --git a/test/type.senderProfile.test.js b/test/type.senderProfile.test.js
new file mode 100644
index 00000000..64cbfa33
--- /dev/null
+++ b/test/type.senderProfile.test.js
@@ -0,0 +1,151 @@
+import * as chai from 'chai';
+const assert = chai.assert;
+
+import chaiFiles from 'chai-files';
+import cache from '../lib/util/cache.js';
+import * as testUtils from './utils.js';
+import handler from '../lib/index.js';
+chai.use(chaiFiles);
+
+describe('type: senderProfile', () => {
+    beforeEach(() => {
+        testUtils.mockSetup();
+    });
+
+    afterEach(() => {
+        testUtils.mockReset();
+    });
+
+    describe('Retrieve ================', () => {
+        it('Should retrieve a senderProfile', async () => {
+            // WHEN
+            await handler.retrieve('testInstance/testBU', ['senderProfile']);
+            // THEN
+            assert.equal(process.exitCode, 0, 'retrieve should not have thrown an error');
+            // get results from cache
+            const result = cache.getCache();
+            assert.equal(
+                result.senderProfile ? Object.keys(result.senderProfile).length : 0,
+                1,
+                'only one senderProfile expected'
+            );
+            assert.deepEqual(
+                await testUtils.getActualJson('testExisting_senderProfile', 'senderProfile'),
+                await testUtils.getExpectedJson('9999999', 'senderProfile', 'get'),
+                'returned JSON was not equal expected'
+            );
+            assert.equal(
+                testUtils.getAPIHistoryLength(),
+                1,
+                'Unexpected number of requests made. Run testUtils.logAPIHistoryDebug() to see the requests'
+            );
+            return;
+        });
+    });
+
+    describe('Deploy ================', () => {
+        beforeEach(() => {
+            testUtils.mockSetup(true);
+        });
+
+        it('Should create & upsert a senderProfile', async () => {
+            // WHEN
+
+            await handler.deploy('testInstance/testBU', ['senderProfile']);
+            // THEN
+            assert.equal(process.exitCode, 0, 'deploy should not have thrown an error');
+            // get results from cache
+            const result = cache.getCache();
+            assert.equal(
+                result.senderProfile ? Object.keys(result.senderProfile).length : 0,
+                2,
+                'two senderProfiles expected'
+            );
+            // confirm created item
+            assert.deepEqual(
+                await testUtils.getActualJson('testNew_senderProfile', 'senderProfile'),
+                await testUtils.getExpectedJson('9999999', 'senderProfile', 'post'),
+                'returned new-JSON was not equal expected for insert senderProfile'
+            );
+            // confirm updated item
+            assert.deepEqual(
+                await testUtils.getActualJson('testExisting_senderProfile', 'senderProfile'),
+                await testUtils.getExpectedJson('9999999', 'senderProfile', 'patch'),
+                'returned existing-JSON was not equal expected for update senderProfile'
+            );
+            // check number of API calls
+            assert.equal(
+                testUtils.getAPIHistoryLength(),
+                3,
+                'Unexpected number of requests made. Run testUtils.logAPIHistoryDebug() to see the requests'
+            );
+            return;
+        });
+    });
+
+    describe('Templating ================', () => {
+        it('Should create a senderProfile template via buildTemplate and build it', async () => {
+            // download first before we test buildTemplate
+            await handler.retrieve('testInstance/testBU', ['senderProfile']);
+            // buildTemplate
+            const result = await handler.buildTemplate(
+                'testInstance/testBU',
+                'senderProfile',
+                ['testExisting_senderProfile'],
+                'testSourceMarket'
+            );
+            assert.equal(process.exitCode, 0, 'buildTemplate should not have thrown an error');
+            assert.equal(
+                result.senderProfile ? Object.keys(result.senderProfile).length : 0,
+                1,
+                'only one senderProfile expected'
+            );
+            assert.deepEqual(
+                await testUtils.getActualTemplateJson(
+                    'testExisting_senderProfile',
+                    'senderProfile'
+                ),
+                await testUtils.getExpectedJson('9999999', 'senderProfile', 'template'),
+                'returned template JSON was not equal expected'
+            );
+            // buildDefinition
+            await handler.buildDefinition(
+                'testInstance/testBU',
+                'senderProfile',
+                'testExisting_senderProfile',
+                'testTargetMarket'
+            );
+            assert.equal(
+                process.exitCode,
+                false,
+                'buildDefinition should not have thrown an error'
+            );
+            assert.deepEqual(
+                await testUtils.getActualDeployJson('testTemplated_senderProfile', 'senderProfile'),
+                await testUtils.getExpectedJson('9999999', 'senderProfile', 'build'),
+                'returned deployment JSON was not equal expected'
+            );
+            assert.equal(
+                testUtils.getAPIHistoryLength(),
+                1,
+                'Unexpected number of requests made. Run testUtils.logAPIHistoryDebug() to see the requests'
+            );
+            return;
+        });
+    });
+
+    describe('Delete ================', () => {
+        it('Should delete the item', async () => {
+            // WHEN
+            const isDeleted = await handler.deleteByKey(
+                'testInstance/testBU',
+                'senderProfile',
+                'testExisting_senderProfile'
+            );
+            // THEN
+            assert.equal(process.exitCode, 0, 'deleteByKey should not have thrown an error');
+            assert.equal(isDeleted, true, 'deleteByKey should have returned true');
+            return;
+        });
+    });
+});