Skip to main content
Remote services are internet-facing services that can be accessed publicly. Unlike self-hosted services, remote services don’t require deployment and only establish a connection to the external endpoint.

Remote service characteristics

Remote services:
  • No deployment required - Services are already running and accessible
  • Require credentials - Need valid account credentials to connect
  • Internet connectivity - Tests must have network access to the service endpoint
  • Lifecycle management - Only handle connection setup and teardown

Common remote services

Cloud providers

AWS, Azure, Google Cloud Platform

SaaS platforms

Salesforce, Jira, ServiceNow, Slack

Communication

Telegram, Microsoft Exchange, Gmail

Webhooks

Webhook.site for webhook testing

Implementing a remote service

Here’s the structure of a typical remote service implementation:
/home/daytona/workspace/source/system-x/services/salesforce/src/main/java/software/tnb/salesforce/service/Salesforce.java
@AutoService(Salesforce.class)
public class Salesforce extends Service<SalesforceAccount, ForceApi, SalesforceValidation> {
    private static final Logger LOG = LoggerFactory.getLogger(Salesforce.class);

    protected ForceApi client() {
        if (client == null) {
            client = new ForceApi(new ApiConfig()
                .setClientId(account().clientId())
                .setClientSecret(account().clientSecret())
                .setUsername(account().userName())
                .setPassword(account().password())
                .setForceURL(account().loginUrl()));
        }
        return client;
    }

    public SalesforceValidation validation() {
        if (validation == null) {
            LOG.debug("Creating new Salesforce validation");
            validation = new SalesforceValidation(client());
        }
        return validation;
    }

    @Override
    public void afterAll(ExtensionContext extensionContext) throws Exception {
        // Cleanup resources if needed
    }

    @Override
    public void beforeAll(ExtensionContext extensionContext) throws Exception {
        // Initialize connection
    }
}

Key implementation details

The @AutoService annotation

@AutoService(Salesforce.class)
public class Salesforce extends Service<...> {
    // Implementation
}
The @AutoService annotation enables automatic service discovery via Java’s ServiceLoader mechanism. This allows ServiceFactory to locate and instantiate the service.

Service lifecycle methods

Remote services implement JUnit 5 lifecycle callbacks:
1

beforeAll()

Called before any tests run. Initialize the client connection and create the validation instance.
2

Test execution

Tests interact with the service through the validation API.
3

afterAll()

Called after all tests complete. Close client connections and cleanup resources.

Account management

Remote services require credentials stored in an Account class. The account is loaded automatically:
// Account is loaded via AccountFactory
public A account() {
    if (account == null) {
        Class<A> accountClass = (Class<A>) ReflectionUtil.getGenericTypesOf(Service.class, this.getClass())[0];
        account = AccountFactory.create(accountClass);
    }
    return account;
}

Configuring credentials

Remote services need credentials to connect. TNB supports multiple credential sources:

YAML credentials file

Create a credentials file and reference it with the test.credentials.file property:
services:
    salesforce:
        credentials:
            client_id: your_client_id
            client_secret: your_client_secret
            user_name: your_username
            password: your_password
            login_url: https://login.salesforce.com
    aws:
        credentials:
            access_key: AKIA...
            secret_key: xyz...
            region: us-east-1
            account_id: "123456789012"
    jira:
        credentials:
            username: [email protected]
            password: your_api_token
            url: https://your-domain.atlassian.net
Then run your tests with:
mvn test -Dtest.credentials.file=/path/to/credentials.yaml

HashiCorp Vault

Load credentials from Vault by setting these properties:
test.credentials.use.vault=true
test.credentials.vault.address=https://vault.example.com
test.credentials.vault.token=your_vault_token
# Or use role-based authentication:
test.credentials.vault.role.id=your_role_id
test.credentials.vault.secret.id=your_secret_id

# Path pattern where credentials are stored
test.credentials.vault.path.pattern=/secret/services/%s/credentials

System property YAML

Pass credentials directly as a YAML string:
mvn test -Dtest.credentials="services:
  aws:
    credentials:
      access_key: AKIA..."

Overriding account IDs

Each account has a default ID that matches the credential key in the YAML file. Override it using:
# Override AWS account ID lookup
-Dtnb.awsaccount.id=aws-production

Example: Using AWS S3

Here’s a complete example of using the AWS S3 remote service:
1

Create credentials file

credentials.yaml
services:
    aws:
        credentials:
            access_key: AKIA...
            secret_key: abc123...
            region: us-east-1
            account_id: "123456789012"
2

Write the test

import software.tnb.aws.s3.service.S3;
import software.tnb.common.service.ServiceFactory;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

public class S3Test {
    @RegisterExtension
    public static S3 s3 = ServiceFactory.create(S3.class);

    @Test
    public void testS3Upload() {
        String bucket = "my-test-bucket";
        String key = "test-file.txt";
        String content = "Hello S3!";
        
        // Create bucket
        s3.validation().createBucket(bucket);
        
        // Upload file
        s3.validation().putObject(bucket, key, content);
        
        // Verify
        String retrieved = s3.validation().getObject(bucket, key);
        assertEquals(content, retrieved);
    }
}
3

Run the test

mvn test -Dtest.credentials.file=credentials.yaml

Example: Using Salesforce

import software.tnb.salesforce.service.Salesforce;
import software.tnb.common.service.ServiceFactory;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

public class SalesforceTest {
    @RegisterExtension
    public static Salesforce salesforce = ServiceFactory.create(Salesforce.class);

    @Test
    public void testCreateAccount() {
        // Create a Salesforce Account record
        Map<String, Object> account = new HashMap<>();
        account.put("Name", "Test Company");
        account.put("Industry", "Technology");
        
        String accountId = salesforce.validation().createRecord("Account", account);
        assertNotNull(accountId);
        
        // Query the account
        Map<String, Object> result = salesforce.validation()
            .queryRecord("SELECT Name, Industry FROM Account WHERE Id = '" + accountId + "'");
        
        assertEquals("Test Company", result.get("Name"));
    }
}

Composite accounts

Some services may share credentials with a parent service. TNB supports composite accounts:
public class ParentAccount implements Account, WithId {
    private String parentKey;

    @Override
    public String credentialsId() {
        return "parent";
    }
}

public class ChildAccount extends ParentAccount {
    private String childKey;

    @Override
    public String credentialsId() {
        return "child";
    }
}
services:
    parent:
        credentials:
            parentKey: parentValue
    child:
        credentials:
            childKey: childValue
When AccountFactory.create(ChildAccount.class) is called, both parent and child credentials are merged.
Never commit credentials to your source control. Always use external credential management through YAML files, Vault, or environment variables.

Best practices

Use Vault for CI/CD

Store credentials in HashiCorp Vault for automated testing environments

Validate connections early

Add connection validation in beforeAll() to fail fast if credentials are invalid

Implement cleanup

Clean up created resources in afterAll() to avoid accumulating test data

Handle rate limits

Implement retry logic and backoff for services with rate limiting

Next steps

Self-hosted services

Learn about services that require deployment

Creating services

Build your own custom System-X service

Build docs developers (and LLMs) love