diff --git a/airbyte-integrations/connectors/source-facebook-marketing/acceptance-test-config.yml b/airbyte-integrations/connectors/source-facebook-marketing/acceptance-test-config.yml
index dae55df93f18f..c7cdf62e8bb7e 100644
--- a/airbyte-integrations/connectors/source-facebook-marketing/acceptance-test-config.yml
+++ b/airbyte-integrations/connectors/source-facebook-marketing/acceptance-test-config.yml
@@ -6,9 +6,10 @@ acceptance_tests:
spec:
tests:
- spec_path: "integration_tests/spec.json"
+ # the source now supports 2 auth types, thus we should skip the check,
+ # this change is intentional
backward_compatibility_tests_config:
- disable_for_version: "1.2.2"
- previous_connector_version: "1.2.1"
+ disable_for_version: "3.1.0"
connection:
tests:
- config_path: "secrets/config.json"
diff --git a/airbyte-integrations/connectors/source-facebook-marketing/integration_tests/spec.json b/airbyte-integrations/connectors/source-facebook-marketing/integration_tests/spec.json
index 4566a1a627e7e..7e56eab74cf6d 100644
--- a/airbyte-integrations/connectors/source-facebook-marketing/integration_tests/spec.json
+++ b/airbyte-integrations/connectors/source-facebook-marketing/integration_tests/spec.json
@@ -26,6 +26,72 @@
"airbyte_secret": true,
"type": "string"
},
+ "credentials": {
+ "title": "Authentication",
+ "description": "Credentials for connecting to the Facebook Marketing API",
+ "type": "object",
+ "discriminator": {
+ "propertyName": "auth_type",
+ "mapping": {
+ "Client": "#/definitions/OAuthCredentials",
+ "Service": "#/definitions/ServiceAccountCredentials"
+ }
+ },
+ "oneOf": [
+ {
+ "title": "Authenticate via Facebook Marketing (Oauth)",
+ "type": "object",
+ "properties": {
+ "auth_type": {
+ "title": "Auth Type",
+ "default": "Client",
+ "const": "Client",
+ "enum": ["Client"],
+ "type": "string"
+ },
+ "client_id": {
+ "title": "Client ID",
+ "description": "Client ID for the Facebook Marketing API",
+ "airbyte_secret": true,
+ "type": "string"
+ },
+ "client_secret": {
+ "title": "Client Secret",
+ "description": "Client Secret for the Facebook Marketing API",
+ "airbyte_secret": true,
+ "type": "string"
+ },
+ "access_token": {
+ "title": "Access Token",
+ "description": "The value of the generated access token. From your App\u2019s Dashboard, click on \"Marketing API\" then \"Tools\". Select permissions ads_management, ads_read, read_insights, business_management. Then click on \"Get token\". See the docs for more information.",
+ "airbyte_secret": true,
+ "type": "string"
+ }
+ },
+ "required": ["client_id", "client_secret", "auth_type"]
+ },
+ {
+ "title": "Service Account Key Authentication",
+ "type": "object",
+ "properties": {
+ "auth_type": {
+ "title": "Auth Type",
+ "default": "Service",
+ "const": "Service",
+ "enum": ["Service"],
+ "type": "string"
+ },
+ "access_token": {
+ "title": "Access Token",
+ "description": "The value of the generated access token. From your App\u2019s Dashboard, click on \"Marketing API\" then \"Tools\". Select permissions ads_management, ads_read, read_insights, business_management. Then click on \"Get token\". See the docs for more information.",
+ "airbyte_secret": true,
+ "type": "string"
+ }
+ },
+ "required": ["access_token", "auth_type"]
+ }
+ ]
+ },
"start_date": {
"title": "Start Date",
"description": "The date from which you'd like to replicate data for all incremental streams, in the format YYYY-MM-DDT00:00:00Z. If not set then all data will be replicated for usual streams and only last 2 years for insight streams.",
@@ -462,19 +528,21 @@
"type": "string"
}
},
- "required": ["account_ids", "access_token"]
+ "required": ["account_ids"]
},
"supportsIncremental": true,
"supported_destination_sync_modes": ["append"],
"advanced_auth": {
"auth_flow_type": "oauth2.0",
+ "predicate_key": ["credentials", "auth_type"],
+ "predicate_value": "Client",
"oauth_config_specification": {
"complete_oauth_output_specification": {
"type": "object",
"properties": {
"access_token": {
"type": "string",
- "path_in_connector_config": ["access_token"]
+ "path_in_connector_config": ["credentials", "access_token"]
}
}
},
@@ -495,11 +563,11 @@
"properties": {
"client_id": {
"type": "string",
- "path_in_connector_config": ["client_id"]
+ "path_in_connector_config": ["credentials", "client_id"]
},
"client_secret": {
"type": "string",
- "path_in_connector_config": ["client_secret"]
+ "path_in_connector_config": ["credentials", "client_secret"]
}
}
}
diff --git a/airbyte-integrations/connectors/source-facebook-marketing/metadata.yaml b/airbyte-integrations/connectors/source-facebook-marketing/metadata.yaml
index 432dda39f892b..af996f249492a 100644
--- a/airbyte-integrations/connectors/source-facebook-marketing/metadata.yaml
+++ b/airbyte-integrations/connectors/source-facebook-marketing/metadata.yaml
@@ -10,7 +10,7 @@ data:
connectorSubtype: api
connectorType: source
definitionId: e7778cfc-e97c-4458-9ecb-b4f2bba8946c
- dockerImageTag: 3.1.0
+ dockerImageTag: 3.2.0
dockerRepository: airbyte/source-facebook-marketing
documentationUrl: https://docs.airbyte.com/integrations/sources/facebook-marketing
githubIssueLabel: source-facebook-marketing
diff --git a/airbyte-integrations/connectors/source-facebook-marketing/pyproject.toml b/airbyte-integrations/connectors/source-facebook-marketing/pyproject.toml
index 68df9dfb0b75e..fb99288fdc3ca 100644
--- a/airbyte-integrations/connectors/source-facebook-marketing/pyproject.toml
+++ b/airbyte-integrations/connectors/source-facebook-marketing/pyproject.toml
@@ -3,7 +3,7 @@ requires = [ "poetry-core>=1.0.0",]
build-backend = "poetry.core.masonry.api"
[tool.poetry]
-version = "3.1.0"
+version = "3.2.0"
name = "source-facebook-marketing"
description = "Source implementation for Facebook Marketing."
authors = [ "Airbyte ",]
diff --git a/airbyte-integrations/connectors/source-facebook-marketing/sample_files/sample_config.json b/airbyte-integrations/connectors/source-facebook-marketing/sample_files/sample_config.json
index f69ba65fd2672..2ca7d6392db08 100644
--- a/airbyte-integrations/connectors/source-facebook-marketing/sample_files/sample_config.json
+++ b/airbyte-integrations/connectors/source-facebook-marketing/sample_files/sample_config.json
@@ -2,5 +2,8 @@
"start_date": "2020-09-25T00:00:00Z",
"end_date": "2021-01-01T00:00:00Z",
"account_id": "",
- "access_token": ""
+ "credentials": {
+ "auth_type": "Service",
+ "access_token": ""
+ }
}
diff --git a/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/config_migrations.py b/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/config_migrations.py
index 8ca9289c97e28..67d54a939428a 100644
--- a/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/config_migrations.py
+++ b/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/config_migrations.py
@@ -121,3 +121,73 @@ def transform(cls, config: Mapping[str, Any]) -> Mapping[str, Any]:
config[stream_filter] = statuses
# return transformed config
return config
+
+
+class MigrateSecretsPathInConnector:
+ """
+ This class stands for migrating the config at runtime.
+ This migration is intended for backwards compatibility with the previous version, so existing secrets configurations gets migrated to new path.
+
+ Starting from `2.2.0`, the `client_id`, `client_secret` and `access_token` will be placed at `credentials` path.
+ """
+
+ @classmethod
+ def _should_migrate(cls, config: Mapping[str, Any]) -> bool:
+ """
+ This method determines whether the config should be migrated to nest existing fields at credentials.
+ It is assumed if credentials does not exist on configuration, `client_id`, `client_secret` and `access_token` exists on root path.
+ Returns:
+ > True, if the migration is necessary
+ > False, otherwise.
+ """
+ return "access_token" in config or "client_id" in config or "client_secret" in config
+
+ @classmethod
+ def migrate(cls, args: List[str], source: Source) -> None:
+ """
+ This method checks the input args, should the config be migrated,
+ transform if neccessary and emit the CONTROL message.
+ """
+ # get config path
+ config_path = AirbyteEntrypoint(source).extract_config(args)
+ # proceed only if `--config` arg is provided
+ if config_path:
+ # read the existing config
+ config = source.read_config(config_path)
+ # migration check
+ if cls._should_migrate(config):
+ cls._emit_control_message(
+ cls._modify_and_save(config_path, source, config),
+ )
+
+ @classmethod
+ def _transform(cls, config: Mapping[str, Any]) -> Mapping[str, Any]:
+ # transform the config
+ if "credentials" not in config:
+ config["credentials"] = {
+ "auth_type": "Service",
+ }
+ if "access_token" in config:
+ config["credentials"]["access_token"] = config.pop("access_token")
+ if "client_id" in config:
+ config["credentials"]["auth_type"] = "Client"
+ config["credentials"]["client_id"] = config.pop("client_id")
+ if "client_secret" in config:
+ config["credentials"]["auth_type"] = "Client"
+ config["credentials"]["client_secret"] = config.pop("client_secret")
+ # return transformed config
+ return config
+
+ @classmethod
+ def _modify_and_save(cls, config_path: str, source: Source, config: Mapping[str, Any]) -> Mapping[str, Any]:
+ # modify the config
+ migrated_config = cls._transform(config)
+ # save the config
+ source.write_config(migrated_config, config_path)
+ # return modified config
+ return migrated_config
+
+ @classmethod
+ def _emit_control_message(cls, migrated_config: Mapping[str, Any]) -> None:
+ # add the Airbyte Control Message to message repo
+ print(create_connector_config_control_message(migrated_config).json(exclude_unset=True))
diff --git a/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/run.py b/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/run.py
index b070561488f09..ecb5dcd5cbad6 100644
--- a/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/run.py
+++ b/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/run.py
@@ -7,7 +7,7 @@
from airbyte_cdk.entrypoint import launch
-from .config_migrations import MigrateAccountIdToArray, MigrateIncludeDeletedToStatusFilters
+from .config_migrations import MigrateAccountIdToArray, MigrateIncludeDeletedToStatusFilters, MigrateSecretsPathInConnector
from .source import SourceFacebookMarketing
@@ -15,4 +15,5 @@ def run():
source = SourceFacebookMarketing()
MigrateAccountIdToArray.migrate(sys.argv[1:], source)
MigrateIncludeDeletedToStatusFilters.migrate(sys.argv[1:], source)
+ MigrateSecretsPathInConnector.migrate(sys.argv[1:], source)
launch(source, sys.argv[1:])
diff --git a/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/source.py b/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/source.py
index 0e4744be48f29..811d3c41b84ac 100644
--- a/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/source.py
+++ b/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/source.py
@@ -95,7 +95,10 @@ def check_connection(self, logger: logging.Logger, config: Mapping[str, Any]) ->
if config.start_date and config.end_date < config.start_date:
return False, "End date must be equal or after start date."
- api = API(access_token=config.access_token, page_size=config.page_size)
+ if config.credentials is not None:
+ api = API(access_token=config.credentials.access_token, page_size=config.page_size)
+ else:
+ api = API(access_token=config.access_token, page_size=config.page_size)
for account_id in config.account_ids:
# Get Ad Account to check creds
@@ -129,7 +132,10 @@ def streams(self, config: Mapping[str, Any]) -> List[Type[Stream]]:
config.start_date = validate_start_date(config.start_date)
config.end_date = validate_end_date(config.start_date, config.end_date)
- api = API(access_token=config.access_token, page_size=config.page_size)
+ if config.credentials is not None:
+ api = API(access_token=config.credentials.access_token, page_size=config.page_size)
+ else:
+ api = API(access_token=config.access_token, page_size=config.page_size)
# if start_date not specified then set default start_date for report streams to 2 years ago
report_start_date = config.start_date or pendulum.now().add(years=-2)
@@ -242,14 +248,16 @@ def spec(self, *args, **kwargs) -> ConnectorSpecification:
connectionSpecification=ConnectorConfig.schema(),
advanced_auth=AdvancedAuth(
auth_flow_type=AuthFlowType.oauth2_0,
+ predicate_key=["credentials", "auth_type"],
+ predicate_value="Client",
oauth_config_specification=OAuthConfigSpecification(
complete_oauth_output_specification={
"type": "object",
"properties": {
"access_token": {
"type": "string",
- "path_in_connector_config": ["access_token"],
- }
+ "path_in_connector_config": ["credentials", "access_token"],
+ },
},
},
complete_oauth_server_input_specification={
@@ -265,11 +273,11 @@ def spec(self, *args, **kwargs) -> ConnectorSpecification:
"properties": {
"client_id": {
"type": "string",
- "path_in_connector_config": ["client_id"],
+ "path_in_connector_config": ["credentials", "client_id"],
},
"client_secret": {
"type": "string",
- "path_in_connector_config": ["client_secret"],
+ "path_in_connector_config": ["credentials", "client_secret"],
},
},
},
diff --git a/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/spec.py b/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/spec.py
index 470312b4db04b..a578830ab3ad9 100644
--- a/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/spec.py
+++ b/airbyte-integrations/connectors/source-facebook-marketing/source_facebook_marketing/spec.py
@@ -4,9 +4,10 @@
import logging
from datetime import datetime, timezone
from enum import Enum
-from typing import List, Optional, Set
+from typing import List, Literal, Optional, Set, Union
from airbyte_cdk.sources.config import BaseConfig
+from airbyte_cdk.utils.oneof_option_config import OneOfOptionConfig
from facebook_business.adobjects.ad import Ad
from facebook_business.adobjects.adset import AdSet
from facebook_business.adobjects.adsinsights import AdsInsights
@@ -31,6 +32,48 @@
EMPTY_PATTERN = "^$"
+class OAuthCredentials(BaseModel):
+ class Config(OneOfOptionConfig):
+ title = "Authenticate via Facebook Marketing (Oauth)"
+ discriminator = "auth_type"
+
+ auth_type: Literal["Client"] = Field("Client", const=True)
+ client_id: str = Field(
+ title="Client ID",
+ description="Client ID for the Facebook Marketing API",
+ airbyte_secret=True,
+ )
+ client_secret: str = Field(
+ title="Client Secret",
+ description="Client Secret for the Facebook Marketing API",
+ airbyte_secret=True,
+ )
+ access_token: Optional[str] = Field(
+ title="Access Token",
+ description="The value of the generated access token. "
+ 'From your App’s Dashboard, click on "Marketing API" then "Tools". '
+ 'Select permissions ads_management, ads_read, read_insights, business_management. Then click on "Get token". '
+ 'See the docs for more information.',
+ airbyte_secret=True,
+ )
+
+
+class ServiceAccountCredentials(BaseModel):
+ class Config(OneOfOptionConfig):
+ title = "Service Account Key Authentication"
+ discriminator = "auth_type"
+
+ auth_type: Literal["Service"] = Field("Service", const=True)
+ access_token: str = Field(
+ title="Access Token",
+ description="The value of the generated access token. "
+ 'From your App’s Dashboard, click on "Marketing API" then "Tools". '
+ 'Select permissions ads_management, ads_read, read_insights, business_management. Then click on "Get token". '
+ 'See the docs for more information.',
+ airbyte_secret=True,
+ )
+
+
class InsightConfig(BaseModel):
"""Config for custom insights"""
@@ -142,7 +185,7 @@ class Config:
min_items=1,
)
- access_token: str = Field(
+ access_token: Optional[str] = Field(
title="Access Token",
order=1,
description=(
@@ -154,6 +197,13 @@ class Config:
airbyte_secret=True,
)
+ credentials: Optional[Union[OAuthCredentials, ServiceAccountCredentials]] = Field(
+ title="Authentication",
+ description="Credentials for connecting to the Facebook Marketing API",
+ discriminator="auth_type",
+ type="object",
+ )
+
start_date: Optional[datetime] = Field(
title="Start Date",
order=2,
diff --git a/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/integration/config.py b/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/integration/config.py
index c8e65f5085b0a..1e76b6403a501 100644
--- a/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/integration/config.py
+++ b/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/integration/config.py
@@ -26,6 +26,10 @@ def __init__(self) -> None:
self._config: MutableMapping[str, Any] = {
"account_ids": [ACCOUNT_ID],
"access_token": ACCESS_TOKEN,
+ "credentials": {
+ "auth_type": "Service",
+ "access_token": ACCESS_TOKEN,
+ },
"start_date": START_DATE,
"end_date": END_DATE,
"include_deleted": True,
diff --git a/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_config_migrations.py b/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_config_migrations.py
index 759eb6e8f8485..54bb48f02a1b9 100644
--- a/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_config_migrations.py
+++ b/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_config_migrations.py
@@ -10,7 +10,11 @@
import pytest
from airbyte_cdk.models import OrchestratorType, Type
from airbyte_cdk.sources import Source
-from source_facebook_marketing.config_migrations import MigrateAccountIdToArray, MigrateIncludeDeletedToStatusFilters
+from source_facebook_marketing.config_migrations import (
+ MigrateAccountIdToArray,
+ MigrateIncludeDeletedToStatusFilters,
+ MigrateSecretsPathInConnector,
+)
from source_facebook_marketing.source import SourceFacebookMarketing
# BASE ARGS
@@ -19,6 +23,7 @@
_EXCLUDE_DELETE_CONFIGS_PATH = "test_migrations/include_deleted_to_status_filters/include_deleted_false"
_INCLUDE_DELETE_CONFIGS_PATH = "test_migrations/include_deleted_to_status_filters/include_deleted_true"
_ACCOUNT_ID_TO_ARRAY_CONFIGS_PATH = "test_migrations/account_id_to_array"
+_SECRETS_TO_CREDENTIALS_CONFIGS_PATH = "test_migrations/secrets_to_credentials"
def load_config(config_path: str) -> Mapping[str, Any]:
@@ -162,3 +167,69 @@ def test_should_not_migrate_upgraded_config(self):
new_config = load_config(self.UPGRADED_TEST_CONFIG_PATH)
migration_instance = MigrateIncludeDeletedToStatusFilters()
assert not migration_instance.should_migrate(new_config)
+
+class TestMigrateSecretsPathInConnector:
+ OLD_TEST_CONFIG_PATH_ACCESS_TOKEN = _config_path(f"{_SECRETS_TO_CREDENTIALS_CONFIGS_PATH}/test_old_access_token_config.json")
+ NEW_TEST_CONFIG_PATH_ACCESS_TOKEN = _config_path(f"{_SECRETS_TO_CREDENTIALS_CONFIGS_PATH}/test_new_access_token_config.json")
+ OLD_TEST_CONFIG_PATH_CLIENT = _config_path(f"{_SECRETS_TO_CREDENTIALS_CONFIGS_PATH}/test_old_client_config.json")
+ NEW_TEST_CONFIG_PATH_CLIENT = _config_path(f"{_SECRETS_TO_CREDENTIALS_CONFIGS_PATH}/test_new_client_config.json")
+
+ @staticmethod
+ def revert_migration(config_path: str) -> None:
+ with open(config_path, "r") as test_config:
+ config = json.load(test_config)
+ credentials = config.pop("credentials",{})
+ credentials.pop("auth_type", None)
+ with open(config_path, "w") as updated_config:
+ config = json.dumps({**config, **credentials})
+ updated_config.write(config)
+
+ def test_migrate_access_token_config(self):
+ migration_instance = MigrateSecretsPathInConnector()
+ original_config = load_config(self.OLD_TEST_CONFIG_PATH_ACCESS_TOKEN)
+ # migrate the test_config
+ migration_instance.migrate([CMD, "--config", self.OLD_TEST_CONFIG_PATH_ACCESS_TOKEN], SOURCE)
+ # load the updated config
+ test_migrated_config = load_config(self.OLD_TEST_CONFIG_PATH_ACCESS_TOKEN)
+ # check migrated property
+ assert "credentials" in test_migrated_config
+ assert isinstance(test_migrated_config["credentials"], dict)
+ credentials = test_migrated_config["credentials"]
+ assert "access_token" in credentials
+ # check the migration should be skipped, once already done
+ assert not migration_instance._should_migrate(test_migrated_config)
+ # load the old custom reports VS migrated
+ assert original_config["access_token"] == credentials["access_token"]
+ # revert the test_config to the starting point
+ self.revert_migration(self.OLD_TEST_CONFIG_PATH_ACCESS_TOKEN)
+
+ def test_migrate_client_config(self):
+ migration_instance = MigrateSecretsPathInConnector()
+ original_config = load_config(self.OLD_TEST_CONFIG_PATH_CLIENT)
+ # migrate the test_config
+ migration_instance.migrate([CMD, "--config", self.OLD_TEST_CONFIG_PATH_CLIENT], SOURCE)
+ # load the updated config
+ test_migrated_config = load_config(self.OLD_TEST_CONFIG_PATH_CLIENT)
+ # check migrated property
+ assert "credentials" in test_migrated_config
+ assert isinstance(test_migrated_config["credentials"], dict)
+ credentials = test_migrated_config["credentials"]
+ assert "client_id" in credentials
+ assert "client_secret" in credentials
+ # check the migration should be skipped, once already done
+ assert not migration_instance._should_migrate(test_migrated_config)
+ # load the old custom reports VS migrated
+ assert original_config["client_id"] == credentials["client_id"]
+ assert original_config["client_secret"] == credentials["client_secret"]
+ # revert the test_config to the starting point
+ self.revert_migration(self.OLD_TEST_CONFIG_PATH_CLIENT)
+
+ def test_should_not_migrate_new_client_config(self):
+ new_config = load_config(self.NEW_TEST_CONFIG_PATH_CLIENT)
+ migration_instance = MigrateSecretsPathInConnector()
+ assert not migration_instance._should_migrate(new_config)
+
+ def test_should_not_migrate_new_access_token_config(self):
+ new_config = load_config(self.NEW_TEST_CONFIG_PATH_ACCESS_TOKEN)
+ migration_instance = MigrateSecretsPathInConnector()
+ assert not migration_instance._should_migrate(new_config)
diff --git a/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_migrations/secrets_to_credentials/test_new_access_token_config.json b/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_migrations/secrets_to_credentials/test_new_access_token_config.json
new file mode 100644
index 0000000000000..f735cac033a0a
--- /dev/null
+++ b/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_migrations/secrets_to_credentials/test_new_access_token_config.json
@@ -0,0 +1,17 @@
+{
+ "start_date": "2021-02-08T00:00:00Z",
+ "end_date": "2021-02-15T00:00:00Z",
+ "custom_insights": [
+ {
+ "name": "custom_insight_stream",
+ "fields": ["account_name", "clicks", "cpc", "account_id", "ad_id"],
+ "breakdowns": ["gender"],
+ "action_breakdowns": []
+ }
+ ],
+ "account_ids": ["01234567890"],
+ "credentials": {
+ "auth_type": "Service",
+ "access_token": "access_token"
+ }
+}
diff --git a/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_migrations/secrets_to_credentials/test_new_client_config.json b/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_migrations/secrets_to_credentials/test_new_client_config.json
new file mode 100644
index 0000000000000..6a30bdb8b82a3
--- /dev/null
+++ b/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_migrations/secrets_to_credentials/test_new_client_config.json
@@ -0,0 +1,18 @@
+{
+ "start_date": "2021-02-08T00:00:00Z",
+ "end_date": "2021-02-15T00:00:00Z",
+ "custom_insights": [
+ {
+ "name": "custom_insight_stream",
+ "fields": ["account_name", "clicks", "cpc", "account_id", "ad_id"],
+ "breakdowns": ["gender"],
+ "action_breakdowns": []
+ }
+ ],
+ "account_ids": ["01234567890"],
+ "credentials": {
+ "auth_type": "Client",
+ "client_id": "client_id",
+ "client_secret": "client_secret"
+ }
+}
diff --git a/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_migrations/secrets_to_credentials/test_old_access_token_config.json b/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_migrations/secrets_to_credentials/test_old_access_token_config.json
new file mode 100644
index 0000000000000..b499ed5ab91a8
--- /dev/null
+++ b/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_migrations/secrets_to_credentials/test_old_access_token_config.json
@@ -0,0 +1,15 @@
+{
+ "start_date": "2021-02-08T00:00:00Z",
+ "end_date": "2021-02-15T00:00:00Z",
+ "custom_insights": [
+ {
+ "name": "custom_insight_stream",
+ "fields": ["account_name", "clicks", "cpc", "account_id", "ad_id"],
+ "breakdowns": ["gender"],
+ "action_breakdowns": []
+ }
+ ],
+ "account_id": "01234567890",
+ "auth_type": "Service",
+ "access_token": "access_token"
+}
diff --git a/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_migrations/secrets_to_credentials/test_old_client_config.json b/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_migrations/secrets_to_credentials/test_old_client_config.json
new file mode 100644
index 0000000000000..e8434efbec9b7
--- /dev/null
+++ b/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_migrations/secrets_to_credentials/test_old_client_config.json
@@ -0,0 +1,16 @@
+{
+ "start_date": "2021-02-08T00:00:00Z",
+ "end_date": "2021-02-15T00:00:00Z",
+ "custom_insights": [
+ {
+ "name": "custom_insight_stream",
+ "fields": ["account_name", "clicks", "cpc", "account_id", "ad_id"],
+ "breakdowns": ["gender"],
+ "action_breakdowns": []
+ }
+ ],
+ "account_id": "01234567890",
+ "auth_type": "Client",
+ "client_id": "client_id",
+ "client_secret": "client_secret"
+}
diff --git a/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_source.py b/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_source.py
index 202c1ce1fd67e..70d7653152943 100644
--- a/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_source.py
+++ b/airbyte-integrations/connectors/source-facebook-marketing/unit_tests/test_source.py
@@ -28,7 +28,11 @@
def config_fixture(requests_mock):
config = {
"account_ids": ["123"],
- "access_token": "TOKEN",
+ "access_token": "ACCESS_TOKEN",
+ "credentials": {
+ "auth_type": "Service",
+ "access_token": "ACCESS_TOKEN",
+ },
"start_date": "2019-10-10T00:00:00Z",
"end_date": "2020-10-10T00:00:00Z",
}
diff --git a/docs/integrations/sources/facebook-marketing.md b/docs/integrations/sources/facebook-marketing.md
index b9d69b7ea2347..df21cfb3c30eb 100644
--- a/docs/integrations/sources/facebook-marketing.md
+++ b/docs/integrations/sources/facebook-marketing.md
@@ -205,6 +205,7 @@ The Facebook Marketing connector uses the `lookback_window` parameter to repeate
| Version | Date | Pull Request | Subject |
|:--------|:-----------|:---------------------------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| 3.2.0 | 2024-06-05 | [37625](https://github.com/airbytehq/airbyte/pull/37625) | Source Facebook-Marketing: Add Selectable Auth |
| 3.1.0 | 2024-06-01 | [38845](https://github.com/airbytehq/airbyte/pull/38845) | Update AdsInsights fields - removed `cost_per_conversion_lead` and `conversion_lead_rate` |
| 3.0.0 | 2024-04-30 | [36608](https://github.com/airbytehq/airbyte/pull/36608) | Update `body_asset, call_to_action_asset, description_asset, image_asset, link_url_asset, title_asset, video_asset` breakdowns schema. |
| 2.1.9 | 2024-05-17 | [38301](https://github.com/airbytehq/airbyte/pull/38301) | Fix data inaccuracies when `wish_bid` is requested |