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
8 changes: 4 additions & 4 deletions ballerina/Dependencies.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ distribution-version = "2201.12.0"
[[package]]
org = "ballerina"
name = "crypto"
version = "2.10.0"
version = "2.10.1"
dependencies = [
{org = "ballerina", name = "jballerina.java"},
{org = "ballerina", name = "time"}
Expand Down Expand Up @@ -46,7 +46,7 @@ modules = [
[[package]]
org = "ballerina"
name = "data.xmldata"
version = "1.5.2"
version = "1.5.3"
dependencies = [
{org = "ballerina", name = "jballerina.java"},
{org = "ballerina", name = "lang.object"}
Expand Down Expand Up @@ -215,7 +215,7 @@ modules = [
[[package]]
org = "ballerina"
name = "observe"
version = "1.5.0"
version = "1.5.1"
dependencies = [
{org = "ballerina", name = "jballerina.java"}
]
Expand All @@ -233,7 +233,7 @@ dependencies = [
[[package]]
org = "ballerina"
name = "task"
version = "2.11.0"
version = "2.11.1"
dependencies = [
{org = "ballerina", name = "jballerina.java"},
{org = "ballerina", name = "time"},
Expand Down
20 changes: 20 additions & 0 deletions ballerina/error.bal
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,23 @@

# Defines the common error type for the module.
public type Error distinct error;

# Represents an error that occurs when connecting to the FTP/SFTP server.
# This includes network failures, host unreachable, connection refused, etc.
public type ConnectionError distinct Error;

# Represents an error that occurs when a requested file or directory is not found.
public type FileNotFoundError distinct Error;

# Represents an error that occurs when attempting to create a file or directory that already exists.
public type FileAlreadyExistsError distinct Error;

# Represents an error that occurs when FTP/SFTP configuration is invalid.
# This includes invalid port numbers, invalid regex patterns, invalid timeout values, etc.
public type InvalidConfigError distinct Error;

# Represents an error that occurs when the FTP/SFTP service is temporarily unavailable.
# This is a transient error indicating the operation may succeed on retry.
# Common causes include: server overload (421), connection issues (425, 426),
# temporary file locks (450), or server-side processing errors (451).
public type ServiceUnavailableError distinct Error;
2 changes: 1 addition & 1 deletion ballerina/tests/advanced_file_selection_test.bal
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ function testFileAgeFilterRespectsMaxAge() returns error? {
}
},
path: "/home/in/advanced/age",
pollingInterval: 5,
pollingInterval: 2,
fileAgeFilter: {
maxAge: 60
},
Expand Down
175 changes: 130 additions & 45 deletions ballerina/tests/client_endpoint_negative_test.bal
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,11 @@ public function testConnectionWithNonExistingServer() returns error? {
auth: {credentials: {username: "wso2", password: "wso2123"}}
};
Client|Error nonExistingServerClientEp = new (nonExistingServerConfig);
if nonExistingServerClientEp is Error {
test:assertTrue(nonExistingServerClientEp is ConnectionError,
msg = "Expected ConnectionError when connecting to non-existing server");
if nonExistingServerClientEp is ConnectionError {
test:assertTrue(nonExistingServerClientEp.message().startsWith("Error while connecting to the FTP server with URL: "),
msg = "Unexpected error when tried to connect to a non existing server. " + nonExistingServerClientEp.message());
} else {
test:assertFail(msg = "Found a non-error response when tried to connect to a non existing server.");
}
}

Expand Down Expand Up @@ -60,11 +60,11 @@ public function testConnectionWithInvalidConfiguration() returns error? {
}
public function testReadNonExistingFile() returns error? {
stream<byte[] & readonly, io:Error?>|Error str = (<Client>clientEp)->get("/home/in/nonexisting.txt");
if str is Error {
test:assertTrue(str is FileNotFoundError,
msg = "Expected FileNotFoundError when reading non-existing file");
if str is FileNotFoundError {
test:assertEquals(str.message(), "Failed to read file: ftp://wso2:***@127.0.0.1:21212/home/in/nonexisting.txt not found",
msg = "Unexpected error during the `get` operation of an non-existing file.");
} else {
test:assertFail(msg = "Found a non-error response while accessing a non-existing file path");
}
}

Expand Down Expand Up @@ -101,11 +101,11 @@ public function testPutFileContentAtInvalidFileLocation() returns error? {
}
public function testIsDirectoryWithNonExistingDirectory() {
boolean|Error receivedError = (<Client>clientEp)->isDirectory("/home/in/nonexisting");
if receivedError is Error {
test:assertEquals(receivedError.message(), "/home/in/nonexisting does not exists to check if it is a directory.",
test:assertTrue(receivedError is FileNotFoundError,
msg = "Expected FileNotFoundError when checking isDirectory on non-existing path");
if receivedError is FileNotFoundError {
test:assertEquals(receivedError.message(), "/home/in/nonexisting does not exist to check if it is a directory.",
msg = "Unexpected error during the `isDirectory` operation of an non-existing directory. " + receivedError.message());
} else {
test:assertFail(msg = "Found a non-error response while accessing a non-existing directory path.");
}
}

Expand All @@ -130,11 +130,11 @@ public function testRenameNonExistingDirectory() {
string newName = "/home/in/differentDirectory";
Error? receivedError = (<Client>clientEp)->rename(existingName, newName);

if receivedError is Error {
test:assertTrue(receivedError is FileNotFoundError,
msg = "Expected FileNotFoundError when renaming non-existing file");
if receivedError is FileNotFoundError {
test:assertTrue(receivedError.message().startsWith("Failed to rename file: "),
msg = "Unexpected error during the `rename` operation of an non-existing file. " + receivedError.message());
} else {
test:assertFail(msg = "Found a non-error response while accessing a non existing directory path.");
}
}

Expand Down Expand Up @@ -169,11 +169,11 @@ public function testListFilesFromNonExistingDirectory() {
}
public function testDeleteFileAtNonExistingLocation() returns error? {
Error? receivedError = (<Client>clientEp)->delete("/nonExistingFile");
if receivedError is Error {
test:assertTrue(receivedError is FileNotFoundError,
msg = "Expected FileNotFoundError when deleting non-existing file");
if receivedError is FileNotFoundError {
test:assertTrue(receivedError.message().startsWith("Failed to delete file: "),
msg = "Unexpected error during the `delete` operation of an non-existing file. " + receivedError.message());
} else {
test:assertFail(msg = "Found a non-error response while accessing a non-existing file path.");
}
}

Expand All @@ -182,11 +182,11 @@ public function testDeleteFileAtNonExistingLocation() returns error? {
}
public function testRemoveDirectoryWithWrongUrl() {
Error? receivedError = (<Client>clientEp)->rmdir("/nonExistingDirectory");
if receivedError is Error {
test:assertTrue(receivedError is FileNotFoundError,
msg = "Expected FileNotFoundError when removing non-existing directory");
if receivedError is FileNotFoundError {
test:assertTrue(receivedError.message().startsWith("Failed to delete directory: "),
msg = "Unexpected error during the `rmdir` operation of a non-existing directory. " + receivedError.message());
} else {
test:assertFail(msg = "Found a non-error response while accessing a non-existing directory path.");
}
}

Expand Down Expand Up @@ -227,14 +227,14 @@ public function testConnectionWithNonExistingServerDetailedError() returns error
auth: {credentials: {username: "wso2", password: "wso2123"}}
};
Client|Error nonExistingServerClientEp = new (nonExistingServerConfig);
if nonExistingServerClientEp is Error {
test:assertTrue(nonExistingServerClientEp is ConnectionError,
msg = "Expected ConnectionError when connecting to non-existing server");
if nonExistingServerClientEp is ConnectionError {
test:assertTrue(nonExistingServerClientEp.message().startsWith("Error while connecting to the FTP server with URL: "),
msg = "Unexpected error when tried to connect to a non existing server. " + nonExistingServerClientEp.message());
// Verify that the error message contains additional details from the root cause
test:assertTrue(nonExistingServerClientEp.message().length() > "Error while connecting to the FTP server with URL: ftp://wso2:***@127.0.0.1:21299".length(),
msg = "Error message should contain detailed root cause information");
} else {
test:assertFail(msg = "Found a non-error response when tried to connect to a non existing server.");
}
}

Expand All @@ -249,14 +249,14 @@ public function testConnectionWithInvalidHostDetailedError() returns error? {
auth: {credentials: {username: "wso2", password: "wso2123"}}
};
Client|Error invalidHostClientEp = new (invalidHostConfig);
if invalidHostClientEp is Error {
test:assertTrue(invalidHostClientEp is ConnectionError,
msg = "Expected ConnectionError when connecting to invalid host");
if invalidHostClientEp is ConnectionError {
test:assertTrue(invalidHostClientEp.message().startsWith("Error while connecting to the FTP server with URL: "),
msg = "Unexpected error when tried to connect to an invalid host. " + invalidHostClientEp.message());
// Verify that the error message contains additional details from the root cause
test:assertTrue(invalidHostClientEp.message().length() > "Error while connecting to the FTP server with URL: ".length(),
msg = "Error message should contain detailed root cause information");
} else {
test:assertFail(msg = "Found a non-error response when tried to connect to an invalid host.");
}
}

Expand All @@ -266,12 +266,13 @@ public function testConnectionWithInvalidHostDetailedError() returns error? {
public function testMoveNonExistingFile() returns error? {
string nonExistingPath = "/home/in/nonexistent_file_for_move.txt";
string destinationPath = "/home/in/moved_nonexistent.txt";
// Note: VFS may not throw error for non-existing source in move/rename operations
// This test verifies that the operation completes without crashing
Error? result = (<Client>clientEp)->move(nonExistingPath, destinationPath);
// If the VFS implementation doesn't throw an error, that's also acceptable
// The important thing is the client handles it gracefully
test:assertTrue(true, msg = "Move non-existing file handled");
test:assertTrue(result is FileNotFoundError,
msg = "Expected FileNotFoundError when moving non-existing file");
if result is FileNotFoundError {
test:assertTrue(result.message().includes("not found"),
msg = "Error message should indicate file not found");
}
}

@test:Config {
Expand All @@ -280,12 +281,13 @@ public function testMoveNonExistingFile() returns error? {
public function testCopyNonExistingFile() returns error? {
string nonExistingPath = "/home/in/nonexistent_file_for_copy.txt";
string destinationPath = "/home/in/copied_nonexistent.txt";
// Note: VFS may not throw error for non-existing source in copy operations
// This test verifies that the operation completes without crashing
Error? result = (<Client>clientEp)->copy(nonExistingPath, destinationPath);
// If the VFS implementation doesn't throw an error, that's also acceptable
// The important thing is the client handles it gracefully
test:assertTrue(true, msg = "Copy non-existing file handled");
test:assertTrue(result is FileNotFoundError,
msg = "Expected FileNotFoundError when copying non-existing file");
if result is FileNotFoundError {
test:assertTrue(result.message().includes("not found"),
msg = "Error message should indicate file not found");
}
}

@test:Config {
Expand All @@ -294,27 +296,28 @@ public function testCopyNonExistingFile() returns error? {
public function testCopyToExistingFile() returns error? {
string sourcePath = "/home/in/test_copy_source.txt";
string destinationPath = "/home/in/test_copy_dest.txt";

// Clean up any existing files first
Error? cleanupResult1 = (<Client>clientEp)->delete(sourcePath);
Error? cleanupResult2 = (<Client>clientEp)->delete(destinationPath);

// Create source file
stream<io:Block, io:Error?> sourceStream = check io:fileReadBlocksAsStream(putFilePath, 5);
check (<Client>clientEp)->put(sourcePath, sourceStream);

// Create destination file
stream<io:Block, io:Error?> destStream = check io:fileReadBlocksAsStream(putFilePath, 5);
check (<Client>clientEp)->put(destinationPath, destStream);
// Copy to existing file should fail (VFS doesn't overwrite by default)

// Copy to existing file should fail with FileAlreadyExistsError
Error? result = (<Client>clientEp)->copy(sourcePath, destinationPath);
test:assertTrue(result is Error, msg = "Copy to existing file should fail");
if result is Error {
test:assertTrue(result.message().includes("already exists"),
test:assertTrue(result is FileAlreadyExistsError,
msg = "Expected FileAlreadyExistsError when copying to existing file");
if result is FileAlreadyExistsError {
test:assertTrue(result.message().includes("already exists"),
msg = "Error message should indicate file already exists");
}

// Cleanup
Error? cleanup1 = (<Client>clientEp)->delete(sourcePath);
Error? cleanup2 = (<Client>clientEp)->delete(destinationPath);
Expand All @@ -329,7 +332,89 @@ public function testExistsNonExistingFile() returns error? {
if result is boolean {
test:assertFalse(result, msg = "Non-existing file should return false");
} else {
test:assertFail(msg = "Expected boolean result for exists on non-existing file, got error: "
test:assertFail(msg = "Expected boolean result for exists on non-existing file, got error: "
+ result.message());
}
}

// Invalid Configuration Tests

@test:Config {}
public function testListenerWithInvalidFileNamePattern() returns error? {
ListenerConfiguration invalidPatternConfig = {
protocol: FTP,
host: "127.0.0.1",
port: 21212,
auth: {credentials: {username: "wso2", password: "wso2123"}},
path: "/home/in",
pollingInterval: 2,
fileNamePattern: "[invalid(regex" // Invalid regex pattern (unclosed bracket)
};
Listener|Error listenerResult = new (invalidPatternConfig);
test:assertTrue(listenerResult is InvalidConfigError,
msg = "Expected InvalidConfigError when creating listener with invalid fileNamePattern");
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("fileNamePattern"),
msg = "Error message should mention fileNamePattern field");
}
}

@test:Config {}
public function testListenerWithInvalidDependencyTargetPattern() returns error? {
ListenerConfiguration invalidDependencyConfig = {
protocol: FTP,
host: "127.0.0.1",
port: 21212,
auth: {credentials: {username: "wso2", password: "wso2123"}},
path: "/home/in",
pollingInterval: 2,
fileDependencyConditions: [
{
targetPattern: "*.txt[invalid", // Invalid regex pattern
requiredFiles: ["done.txt"],
matchingMode: ALL,
requiredFileCount: 1
}
]
};
Listener|Error listenerResult = new (invalidDependencyConfig);
test:assertTrue(listenerResult 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"),
msg = "Error message should mention targetPattern field");
}
}

@test:Config {}
public function testListenerWithInvalidDependencyRequiredFilePattern() returns error? {
ListenerConfiguration invalidRequiredFileConfig = {
protocol: FTP,
host: "127.0.0.1",
port: 21212,
auth: {credentials: {username: "wso2", password: "wso2123"}},
path: "/home/in",
pollingInterval: 2,
fileDependencyConditions: [
{
targetPattern: ".*\\.txt",
requiredFiles: ["done[unclosed"], // Invalid regex pattern
matchingMode: ALL,
requiredFileCount: 1
}
]
};
Listener|Error listenerResult = new (invalidRequiredFileConfig);
test:assertTrue(listenerResult 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"),
msg = "Error message should mention requiredFiles field");
}
}
4 changes: 2 additions & 2 deletions ballerina/tests/client_endpoint_test.bal
Original file line number Diff line number Diff line change
Expand Up @@ -938,7 +938,7 @@ public function testRenameDirectory() {
boolean|Error response2 = (<Client>clientEp)->isDirectory(existingName);
log:printInfo("Executed `isDirectory` operation on original directory after renaming a directory");
if response2 is Error {
test:assertEquals(response2.message(), "/home/in/out does not exists to check if it is a directory.",
test:assertEquals(response2.message(), "/home/in/out does not exist to check if it is a directory.",
msg = "Incorrect error message for non-existing file/directory at `isDirectory` operation");
} else {
test:assertFail("Error not created while invoking `isDirectory` operation after `rename` operation");
Expand Down Expand Up @@ -1247,7 +1247,7 @@ function testGenericRmdir(string path) returns error? {
i += 1;
}
if response2 is Error {
test:assertEquals(response2.message(), path + " does not exists to check if it is a directory.",
test:assertEquals(response2.message(), path + " does not exist to check if it is a directory.",
msg = "Incorrect error message for non-existing file/directory at `isDirectory` operation after `rmdir` operation");
} else {
// test:assertFail(msg = "Error not created while invoking `isDirectory` operation after `rmdir` operation on " + path );
Expand Down
Loading