diff --git a/README.md b/README.md index 97db32f..f0fa2ff 100644 --- a/README.md +++ b/README.md @@ -64,42 +64,32 @@ Truelist client = Truelist.builder("your-api-key") .baseUrl("https://api.truelist.io") // default .timeout(Duration.ofSeconds(10)) // default: 30s .maxRetries(2) // default: 2 - .formApiKey("your-form-key") // optional, for formValidate() .build(); ``` The client is thread-safe and should be reused across your application. -### Validate an Email (Server-side) - -Use `validate()` with your server API key for backend validation. Rate limit: 10 requests/second. +### Validate an Email ```java ValidationResult result = client.validate("user@example.com"); -result.getState(); // "valid", "invalid", "risky", or "unknown" -result.getSubState(); // "ok", "disposable_address", "role_address", etc. +result.getEmail(); // "user@example.com" +result.getDomain(); // "example.com" +result.getCanonical(); // "user" +result.getState(); // "ok", "email_invalid", "accept_all", or "unknown" +result.getSubState(); // "email_ok", "is_disposable", "is_role", etc. result.getSuggestion(); // suggested correction, or null -result.isValid(); // true if state is "valid" -result.isValid(true); // true if state is "valid" or "risky" -result.isInvalid(); // true if state is "invalid" -result.isRisky(); // true if state is "risky" +result.getMxRecord(); // MX record for the domain, or null +result.getFirstName(); // first name, or null +result.getLastName(); // last name, or null +result.getVerifiedAt(); // verification timestamp +result.isValid(); // true if state is "ok" +result.isInvalid(); // true if state is "email_invalid" +result.isAcceptAll(); // true if state is "accept_all" result.isUnknown(); // true if state is "unknown" -result.isFreeEmail(); // true if free email provider (Gmail, Yahoo, etc.) -result.isRole(); // true if role address (admin@, support@, etc.) -result.isDisposable(); // true if disposable/temporary address -``` - -### Validate an Email (Frontend/Form) - -Use `formValidate()` with your form API key for frontend validation. Rate limit: 60 requests/minute. - -```java -Truelist client = Truelist.builder("your-server-key") - .formApiKey("your-form-key") - .build(); - -ValidationResult result = client.formValidate("user@example.com"); +result.isDisposable(); // true if sub-state is "is_disposable" +result.isRole(); // true if sub-state is "is_role" ``` ### Get Account Info @@ -109,34 +99,34 @@ import io.truelist.AccountInfo; AccountInfo account = client.getAccount(); -account.getEmail(); // "you@company.com" -account.getPlan(); // "pro" -account.getCredits(); // 9542 +account.getEmail(); // "you@company.com" +account.getName(); // "Your Name" +account.getUuid(); // "a3828d19-..." +account.getTimeZone(); // "America/New_York" +account.isAdminRole(); // true/false +account.getPlan(); // "pro" (shortcut) +account.getAccount().getName(); // "Company Inc" +account.getAccount().getPaymentPlan(); // "pro" ``` ## Response States -| State | Description | -|-----------|--------------------------------------| -| `valid` | Email address is valid | -| `invalid` | Email address is invalid | -| `risky` | Email exists but may have issues | -| `unknown` | Could not determine validity | +| State | Description | +|-----------------|--------------------------------------| +| `ok` | Email address is valid | +| `email_invalid` | Email address is invalid | +| `accept_all` | Domain accepts all addresses | +| `unknown` | Could not determine validity | ### Sub-states -| Sub-state | Description | -|------------------------|------------------------------------| -| `ok` | Valid, no issues | -| `accept_all` | Domain accepts all addresses | -| `disposable_address` | Disposable/temporary email | -| `role_address` | Role-based address (admin@, etc.) | -| `failed_mx_check` | Domain has no MX records | -| `failed_spam_trap` | Known spam trap address | -| `failed_no_mailbox` | Mailbox does not exist | -| `failed_greylisted` | Server temporarily rejected | -| `failed_syntax_check` | Invalid email syntax | -| `unknown` | Could not determine | +| Sub-state | Description | +|---------------------|------------------------------------| +| `email_ok` | Valid, no issues | +| `is_disposable` | Disposable/temporary email | +| `is_role` | Role-based address (admin@, etc.) | +| `failed_smtp_check` | SMTP check failed | +| `unknown_error` | Could not determine | ## Error Handling @@ -149,23 +139,23 @@ try { // Invalid API key (HTTP 401) - always thrown, never retried System.err.println("Bad API key: " + e.getMessage()); } catch (RateLimitException e) { - // Too many requests (HTTP 429) - never retried + // Too many requests (HTTP 429) System.err.println("Slow down: " + e.getMessage()); } catch (ApiException e) { - // Server error (5xx) or unexpected status - retried per maxRetries config + // Server error (5xx) or unexpected status System.err.println("API error (" + e.getStatusCode() + "): " + e.getMessage()); } catch (TruelistException e) { - // Network error or other failure - retried per maxRetries config + // Network error or other failure System.err.println("Request failed: " + e.getMessage()); } ``` ### Retry Behavior -The client automatically retries on transient errors (5xx server errors, network failures) with exponential backoff. Configure with `maxRetries()`: +The client automatically retries on transient errors with exponential backoff. Configure with `maxRetries()`: - **Authentication errors (401)**: Never retried, always thrown immediately -- **Rate limit errors (429)**: Never retried, always thrown immediately +- **Rate limit errors (429)**: Retried up to `maxRetries` times - **Server errors (5xx)**: Retried up to `maxRetries` times - **Network errors**: Retried up to `maxRetries` times diff --git a/src/main/java/io/truelist/AccountInfo.java b/src/main/java/io/truelist/AccountInfo.java index 78faa8b..08c7c4d 100644 --- a/src/main/java/io/truelist/AccountInfo.java +++ b/src/main/java/io/truelist/AccountInfo.java @@ -1,25 +1,73 @@ package io.truelist; +import com.google.gson.annotations.SerializedName; + /** * Represents account information from the Truelist API. */ public class AccountInfo { private final String email; - private final String plan; - private final int credits; + private final String name; + private final String uuid; + + @SerializedName("time_zone") + private final String timeZone; + + @SerializedName("is_admin_role") + private final boolean adminRole; + + private final Account account; + + /** + * Nested account details. + */ + public static class Account { + private final String name; + + @SerializedName("payment_plan") + private final String paymentPlan; + + public Account(String name, String paymentPlan) { + this.name = name; + this.paymentPlan = paymentPlan; + } + + public String getName() { + return name; + } + + public String getPaymentPlan() { + return paymentPlan; + } + + @Override + public String toString() { + return "Account{" + + "name='" + name + '\'' + + ", paymentPlan='" + paymentPlan + '\'' + + '}'; + } + } /** * Creates a new AccountInfo. * - * @param email the account email address - * @param plan the account plan name - * @param credits the number of remaining credits + * @param email the account email address + * @param name the user name + * @param uuid the user UUID + * @param timeZone the user's time zone + * @param adminRole whether the user has admin role + * @param account the nested account details */ - public AccountInfo(String email, String plan, int credits) { + public AccountInfo(String email, String name, String uuid, + String timeZone, boolean adminRole, Account account) { this.email = email; - this.plan = plan; - this.credits = credits; + this.name = name; + this.uuid = uuid; + this.timeZone = timeZone; + this.adminRole = adminRole; + this.account = account; } /** @@ -32,29 +80,68 @@ public String getEmail() { } /** - * Returns the account plan name. + * Returns the user name. * - * @return the plan name + * @return the name */ - public String getPlan() { - return plan; + public String getName() { + return name; } /** - * Returns the number of remaining validation credits. + * Returns the user UUID. * - * @return the credit count + * @return the UUID */ - public int getCredits() { - return credits; + public String getUuid() { + return uuid; + } + + /** + * Returns the user's time zone. + * + * @return the time zone + */ + public String getTimeZone() { + return timeZone; + } + + /** + * Returns whether the user has admin role. + * + * @return true if admin + */ + public boolean isAdminRole() { + return adminRole; + } + + /** + * Returns the nested account details. + * + * @return the account + */ + public Account getAccount() { + return account; + } + + /** + * Returns the payment plan from the nested account. + * + * @return the plan name, or null if account is null + */ + public String getPlan() { + return account != null ? account.getPaymentPlan() : null; } @Override public String toString() { return "AccountInfo{" + "email='" + email + '\'' + - ", plan='" + plan + '\'' + - ", credits=" + credits + + ", name='" + name + '\'' + + ", uuid='" + uuid + '\'' + + ", timeZone='" + timeZone + '\'' + + ", adminRole=" + adminRole + + ", account=" + account + '}'; } } diff --git a/src/main/java/io/truelist/Truelist.java b/src/main/java/io/truelist/Truelist.java index 96949b1..67c0cfd 100644 --- a/src/main/java/io/truelist/Truelist.java +++ b/src/main/java/io/truelist/Truelist.java @@ -1,7 +1,6 @@ package io.truelist; import com.google.gson.Gson; -import com.google.gson.JsonObject; import io.truelist.exceptions.ApiException; import io.truelist.exceptions.AuthenticationException; import io.truelist.exceptions.RateLimitException; @@ -9,10 +8,13 @@ import java.io.IOException; import java.net.URI; +import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; import java.time.Duration; +import java.util.List; /** * Main client for the Truelist email validation API. @@ -70,10 +72,7 @@ public static Builder builder(String apiKey) { } /** - * Validates an email address using server-side validation. - * - *

