diff --git a/base_operations.yaml b/base_operations.yaml index a44ead9..fc12828 100644 --- a/base_operations.yaml +++ b/base_operations.yaml @@ -3348,6 +3348,12 @@ paths: link_rows: {} summaries: {} colors: {} + "400": + description: A view with this name already exists in the table. + content: + application/json: + example: + error_msg: params invalid /api-gateway/api/v2/dtables/{base_uuid}/views/{view_name}/: get: tags: @@ -3549,6 +3555,12 @@ paths: link_rows: {} summaries: {} colors: {} + "400": + description: The target view name already exists in the table. + content: + application/json: + example: + error_msg: params invalid delete: tags: - Views @@ -3702,6 +3714,11 @@ paths: conditions: [] permission_type: "" permitted_users: [] + "400": + description: A column with this name already exists in the table. + content: + application/json: + example: Column ColA exists. put: tags: - Columns @@ -3773,6 +3790,13 @@ paths: data: null permission_type: "" permitted_users: [] + "400": + description: A column with this name already exists in the table. + content: + application/json: + example: + error_type: column_already_exists + error_message: column name already exists delete: tags: - Columns diff --git a/tests/test_columns.py b/tests/test_columns.py index ef97c08..23ffb35 100644 --- a/tests/test_columns.py +++ b/tests/test_columns.py @@ -378,3 +378,67 @@ def test_deleteColumn(base: Base): column_names = [c['name'] for c in response.json()['columns']] assert 'keep' in column_names assert 'delete-me' not in column_names + + +def test_insertColumn_duplicate_name_returns_400(base: Base): + table_name = 'test_insertColumn_duplicate' + create_table(base, table_name, [{'column_name': 'existing', 'column_type': 'text'}]) + + path_parameters = {'base_uuid': base.uuid} + headers = {'Authorization': f'Bearer {base.token}'} + body = {'table_name': table_name, 'column_name': 'existing', 'column_type': 'text'} + + # Inserting a column whose name collides with an existing column is rejected + case: Case = base_operations_schema.find_operation_by_id('insertColumn') \ + .Case(path_parameters=path_parameters, body=body, headers=headers) + response = case.call() + + assert response.status_code == 400 + # application/json is also enforced by the conformance hook against the documented 400 + assert response.headers['content-type'][0].startswith('application/json') + + +@pytest.mark.xfail( + reason="insertColumn returns the plain-text body 'Column ColA exists.' with an " + "application/json content-type on a duplicate-column 400 (not valid JSON)", +) +def test_insertColumn_duplicate_name_returns_json(base: Base): + table_name = 'test_insertColumn_duplicate_json' + create_table(base, table_name, [{'column_name': 'existing', 'column_type': 'text'}]) + + path_parameters = {'base_uuid': base.uuid} + headers = {'Authorization': f'Bearer {base.token}'} + body = {'table_name': table_name, 'column_name': 'existing', 'column_type': 'text'} + + case: Case = base_operations_schema.find_operation_by_id('insertColumn') \ + .Case(path_parameters=path_parameters, body=body, headers=headers) + response = case.call() + + assert response.status_code == 400 + # The body should be valid JSON; currently raises JSONDecodeError (plain text) + response.json() + + +def test_updateColumn_rename_collision_returns_400(base: Base): + table_name = 'test_updateColumn_collision' + create_table(base, table_name, [ + {'column_name': 'col-a', 'column_type': 'text'}, + {'column_name': 'col-b', 'column_type': 'text'}, + ]) + + path_parameters = {'base_uuid': base.uuid} + headers = {'Authorization': f'Bearer {base.token}'} + body = { + 'op_type': 'rename_column', + 'table_name': table_name, + 'column': 'col-b', + 'new_column_name': 'col-a', + } + + # Renaming a column to collide with an existing column name is rejected + case: Case = base_operations_schema.find_operation_by_id('updateColumn') \ + .Case(path_parameters=path_parameters, body=body, headers=headers) + response = case.call() + + assert response.status_code == 400 + assert response.headers['content-type'][0].startswith('application/json') diff --git a/tests/test_views.py b/tests/test_views.py index d02512c..f8f94f6 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -153,3 +153,54 @@ def test_deleteView(base: Base): assert response.status_code == 200 view_names = [v['name'] for v in response.json()['views']] assert 'View To Delete' not in view_names + + +def test_createView_duplicate_name_returns_400(base: Base): + table_name = 'test_createView_duplicate' + create_table(base, table_name, SIMPLE_COLUMNS) + + path_parameters = {'base_uuid': base.uuid} + query = {'table_name': table_name} + headers = {'Authorization': f'Bearer {base.token}'} + body = {'name': 'Collision View', 'type': 'table'} + + # Create the view once + case: Case = base_operations_schema.find_operation_by_id('createView') \ + .Case(path_parameters=path_parameters, query=query, body=body, headers=headers) + response = case.call() + assert response.status_code == 200 + + # Creating a second view with the same name is rejected + case = base_operations_schema.find_operation_by_id('createView') \ + .Case(path_parameters=path_parameters, query=query, body=body, headers=headers) + response = case.call() + + assert response.status_code == 400 + # application/json is also enforced by the conformance hook against the documented 400 + assert response.headers['content-type'][0].startswith('application/json') + + +def test_updateView_rename_collision_returns_400(base: Base): + table_name = 'test_updateView_collision' + create_table(base, table_name, SIMPLE_COLUMNS) + + path_parameters = {'base_uuid': base.uuid} + query = {'table_name': table_name} + headers = {'Authorization': f'Bearer {base.token}'} + + # Create a second view (the table already has a "Default View") + body = {'name': 'Second View', 'type': 'table'} + case: Case = base_operations_schema.find_operation_by_id('createView') \ + .Case(path_parameters=path_parameters, query=query, body=body, headers=headers) + response = case.call() + assert response.status_code == 200 + + # Renaming it to collide with the existing "Default View" is rejected + rename_path = {'base_uuid': base.uuid, 'view_name': 'Second View'} + rename_body = {'name': 'Default View'} + case = base_operations_schema.find_operation_by_id('updateView') \ + .Case(path_parameters=rename_path, query=query, body=rename_body, headers=headers) + response = case.call() + + assert response.status_code == 400 + assert response.headers['content-type'][0].startswith('application/json')