Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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