Skip to content

Commit 7f0e101

Browse files
authored
Added 2 new tests related to the SGW database - CBS bucket connectivity (#309)
1 parent 9e6f65b commit 7f0e101

File tree

6 files changed

+329
-213
lines changed

6 files changed

+329
-213
lines changed

client/src/cbltest/api/syncgateway.py

Lines changed: 42 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1-
import re
1+
import asyncio
22
import ssl
33
from abc import ABC, abstractmethod
44
from json import dumps, loads
55
from pathlib import Path
66
from typing import Any, cast
77
from urllib.parse import urljoin
88

9-
import paramiko
109
import requests
1110
from aiohttp import BasicAuth, ClientSession, TCPConnector
1211
from opentelemetry.trace import get_tracer
@@ -183,6 +182,32 @@ def __init__(self, key: str, id: str, revid: str | None, cv: str | None) -> None
183182
self.__cv = cv
184183

185184

185+
class DatabaseStatusResponse:
186+
"""
187+
A class representing a database status response from Sync Gateway
188+
"""
189+
190+
@property
191+
def db_name(self) -> str:
192+
"""Gets the database name"""
193+
return self.__db_name
194+
195+
@property
196+
def state(self) -> str:
197+
"""Gets the database state ('Online', 'Offline', etc.)"""
198+
return self.__state
199+
200+
@property
201+
def update_seq(self) -> int:
202+
"""Gets the update sequence number"""
203+
return self.__update_seq
204+
205+
def __init__(self, response: dict):
206+
self.__db_name = response.get("db_name", "")
207+
self.__state = response.get("state", "Unknown")
208+
self.__update_seq = response.get("update_seq", 0)
209+
210+
186211
class AllDocumentsResponse:
187212
"""
188213
A class representing an all_docs response from Sync Gateway
@@ -629,20 +654,24 @@ async def put_database(self, db_name: str, payload: PutDatabasePayload) -> None:
629654
"""
630655
await self._put_database(db_name, payload, 0)
631656

632-
async def database_exists(self, db_name: str) -> bool:
657+
async def get_database_status(self, db_name: str) -> DatabaseStatusResponse | None:
633658
"""
634-
Checks if a database exists in Sync Gateway's configuration.
659+
Gets the status of a database including its online/offline state.
635660
636-
:param db_name: The name of the Database to check
637-
:return: True if the database exists, False otherwise
661+
:param db_name: The name of the Database
662+
:return: DatabaseStatusResponse with state, sequences, etc. Returns None if database doesn't exist (404/403)
638663
"""
639-
try:
640-
await self._send_request("get", f"/{db_name}")
641-
return True
642-
except CblSyncGatewayBadResponseError as e:
643-
if e.code == 403: # Database does not exist
644-
return False
645-
raise
664+
with self.__tracer.start_as_current_span(
665+
"get_database_status", attributes={"cbl.database.name": db_name}
666+
):
667+
try:
668+
resp = await self._send_request("get", f"/{db_name}/")
669+
assert isinstance(resp, dict)
670+
return DatabaseStatusResponse(cast(dict, resp))
671+
except CblSyncGatewayBadResponseError as e:
672+
if e.code in [403, 404]: # Database doesn't exist
673+
return None
674+
raise
646675

647676
async def _delete_database(self, db_name: str, retry_count: int = 0) -> None:
648677
with self.__tracer.start_as_current_span(
@@ -656,8 +685,6 @@ async def _delete_database(self, db_name: str, retry_count: int = 0) -> None:
656685
f"Sync gateway returned 500 from DELETE database call, retrying ({retry_count + 1})..."
657686
)
658687
current_span.add_event("SGW returned 500, retry")
659-
import asyncio
660-
661688
await asyncio.sleep(2)
662689
await self._delete_database(db_name, retry_count + 1)
663690
elif e.code == 403:
@@ -1286,96 +1313,3 @@ async def get_document_revision_public(
12861313
self.__secure, scheme, self.__hostname, 4984, auth
12871314
) as session:
12881315
return await self._send_request("GET", path, params=params, session=session)
1289-
1290-
async def fetch_log_file(
1291-
self,
1292-
log_type: str,
1293-
ssh_key_path: str,
1294-
ssh_username: str = "ec2-user",
1295-
) -> str:
1296-
"""
1297-
Fetches a log file from the remote Sync Gateway server via SSH
1298-
1299-
:param log_type: The type of log to fetch (e.g., 'debug', 'info', 'error', 'warn')
1300-
:param ssh_key_path: Path to SSH private key for authentication
1301-
:param ssh_username: SSH username (default: ec2-user)
1302-
:return: Contents of the log file as a string
1303-
"""
1304-
# Get log directory from SG configuration
1305-
server_config = await self._send_request("GET", "/_config")
1306-
log_dir = server_config.get("logging", {}).get(
1307-
"log_file_path", "/home/ec2-user/log"
1308-
)
1309-
remote_log_path = f"{log_dir}/sg_{log_type}.log"
1310-
1311-
with self.__tracer.start_as_current_span(
1312-
"fetch_log_file",
1313-
attributes={
1314-
"log.type": log_type,
1315-
"remote.path": remote_log_path,
1316-
"ssh.username": ssh_username,
1317-
},
1318-
):
1319-
ssh = paramiko.SSHClient()
1320-
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
1321-
1322-
# Load private key
1323-
private_key = paramiko.Ed25519Key.from_private_key_file(ssh_key_path)
1324-
1325-
# Connect to the remote server
1326-
ssh.connect(
1327-
self.__hostname,
1328-
username=ssh_username,
1329-
pkey=private_key,
1330-
)
1331-
1332-
# Read the log file
1333-
sftp = ssh.open_sftp()
1334-
try:
1335-
with sftp.open(remote_log_path, "r") as remote_file:
1336-
log_contents = remote_file.read().decode("utf-8")
1337-
finally:
1338-
sftp.close()
1339-
ssh.close()
1340-
1341-
return log_contents
1342-
1343-
1344-
def scan_logs_for_untagged_sensitive_data(
1345-
log_content: str,
1346-
sensitive_patterns: list[str],
1347-
) -> list[str]:
1348-
"""
1349-
Scans log content for sensitive data that is NOT wrapped in <ud>...</ud> tags
1350-
1351-
:param log_content: The log file content as a string
1352-
:param sensitive_patterns: List of sensitive strings to look for (e.g., doc IDs, usernames)
1353-
:return: List of violations found (sensitive data without <ud> tags)
1354-
"""
1355-
violations = []
1356-
for pattern in sensitive_patterns:
1357-
# Escape special regex characters in the pattern
1358-
escaped_pattern = re.escape(pattern)
1359-
for match in re.finditer(escaped_pattern, log_content):
1360-
start_pos = match.start()
1361-
end_pos = match.end()
1362-
1363-
# Check if this occurrence is within <ud>...</ud> tags
1364-
# Look backwards for <ud> and forwards for </ud>
1365-
before_text = log_content[max(0, start_pos - 100) : start_pos]
1366-
after_text = log_content[end_pos : min(len(log_content), end_pos + 100)]
1367-
1368-
# Check if there's an opening <ud> before and closing </ud> after
1369-
has_opening_tag = "<ud>" in before_text and before_text.rfind(
1370-
"<ud>"
1371-
) > before_text.rfind("</ud>")
1372-
has_closing_tag = "</ud>" in after_text
1373-
1374-
if not (has_opening_tag and has_closing_tag):
1375-
context_start = max(0, start_pos - 50)
1376-
context_end = min(len(log_content), end_pos + 50)
1377-
context = log_content[context_start:context_end]
1378-
violations.append(
1379-
f"Untagged '{pattern}' at position {start_pos}: ...{context}..."
1380-
)
1381-
return violations
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Database Online/Offline Tests
2+
3+
## test_db_offline_on_bucket_deletion
4+
5+
Test that database goes offline when its bucket is deleted.
6+
7+
This test verifies that when the Couchbase Server bucket backing a Sync Gateway database is deleted, the database properly enters offline state and all REST API endpoints return 403.
8+
9+
1. Create bucket and default collection
10+
2. Configure Sync Gateway database endpoint
11+
3. Create user 'vipul' with access to ['ABC']
12+
4. Create 10 docs via Sync Gateway
13+
5. Verify database is online - REST endpoints work
14+
6. Delete bucket to sever connection
15+
7. Verify database is offline - REST endpoints return 403
16+
17+
## test_multiple_dbs_bucket_deletion
18+
19+
Test that deleting specific buckets causes only those databases to go offline.
20+
21+
This test creates 4 databases with unique buckets, deletes 2 buckets, and verifies that only those 2 databases go offline while the other 2 remain online.
22+
23+
1. Create buckets and configure databases
24+
2. Create 10 docs via Sync Gateway
25+
3. Verify all databases are online
26+
4. Delete buckets for db1 and db3
27+
5. Verify db2 and db4 remain online
28+
6. Verify db1 and db3 are offline (return 403)

spec/tests/QE/test_log_redaction.md

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,27 @@ This test verifies that NO document IDs or usernames appear in logs WITHOUT `<ud
1010

1111
1. Create bucket and default collection
1212
2. Configure Sync Gateway with log redaction enabled
13-
3. Create user 'autotest' with access to channels
14-
4. Create 10 docs via Sync Gateway with xattrs
15-
5. Verify docs were created
13+
3. Create user 'vipul' with access to channels
14+
4. Create 10 docs via Sync Gateway
15+
5. Verify docs were created (public API)
1616
6. Fetch and scan SG logs for redaction violations
17+
18+
## test_sgcollect_redacted_files_and_contents
19+
20+
Test SGCollect REST API for redacted files and log file contents (Combined test).
21+
22+
This comprehensive test uses the `/_sgcollect_info` REST API to trigger SGCollect with partial redaction, then verifies:
23+
1. All expected log files are present in the zip
24+
2. Sensitive data is marked with `<ud>` tags in redacted files
25+
3. sync_gateway.log contains correct system information (hostname)
26+
27+
**Prerequisites**: Sync Gateway bootstrap.json must be configured with `redaction_level: "partial"` in the logging section.
28+
29+
1. Create bucket and default collection
30+
2. Configure Sync Gateway
31+
3. Create user 'vipul' with access to ['logging']
32+
4. Create 10 docs via Sync Gateway
33+
5. Start SGCollect via REST API and wait for it to complete
34+
6. Download and extract SGCollect redacted zip
35+
7. Verify redacted zip marks sensitive data with <ud> tags
36+
8. Verify content of sync_gateway.log

0 commit comments

Comments
 (0)