This is an automated email from the ASF dual-hosted git repository.
pierrejeambrun pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new 1328d1a034 AIP-84 Migrate post a connection to FastAPI API (#43396)
1328d1a034 is described below
commit 1328d1a03401c229d723697642cada78b06a00db
Author: Bugra Ozturk <[email protected]>
AuthorDate: Tue Nov 5 16:29:17 2024 +0100
AIP-84 Migrate post a connection to FastAPI API (#43396)
* Migrate Create a Connection to FastAPI
* Remove additional duplicate comment
* Include password in connection and move dashboard.py to serializers/ui/
* Fix test for password
* Include password field to response and redact it, run pre-commit after
rebase
* Convert redact to field_validator and fix tests
* Pass field name into redact
* run pre-commit after rebase
---
.../api_connexion/endpoints/connection_endpoint.py | 1 +
.../api_fastapi/core_api/openapi/v1-generated.yaml | 102 +++++++++++++++++-
.../core_api/routes/public/connections.py | 23 ++++
.../api_fastapi/core_api/routes/ui/dashboard.py | 2 +-
.../core_api/serializers/connections.py | 25 +++++
.../core_api/serializers/{ => ui}/dashboard.py | 0
airflow/ui/openapi-gen/queries/common.ts | 3 +
airflow/ui/openapi-gen/queries/queries.ts | 40 +++++++
airflow/ui/openapi-gen/requests/schemas.gen.ts | 110 ++++++++++++++++++-
airflow/ui/openapi-gen/requests/services.gen.ts | 27 +++++
airflow/ui/openapi-gen/requests/types.gen.ts | 51 ++++++++-
.../core_api/routes/public/test_connections.py | 119 +++++++++++++++++++++
12 files changed, 496 insertions(+), 7 deletions(-)
diff --git a/airflow/api_connexion/endpoints/connection_endpoint.py
b/airflow/api_connexion/endpoints/connection_endpoint.py
index 37c91c44eb..c0c2fcbf46 100644
--- a/airflow/api_connexion/endpoints/connection_endpoint.py
+++ b/airflow/api_connexion/endpoints/connection_endpoint.py
@@ -151,6 +151,7 @@ def patch_connection(
return connection_schema.dump(connection)
+@mark_fastapi_migration_done
@security.requires_access_connection("POST")
@provide_session
@action_logging(
diff --git a/airflow/api_fastapi/core_api/openapi/v1-generated.yaml
b/airflow/api_fastapi/core_api/openapi/v1-generated.yaml
index 28e3888480..06a041b55d 100644
--- a/airflow/api_fastapi/core_api/openapi/v1-generated.yaml
+++ b/airflow/api_fastapi/core_api/openapi/v1-generated.yaml
@@ -1104,6 +1104,49 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
+ post:
+ tags:
+ - Connection
+ summary: Post Connection
+ description: Create connection entry.
+ operationId: post_connection
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ConnectionBody'
+ responses:
+ '201':
+ description: Successful Response
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ConnectionResponse'
+ '401':
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/HTTPExceptionResponse'
+ description: Unauthorized
+ '403':
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/HTTPExceptionResponse'
+ description: Forbidden
+ '409':
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/HTTPExceptionResponse'
+ description: Conflict
+ '422':
+ description: Validation Error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/HTTPValidationError'
/public/dags/{dag_id}/dagRuns/{dag_run_id}:
get:
tags:
@@ -2515,6 +2558,55 @@ components:
- status
title: BaseInfoSchema
description: Base status field for metadatabase and scheduler.
+ ConnectionBody:
+ properties:
+ connection_id:
+ type: string
+ title: Connection Id
+ conn_type:
+ type: string
+ title: Conn Type
+ description:
+ anyOf:
+ - type: string
+ - type: 'null'
+ title: Description
+ host:
+ anyOf:
+ - type: string
+ - type: 'null'
+ title: Host
+ login:
+ anyOf:
+ - type: string
+ - type: 'null'
+ title: Login
+ schema:
+ anyOf:
+ - type: string
+ - type: 'null'
+ title: Schema
+ port:
+ anyOf:
+ - type: integer
+ - type: 'null'
+ title: Port
+ password:
+ anyOf:
+ - type: string
+ - type: 'null'
+ title: Password
+ extra:
+ anyOf:
+ - type: string
+ - type: 'null'
+ title: Extra
+ type: object
+ required:
+ - connection_id
+ - conn_type
+ title: ConnectionBody
+ description: Connection Serializer for requests body.
ConnectionCollectionResponse:
properties:
connections:
@@ -2564,6 +2656,11 @@ components:
- type: integer
- type: 'null'
title: Port
+ password:
+ anyOf:
+ - type: string
+ - type: 'null'
+ title: Password
extra:
anyOf:
- type: string
@@ -2578,6 +2675,7 @@ components:
- login
- schema
- port
+ - password
- extra
title: ConnectionResponse
description: Connection serializer for responses.
@@ -3545,7 +3643,7 @@ components:
dag_run_states:
$ref: '#/components/schemas/DAGRunStates'
task_instance_states:
- $ref:
'#/components/schemas/airflow__api_fastapi__core_api__serializers__dashboard__TaskInstanceState'
+ $ref:
'#/components/schemas/airflow__api_fastapi__core_api__serializers__ui__dashboard__TaskInstanceState'
type: object
required:
- dag_run_types
@@ -4224,7 +4322,7 @@ components:
- git_version
title: VersionInfo
description: Version information serializer for responses.
- airflow__api_fastapi__core_api__serializers__dashboard__TaskInstanceState:
+
airflow__api_fastapi__core_api__serializers__ui__dashboard__TaskInstanceState:
properties:
no_status:
type: integer
diff --git a/airflow/api_fastapi/core_api/routes/public/connections.py
b/airflow/api_fastapi/core_api/routes/public/connections.py
index 8d9f9ddb8e..a31c97c914 100644
--- a/airflow/api_fastapi/core_api/routes/public/connections.py
+++ b/airflow/api_fastapi/core_api/routes/public/connections.py
@@ -26,10 +26,12 @@ from airflow.api_fastapi.common.parameters import
QueryLimit, QueryOffset, SortP
from airflow.api_fastapi.common.router import AirflowRouter
from airflow.api_fastapi.core_api.openapi.exceptions import
create_openapi_http_exception_doc
from airflow.api_fastapi.core_api.serializers.connections import (
+ ConnectionBody,
ConnectionCollectionResponse,
ConnectionResponse,
)
from airflow.models import Connection
+from airflow.utils import helpers
connections_router = AirflowRouter(tags=["Connection"], prefix="/connections")
@@ -114,3 +116,24 @@ async def get_connections(
],
total_entries=total_entries,
)
+
+
+@connections_router.post("/", status_code=201,
responses=create_openapi_http_exception_doc([401, 403, 409]))
+async def post_connection(
+ post_body: ConnectionBody,
+ session: Annotated[Session, Depends(get_session)],
+) -> ConnectionResponse:
+ """Create connection entry."""
+ try:
+ helpers.validate_key(post_body.connection_id, max_length=200)
+ except Exception as e:
+ raise HTTPException(400, f"{e}")
+
+ connection =
session.scalar(select(Connection).filter_by(conn_id=post_body.connection_id))
+ if connection is not None:
+ raise HTTPException(409, f"Connection with connection_id:
`{post_body.connection_id}` already exists")
+
+ connection = Connection(**post_body.model_dump(by_alias=True))
+ session.add(connection)
+
+ return ConnectionResponse.model_validate(connection, from_attributes=True)
diff --git a/airflow/api_fastapi/core_api/routes/ui/dashboard.py
b/airflow/api_fastapi/core_api/routes/ui/dashboard.py
index e101ca78be..0eeea4d0dc 100644
--- a/airflow/api_fastapi/core_api/routes/ui/dashboard.py
+++ b/airflow/api_fastapi/core_api/routes/ui/dashboard.py
@@ -25,7 +25,7 @@ from typing_extensions import Annotated
from airflow.api_fastapi.common.parameters import DateTimeQuery
from airflow.api_fastapi.core_api.openapi.exceptions import
create_openapi_http_exception_doc
-from airflow.api_fastapi.core_api.serializers.dashboard import
HistoricalMetricDataResponse
+from airflow.api_fastapi.core_api.serializers.ui.dashboard import
HistoricalMetricDataResponse
from airflow.models.dagrun import DagRun, DagRunType
from airflow.models.taskinstance import TaskInstance
from airflow.utils.state import DagRunState, TaskInstanceState
diff --git a/airflow/api_fastapi/core_api/serializers/connections.py
b/airflow/api_fastapi/core_api/serializers/connections.py
index 1cc069cac0..c5956b6ec5 100644
--- a/airflow/api_fastapi/core_api/serializers/connections.py
+++ b/airflow/api_fastapi/core_api/serializers/connections.py
@@ -20,10 +20,12 @@ from __future__ import annotations
import json
from pydantic import BaseModel, Field, field_validator
+from pydantic_core.core_schema import ValidationInfo
from airflow.utils.log.secrets_masker import redact
+# Response Models
class ConnectionResponse(BaseModel):
"""Connection serializer for responses."""
@@ -34,8 +36,16 @@ class ConnectionResponse(BaseModel):
login: str | None
schema_: str | None = Field(alias="schema")
port: int | None
+ password: str | None
extra: str | None
+ @field_validator("password", mode="after")
+ @classmethod
+ def redact_password(cls, v: str | None, field_info: ValidationInfo) -> str
| None:
+ if v is None:
+ return None
+ return redact(v, field_info.field_name)
+
@field_validator("extra", mode="before")
@classmethod
def redact_extra(cls, v: str | None) -> str | None:
@@ -55,3 +65,18 @@ class ConnectionCollectionResponse(BaseModel):
connections: list[ConnectionResponse]
total_entries: int
+
+
+# Request Models
+class ConnectionBody(BaseModel):
+ """Connection Serializer for requests body."""
+
+ connection_id: str = Field(serialization_alias="conn_id")
+ conn_type: str
+ description: str | None = Field(default=None)
+ host: str | None = Field(default=None)
+ login: str | None = Field(default=None)
+ schema_: str | None = Field(None, alias="schema")
+ port: int | None = Field(default=None)
+ password: str | None = Field(default=None)
+ extra: str | None = Field(default=None)
diff --git a/airflow/api_fastapi/core_api/serializers/dashboard.py
b/airflow/api_fastapi/core_api/serializers/ui/dashboard.py
similarity index 100%
rename from airflow/api_fastapi/core_api/serializers/dashboard.py
rename to airflow/api_fastapi/core_api/serializers/ui/dashboard.py
diff --git a/airflow/ui/openapi-gen/queries/common.ts
b/airflow/ui/openapi-gen/queries/common.ts
index 36ea524e01..2ed842201c 100644
--- a/airflow/ui/openapi-gen/queries/common.ts
+++ b/airflow/ui/openapi-gen/queries/common.ts
@@ -682,6 +682,9 @@ export const UseVersionServiceGetVersionKeyFn = (queryKey?:
Array<unknown>) => [
export type BackfillServiceCreateBackfillMutationResult = Awaited<
ReturnType<typeof BackfillService.createBackfill>
>;
+export type ConnectionServicePostConnectionMutationResult = Awaited<
+ ReturnType<typeof ConnectionService.postConnection>
+>;
export type PoolServicePostPoolMutationResult = Awaited<
ReturnType<typeof PoolService.postPool>
>;
diff --git a/airflow/ui/openapi-gen/queries/queries.ts
b/airflow/ui/openapi-gen/queries/queries.ts
index f4b7c41195..583f14f771 100644
--- a/airflow/ui/openapi-gen/queries/queries.ts
+++ b/airflow/ui/openapi-gen/queries/queries.ts
@@ -28,6 +28,7 @@ import {
} from "../requests/services.gen";
import {
BackfillPostBody,
+ ConnectionBody,
DAGPatchBody,
DAGRunPatchBody,
DagRunState,
@@ -1130,6 +1131,45 @@ export const useBackfillServiceCreateBackfill = <
}) as unknown as Promise<TData>,
...options,
});
+/**
+ * Post Connection
+ * Create connection entry.
+ * @param data The data for the request.
+ * @param data.requestBody
+ * @returns ConnectionResponse Successful Response
+ * @throws ApiError
+ */
+export const useConnectionServicePostConnection = <
+ TData = Common.ConnectionServicePostConnectionMutationResult,
+ TError = unknown,
+ TContext = unknown,
+>(
+ options?: Omit<
+ UseMutationOptions<
+ TData,
+ TError,
+ {
+ requestBody: ConnectionBody;
+ },
+ TContext
+ >,
+ "mutationFn"
+ >,
+) =>
+ useMutation<
+ TData,
+ TError,
+ {
+ requestBody: ConnectionBody;
+ },
+ TContext
+ >({
+ mutationFn: ({ requestBody }) =>
+ ConnectionService.postConnection({
+ requestBody,
+ }) as unknown as Promise<TData>,
+ ...options,
+ });
/**
* Post Pool
* Create a Pool.
diff --git a/airflow/ui/openapi-gen/requests/schemas.gen.ts
b/airflow/ui/openapi-gen/requests/schemas.gen.ts
index 517743af17..c1dc8cd345 100644
--- a/airflow/ui/openapi-gen/requests/schemas.gen.ts
+++ b/airflow/ui/openapi-gen/requests/schemas.gen.ts
@@ -151,6 +151,100 @@ export const $BaseInfoSchema = {
description: "Base status field for metadatabase and scheduler.",
} as const;
+export const $ConnectionBody = {
+ properties: {
+ connection_id: {
+ type: "string",
+ title: "Connection Id",
+ },
+ conn_type: {
+ type: "string",
+ title: "Conn Type",
+ },
+ description: {
+ anyOf: [
+ {
+ type: "string",
+ },
+ {
+ type: "null",
+ },
+ ],
+ title: "Description",
+ },
+ host: {
+ anyOf: [
+ {
+ type: "string",
+ },
+ {
+ type: "null",
+ },
+ ],
+ title: "Host",
+ },
+ login: {
+ anyOf: [
+ {
+ type: "string",
+ },
+ {
+ type: "null",
+ },
+ ],
+ title: "Login",
+ },
+ schema: {
+ anyOf: [
+ {
+ type: "string",
+ },
+ {
+ type: "null",
+ },
+ ],
+ title: "Schema",
+ },
+ port: {
+ anyOf: [
+ {
+ type: "integer",
+ },
+ {
+ type: "null",
+ },
+ ],
+ title: "Port",
+ },
+ password: {
+ anyOf: [
+ {
+ type: "string",
+ },
+ {
+ type: "null",
+ },
+ ],
+ title: "Password",
+ },
+ extra: {
+ anyOf: [
+ {
+ type: "string",
+ },
+ {
+ type: "null",
+ },
+ ],
+ title: "Extra",
+ },
+ },
+ type: "object",
+ required: ["connection_id", "conn_type"],
+ title: "ConnectionBody",
+ description: "Connection Serializer for requests body.",
+} as const;
+
export const $ConnectionCollectionResponse = {
properties: {
connections: {
@@ -236,6 +330,17 @@ export const $ConnectionResponse = {
],
title: "Port",
},
+ password: {
+ anyOf: [
+ {
+ type: "string",
+ },
+ {
+ type: "null",
+ },
+ ],
+ title: "Password",
+ },
extra: {
anyOf: [
{
@@ -257,6 +362,7 @@ export const $ConnectionResponse = {
"login",
"schema",
"port",
+ "password",
"extra",
],
title: "ConnectionResponse",
@@ -1731,7 +1837,7 @@ export const $HistoricalMetricDataResponse = {
$ref: "#/components/schemas/DAGRunStates",
},
task_instance_states: {
- $ref:
"#/components/schemas/airflow__api_fastapi__core_api__serializers__dashboard__TaskInstanceState",
+ $ref:
"#/components/schemas/airflow__api_fastapi__core_api__serializers__ui__dashboard__TaskInstanceState",
},
},
type: "object",
@@ -2766,7 +2872,7 @@ export const $VersionInfo = {
description: "Version information serializer for responses.",
} as const;
-export const
$airflow__api_fastapi__core_api__serializers__dashboard__TaskInstanceState =
+export const
$airflow__api_fastapi__core_api__serializers__ui__dashboard__TaskInstanceState =
{
properties: {
no_status: {
diff --git a/airflow/ui/openapi-gen/requests/services.gen.ts
b/airflow/ui/openapi-gen/requests/services.gen.ts
index 5597b0a6a9..4eecb848a5 100644
--- a/airflow/ui/openapi-gen/requests/services.gen.ts
+++ b/airflow/ui/openapi-gen/requests/services.gen.ts
@@ -41,6 +41,8 @@ import type {
GetConnectionResponse,
GetConnectionsData,
GetConnectionsResponse,
+ PostConnectionData,
+ PostConnectionResponse,
GetDagRunData,
GetDagRunResponse,
DeleteDagRunData,
@@ -661,6 +663,31 @@ export class ConnectionService {
},
});
}
+
+ /**
+ * Post Connection
+ * Create connection entry.
+ * @param data The data for the request.
+ * @param data.requestBody
+ * @returns ConnectionResponse Successful Response
+ * @throws ApiError
+ */
+ public static postConnection(
+ data: PostConnectionData,
+ ): CancelablePromise<PostConnectionResponse> {
+ return __request(OpenAPI, {
+ method: "POST",
+ url: "/public/connections/",
+ body: data.requestBody,
+ mediaType: "application/json",
+ errors: {
+ 401: "Unauthorized",
+ 403: "Forbidden",
+ 409: "Conflict",
+ 422: "Validation Error",
+ },
+ });
+ }
}
export class DagRunService {
diff --git a/airflow/ui/openapi-gen/requests/types.gen.ts
b/airflow/ui/openapi-gen/requests/types.gen.ts
index e3071b6493..603a20d090 100644
--- a/airflow/ui/openapi-gen/requests/types.gen.ts
+++ b/airflow/ui/openapi-gen/requests/types.gen.ts
@@ -43,6 +43,21 @@ export type BaseInfoSchema = {
status: string | null;
};
+/**
+ * Connection Serializer for requests body.
+ */
+export type ConnectionBody = {
+ connection_id: string;
+ conn_type: string;
+ description?: string | null;
+ host?: string | null;
+ login?: string | null;
+ schema?: string | null;
+ port?: number | null;
+ password?: string | null;
+ extra?: string | null;
+};
+
/**
* Connection Collection serializer for responses.
*/
@@ -62,6 +77,7 @@ export type ConnectionResponse = {
login: string | null;
schema: string | null;
port: number | null;
+ password: string | null;
extra: string | null;
};
@@ -413,7 +429,7 @@ export type HealthInfoSchema = {
export type HistoricalMetricDataResponse = {
dag_run_types: DAGRunTypes;
dag_run_states: DAGRunStates;
- task_instance_states:
airflow__api_fastapi__core_api__serializers__dashboard__TaskInstanceState;
+ task_instance_states:
airflow__api_fastapi__core_api__serializers__ui__dashboard__TaskInstanceState;
};
/**
@@ -651,7 +667,7 @@ export type VersionInfo = {
/**
* TaskInstance serializer for responses.
*/
-export type
airflow__api_fastapi__core_api__serializers__dashboard__TaskInstanceState =
+export type
airflow__api_fastapi__core_api__serializers__ui__dashboard__TaskInstanceState =
{
no_status: number;
removed: number;
@@ -841,6 +857,12 @@ export type GetConnectionsData = {
export type GetConnectionsResponse = ConnectionCollectionResponse;
+export type PostConnectionData = {
+ requestBody: ConnectionBody;
+};
+
+export type PostConnectionResponse = ConnectionResponse;
+
export type GetDagRunData = {
dagId: string;
dagRunId: string;
@@ -1512,6 +1534,31 @@ export type $OpenApiTs = {
422: HTTPValidationError;
};
};
+ post: {
+ req: PostConnectionData;
+ res: {
+ /**
+ * Successful Response
+ */
+ 201: ConnectionResponse;
+ /**
+ * Unauthorized
+ */
+ 401: HTTPExceptionResponse;
+ /**
+ * Forbidden
+ */
+ 403: HTTPExceptionResponse;
+ /**
+ * Conflict
+ */
+ 409: HTTPExceptionResponse;
+ /**
+ * Validation Error
+ */
+ 422: HTTPValidationError;
+ };
+ };
};
"/public/dags/{dag_id}/dagRuns/{dag_run_id}": {
get: {
diff --git a/tests/api_fastapi/core_api/routes/public/test_connections.py
b/tests/api_fastapi/core_api/routes/public/test_connections.py
index ee9c80219e..1dc3cf9d2c 100644
--- a/tests/api_fastapi/core_api/routes/public/test_connections.py
+++ b/tests/api_fastapi/core_api/routes/public/test_connections.py
@@ -169,3 +169,122 @@ class TestGetConnections(TestConnectionEndpoint):
body = response.json()
assert body["total_entries"] == expected_total_entries
assert [connection["connection_id"] for connection in
body["connections"]] == expected_ids
+
+
+class TestPostConnection(TestConnectionEndpoint):
+ @pytest.mark.parametrize(
+ "body",
+ [
+ {"connection_id": TEST_CONN_ID, "conn_type": TEST_CONN_TYPE},
+ {"connection_id": TEST_CONN_ID, "conn_type": TEST_CONN_TYPE,
"extra": None},
+ {"connection_id": TEST_CONN_ID, "conn_type": TEST_CONN_TYPE,
"extra": "{}"},
+ {"connection_id": TEST_CONN_ID, "conn_type": TEST_CONN_TYPE,
"extra": '{"key": "value"}'},
+ {
+ "connection_id": TEST_CONN_ID,
+ "conn_type": TEST_CONN_TYPE,
+ "description": "test_description",
+ "host": "test_host",
+ "login": "test_login",
+ "schema": "test_schema",
+ "port": 8080,
+ "extra": '{"key": "value"}',
+ },
+ ],
+ )
+ def test_post_should_respond_200(self, test_client, session, body):
+ response = test_client.post("/public/connections/", json=body)
+ assert response.status_code == 201
+ connection = session.query(Connection).all()
+ assert len(connection) == 1
+
+ @pytest.mark.parametrize(
+ "body",
+ [
+ {"connection_id": "****", "conn_type": TEST_CONN_TYPE},
+ {"connection_id": "test()", "conn_type": TEST_CONN_TYPE},
+ {"connection_id": "this_^$#is_invalid", "conn_type":
TEST_CONN_TYPE},
+ {"connection_id": "iam_not@#$_connection_id", "conn_type":
TEST_CONN_TYPE},
+ ],
+ )
+ def test_post_should_respond_400_for_invalid_conn_id(self, test_client,
body):
+ response = test_client.post("/public/connections/", json=body)
+ assert response.status_code == 400
+ connection_id = body["connection_id"]
+ assert response.json() == {
+ "detail": f"The key '{connection_id}' has to be made of "
+ "alphanumeric characters, dashes, dots and underscores
exclusively",
+ }
+
+ @pytest.mark.parametrize(
+ "body",
+ [
+ {"connection_id": TEST_CONN_ID, "conn_type": TEST_CONN_TYPE},
+ ],
+ )
+ def test_post_should_respond_already_exist(self, test_client, body):
+ response = test_client.post("/public/connections/", json=body)
+ assert response.status_code == 201
+ # Another request
+ response = test_client.post("/public/connections/", json=body)
+ assert response.status_code == 409
+ assert response.json() == {
+ "detail": f"Connection with connection_id: `{TEST_CONN_ID}`
already exists",
+ }
+
+ @pytest.mark.enable_redact
+ @pytest.mark.parametrize(
+ "body, expected_response",
+ [
+ (
+ {"connection_id": TEST_CONN_ID, "conn_type": TEST_CONN_TYPE,
"password": "test-password"},
+ {
+ "connection_id": TEST_CONN_ID,
+ "conn_type": TEST_CONN_TYPE,
+ "description": None,
+ "extra": None,
+ "host": None,
+ "login": None,
+ "password": "***",
+ "port": None,
+ "schema": None,
+ },
+ ),
+ (
+ {"connection_id": TEST_CONN_ID, "conn_type": TEST_CONN_TYPE,
"password": "?>@#+!_%()#"},
+ {
+ "connection_id": TEST_CONN_ID,
+ "conn_type": TEST_CONN_TYPE,
+ "description": None,
+ "extra": None,
+ "host": None,
+ "login": None,
+ "password": "***",
+ "port": None,
+ "schema": None,
+ },
+ ),
+ (
+ {
+ "connection_id": TEST_CONN_ID,
+ "conn_type": TEST_CONN_TYPE,
+ "password": "A!rF|0wi$aw3s0m3",
+ "extra": '{"password": "test-password"}',
+ },
+ {
+ "connection_id": TEST_CONN_ID,
+ "conn_type": TEST_CONN_TYPE,
+ "description": None,
+ "extra": '{"password": "***"}',
+ "host": None,
+ "login": None,
+ "password": "***",
+ "port": None,
+ "schema": None,
+ },
+ ),
+ ],
+ )
+ def test_post_should_response_201_redacted_password(self, test_client,
body, expected_response):
+ response = test_client.post("/public/connections/", json=body)
+ assert response.status_code == 201
+ assert response.json() == expected_response