Uses the server API key for authentication. This endpoint is rate-limited - * to 10 requests per second.

+ * Validates an email address. * * @param email the email address to validate * @return the validation result @@ -88,56 +87,17 @@ public ValidationResult validate(String email) { throw new IllegalArgumentException("Email must not be null or blank"); } - JsonObject body = new JsonObject(); - body.addProperty("email", email); - - String responseBody = executeWithRetry( - "/api/v1/verify", - config.getApiKey(), - body.toString() + String encodedEmail = URLEncoder.encode(email, StandardCharsets.UTF_8); + String responseBody = executePostWithRetry( + "/api/v1/verify_inline?email=" + encodedEmail, + config.getApiKey() ); - return gson.fromJson(responseBody, ValidationResult.class); - } - - /** - * Validates an email address using frontend/form validation. - * - *

Uses the form API key for authentication. This endpoint is rate-limited - * to 60 requests per minute. A form API key must be configured via - * {@link Builder#formApiKey(String)}.

- * - * @param email the email address to validate - * @return the validation result - * @throws AuthenticationException if the form API key is invalid (401) - * @throws RateLimitException if the rate limit is exceeded (429) - * @throws ApiException if the API returns an unexpected error - * @throws TruelistException if a network or other error occurs - * @throws IllegalStateException if no form API key is configured - * @throws IllegalArgumentException if email is null or blank - */ - public ValidationResult formValidate(String email) { - if (email == null || email.trim().isEmpty()) { - throw new IllegalArgumentException("Email must not be null or blank"); - } - - String formApiKey = config.getFormApiKey(); - if (formApiKey == null || formApiKey.trim().isEmpty()) { - throw new IllegalStateException( - "Form API key is required for form validation. " + - "Configure it via Truelist.builder(apiKey).formApiKey(formKey).build()"); + VerifyResponse wrapper = gson.fromJson(responseBody, VerifyResponse.class); + if (wrapper.emails == null || wrapper.emails.isEmpty()) { + throw new ApiException("No results returned from API", 0); } - - JsonObject body = new JsonObject(); - body.addProperty("email", email); - - String responseBody = executeWithRetry( - "/api/v1/form_verify", - formApiKey, - body.toString() - ); - - return gson.fromJson(responseBody, ValidationResult.class); + return wrapper.emails.get(0); } /** @@ -150,7 +110,7 @@ public ValidationResult formValidate(String email) { * @throws TruelistException if a network or other error occurs */ public AccountInfo getAccount() { - String responseBody = executeGetWithRetry("/api/v1/account", config.getApiKey()); + String responseBody = executeGetWithRetry("/me", config.getApiKey()); return gson.fromJson(responseBody, AccountInfo.class); } @@ -163,7 +123,12 @@ public TruelistConfig getConfig() { return config; } - private String executeWithRetry(String path, String token, String jsonBody) { + /** Wrapper for the verify response envelope. */ + private static class VerifyResponse { + List emails; + } + + private String executePostWithRetry(String path, String token) { int attempts = 0; int maxAttempts = config.getMaxRetries() + 1; @@ -173,10 +138,9 @@ private String executeWithRetry(String path, String token, String jsonBody) { HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(config.getBaseUrl() + path)) .header("Authorization", "Bearer " + token) - .header("Content-Type", "application/json") .header("Accept", "application/json") .timeout(config.getTimeout()) - .POST(HttpRequest.BodyPublishers.ofString(jsonBody)) + .POST(HttpRequest.BodyPublishers.noBody()) .build(); HttpResponse response = httpClient.send(request, @@ -299,7 +263,6 @@ private void sleep(int attempt) { public static class Builder { private final String apiKey; - private String formApiKey; private String baseUrl = DEFAULT_BASE_URL; private Duration timeout = DEFAULT_TIMEOUT; private int maxRetries = DEFAULT_MAX_RETRIES; @@ -355,17 +318,6 @@ public Builder maxRetries(int maxRetries) { return this; } - /** - * Sets the form API key for frontend validation via {@link Truelist#formValidate(String)}. - * - * @param formApiKey the form API key - * @return this builder - */ - public Builder formApiKey(String formApiKey) { - this.formApiKey = formApiKey; - return this; - } - /** * Builds and returns a new {@link Truelist} client instance. * @@ -373,7 +325,7 @@ public Builder formApiKey(String formApiKey) { */ public Truelist build() { TruelistConfig config = new TruelistConfig( - apiKey, formApiKey, baseUrl, timeout, maxRetries); + apiKey, baseUrl, timeout, maxRetries); return new Truelist(config); } } diff --git a/src/main/java/io/truelist/TruelistConfig.java b/src/main/java/io/truelist/TruelistConfig.java index 2efb543..210fc48 100644 --- a/src/main/java/io/truelist/TruelistConfig.java +++ b/src/main/java/io/truelist/TruelistConfig.java @@ -10,15 +10,13 @@ public class TruelistConfig { private final String apiKey; - private final String formApiKey; private final String baseUrl; private final Duration timeout; private final int maxRetries; - TruelistConfig(String apiKey, String formApiKey, String baseUrl, + TruelistConfig(String apiKey, String baseUrl, Duration timeout, int maxRetries) { this.apiKey = apiKey; - this.formApiKey = formApiKey; this.baseUrl = baseUrl; this.timeout = timeout; this.maxRetries = maxRetries; @@ -33,15 +31,6 @@ public String getApiKey() { return apiKey; } - /** - * Returns the form API key for frontend validation, or null if not set. - * - * @return the form API key - */ - public String getFormApiKey() { - return formApiKey; - } - /** * Returns the base URL for the API. * diff --git a/src/main/java/io/truelist/ValidationResult.java b/src/main/java/io/truelist/ValidationResult.java index 815e597..2dd6867 100644 --- a/src/main/java/io/truelist/ValidationResult.java +++ b/src/main/java/io/truelist/ValidationResult.java @@ -4,50 +4,125 @@ /** * Represents the result of an email validation request. - * - *

The result contains the validation state, sub-state, and various flags - * indicating properties of the email address.

*/ public class ValidationResult { - private final String state; + @SerializedName("address") + private final String email; - @SerializedName("sub_state") - private final String subState; + private final String domain; - private final String suggestion; + private final String canonical; + + @SerializedName("mx_record") + private final String mxRecord; - @SerializedName("free_email") - private final boolean freeEmail; + @SerializedName("first_name") + private final String firstName; - private final boolean role; + @SerializedName("last_name") + private final String lastName; + + @SerializedName("email_state") + private final String state; + + @SerializedName("email_sub_state") + private final String subState; - private final boolean disposable; + @SerializedName("verified_at") + private final String verifiedAt; + + @SerializedName("did_you_mean") + private final String suggestion; /** * Creates a new ValidationResult. * - * @param state the validation state (valid, invalid, risky, unknown) + * @param email the email address + * @param domain the email domain + * @param canonical the canonical local part + * @param mxRecord the MX record, or null + * @param firstName the first name, or null + * @param lastName the last name, or null + * @param state the validation state (ok, email_invalid, accept_all, unknown) * @param subState the validation sub-state + * @param verifiedAt the verification timestamp * @param suggestion a suggested correction, or null - * @param freeEmail whether the email is from a free provider - * @param role whether the email is a role address - * @param disposable whether the email is a disposable address */ - public ValidationResult(String state, String subState, String suggestion, - boolean freeEmail, boolean role, boolean disposable) { + public ValidationResult(String email, String domain, String canonical, + String mxRecord, String firstName, String lastName, + String state, String subState, String verifiedAt, + String suggestion) { + this.email = email; + this.domain = domain; + this.canonical = canonical; + this.mxRecord = mxRecord; + this.firstName = firstName; + this.lastName = lastName; this.state = state; this.subState = subState; + this.verifiedAt = verifiedAt; this.suggestion = suggestion; - this.freeEmail = freeEmail; - this.role = role; - this.disposable = disposable; + } + + /** + * Returns the email address. + * + * @return the email address + */ + public String getEmail() { + return email; + } + + /** + * Returns the email domain. + * + * @return the domain + */ + public String getDomain() { + return domain; + } + + /** + * Returns the canonical local part of the email. + * + * @return the canonical part + */ + public String getCanonical() { + return canonical; + } + + /** + * Returns the MX record for the domain, or null. + * + * @return the MX record + */ + public String getMxRecord() { + return mxRecord; + } + + /** + * Returns the first name associated with the email, or null. + * + * @return the first name + */ + public String getFirstName() { + return firstName; + } + + /** + * Returns the last name associated with the email, or null. + * + * @return the last name + */ + public String getLastName() { + return lastName; } /** * Returns the validation state. * - * @return one of "valid", "invalid", "risky", or "unknown" + * @return one of "ok", "email_invalid", "accept_all", or "unknown" */ public String getState() { return state; @@ -56,12 +131,21 @@ public String getState() { /** * Returns the validation sub-state providing more detail about the result. * - * @return the sub-state (e.g., "ok", "disposable_address", "role_address") + * @return the sub-state (e.g., "email_ok", "is_disposable", "is_role") */ public String getSubState() { return subState; } + /** + * Returns the verification timestamp. + * + * @return the verified_at timestamp string + */ + public String getVerifiedAt() { + return verifiedAt; + } + /** * Returns a suggested correction for the email address, if any. * @@ -74,41 +158,28 @@ public String getSuggestion() { /** * Returns whether the email is valid. * - * @return true if the state is "valid" + * @return true if the state is "ok" */ public boolean isValid() { - return "valid".equals(state); - } - - /** - * Returns whether the email is valid, optionally treating risky emails as valid. - * - * @param allowRisky if true, both "valid" and "risky" states are considered valid - * @return true if the email passes validation - */ - public boolean isValid(boolean allowRisky) { - if (allowRisky) { - return "valid".equals(state) || "risky".equals(state); - } - return "valid".equals(state); + return "ok".equals(state); } /** * Returns whether the email is invalid. * - * @return true if the state is "invalid" + * @return true if the state is "email_invalid" */ public boolean isInvalid() { - return "invalid".equals(state); + return "email_invalid".equals(state); } /** - * Returns whether the email is risky. + * Returns whether the domain accepts all addresses. * - * @return true if the state is "risky" + * @return true if the state is "accept_all" */ - public boolean isRisky() { - return "risky".equals(state); + public boolean isAcceptAll() { + return "accept_all".equals(state); } /** @@ -121,41 +192,36 @@ public boolean isUnknown() { } /** - * Returns whether the email is from a free email provider. + * Returns whether the email is a disposable/temporary address. * - * @return true if the email is free + * @return true if the sub-state is "is_disposable" */ - public boolean isFreeEmail() { - return freeEmail; + public boolean isDisposable() { + return "is_disposable".equals(subState); } /** * Returns whether the email is a role address (e.g., admin@, support@). * - * @return true if the email is a role address + * @return true if the sub-state is "is_role" */ public boolean isRole() { - return role; - } - - /** - * Returns whether the email is a disposable/temporary address. - * - * @return true if the email is disposable - */ - public boolean isDisposable() { - return disposable; + return "is_role".equals(subState); } @Override public String toString() { return "ValidationResult{" + - "state='" + state + '\'' + + "email='" + email + '\'' + + ", domain='" + domain + '\'' + + ", canonical='" + canonical + '\'' + + ", mxRecord='" + mxRecord + '\'' + + ", firstName='" + firstName + '\'' + + ", lastName='" + lastName + '\'' + + ", state='" + state + '\'' + ", subState='" + subState + '\'' + + ", verifiedAt='" + verifiedAt + '\'' + ", suggestion='" + suggestion + '\'' + - ", freeEmail=" + freeEmail + - ", role=" + role + - ", disposable=" + disposable + '}'; } } diff --git a/src/test/java/io/truelist/AccountInfoTest.java b/src/test/java/io/truelist/AccountInfoTest.java index df85868..ca88b04 100644 --- a/src/test/java/io/truelist/AccountInfoTest.java +++ b/src/test/java/io/truelist/AccountInfoTest.java @@ -8,37 +8,53 @@ class AccountInfoTest { @Test void gettersReturnCorrectValues() { - AccountInfo account = new AccountInfo("user@company.com", "pro", 9542); - - assertEquals("user@company.com", account.getEmail()); - assertEquals("pro", account.getPlan()); - assertEquals(9542, account.getCredits()); + AccountInfo.Account account = new AccountInfo.Account("Company Inc", "pro"); + AccountInfo info = new AccountInfo( + "team@company.com", "Team Lead", + "a3828d19-1234-5678-9abc-def012345678", + "America/New_York", true, account); + + assertEquals("team@company.com", info.getEmail()); + assertEquals("Team Lead", info.getName()); + assertEquals("a3828d19-1234-5678-9abc-def012345678", info.getUuid()); + assertEquals("America/New_York", info.getTimeZone()); + assertTrue(info.isAdminRole()); + assertNotNull(info.getAccount()); + assertEquals("Company Inc", info.getAccount().getName()); + assertEquals("pro", info.getAccount().getPaymentPlan()); + assertEquals("pro", info.getPlan()); } @Test void toStringContainsAllFields() { - AccountInfo account = new AccountInfo("user@company.com", "starter", 100); + AccountInfo.Account account = new AccountInfo.Account("Company", "starter"); + AccountInfo info = new AccountInfo( + "user@company.com", "User", + "uuid-123", "UTC", false, account); - String str = account.toString(); + String str = info.toString(); assertTrue(str.contains("user@company.com")); - assertTrue(str.contains("starter")); - assertTrue(str.contains("100")); + assertTrue(str.contains("User")); + assertTrue(str.contains("uuid-123")); + assertTrue(str.contains("UTC")); } @Test - void zeroCredits() { - AccountInfo account = new AccountInfo("user@example.com", "free", 0); - assertEquals(0, account.getCredits()); + void getPlanReturnsNullWithoutAccount() { + AccountInfo info = new AccountInfo( + "user@example.com", "User", + "uuid", "UTC", false, null); + assertNull(info.getPlan()); } @Test void differentPlans() { - AccountInfo free = new AccountInfo("a@b.com", "free", 10); - AccountInfo pro = new AccountInfo("a@b.com", "pro", 5000); - AccountInfo enterprise = new AccountInfo("a@b.com", "enterprise", 100000); + AccountInfo.Account free = new AccountInfo.Account("Co", "free"); + AccountInfo.Account pro = new AccountInfo.Account("Co", "pro"); + AccountInfo.Account enterprise = new AccountInfo.Account("Co", "enterprise"); - assertEquals("free", free.getPlan()); - assertEquals("pro", pro.getPlan()); - assertEquals("enterprise", enterprise.getPlan()); + assertEquals("free", free.getPaymentPlan()); + assertEquals("pro", pro.getPaymentPlan()); + assertEquals("enterprise", enterprise.getPaymentPlan()); } } diff --git a/src/test/java/io/truelist/TruelistTest.java b/src/test/java/io/truelist/TruelistTest.java index 8314bb1..d81d7f8 100644 --- a/src/test/java/io/truelist/TruelistTest.java +++ b/src/test/java/io/truelist/TruelistTest.java @@ -5,7 +5,6 @@ import io.truelist.exceptions.ApiException; import io.truelist.exceptions.AuthenticationException; import io.truelist.exceptions.RateLimitException; -import io.truelist.exceptions.TruelistException; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -20,6 +19,52 @@ class TruelistTest { private WireMockServer wireMock; private Truelist client; + private static final String VALID_RESPONSE = "{\"emails\":[{" + + "\"address\":\"user@example.com\"," + + "\"domain\":\"example.com\"," + + "\"canonical\":\"user\"," + + "\"mx_record\":null," + + "\"first_name\":null," + + "\"last_name\":null," + + "\"email_state\":\"ok\"," + + "\"email_sub_state\":\"email_ok\"," + + "\"verified_at\":\"2026-02-21T10:00:00.000Z\"," + + "\"did_you_mean\":null}]}"; + + private static final String INVALID_RESPONSE = "{\"emails\":[{" + + "\"address\":\"bad@example.com\"," + + "\"domain\":\"example.com\"," + + "\"canonical\":\"bad\"," + + "\"mx_record\":null," + + "\"first_name\":null," + + "\"last_name\":null," + + "\"email_state\":\"email_invalid\"," + + "\"email_sub_state\":\"failed_smtp_check\"," + + "\"verified_at\":\"2026-02-21T10:00:00.000Z\"," + + "\"did_you_mean\":null}]}"; + + private static final String DISPOSABLE_RESPONSE = "{\"emails\":[{" + + "\"address\":\"user@tempmail.com\"," + + "\"domain\":\"tempmail.com\"," + + "\"canonical\":\"user\"," + + "\"mx_record\":null," + + "\"first_name\":null," + + "\"last_name\":null," + + "\"email_state\":\"ok\"," + + "\"email_sub_state\":\"is_disposable\"," + + "\"verified_at\":\"2026-02-21T10:00:00.000Z\"," + + "\"did_you_mean\":null}]}"; + + private static final String ACCOUNT_RESPONSE = "{" + + "\"email\":\"team@company.com\"," + + "\"name\":\"Team Lead\"," + + "\"uuid\":\"a3828d19-1234-5678-9abc-def012345678\"," + + "\"time_zone\":\"America/New_York\"," + + "\"is_admin_role\":true," + + "\"token\":\"test_token\"," + + "\"api_keys\":[]," + + "\"account\":{\"name\":\"Company Inc\",\"payment_plan\":\"pro\",\"users\":[]}}"; + @BeforeEach void setUp() { wireMock = new WireMockServer(WireMockConfiguration.wireMockConfig().dynamicPort()); @@ -57,7 +102,6 @@ void builderSetsDefaults() { assertEquals("https://api.truelist.io", config.getBaseUrl()); assertEquals(Duration.ofSeconds(30), config.getTimeout()); assertEquals(2, config.getMaxRetries()); - assertNull(config.getFormApiKey()); } @Test @@ -66,7 +110,6 @@ void builderConfiguresAllOptions() { .baseUrl("https://custom.api.com") .timeout(Duration.ofSeconds(15)) .maxRetries(5) - .formApiKey("form-key") .build(); TruelistConfig config = customClient.getConfig(); @@ -74,7 +117,6 @@ void builderConfiguresAllOptions() { assertEquals("https://custom.api.com", config.getBaseUrl()); assertEquals(Duration.ofSeconds(15), config.getTimeout()); assertEquals(5, config.getMaxRetries()); - assertEquals("form-key", config.getFormApiKey()); } @Test @@ -95,104 +137,109 @@ void builderRejectsNegativeMaxRetries() { @Test void validateReturnsValidResult() { - wireMock.stubFor(post(urlEqualTo("/api/v1/verify")) + wireMock.stubFor(post(urlPathEqualTo("/api/v1/verify_inline")) + .withQueryParam("email", equalTo("user@example.com")) .withHeader("Authorization", equalTo("Bearer test-api-key")) - .withHeader("Content-Type", equalTo("application/json")) - .willReturn(okJson("{" + - "\"state\":\"valid\"," + - "\"sub_state\":\"ok\"," + - "\"suggestion\":null," + - "\"free_email\":true," + - "\"role\":false," + - "\"disposable\":false" + - "}"))); - - ValidationResult result = client.validate("user@gmail.com"); - - assertEquals("valid", result.getState()); - assertEquals("ok", result.getSubState()); + .willReturn(okJson(VALID_RESPONSE))); + + ValidationResult result = client.validate("user@example.com"); + + assertEquals("user@example.com", result.getEmail()); + assertEquals("example.com", result.getDomain()); + assertEquals("user", result.getCanonical()); + assertEquals("ok", result.getState()); + assertEquals("email_ok", result.getSubState()); assertNull(result.getSuggestion()); assertTrue(result.isValid()); assertFalse(result.isInvalid()); - assertFalse(result.isRisky()); + assertFalse(result.isAcceptAll()); assertFalse(result.isUnknown()); - assertTrue(result.isFreeEmail()); - assertFalse(result.isRole()); - assertFalse(result.isDisposable()); } @Test void validateReturnsInvalidResult() { - wireMock.stubFor(post(urlEqualTo("/api/v1/verify")) - .willReturn(okJson("{" + - "\"state\":\"invalid\"," + - "\"sub_state\":\"failed_no_mailbox\"," + - "\"suggestion\":null," + - "\"free_email\":false," + - "\"role\":false," + - "\"disposable\":false" + - "}"))); + wireMock.stubFor(post(urlPathEqualTo("/api/v1/verify_inline")) + .willReturn(okJson(INVALID_RESPONSE))); ValidationResult result = client.validate("bad@example.com"); - assertEquals("invalid", result.getState()); - assertEquals("failed_no_mailbox", result.getSubState()); + assertEquals("email_invalid", result.getState()); + assertEquals("failed_smtp_check", result.getSubState()); assertTrue(result.isInvalid()); assertFalse(result.isValid()); } @Test - void validateReturnsRiskyResult() { - wireMock.stubFor(post(urlEqualTo("/api/v1/verify")) - .willReturn(okJson("{" + - "\"state\":\"risky\"," + - "\"sub_state\":\"disposable_address\"," + - "\"suggestion\":null," + - "\"free_email\":false," + - "\"role\":false," + - "\"disposable\":true" + - "}"))); + void validateReturnsDisposableResult() { + wireMock.stubFor(post(urlPathEqualTo("/api/v1/verify_inline")) + .willReturn(okJson(DISPOSABLE_RESPONSE))); ValidationResult result = client.validate("user@tempmail.com"); - assertEquals("risky", result.getState()); - assertTrue(result.isRisky()); + assertEquals("ok", result.getState()); + assertEquals("is_disposable", result.getSubState()); assertTrue(result.isDisposable()); + assertFalse(result.isRole()); + } + + @Test + void validateReturnsAcceptAllResult() { + String acceptAllResponse = "{\"emails\":[{" + + "\"address\":\"user@catchall.com\"," + + "\"domain\":\"catchall.com\"," + + "\"canonical\":\"user\"," + + "\"mx_record\":null," + + "\"first_name\":null," + + "\"last_name\":null," + + "\"email_state\":\"accept_all\"," + + "\"email_sub_state\":\"email_ok\"," + + "\"verified_at\":\"2026-02-21T10:00:00.000Z\"," + + "\"did_you_mean\":null}]}"; + + wireMock.stubFor(post(urlPathEqualTo("/api/v1/verify_inline")) + .willReturn(okJson(acceptAllResponse))); + + ValidationResult result = client.validate("user@catchall.com"); + + assertEquals("accept_all", result.getState()); + assertTrue(result.isAcceptAll()); assertFalse(result.isValid()); - assertTrue(result.isValid(true)); } @Test void validateReturnsUnknownResult() { - wireMock.stubFor(post(urlEqualTo("/api/v1/verify")) - .willReturn(okJson("{" + - "\"state\":\"unknown\"," + - "\"sub_state\":\"unknown\"," + - "\"suggestion\":null," + - "\"free_email\":false," + - "\"role\":false," + - "\"disposable\":false" + - "}"))); + String unknownResponse = "{\"emails\":[{" + + "\"address\":\"user@mystery.com\"," + + "\"domain\":\"mystery.com\"," + + "\"canonical\":\"user\"," + + "\"mx_record\":null," + + "\"first_name\":null," + + "\"last_name\":null," + + "\"email_state\":\"unknown\"," + + "\"email_sub_state\":\"unknown_error\"," + + "\"verified_at\":\"2026-02-21T10:00:00.000Z\"," + + "\"did_you_mean\":null}]}"; + + wireMock.stubFor(post(urlPathEqualTo("/api/v1/verify_inline")) + .willReturn(okJson(unknownResponse))); ValidationResult result = client.validate("user@mystery.com"); assertEquals("unknown", result.getState()); assertTrue(result.isUnknown()); assertFalse(result.isValid()); - assertFalse(result.isValid(true)); } @Test - void validateSendsCorrectRequestBody() { - wireMock.stubFor(post(urlEqualTo("/api/v1/verify")) - .withRequestBody(equalToJson("{\"email\":\"test@example.com\"}")) - .willReturn(okJson("{\"state\":\"valid\",\"sub_state\":\"ok\"," + - "\"free_email\":false,\"role\":false,\"disposable\":false}"))); + void validateSendsEmailAsQueryParam() { + wireMock.stubFor(post(urlPathEqualTo("/api/v1/verify_inline")) + .withQueryParam("email", equalTo("test@example.com")) + .willReturn(okJson(VALID_RESPONSE))); client.validate("test@example.com"); - wireMock.verify(postRequestedFor(urlEqualTo("/api/v1/verify")) - .withRequestBody(equalToJson("{\"email\":\"test@example.com\"}"))); + wireMock.verify(postRequestedFor(urlPathEqualTo("/api/v1/verify_inline")) + .withQueryParam("email", equalTo("test@example.com"))); } @Test @@ -207,83 +254,52 @@ void validateRejectsBlankEmail() { @Test void validateReturnsSuggestion() { - wireMock.stubFor(post(urlEqualTo("/api/v1/verify")) - .willReturn(okJson("{" + - "\"state\":\"invalid\"," + - "\"sub_state\":\"failed_syntax_check\"," + - "\"suggestion\":\"user@gmail.com\"," + - "\"free_email\":false," + - "\"role\":false," + - "\"disposable\":false" + - "}"))); + String suggestionResponse = "{\"emails\":[{" + + "\"address\":\"user@gmial.com\"," + + "\"domain\":\"gmial.com\"," + + "\"canonical\":\"user\"," + + "\"mx_record\":null," + + "\"first_name\":null," + + "\"last_name\":null," + + "\"email_state\":\"email_invalid\"," + + "\"email_sub_state\":\"unknown_error\"," + + "\"verified_at\":\"2026-02-21T10:00:00.000Z\"," + + "\"did_you_mean\":\"user@gmail.com\"}]}"; + + wireMock.stubFor(post(urlPathEqualTo("/api/v1/verify_inline")) + .willReturn(okJson(suggestionResponse))); ValidationResult result = client.validate("user@gmial.com"); assertEquals("user@gmail.com", result.getSuggestion()); } - // --- formValidate() Tests --- - - @Test - void formValidateUsesFormApiKey() { - Truelist formClient = Truelist.builder("server-key") - .baseUrl("http://localhost:" + wireMock.port()) - .timeout(Duration.ofSeconds(5)) - .maxRetries(0) - .formApiKey("form-api-key") - .build(); - - wireMock.stubFor(post(urlEqualTo("/api/v1/form_verify")) - .withHeader("Authorization", equalTo("Bearer form-api-key")) - .willReturn(okJson("{\"state\":\"valid\",\"sub_state\":\"ok\"," + - "\"free_email\":false,\"role\":false,\"disposable\":false}"))); - - ValidationResult result = formClient.formValidate("user@example.com"); - - assertEquals("valid", result.getState()); - wireMock.verify(postRequestedFor(urlEqualTo("/api/v1/form_verify")) - .withHeader("Authorization", equalTo("Bearer form-api-key"))); - } - - @Test - void formValidateThrowsWithoutFormApiKey() { - assertThrows(IllegalStateException.class, () -> - client.formValidate("user@example.com")); - } - - @Test - void formValidateRejectsNullEmail() { - Truelist formClient = Truelist.builder("key") - .formApiKey("form-key") - .build(); - assertThrows(IllegalArgumentException.class, () -> - formClient.formValidate(null)); - } - // --- getAccount() Tests --- @Test void getAccountReturnsAccountInfo() { - wireMock.stubFor(get(urlEqualTo("/api/v1/account")) + wireMock.stubFor(get(urlEqualTo("/me")) .withHeader("Authorization", equalTo("Bearer test-api-key")) - .willReturn(okJson("{" + - "\"email\":\"user@company.com\"," + - "\"plan\":\"pro\"," + - "\"credits\":9542" + - "}"))); + .willReturn(okJson(ACCOUNT_RESPONSE))); AccountInfo account = client.getAccount(); - assertEquals("user@company.com", account.getEmail()); + assertEquals("team@company.com", account.getEmail()); + assertEquals("Team Lead", account.getName()); + assertEquals("a3828d19-1234-5678-9abc-def012345678", account.getUuid()); + assertEquals("America/New_York", account.getTimeZone()); + assertTrue(account.isAdminRole()); + assertNotNull(account.getAccount()); + assertEquals("Company Inc", account.getAccount().getName()); + assertEquals("pro", account.getAccount().getPaymentPlan()); assertEquals("pro", account.getPlan()); - assertEquals(9542, account.getCredits()); } // --- Error Handling Tests --- @Test void authErrorAlwaysThrowsOnValidate() { - wireMock.stubFor(post(urlEqualTo("/api/v1/verify")) + wireMock.stubFor(post(urlPathEqualTo("/api/v1/verify_inline")) .willReturn(unauthorized().withBody("Unauthorized"))); AuthenticationException ex = assertThrows(AuthenticationException.class, () -> @@ -294,30 +310,15 @@ void authErrorAlwaysThrowsOnValidate() { @Test void authErrorAlwaysThrowsOnGetAccount() { - wireMock.stubFor(get(urlEqualTo("/api/v1/account")) + wireMock.stubFor(get(urlEqualTo("/me")) .willReturn(unauthorized().withBody("Unauthorized"))); assertThrows(AuthenticationException.class, () -> client.getAccount()); } - @Test - void authErrorAlwaysThrowsOnFormValidate() { - Truelist formClient = Truelist.builder("key") - .baseUrl("http://localhost:" + wireMock.port()) - .maxRetries(0) - .formApiKey("bad-form-key") - .build(); - - wireMock.stubFor(post(urlEqualTo("/api/v1/form_verify")) - .willReturn(unauthorized().withBody("Unauthorized"))); - - assertThrows(AuthenticationException.class, () -> - formClient.formValidate("user@example.com")); - } - @Test void rateLimitThrows429Exception() { - wireMock.stubFor(post(urlEqualTo("/api/v1/verify")) + wireMock.stubFor(post(urlPathEqualTo("/api/v1/verify_inline")) .willReturn(aResponse().withStatus(429).withBody("Too Many Requests"))); RateLimitException ex = assertThrows(RateLimitException.class, () -> @@ -328,7 +329,7 @@ void rateLimitThrows429Exception() { @Test void serverErrorThrowsApiException() { - wireMock.stubFor(post(urlEqualTo("/api/v1/verify")) + wireMock.stubFor(post(urlPathEqualTo("/api/v1/verify_inline")) .willReturn(serverError().withBody("Internal Server Error"))); ApiException ex = assertThrows(ApiException.class, () -> @@ -339,7 +340,7 @@ void serverErrorThrowsApiException() { @Test void unexpectedStatusThrowsApiException() { - wireMock.stubFor(post(urlEqualTo("/api/v1/verify")) + wireMock.stubFor(post(urlPathEqualTo("/api/v1/verify_inline")) .willReturn(aResponse().withStatus(403).withBody("Forbidden"))); ApiException ex = assertThrows(ApiException.class, () -> @@ -358,28 +359,27 @@ void retriesOnServerError() { .maxRetries(2) .build(); - wireMock.stubFor(post(urlEqualTo("/api/v1/verify")) + wireMock.stubFor(post(urlPathEqualTo("/api/v1/verify_inline")) .inScenario("retry") .whenScenarioStateIs("Started") .willReturn(serverError().withBody("Error")) .willSetStateTo("first-retry")); - wireMock.stubFor(post(urlEqualTo("/api/v1/verify")) + wireMock.stubFor(post(urlPathEqualTo("/api/v1/verify_inline")) .inScenario("retry") .whenScenarioStateIs("first-retry") .willReturn(serverError().withBody("Error")) .willSetStateTo("second-retry")); - wireMock.stubFor(post(urlEqualTo("/api/v1/verify")) + wireMock.stubFor(post(urlPathEqualTo("/api/v1/verify_inline")) .inScenario("retry") .whenScenarioStateIs("second-retry") - .willReturn(okJson("{\"state\":\"valid\",\"sub_state\":\"ok\"," + - "\"free_email\":false,\"role\":false,\"disposable\":false}"))); + .willReturn(okJson(VALID_RESPONSE))); ValidationResult result = retryClient.validate("user@example.com"); - assertEquals("valid", result.getState()); - wireMock.verify(3, postRequestedFor(urlEqualTo("/api/v1/verify"))); + assertEquals("ok", result.getState()); + wireMock.verify(3, postRequestedFor(urlPathEqualTo("/api/v1/verify_inline"))); } @Test @@ -390,13 +390,13 @@ void doesNotRetryAuthErrors() { .maxRetries(2) .build(); - wireMock.stubFor(post(urlEqualTo("/api/v1/verify")) + wireMock.stubFor(post(urlPathEqualTo("/api/v1/verify_inline")) .willReturn(unauthorized().withBody("Unauthorized"))); assertThrows(AuthenticationException.class, () -> retryClient.validate("user@example.com")); - wireMock.verify(1, postRequestedFor(urlEqualTo("/api/v1/verify"))); + wireMock.verify(1, postRequestedFor(urlPathEqualTo("/api/v1/verify_inline"))); } @Test @@ -407,22 +407,21 @@ void retriesOnRateLimitError() { .maxRetries(2) .build(); - wireMock.stubFor(post(urlEqualTo("/api/v1/verify")) + wireMock.stubFor(post(urlPathEqualTo("/api/v1/verify_inline")) .inScenario("rate-limit-retry") .whenScenarioStateIs("Started") .willReturn(aResponse().withStatus(429).withBody("Rate limited")) .willSetStateTo("first-retry")); - wireMock.stubFor(post(urlEqualTo("/api/v1/verify")) + wireMock.stubFor(post(urlPathEqualTo("/api/v1/verify_inline")) .inScenario("rate-limit-retry") .whenScenarioStateIs("first-retry") - .willReturn(okJson("{\"state\":\"valid\",\"sub_state\":\"ok\"," + - "\"free_email\":false,\"role\":false,\"disposable\":false}"))); + .willReturn(okJson(VALID_RESPONSE))); ValidationResult result = retryClient.validate("user@example.com"); - assertEquals("valid", result.getState()); - wireMock.verify(2, postRequestedFor(urlEqualTo("/api/v1/verify"))); + assertEquals("ok", result.getState()); + wireMock.verify(2, postRequestedFor(urlPathEqualTo("/api/v1/verify_inline"))); } @Test @@ -433,13 +432,13 @@ void exhaustsRetriesOnPersistentRateLimitError() { .maxRetries(1) .build(); - wireMock.stubFor(post(urlEqualTo("/api/v1/verify")) + wireMock.stubFor(post(urlPathEqualTo("/api/v1/verify_inline")) .willReturn(aResponse().withStatus(429).withBody("Rate limited"))); assertThrows(RateLimitException.class, () -> retryClient.validate("user@example.com")); - wireMock.verify(2, postRequestedFor(urlEqualTo("/api/v1/verify"))); + wireMock.verify(2, postRequestedFor(urlPathEqualTo("/api/v1/verify_inline"))); } @Test @@ -450,13 +449,13 @@ void doesNotRetryClientErrors() { .maxRetries(2) .build(); - wireMock.stubFor(post(urlEqualTo("/api/v1/verify")) + wireMock.stubFor(post(urlPathEqualTo("/api/v1/verify_inline")) .willReturn(aResponse().withStatus(403).withBody("Forbidden"))); assertThrows(ApiException.class, () -> retryClient.validate("user@example.com")); - wireMock.verify(1, postRequestedFor(urlEqualTo("/api/v1/verify"))); + wireMock.verify(1, postRequestedFor(urlPathEqualTo("/api/v1/verify_inline"))); } @Test @@ -467,22 +466,22 @@ void exhaustsRetriesOnPersistentServerError() { .maxRetries(1) .build(); - wireMock.stubFor(post(urlEqualTo("/api/v1/verify")) + wireMock.stubFor(post(urlPathEqualTo("/api/v1/verify_inline")) .willReturn(serverError().withBody("Error"))); assertThrows(ApiException.class, () -> retryClient.validate("user@example.com")); - wireMock.verify(2, postRequestedFor(urlEqualTo("/api/v1/verify"))); + wireMock.verify(2, postRequestedFor(urlPathEqualTo("/api/v1/verify_inline"))); } @Test void zeroRetriesMeansOneAttempt() { - wireMock.stubFor(post(urlEqualTo("/api/v1/verify")) + wireMock.stubFor(post(urlPathEqualTo("/api/v1/verify_inline")) .willReturn(serverError().withBody("Error"))); assertThrows(ApiException.class, () -> client.validate("user@example.com")); - wireMock.verify(1, postRequestedFor(urlEqualTo("/api/v1/verify"))); + wireMock.verify(1, postRequestedFor(urlPathEqualTo("/api/v1/verify_inline"))); } } diff --git a/src/test/java/io/truelist/ValidationResultTest.java b/src/test/java/io/truelist/ValidationResultTest.java index a9ddb53..4557ab7 100644 --- a/src/test/java/io/truelist/ValidationResultTest.java +++ b/src/test/java/io/truelist/ValidationResultTest.java @@ -9,122 +9,123 @@ class ValidationResultTest { @Test void validEmailPredicates() { ValidationResult result = new ValidationResult( - "valid", "ok", null, true, false, false); + "user@example.com", "example.com", "user", + null, null, null, + "ok", "email_ok", "2026-02-21T10:00:00.000Z", null); assertTrue(result.isValid()); - assertTrue(result.isValid(false)); - assertTrue(result.isValid(true)); assertFalse(result.isInvalid()); - assertFalse(result.isRisky()); + assertFalse(result.isAcceptAll()); assertFalse(result.isUnknown()); } @Test void invalidEmailPredicates() { ValidationResult result = new ValidationResult( - "invalid", "failed_no_mailbox", null, false, false, false); + "bad@example.com", "example.com", "bad", + null, null, null, + "email_invalid", "failed_smtp_check", "2026-02-21T10:00:00.000Z", null); assertFalse(result.isValid()); - assertFalse(result.isValid(true)); assertTrue(result.isInvalid()); - assertFalse(result.isRisky()); + assertFalse(result.isAcceptAll()); assertFalse(result.isUnknown()); } @Test - void riskyEmailPredicates() { + void acceptAllPredicates() { ValidationResult result = new ValidationResult( - "risky", "disposable_address", null, false, false, true); + "user@catchall.com", "catchall.com", "user", + null, null, null, + "accept_all", "email_ok", "2026-02-21T10:00:00.000Z", null); assertFalse(result.isValid()); - assertFalse(result.isValid(false)); - assertTrue(result.isValid(true)); assertFalse(result.isInvalid()); - assertTrue(result.isRisky()); + assertTrue(result.isAcceptAll()); assertFalse(result.isUnknown()); - assertTrue(result.isDisposable()); } @Test void unknownEmailPredicates() { ValidationResult result = new ValidationResult( - "unknown", "unknown", null, false, false, false); + "user@mystery.com", "mystery.com", "user", + null, null, null, + "unknown", "unknown_error", "2026-02-21T10:00:00.000Z", null); assertFalse(result.isValid()); - assertFalse(result.isValid(true)); assertFalse(result.isInvalid()); - assertFalse(result.isRisky()); + assertFalse(result.isAcceptAll()); assertTrue(result.isUnknown()); } @Test - void freeEmailFlag() { + void disposableSubState() { ValidationResult result = new ValidationResult( - "valid", "ok", null, true, false, false); - assertTrue(result.isFreeEmail()); - - ValidationResult nonFree = new ValidationResult( - "valid", "ok", null, false, false, false); - assertFalse(nonFree.isFreeEmail()); - } - - @Test - void roleFlag() { - ValidationResult result = new ValidationResult( - "risky", "role_address", null, false, true, false); - assertTrue(result.isRole()); + "user@tempmail.com", "tempmail.com", "user", + null, null, null, + "ok", "is_disposable", "2026-02-21T10:00:00.000Z", null); - ValidationResult nonRole = new ValidationResult( - "valid", "ok", null, false, false, false); - assertFalse(nonRole.isRole()); + assertTrue(result.isDisposable()); + assertFalse(result.isRole()); } @Test - void disposableFlag() { + void roleSubState() { ValidationResult result = new ValidationResult( - "risky", "disposable_address", null, false, false, true); - assertTrue(result.isDisposable()); + "admin@company.com", "company.com", "admin", + null, null, null, + "ok", "is_role", "2026-02-21T10:00:00.000Z", null); - ValidationResult nonDisposable = new ValidationResult( - "valid", "ok", null, false, false, false); - assertFalse(nonDisposable.isDisposable()); + assertTrue(result.isRole()); + assertFalse(result.isDisposable()); } @Test void suggestion() { ValidationResult withSuggestion = new ValidationResult( - "invalid", "failed_syntax_check", "user@gmail.com", - false, false, false); + "user@gmial.com", "gmial.com", "user", + null, null, null, + "email_invalid", "unknown_error", "2026-02-21T10:00:00.000Z", + "user@gmail.com"); assertEquals("user@gmail.com", withSuggestion.getSuggestion()); ValidationResult withoutSuggestion = new ValidationResult( - "valid", "ok", null, false, false, false); + "user@example.com", "example.com", "user", + null, null, null, + "ok", "email_ok", "2026-02-21T10:00:00.000Z", null); assertNull(withoutSuggestion.getSuggestion()); } @Test void gettersReturnCorrectValues() { ValidationResult result = new ValidationResult( - "valid", "ok", "suggestion@test.com", true, true, true); - - assertEquals("valid", result.getState()); - assertEquals("ok", result.getSubState()); - assertEquals("suggestion@test.com", result.getSuggestion()); - assertTrue(result.isFreeEmail()); - assertTrue(result.isRole()); - assertTrue(result.isDisposable()); + "user@example.com", "example.com", "user", + "mx.example.com", "John", "Doe", + "ok", "email_ok", "2026-02-21T10:00:00.000Z", null); + + assertEquals("user@example.com", result.getEmail()); + assertEquals("example.com", result.getDomain()); + assertEquals("user", result.getCanonical()); + assertEquals("mx.example.com", result.getMxRecord()); + assertEquals("John", result.getFirstName()); + assertEquals("Doe", result.getLastName()); + assertEquals("ok", result.getState()); + assertEquals("email_ok", result.getSubState()); + assertEquals("2026-02-21T10:00:00.000Z", result.getVerifiedAt()); + assertNull(result.getSuggestion()); } @Test void toStringContainsAllFields() { ValidationResult result = new ValidationResult( - "valid", "ok", null, true, false, false); + "user@example.com", "example.com", "user", + null, null, null, + "ok", "email_ok", "2026-02-21T10:00:00.000Z", null); String str = result.toString(); - assertTrue(str.contains("valid")); + assertTrue(str.contains("user@example.com")); + assertTrue(str.contains("example.com")); assertTrue(str.contains("ok")); - assertTrue(str.contains("freeEmail=true")); - assertTrue(str.contains("role=false")); - assertTrue(str.contains("disposable=false")); + assertTrue(str.contains("email_ok")); } }