Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ballerina/Dependencies.toml
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ dependencies = [
[[package]]
org = "ballerina"
name = "os"
version = "1.10.0"
version = "1.10.1"
scope = "testOnly"
dependencies = [
{org = "ballerina", name = "io"},
Expand Down
21 changes: 21 additions & 0 deletions ballerina/annotations.bal
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,24 @@ public type FtpFunctionConfig record {|
# Annotation to configure FTP service remote functions.
# This can be used to specify which file patterns should be handled by a particular content method.
public annotation FtpFunctionConfig FunctionConfig on service remote function;

# Configuration for FTP service monitoring.
# Use this to specify the directory path and file patterns this service should monitor.
# When this annotation is used, the listener-level `path`, `fileNamePattern`, `fileAgeFilter`,
# and `fileDependencyConditions` fields are ignored.
#
# + path - Directory path on the FTP server to monitor for file changes
# + fileNamePattern - File name pattern (regex) to filter which files trigger events
# + fileAgeFilter - Configuration for filtering files based on age (optional)
# + fileDependencyConditions - Array of dependency conditions for conditional file processing
public type ServiceConfiguration record {|
string path;
string fileNamePattern?;
FileAgeFilter fileAgeFilter?;
FileDependencyCondition[] fileDependencyConditions = [];
|};

# Annotation to configure FTP service monitoring path and file patterns.
# This annotation allows each service to define its own monitoring configuration,
# enabling multiple services on a single listener to monitor different paths independently.
public annotation ServiceConfiguration ServiceConfig on service;
20 changes: 14 additions & 6 deletions ballerina/listener_endpoint.bal
Original file line number Diff line number Diff line change
Expand Up @@ -168,20 +168,24 @@ isolated function getPollingService(Listener initializedListener) returns task:S
# + host - Target server hostname or IP address
# + port - Port number of the remote service
# + auth - Authentication options for connecting to the server
# + path - Directory path on the FTP server to monitor for file changes
# + fileNamePattern - File name pattern (regex) to filter which files trigger events
# + path - Deprecated: Use @ftp:ServiceConfig annotation on service instead.
# Directory path on the FTP server to monitor for file changes
# + fileNamePattern - Deprecated: Use @ftp:ServiceConfig annotation on service instead.
# File name pattern (regex) to filter which files trigger events
# + pollingInterval - Polling interval in seconds for checking file changes
# + userDirIsRoot - If set to `true`, treats the login home directory as the root (`/`) and
# prevents the underlying VFS from attempting to change to the actual server root.
# If `false`, treats the actual server root as `/`, which may cause a `CWD /` command
# that can fail on servers restricting root access (e.g., chrooted environments).
# + fileAgeFilter - Configuration for filtering files based on age (optional)
# + fileDependencyConditions - Array of dependency conditions for conditional file processing (default: [])
# + fileAgeFilter - Deprecated: Use @ftp:ServiceConfig annotation on service instead.
# Configuration for filtering files based on age (optional)
# + fileDependencyConditions - Deprecated: Use @ftp:ServiceConfig annotation on service instead.
# Array of dependency conditions for conditional file processing (default: [])
# + laxDataBinding - If set to `true`, enables relaxed data binding for XML and JSON responses.
# null values in JSON/XML are allowed to be mapped to optional fields
# missing fields in JSON/XML are allowed to be mapped as null values
# + connectTimeout - Connection timeout in seconds
# + socketConfig - Socket timeout configurations
# + connectTimeout - Connection timeout in seconds
# + socketConfig - Socket timeout configurations
# + fileTransferMode - File transfer mode: BINARY or ASCII (FTP only)
# + sftpCompression - Compression algorithms (SFTP only)
# + sftpSshKnownHosts - Path to SSH known_hosts file (SFTP only)
Expand All @@ -195,11 +199,15 @@ public type ListenerConfiguration record {|
string host = "127.0.0.1";
int port = 21;
AuthConfiguration auth?;
@deprecated
string path = "/";
@deprecated
string fileNamePattern?;
decimal pollingInterval = 60;
boolean userDirIsRoot = false;
@deprecated
FileAgeFilter fileAgeFilter?;
@deprecated
FileDependencyCondition[] fileDependencyConditions = [];
boolean laxDataBinding = false;
decimal connectTimeout = 30.0;
Expand Down
34 changes: 22 additions & 12 deletions ballerina/tests/client_endpoint_negative_test.bal
Original file line number Diff line number Diff line change
Expand Up @@ -379,13 +379,18 @@ public function testListenerWithInvalidDependencyTargetPattern() returns error?
}
]
};
Listener|Error listenerResult = new (invalidDependencyConfig);
test:assertTrue(listenerResult is InvalidConfigError,
Listener listenerResult = check new (invalidDependencyConfig);
Service attachService = service object {
remote function onFileChange(WatchEvent & readonly event) {
}
};
error? attachResult = listenerResult.attach(attachService);
test:assertTrue(attachResult is InvalidConfigError,
msg = "Expected InvalidConfigError for invalid dependency targetPattern");
if listenerResult is InvalidConfigError {
test:assertTrue(listenerResult.message().includes("Invalid regex pattern"),
msg = "Error message should indicate invalid regex pattern. Got: " + listenerResult.message());
test:assertTrue(listenerResult.message().includes("targetPattern"),
if attachResult is InvalidConfigError {
test:assertTrue(attachResult.message().includes("Invalid regex pattern"),
msg = "Error message should indicate invalid regex pattern. Got: " + attachResult.message());
test:assertTrue(attachResult.message().includes("targetPattern"),
msg = "Error message should mention targetPattern field");
}
}
Expand All @@ -408,13 +413,18 @@ public function testListenerWithInvalidDependencyRequiredFilePattern() returns e
}
]
};
Listener|Error listenerResult = new (invalidRequiredFileConfig);
test:assertTrue(listenerResult is InvalidConfigError,
Listener listenerResult = check new (invalidRequiredFileConfig);
Service attachService = service object {
remote function onFileChange(WatchEvent & readonly event) {
}
};
error? attachResult = listenerResult.attach(attachService);
test:assertTrue(attachResult is InvalidConfigError,
msg = "Expected InvalidConfigError for invalid requiredFiles pattern");
if listenerResult is InvalidConfigError {
test:assertTrue(listenerResult.message().includes("Invalid regex pattern"),
msg = "Error message should indicate invalid regex pattern. Got: " + listenerResult.message());
test:assertTrue(listenerResult.message().includes("requiredFiles"),
if attachResult is InvalidConfigError {
test:assertTrue(attachResult.message().includes("Invalid regex pattern"),
msg = "Error message should indicate invalid regex pattern. Got: " + attachResult.message());
test:assertTrue(attachResult.message().includes("requiredFiles"),
msg = "Error message should mention requiredFiles field");
}
}
66 changes: 37 additions & 29 deletions ballerina/tests/client_endpoint_test.bal
Original file line number Diff line number Diff line change
Expand Up @@ -1200,40 +1200,48 @@ public function testGetFileSize() {
dependsOn: [testGetFileSize]
}
public function testListFiles() {
string[] resourceNames = [
"cron",
"test1.txt",
"complexDirectory",
"test",
"advanced",
"dependency",
"folder1",
"test3.zip",
"childDirectory",
"delete",
"test2.txt",
"onerror-tests",
"test4.txt",
"child_directory",
"content-methods",
"age",
"retry",
"test3.txt"
];
int[] fileSizes = [0, 61, 0, 0, 0, 0, 0, 145, 0, 0, 16400, 0, 9000, 0, 0, 0, 0, 12];
map<int> expectedSizes = {
"/home/in/cron": 0,
"/home/in/test1.txt": 61,
"/home/in/complexDirectory": 0,
"/home/in/test": 0,
"/home/in/advanced": 0,
"/home/in/dependency": 0,
"/home/in/folder1": 0,
"/home/in/test3.zip": 145,
"/home/in/childDirectory": 0,
"/home/in/delete": 0,
"/home/in/test2.txt": 16400,
"/home/in/onerror-tests": 0,
"/home/in/test4.txt": 9000,
"/home/in/child_directory": 0,
"/home/in/content-methods": 0,
"/home/in/age": 0,
"/home/in/retry": 0,
"/home/in/test3.txt": 12,
"/home/in/sc-route-a": 0,
"/home/in/sc-route-b": 0,
"/home/in/sc-single": 0,
"/home/in/sc-legacy": 0
};
FileInfo[]|Error response = (<Client>clientEp)->list("/home/in");
if response is FileInfo[] {
log:printInfo("List of files/directories: ");
int i = 0;
map<FileInfo> fileInfoMap = {};
foreach var fileInfo in response {
log:printInfo(fileInfo.toString());
test:assertEquals(fileInfo.path, "/home/in/" + resourceNames[i],
msg = "File path is not matched during the `list` operation");
test:assertTrue(fileInfo.lastModifiedTimestamp > 0,
msg = "Last Modified Timestamp of the file is not correct during the `list` operation");
test:assertEquals(fileInfo.size, fileSizes[i],
msg = "File size is not matched during the `list` operation");
i = i + 1;
fileInfoMap[fileInfo.path] = fileInfo;
}
foreach string expectedPath in expectedSizes.keys() {
FileInfo? expectedInfo = fileInfoMap[expectedPath];
test:assertTrue(expectedInfo is FileInfo,
msg = "Expected path not found during the `list` operation: " + expectedPath);
if expectedInfo is FileInfo {
test:assertTrue(expectedInfo.lastModifiedTimestamp > 0,
msg = "Last Modified Timestamp of the file is not correct during the `list` operation");
test:assertEquals(expectedInfo.size, <int>expectedSizes[expectedPath],
msg = "File size is not matched during the `list` operation");
}
}
log:printInfo("Executed `list` operation");
} else {
Expand Down
70 changes: 50 additions & 20 deletions ballerina/tests/listener_endpoint_test.bal
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ public function testFtpServerDeregistration() returns error? {

@test:Config {}
public function testServerRegisterFailureEmptyPassword() returns error? {
Listener|Error emptyPasswordServer = new ({
Listener emptyPasswordServer = check new ({
protocol: FTP,
host: "127.0.0.1",
auth: {
Expand All @@ -188,8 +188,13 @@ public function testServerRegisterFailureEmptyPassword() returns error? {
fileNamePattern: "(.*).txt"
});

if emptyPasswordServer is Error {
test:assertTrue(emptyPasswordServer.message().startsWith("Failed to initialize File server connector."));
Service attachService = service object {
remote function onFileChange(WatchEvent & readonly event) {
}
};
error? attachResult = emptyPasswordServer.attach(attachService);
if attachResult is Error {
test:assertTrue(attachResult.message().startsWith("Failed to initialize File server connector."));
} else {
test:assertFail("Non-error result when empty password is used for creating a Listener.");
}
Expand Down Expand Up @@ -221,7 +226,7 @@ public function testServerRegisterFailureEmptyUsername() returns error? {

@test:Config {}
public function testServerRegisterFailureInvalidUsername() returns error? {
Listener|Error invalidUsernameServer = new (
Listener invalidUsernameServer = check new (
protocol = FTP,
host = "127.0.0.1",
auth = {
Expand All @@ -236,16 +241,21 @@ public function testServerRegisterFailureInvalidUsername() returns error? {
fileNamePattern = "(.*).txt"
);

if invalidUsernameServer is Error {
test:assertTrue(invalidUsernameServer.message().startsWith("Failed to initialize File server connector."));
Service attachService = service object {
remote function onFileChange(WatchEvent & readonly event) {
}
};
error? attachResult = invalidUsernameServer.attach(attachService);
if attachResult is Error {
test:assertTrue(attachResult.message().startsWith("Failed to initialize File server connector."));
} else {
test:assertFail("Non-error result when invalid username is used for creating a Listener.");
}
}

@test:Config {}
public function testServerRegisterFailureInvalidPassword() returns error? {
Listener|Error invalidPasswordServer = new ({
Listener invalidPasswordServer = check new ({
protocol: FTP,
host: "127.0.0.1",
auth: {
Expand All @@ -260,16 +270,21 @@ public function testServerRegisterFailureInvalidPassword() returns error? {
fileNamePattern: "(.*).txt"
});

if invalidPasswordServer is Error {
test:assertTrue(invalidPasswordServer.message().startsWith("Failed to initialize File server connector."));
Service attachService = service object {
remote function onFileChange(WatchEvent & readonly event) {
}
};
error? attachResult = invalidPasswordServer.attach(attachService);
if attachResult is Error {
test:assertTrue(attachResult.message().startsWith("Failed to initialize File server connector."));
} else {
test:assertFail("Non-error result when invalid password is used for creating a Listener.");
}
}

@test:Config {}
public function testConnectToInvalidUrl() returns error? {
Listener|Error invalidUrlServer = new ({
Listener invalidUrlServer = check new ({
protocol: FTP,
host: "localhost",
port: 21218,
Expand All @@ -281,16 +296,21 @@ public function testConnectToInvalidUrl() returns error? {
fileNamePattern: "(.*).txt"
});

if invalidUrlServer is Error {
test:assertTrue(invalidUrlServer.message().startsWith("Failed to initialize File server connector."));
Service attachService = service object {
remote function onFileChange(WatchEvent & readonly event) {
}
};
error? attachResult = invalidUrlServer.attach(attachService);
if attachResult is Error {
test:assertTrue(attachResult.message().startsWith("Failed to initialize File server connector."));
} else {
test:assertFail("Non-error result when trying to connect to an invalid url.");
}
}

@test:Config {}
public function testServerRegisterFailureWithDetailedErrorMessage() returns error? {
Listener|Error invalidUrlServer = new ({
Listener invalidUrlServer = check new ({
protocol: FTP,
host: "localhost",
port: 21219,
Expand All @@ -302,10 +322,15 @@ public function testServerRegisterFailureWithDetailedErrorMessage() returns erro
fileNamePattern: "(.*).txt"
});

if invalidUrlServer is Error {
test:assertTrue(invalidUrlServer.message().startsWith("Failed to initialize File server connector."));
Service attachService = service object {
remote function onFileChange(WatchEvent & readonly event) {
}
};
error? attachResult = invalidUrlServer.attach(attachService);
if attachResult is Error {
test:assertTrue(attachResult.message().startsWith("Failed to initialize File server connector."));
// Verify that the error message contains additional details from the root cause
test:assertTrue(invalidUrlServer.message().length() > "Failed to initialize File server connector.".length(),
test:assertTrue(attachResult.message().length() > "Failed to initialize File server connector.".length(),
msg = "Error message should contain detailed root cause information");
} else {
test:assertFail("Non-error result when trying to connect to an unreachable server.");
Expand All @@ -314,7 +339,7 @@ public function testServerRegisterFailureWithDetailedErrorMessage() returns erro

@test:Config {}
public function testServerRegisterFailureInvalidCredentialsWithDetails() returns error? {
Listener|Error invalidCredsServer = new ({
Listener invalidCredsServer = check new ({
protocol: FTP,
host: "127.0.0.1",
auth: {
Expand All @@ -329,10 +354,15 @@ public function testServerRegisterFailureInvalidCredentialsWithDetails() returns
fileNamePattern: "(.*).txt"
});

if invalidCredsServer is Error {
test:assertTrue(invalidCredsServer.message().startsWith("Failed to initialize File server connector."));
Service attachService = service object {
remote function onFileChange(WatchEvent & readonly event) {
}
};
error? attachResult = invalidCredsServer.attach(attachService);
if attachResult is Error {
test:assertTrue(attachResult.message().startsWith("Failed to initialize File server connector."));
// Verify that the error message contains additional details from the root cause
test:assertTrue(invalidCredsServer.message().length() > "Failed to initialize File server connector.".length(),
test:assertTrue(attachResult.message().length() > "Failed to initialize File server connector.".length(),
msg = "Error message should contain detailed root cause information");
} else {
test:assertFail("Non-error result when invalid credentials are used for creating a Listener.");
Expand Down
Loading