diff --git a/.github/workflows/code_quality.yml b/.github/workflows/code_quality.yml new file mode 100644 index 0000000..b6bf964 --- /dev/null +++ b/.github/workflows/code_quality.yml @@ -0,0 +1,28 @@ +name: Qodana +on: + workflow_dispatch: + pull_request: + push: + branches: # Specify your branches here + - main # The 'main' branch + - develop + +jobs: + qodana: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + checks: write + steps: + - uses: actions/checkout@v3 + with: + ref: ${{ github.event.pull_request.head.sha }} # to check out the actual pull request commit, not the merge commit + fetch-depth: 0 # a full history is required for pull request analysis + - name: 'Qodana Scan' + uses: JetBrains/qodana-action@v2025.2 + with: + pr-mode: false + env: + QODANA_TOKEN: ${{ secrets.QODANA_TOKEN }} + QODANA_ENDPOINT: 'https://qodana.cloud' \ No newline at end of file diff --git a/.github/workflows/google-java-format.yml b/.github/workflows/google-java-format.yml new file mode 100644 index 0000000..9c31872 --- /dev/null +++ b/.github/workflows/google-java-format.yml @@ -0,0 +1,25 @@ +name: Format + +on: + pull_request: + branches: + - main + +permissions: + contents: write + +jobs: + + formatting: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - name: Set up JDK 21 + uses: actions/setup-java@v5 + with: + distribution: 'temurin' + java-version: '21' + - uses: axel-op/googlejavaformat-action@v4 + with: + args: "--replace" + github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index 92785b5..02a29e9 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # nsecbunker-java -[![CI](https://github.com/tcheeric/nsecbunker-java/actions/workflows/ci.yml/badge.svg)](https://github.com/tcheeric/nsecbunker-java/actions/workflows/ci.yml) +[![CI](https://github.com/398ja/nsecbunker-java/actions/workflows/ci.yml/badge.svg)](https://github.com/398ja/nsecbunker-java/actions/workflows/ci.yml) [![Maven Central](https://img.shields.io/maven-central/v/xyz.tcheeric/nsecbunker-java.svg)](https://search.maven.org/search?q=g:xyz.tcheeric%20AND%20a:nsecbunker-java) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -[![Java Version](https://img.shields.io/badge/Java-17%2B-blue)](https://openjdk.org/) +[![Java Version](https://img.shields.io/badge/Java-21-blue)](https://openjdk.org/) A comprehensive Java client library for interacting with [nsecBunker](https://github.com/kind-0/nsecbunkerd) instances. Provides high-level APIs for key management, remote signing (NIP-46), permission control, and monitoring. diff --git a/docs/integration/bottin.md b/docs/integration/bottin.md new file mode 100644 index 0000000..1533493 --- /dev/null +++ b/docs/integration/bottin.md @@ -0,0 +1,374 @@ +# Bottin Integration Guide + +This guide explains how to integrate nsecbunker-java with [bottin](https://github.com/tcheeric/bottin), a NIP-05 identity registry service. + +## Overview + +nsecbunker-java provides interfaces for NIP-05 identity management through the `nsecbunker-account` module. By default, these use in-memory storage. Bottin provides persistent implementations backed by a database. + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Your Application │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────┐ ┌─────────────────┐ │ +│ │ NsecBunkerClient│ │ Nip05Manager │ │ +│ │ (signing) │ │ (identity mgmt) │ │ +│ └────────┬────────┘ └────────┬────────┘ │ +│ │ │ │ +│ ▼ ▼ │ +│ nsecbunkerd bottin / in-memory │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +## How nsecbunker-java Leverages Bottin + +When `bottin-spring-boot-starter` is on the classpath, nsecbunker-java **automatically** uses bottin's database-backed implementation through a Service Provider Interface (SPI) pattern. The integration is transparent - your code doesn't need to know which implementation is being used. + +### Auto-Configuration Flow + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ Your Application │ +├─────────────────────────────────────────────────────────────────────┤ +│ │ +│ nsecbunker-spring-boot-starter bottin-spring-boot-starter │ +│ ┌───────────────────────────┐ ┌───────────────────────────┐ │ +│ │ NsecBunkerAutoConfiguration│ │ BottinAutoConfiguration │ │ +│ │ │ │ │ │ +│ │ nip05Manager() method: │ │ Creates: │ │ +│ │ - Collects all providers │◄───────│ - PersistentNip05Manager │ │ +│ │ - Sorts by priority │injects │ - BottinNip05Provider(100)│ │ +│ │ - Selects highest (bottin)│ │ │ │ +│ └───────────────────────────┘ └───────────────────────────┘ │ +│ │ │ │ +│ ▼ ▼ │ +│ Nip05Manager interface PostgreSQL / H2 │ +│ │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +### Provider Selection Logic + +The `NsecBunkerAutoConfiguration` automatically selects the best available provider: + +```java +// From NsecBunkerAutoConfiguration.java +Optional best = providers.stream() + .filter(Nip05ManagerProvider::isAvailable) + .max(Comparator.comparingInt(Nip05ManagerProvider::priority)); +``` + +### NIP-05 Operations via Bottin + +When bottin is active, all `Nip05Manager` operations are database-backed: + +| Operation | Method | Description | +|-----------|--------|-------------| +| Check if taken | `verifyNip05(String nip05)` | Returns `true` if NIP-05 exists and is enabled | +| Store NIP-05 | `setupNip05(String username, String domain)` | Creates record with auto-generated identity | +| Lookup pubkey | `findByNip05(String nip05)` | Returns the record with pubkey if found | +| Reverse lookup | `findByPubkey(String pubkey)` | Find NIP-05 by public key | +| List by domain | `findByDomain(String domain)` | List all NIP-05s for a domain | +| Delete | `deleteNip05(String nip05)` | Remove a NIP-05 record | +| Update relays | `updateRelays(String nip05, List relays)` | Update relay list | + +### Transparent Usage Example + +```java +@Service +@RequiredArgsConstructor +public class IdentityService { + // Bottin's PersistentNip05Manager is injected automatically + private final Nip05Manager nip05Manager; + + public CompletableFuture isNip05Taken(String nip05) { + // Checks database via bottin + return nip05Manager.verifyNip05(nip05); + } + + public CompletableFuture registerNip05(String username, String domain) { + // Stores in database via bottin, generates identity via nostr-java + return nip05Manager.setupNip05(username, domain); + } + + public CompletableFuture> lookupPubkey(String nip05) { + // Queries database via bottin + return nip05Manager.findByNip05(nip05) + .thenApply(opt -> opt.map(Nip05Record::getPubkey)); + } +} +``` + +## Integration Patterns + +### Pattern 1: In-Memory (Default) + +No additional dependencies required. Uses `DefaultNip05Manager` and `DefaultAccountManager`. + +```yaml +# application.yml +nsecbunker: + nip05: + enabled: true + provider: in-memory # or "auto" (default) +``` + +### Pattern 2: Standalone Bottin (REST API) + +Bottin runs as a separate service. Your application calls its REST API. + +```java +@Service +public class Nip05RestClient { + private final RestTemplate restTemplate; + private final String bottinUrl; + + public Nip05RecordResponse createNip05(String username, String domain, String pubkey) { + var request = Map.of( + "username", username, + "domain", domain, + "pubkey", pubkey + ); + return restTemplate.postForObject( + bottinUrl + "/api/v1/records", + request, + Nip05RecordResponse.class + ); + } +} +``` + +### Pattern 3: Embedded Bottin (Library) + +Add bottin as a dependency and it automatically provides persistent implementations. + +```xml + + xyz.tcheeric + bottin-spring-boot-starter + 1.0.0 + +``` + +```yaml +# application.yml +nsecbunker: + nip05: + enabled: true + provider: auto # Will auto-select bottin (priority 100) + +spring: + datasource: + url: jdbc:postgresql://localhost:5432/myapp +``` + +## Service Provider Interface (SPI) + +nsecbunker-java uses an SPI pattern for pluggable NIP-05 implementations. + +### Nip05ManagerProvider + +```java +public interface Nip05ManagerProvider { + Nip05Manager create(); + int priority(); // Higher = more preferred + String name(); // e.g., "in-memory", "bottin" + boolean isAvailable(); // Check if provider can be used +} +``` + +### AccountManagerProvider + +```java +public interface AccountManagerProvider { + AccountManager create(); + int priority(); + String name(); + boolean isAvailable(); +} +``` + +### Priority Levels + +| Priority | Provider Type | +|----------|---------------| +| 0 | In-memory (default) | +| 50 | Custom implementations | +| 100 | Persistent (bottin) | + +## Creating a Custom Provider + +```java +@Component +public class RedisNip05ManagerProvider implements Nip05ManagerProvider { + + private final RedisTemplate redis; + + public RedisNip05ManagerProvider(RedisTemplate redis) { + this.redis = redis; + } + + @Override + public Nip05Manager create() { + return new RedisNip05Manager(redis); + } + + @Override + public int priority() { + return 75; // Between in-memory and persistent + } + + @Override + public String name() { + return "redis-cache"; + } + + @Override + public boolean isAvailable() { + try { + redis.getConnectionFactory().getConnection().ping(); + return true; + } catch (Exception e) { + return false; + } + } +} +``` + +## Configuration Reference + +```yaml +nsecbunker: + nip05: + # Enable/disable NIP-05 functionality + enabled: true + + # Provider selection: "auto", "in-memory", "bottin", or custom name + provider: auto + + # Default relays for new NIP-05 records + default-relays: + - wss://relay.example.com + + # Auto-register NIP-05 when creating keys + auto-register: false + + # Default domain for auto-registration + default-domain: example.com +``` + +## Use Cases + +### User Registration Flow + +```java +@Service +@RequiredArgsConstructor +public class UserRegistrationService { + + private final NsecBunkerAdminClient adminClient; + private final Nip05Manager nip05Manager; + + public CompletableFuture registerUser(String username, String domain) { + // 1. Create key in nsecbunkerd + return adminClient.createKey(username) + .thenCompose(bunkerKey -> { + // 2. Create NIP-05 record + return nip05Manager.setupNip05(username, domain) + .thenApply(nip05 -> new UserAccount(bunkerKey, nip05)); + }); + } +} +``` + +### NIP-05 Lookup + +```java +@Service +@RequiredArgsConstructor +public class IdentityService { + + private final Nip05Manager nip05Manager; + + public CompletableFuture> lookupPubkey(String nip05) { + return nip05Manager.findByNip05(nip05) + .thenApply(opt -> opt.map(Nip05Record::getPubkey)); + } + + public CompletableFuture isRegistered(String nip05) { + return nip05Manager.verifyNip05(nip05); + } +} +``` + +### Well-Known Endpoint + +If you need to serve `.well-known/nostr.json`: + +```java +@RestController +public class WellKnownController { + + private final Nip05Manager nip05Manager; + + @GetMapping("/.well-known/nostr.json") + public ResponseEntity nostrJson(@RequestParam(required = false) String name) { + // If using bottin, it provides this endpoint automatically + // Otherwise, implement using nip05Manager.findByDomain() + } +} +``` + +## Troubleshooting + +### Verifying Bottin Integration is Active + +Check the application logs at startup for these messages: + +``` +INFO bottin_autoconfiguration_nip05_manager_created +INFO bottin_autoconfiguration_nip05_provider_created +INFO nip05_manager_created provider=bottin-persistent priority=100 +``` + +If you see `provider=in-memory` instead, bottin is not being used. + +### Provider Not Found + +``` +WARN nip05_manager_provider_not_found requested=bottin using=default +``` + +Ensure bottin-spring-boot-starter is on the classpath and configured correctly. + +### Circular Dependency + +If you see circular dependency errors, ensure your custom provider doesn't inject `Nip05Manager` directly. Use `@Lazy` or restructure dependencies. + +### Database Connection Issues + +With bottin, ensure the database is accessible: + +```yaml +spring: + datasource: + url: jdbc:postgresql://localhost:5432/bottin + username: ${BOTTIN_DB_USER} + password: ${BOTTIN_DB_PASS} +``` + +### Domain Must Be Verified + +When using `setupNip05()`, the domain must already be registered and verified in bottin. Otherwise you'll get: + +- `DomainNotFoundException` - domain not registered +- `DomainNotVerifiedException` - domain registered but not verified + +Register and verify domains via bottin's admin API or dashboard before creating NIP-05 records. + +## Related Documentation + +- [NIP-05 Specification](https://github.com/nostr-protocol/nips/blob/master/05.md) +- [Bottin Project](https://github.com/tcheeric/bottin) diff --git a/nsecbunker-account/pom.xml b/nsecbunker-account/pom.xml index c3118ad..0ec21eb 100644 --- a/nsecbunker-account/pom.xml +++ b/nsecbunker-account/pom.xml @@ -8,7 +8,7 @@ xyz.tcheeric nsecbunker-java - 0.1.0-SNAPSHOT + 0.1.0 nsecbunker-account diff --git a/nsecbunker-account/src/main/java/xyz/tcheeric/nsecbunker/account/nip05/Nip05Manager.java b/nsecbunker-account/src/main/java/xyz/tcheeric/nsecbunker/account/nip05/Nip05Manager.java index d110202..4555679 100644 --- a/nsecbunker-account/src/main/java/xyz/tcheeric/nsecbunker/account/nip05/Nip05Manager.java +++ b/nsecbunker-account/src/main/java/xyz/tcheeric/nsecbunker/account/nip05/Nip05Manager.java @@ -1,34 +1,134 @@ package xyz.tcheeric.nsecbunker.account.nip05; +import java.util.List; +import java.util.Optional; import java.util.concurrent.CompletableFuture; /** * Manages NIP-05 identifiers. + * + *

This interface defines operations for creating, querying, and managing + * NIP-05 identifiers. Implementations may store records in memory or use + * persistent storage.

+ * + *

For pluggable implementations, see + * {@link xyz.tcheeric.nsecbunker.account.nip05.spi.Nip05ManagerProvider}.

*/ public interface Nip05Manager { /** * Sets up a NIP-05 identifier for a username and domain. * - * @param username username - * @param domain domain - * @return record + * @param username username (e.g., "alice") + * @param domain domain (e.g., "example.com") + * @return the created NIP-05 record */ CompletableFuture setupNip05(String username, String domain); /** - * Verifies a NIP-05 identifier. + * Verifies whether a NIP-05 identifier exists and is valid. * - * @param nip05 nip05 string - * @return true if known + * @param nip05 NIP-05 identifier (e.g., "alice@example.com") + * @return true if the identifier is registered and valid */ CompletableFuture verifyNip05(String nip05); /** - * Generates the .well-known JSON for the record. + * Generates the .well-known/nostr.json content for a record. * - * @param record nip05 record - * @return json string + * @param record NIP-05 record + * @return JSON string compliant with NIP-05 specification */ String generateWellKnown(Nip05Record record); + + /** + * Finds a NIP-05 record by its identifier. + * + *

Default implementation returns empty. Persistent implementations + * should override this method to provide lookup functionality.

+ * + * @param nip05 NIP-05 identifier (e.g., "alice@example.com") + * @return the record if found, empty otherwise + */ + default CompletableFuture> findByNip05(String nip05) { + return CompletableFuture.completedFuture(Optional.empty()); + } + + /** + * Finds all NIP-05 records for a given domain. + * + *

Default implementation returns empty list. Persistent implementations + * should override this method to provide domain-based queries.

+ * + * @param domain domain name (e.g., "example.com") + * @return list of records for the domain + */ + default CompletableFuture> findByDomain(String domain) { + return CompletableFuture.completedFuture(List.of()); + } + + /** + * Finds a NIP-05 record by public key. + * + *

Default implementation returns empty. Persistent implementations + * should override this method to provide pubkey-based lookup.

+ * + * @param pubkey public key in hex format (64 characters) + * @return the record if found, empty otherwise + */ + default CompletableFuture> findByPubkey(String pubkey) { + return CompletableFuture.completedFuture(Optional.empty()); + } + + /** + * Deletes a NIP-05 record by its identifier. + * + *

Default implementation does nothing. Persistent implementations + * should override this method to provide deletion functionality.

+ * + * @param nip05 NIP-05 identifier to delete + * @return true if the record was deleted, false if not found + */ + default CompletableFuture deleteNip05(String nip05) { + return CompletableFuture.completedFuture(false); + } + + /** + * Updates the relay list for a NIP-05 record. + * + *

Default implementation does nothing. Persistent implementations + * should override this method to provide update functionality.

+ * + * @param nip05 NIP-05 identifier + * @param relays list of relay URLs + * @return the updated record if found, empty otherwise + */ + default CompletableFuture> updateRelays(String nip05, List relays) { + return CompletableFuture.completedFuture(Optional.empty()); + } + + /** + * Counts the total number of NIP-05 records. + * + *

Default implementation returns 0. Persistent implementations + * should override this method to provide accurate counts.

+ * + * @return total number of records + */ + default CompletableFuture count() { + return CompletableFuture.completedFuture(0L); + } + + /** + * Counts the number of NIP-05 records for a given domain. + * + *

Default implementation returns 0. Persistent implementations + * should override this method to provide accurate counts.

+ * + * @param domain domain name + * @return number of records for the domain + */ + default CompletableFuture countByDomain(String domain) { + return CompletableFuture.completedFuture(0L); + } } diff --git a/nsecbunker-account/src/main/java/xyz/tcheeric/nsecbunker/account/nip05/Nip05Record.java b/nsecbunker-account/src/main/java/xyz/tcheeric/nsecbunker/account/nip05/Nip05Record.java index 3c54708..0dcc182 100644 --- a/nsecbunker-account/src/main/java/xyz/tcheeric/nsecbunker/account/nip05/Nip05Record.java +++ b/nsecbunker-account/src/main/java/xyz/tcheeric/nsecbunker/account/nip05/Nip05Record.java @@ -1,15 +1,209 @@ package xyz.tcheeric.nsecbunker.account.nip05; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; import lombok.Builder; import lombok.Value; +import lombok.extern.slf4j.Slf4j; +import xyz.tcheeric.nsecbunker.core.model.BunkerKey; + +import java.util.List; +import java.util.Objects; /** - * Represents a NIP-05 record. + * Represents a NIP-05 record mapping a username@domain identifier to a Nostr public key. + * + *

NIP-05 is the Nostr Implementation Possibility for mapping internet identifiers + * to public keys. This record stores the identifier, public key, and optional relay + * information.

+ * + *

Example:

+ *
{@code
+ * Nip05Record record = Nip05Record.builder()
+ *     .nip05("alice@example.com")
+ *     .pubkey("79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798")
+ *     .relaysJson("[\"wss://relay.example.com\"]")
+ *     .build();
+ * }
+ * + * @see NIP-05 Specification */ @Value @Builder(toBuilder = true) +@Slf4j public class Nip05Record { + + private static final ObjectMapper DEFAULT_MAPPER = new ObjectMapper(); + private static final TypeReference> STRING_LIST_TYPE = new TypeReference<>() {}; + + /** + * The NIP-05 identifier (e.g., "alice@example.com"). + */ String nip05; + + /** + * The Nostr public key in hex format (64 characters). + */ String pubkey; + + /** + * The relay list as a JSON array string (e.g., "[\"wss://relay.example.com\"]"). + */ String relaysJson; + + /** + * Returns the relay URLs as a parsed list. + * + * @return list of relay URLs, empty list if relaysJson is null or empty + */ + @JsonIgnore + public List getRelays() { + if (relaysJson == null || relaysJson.isBlank() || "[]".equals(relaysJson)) { + return List.of(); + } + try { + return DEFAULT_MAPPER.readValue(relaysJson, STRING_LIST_TYPE); + } catch (JsonProcessingException e) { + log.debug("relays_json_parse_failed nip05={} relaysJson={} error={}", + nip05, relaysJson, e.getMessage()); + return List.of(); + } + } + + /** + * Extracts the username portion of the NIP-05 identifier. + * + * @return the username (e.g., "alice" from "alice@example.com") + */ + @JsonIgnore + public String getUsername() { + if (nip05 == null || !nip05.contains("@")) { + return null; + } + return nip05.split("@")[0]; + } + + /** + * Extracts the domain portion of the NIP-05 identifier. + * + * @return the domain (e.g., "example.com" from "alice@example.com") + */ + @JsonIgnore + public String getDomain() { + if (nip05 == null || !nip05.contains("@")) { + return null; + } + String[] parts = nip05.split("@"); + return parts.length > 1 ? parts[1] : null; + } + + /** + * Checks if this record has any relay information. + * + * @return true if relays are configured + */ + @JsonIgnore + public boolean hasRelays() { + return !getRelays().isEmpty(); + } + + /** + * Creates a NIP-05 record from a BunkerKey. + * + *

Uses the key's hex public key and creates the NIP-05 identifier + * from the provided username and domain.

+ * + * @param key the bunker key containing the public key + * @param username the username for the NIP-05 identifier + * @param domain the domain for the NIP-05 identifier + * @return a new Nip05Record + * @throws NullPointerException if any parameter is null + */ + public static Nip05Record fromBunkerKey(BunkerKey key, String username, String domain) { + Objects.requireNonNull(key, "key must not be null"); + Objects.requireNonNull(username, "username must not be null"); + Objects.requireNonNull(domain, "domain must not be null"); + + return Nip05Record.builder() + .nip05(username + "@" + domain) + .pubkey(key.getPubkeyHex() != null ? key.getPubkeyHex() : key.getNpub()) + .relaysJson("[]") + .build(); + } + + /** + * Creates a NIP-05 record from a BunkerKey with relay information. + * + * @param key the bunker key containing the public key + * @param username the username for the NIP-05 identifier + * @param domain the domain for the NIP-05 identifier + * @param relays list of relay URLs + * @return a new Nip05Record + * @throws NullPointerException if key, username, or domain is null + */ + public static Nip05Record fromBunkerKey(BunkerKey key, String username, String domain, List relays) { + Objects.requireNonNull(key, "key must not be null"); + Objects.requireNonNull(username, "username must not be null"); + Objects.requireNonNull(domain, "domain must not be null"); + + return Nip05Record.builder() + .nip05(username + "@" + domain) + .pubkey(key.getPubkeyHex() != null ? key.getPubkeyHex() : key.getNpub()) + .relaysJson(serializeRelays(relays)) + .build(); + } + + /** + * Creates a NIP-05 record with the specified parameters. + * + * @param username the username + * @param domain the domain + * @param pubkey the public key in hex format + * @return a new Nip05Record + */ + public static Nip05Record of(String username, String domain, String pubkey) { + return Nip05Record.builder() + .nip05(username + "@" + domain) + .pubkey(pubkey) + .relaysJson("[]") + .build(); + } + + /** + * Creates a NIP-05 record with relays. + * + * @param username the username + * @param domain the domain + * @param pubkey the public key in hex format + * @param relays list of relay URLs + * @return a new Nip05Record + */ + public static Nip05Record of(String username, String domain, String pubkey, List relays) { + return Nip05Record.builder() + .nip05(username + "@" + domain) + .pubkey(pubkey) + .relaysJson(serializeRelays(relays)) + .build(); + } + + /** + * Serializes a list of relay URLs to JSON string. + * + *

Returns empty array "[]" if relays is null, empty, or serialization fails. + * + * @param relays the relay URLs to serialize + * @return JSON array string of relays + */ + private static String serializeRelays(List relays) { + if (relays == null || relays.isEmpty()) { + return "[]"; + } + try { + return DEFAULT_MAPPER.writeValueAsString(relays); + } catch (JsonProcessingException e) { + return "[]"; + } + } } diff --git a/nsecbunker-account/src/main/java/xyz/tcheeric/nsecbunker/account/nip05/spi/DefaultNip05ManagerProvider.java b/nsecbunker-account/src/main/java/xyz/tcheeric/nsecbunker/account/nip05/spi/DefaultNip05ManagerProvider.java new file mode 100644 index 0000000..4547ac0 --- /dev/null +++ b/nsecbunker-account/src/main/java/xyz/tcheeric/nsecbunker/account/nip05/spi/DefaultNip05ManagerProvider.java @@ -0,0 +1,70 @@ +package xyz.tcheeric.nsecbunker.account.nip05.spi; + +import com.fasterxml.jackson.databind.ObjectMapper; +import xyz.tcheeric.nsecbunker.account.nip05.DefaultNip05Manager; +import xyz.tcheeric.nsecbunker.account.nip05.Nip05Manager; +import xyz.tcheeric.nsecbunker.account.registration.AccountManager; +import xyz.tcheeric.nsecbunker.account.registration.DefaultAccountManager; + +/** + * Default provider for in-memory {@link Nip05Manager} implementation. + * + *

This provider creates instances of {@link DefaultNip05Manager} which stores + * NIP-05 records in memory. Data is not persisted and will be lost when the + * application restarts.

+ * + *

Priority: 0 (lowest) - will be overridden by persistent implementations.

+ * + *

The provider can be configured with custom {@link AccountManager} and + * {@link ObjectMapper} instances. If not provided, defaults will be used.

+ */ +public class DefaultNip05ManagerProvider implements Nip05ManagerProvider { + + private static final String NAME = "in-memory"; + private static final int PRIORITY = 0; + + private final AccountManager accountManager; + private final ObjectMapper objectMapper; + + /** + * Creates a provider with default dependencies. + */ + public DefaultNip05ManagerProvider() { + this(null, null); + } + + /** + * Creates a provider with a custom AccountManager. + * + * @param accountManager the account manager to use + */ + public DefaultNip05ManagerProvider(AccountManager accountManager) { + this(accountManager, null); + } + + /** + * Creates a provider with custom dependencies. + * + * @param accountManager the account manager to use + * @param objectMapper the object mapper for JSON serialization + */ + public DefaultNip05ManagerProvider(AccountManager accountManager, ObjectMapper objectMapper) { + this.accountManager = accountManager != null ? accountManager : new DefaultAccountManager(); + this.objectMapper = objectMapper != null ? objectMapper : new ObjectMapper(); + } + + @Override + public Nip05Manager create() { + return new DefaultNip05Manager(accountManager, objectMapper); + } + + @Override + public int priority() { + return PRIORITY; + } + + @Override + public String name() { + return NAME; + } +} diff --git a/nsecbunker-account/src/main/java/xyz/tcheeric/nsecbunker/account/nip05/spi/Nip05ManagerProvider.java b/nsecbunker-account/src/main/java/xyz/tcheeric/nsecbunker/account/nip05/spi/Nip05ManagerProvider.java new file mode 100644 index 0000000..c89ccbf --- /dev/null +++ b/nsecbunker-account/src/main/java/xyz/tcheeric/nsecbunker/account/nip05/spi/Nip05ManagerProvider.java @@ -0,0 +1,87 @@ +package xyz.tcheeric.nsecbunker.account.nip05.spi; + +import xyz.tcheeric.nsecbunker.account.nip05.Nip05Manager; + +/** + * Service Provider Interface for creating {@link Nip05Manager} instances. + * + *

Implementations of this interface allow different NIP-05 storage backends + * to be plugged into nsecbunker-java applications. The provider with the highest + * priority will be selected when multiple providers are available.

+ * + *

Default implementations:

+ *
    + *
  • {@code DefaultNip05ManagerProvider} - In-memory storage (priority 0)
  • + *
  • {@code BottinNip05ManagerProvider} - Database-backed via bottin (priority 100)
  • + *
+ * + *

Example implementation:

+ *
{@code
+ * @Component
+ * public class CustomNip05ManagerProvider implements Nip05ManagerProvider {
+ *     @Override
+ *     public Nip05Manager create() {
+ *         return new CustomNip05Manager();
+ *     }
+ *
+ *     @Override
+ *     public int priority() {
+ *         return 50;
+ *     }
+ *
+ *     @Override
+ *     public String name() {
+ *         return "custom";
+ *     }
+ * }
+ * }
+ */ +public interface Nip05ManagerProvider { + + /** + * Creates a new {@link Nip05Manager} instance. + * + * @return a configured Nip05Manager instance + */ + Nip05Manager create(); + + /** + * Returns the priority of this provider. + * + *

Higher values indicate higher priority. When multiple providers are + * available, the one with the highest priority will be selected.

+ * + *

Standard priority levels:

+ *
    + *
  • 0 - Default in-memory implementation
  • + *
  • 50 - Custom implementations
  • + *
  • 100 - Persistent implementations (e.g., bottin)
  • + *
+ * + * @return the priority value (higher = more preferred) + */ + int priority(); + + /** + * Returns the unique name of this provider. + * + *

Used for logging, configuration selection, and debugging.

+ * + *

Examples: "in-memory", "bottin-persistent", "redis-cache"

+ * + * @return the provider name + */ + String name(); + + /** + * Indicates whether this provider is available and can create instances. + * + *

Providers may return false if required dependencies are not present + * or configuration is incomplete.

+ * + * @return true if the provider can create Nip05Manager instances + */ + default boolean isAvailable() { + return true; + } +} diff --git a/nsecbunker-account/src/main/java/xyz/tcheeric/nsecbunker/account/registration/AccountManager.java b/nsecbunker-account/src/main/java/xyz/tcheeric/nsecbunker/account/registration/AccountManager.java index 45fce9a..8d6d1a2 100644 --- a/nsecbunker-account/src/main/java/xyz/tcheeric/nsecbunker/account/registration/AccountManager.java +++ b/nsecbunker-account/src/main/java/xyz/tcheeric/nsecbunker/account/registration/AccountManager.java @@ -1,27 +1,39 @@ package xyz.tcheeric.nsecbunker.account.registration; +import java.util.List; +import java.util.Optional; import java.util.concurrent.CompletableFuture; /** * Manages account registration for nsecBunker. + * + *

This interface defines operations for creating and managing user accounts + * with associated NIP-05 identifiers. Implementations may store accounts in + * memory or use persistent storage.

+ * + *

For pluggable implementations, see + * {@link xyz.tcheeric.nsecbunker.account.registration.spi.AccountManagerProvider}.

*/ public interface AccountManager { /** * Creates an account with the given username and domain. * - * @param username username - * @param domain domain - * @return result of registration + * @param username username (e.g., "alice") + * @param domain domain (e.g., "example.com") + * @return result of registration including NIP-05 identifier */ CompletableFuture createAccount(String username, String domain); /** * Registers an account using the multi-step flow (create + key). * + *

This method first generates a bunker key for the account, then + * creates the account with the generated key name.

+ * * @param username username * @param domain domain - * @return result + * @return result including key information */ CompletableFuture registerAccount(String username, String domain); @@ -29,7 +41,109 @@ public interface AccountManager { * Generates a bunker key name for the account. * * @param username username - * @return generated key name + * @return generated key name (e.g., "key-alice") */ CompletableFuture generateKeyForAccount(String username); + + /** + * Finds an account by its NIP-05 identifier. + * + *

Default implementation returns empty. Persistent implementations + * should override this method to provide lookup functionality.

+ * + * @param nip05 NIP-05 identifier (e.g., "alice@example.com") + * @return the account if found, empty otherwise + */ + default CompletableFuture> findAccount(String nip05) { + return CompletableFuture.completedFuture(Optional.empty()); + } + + /** + * Finds an account by username. + * + *

Default implementation returns empty. Persistent implementations + * should override this method to provide username-based lookup.

+ * + *

Note: Usernames are not unique across domains. For example, + * "alice@example.com" and "alice@another.com" are different accounts. + * This method may return ambiguous results in multi-domain scenarios. + * For unambiguous lookups, use {@link #findByUsernameAndDomain(String, String)} + * or {@link #findAccount(String)} with the full NIP-05 identifier.

+ * + * @param username username to search for + * @return the first matching account if found, empty otherwise + */ + default CompletableFuture> findByUsername(String username) { + return CompletableFuture.completedFuture(Optional.empty()); + } + + /** + * Finds an account by username and domain. + * + *

This method provides unambiguous lookup by requiring both the username + * and domain components of a NIP-05 identifier.

+ * + *

Default implementation delegates to {@link #findAccount(String)} using + * the combined NIP-05 identifier. Persistent implementations may override + * for optimized queries.

+ * + * @param username username (e.g., "alice") + * @param domain domain (e.g., "example.com") + * @return the account if found, empty otherwise + */ + default CompletableFuture> findByUsernameAndDomain( + String username, String domain) { + return findAccount(username + "@" + domain); + } + + /** + * Finds all accounts for a given domain. + * + *

Default implementation returns empty list. Persistent implementations + * should override this method to provide domain-based queries.

+ * + * @param domain domain name (e.g., "example.com") + * @return list of accounts for the domain + */ + default CompletableFuture> findByDomain(String domain) { + return CompletableFuture.completedFuture(List.of()); + } + + /** + * Checks if an account exists for the given NIP-05 identifier. + * + *

Default implementation returns false. Persistent implementations + * should override this method for efficient existence checks.

+ * + * @param nip05 NIP-05 identifier + * @return true if the account exists + */ + default CompletableFuture exists(String nip05) { + return CompletableFuture.completedFuture(false); + } + + /** + * Deletes an account by its NIP-05 identifier. + * + *

Default implementation does nothing. Persistent implementations + * should override this method to provide deletion functionality.

+ * + * @param nip05 NIP-05 identifier to delete + * @return true if the account was deleted, false if not found + */ + default CompletableFuture deleteAccount(String nip05) { + return CompletableFuture.completedFuture(false); + } + + /** + * Counts the total number of accounts. + * + *

Default implementation returns 0. Persistent implementations + * should override this method to provide accurate counts.

+ * + * @return total number of accounts + */ + default CompletableFuture count() { + return CompletableFuture.completedFuture(0L); + } } diff --git a/nsecbunker-account/src/main/java/xyz/tcheeric/nsecbunker/account/registration/spi/AccountManagerProvider.java b/nsecbunker-account/src/main/java/xyz/tcheeric/nsecbunker/account/registration/spi/AccountManagerProvider.java new file mode 100644 index 0000000..ac0793c --- /dev/null +++ b/nsecbunker-account/src/main/java/xyz/tcheeric/nsecbunker/account/registration/spi/AccountManagerProvider.java @@ -0,0 +1,87 @@ +package xyz.tcheeric.nsecbunker.account.registration.spi; + +import xyz.tcheeric.nsecbunker.account.registration.AccountManager; + +/** + * Service Provider Interface for creating {@link AccountManager} instances. + * + *

Implementations of this interface allow different account storage backends + * to be plugged into nsecbunker-java applications. The provider with the highest + * priority will be selected when multiple providers are available.

+ * + *

Default implementations:

+ *
    + *
  • {@code DefaultAccountManagerProvider} - In-memory storage (priority 0)
  • + *
  • {@code BottinAccountManagerProvider} - Database-backed via bottin (priority 100)
  • + *
+ * + *

Example implementation:

+ *
{@code
+ * @Component
+ * public class CustomAccountManagerProvider implements AccountManagerProvider {
+ *     @Override
+ *     public AccountManager create() {
+ *         return new CustomAccountManager();
+ *     }
+ *
+ *     @Override
+ *     public int priority() {
+ *         return 50;
+ *     }
+ *
+ *     @Override
+ *     public String name() {
+ *         return "custom";
+ *     }
+ * }
+ * }
+ */ +public interface AccountManagerProvider { + + /** + * Creates a new {@link AccountManager} instance. + * + * @return a configured AccountManager instance + */ + AccountManager create(); + + /** + * Returns the priority of this provider. + * + *

Higher values indicate higher priority. When multiple providers are + * available, the one with the highest priority will be selected.

+ * + *

Standard priority levels:

+ *
    + *
  • 0 - Default in-memory implementation
  • + *
  • 50 - Custom implementations
  • + *
  • 100 - Persistent implementations (e.g., bottin)
  • + *
+ * + * @return the priority value (higher = more preferred) + */ + int priority(); + + /** + * Returns the unique name of this provider. + * + *

Used for logging, configuration selection, and debugging.

+ * + *

Examples: "in-memory", "bottin-persistent"

+ * + * @return the provider name + */ + String name(); + + /** + * Indicates whether this provider is available and can create instances. + * + *

Providers may return false if required dependencies are not present + * or configuration is incomplete.

+ * + * @return true if the provider can create AccountManager instances + */ + default boolean isAvailable() { + return true; + } +} diff --git a/nsecbunker-account/src/main/java/xyz/tcheeric/nsecbunker/account/registration/spi/DefaultAccountManagerProvider.java b/nsecbunker-account/src/main/java/xyz/tcheeric/nsecbunker/account/registration/spi/DefaultAccountManagerProvider.java new file mode 100644 index 0000000..450d234 --- /dev/null +++ b/nsecbunker-account/src/main/java/xyz/tcheeric/nsecbunker/account/registration/spi/DefaultAccountManagerProvider.java @@ -0,0 +1,34 @@ +package xyz.tcheeric.nsecbunker.account.registration.spi; + +import xyz.tcheeric.nsecbunker.account.registration.AccountManager; +import xyz.tcheeric.nsecbunker.account.registration.DefaultAccountManager; + +/** + * Default provider for in-memory {@link AccountManager} implementation. + * + *

This provider creates instances of {@link DefaultAccountManager} which stores + * account data in memory. Data is not persisted and will be lost when the + * application restarts.

+ * + *

Priority: 0 (lowest) - will be overridden by persistent implementations.

+ */ +public class DefaultAccountManagerProvider implements AccountManagerProvider { + + private static final String NAME = "in-memory"; + private static final int PRIORITY = 0; + + @Override + public AccountManager create() { + return new DefaultAccountManager(); + } + + @Override + public int priority() { + return PRIORITY; + } + + @Override + public String name() { + return NAME; + } +} diff --git a/nsecbunker-account/src/test/java/xyz/tcheeric/nsecbunker/account/nip05/Nip05RecordTest.java b/nsecbunker-account/src/test/java/xyz/tcheeric/nsecbunker/account/nip05/Nip05RecordTest.java index ee5f747..140bf9e 100644 --- a/nsecbunker-account/src/test/java/xyz/tcheeric/nsecbunker/account/nip05/Nip05RecordTest.java +++ b/nsecbunker-account/src/test/java/xyz/tcheeric/nsecbunker/account/nip05/Nip05RecordTest.java @@ -1,8 +1,12 @@ package xyz.tcheeric.nsecbunker.account.nip05; import org.junit.jupiter.api.Test; +import xyz.tcheeric.nsecbunker.core.model.BunkerKey; + +import java.util.List; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; class Nip05RecordTest { @@ -73,4 +77,225 @@ void toStringShouldContainFields() { assertThat(str).contains("alice@example.com"); assertThat(str).contains("abc123"); } + + // ========== Tests for new helper methods ========== + + /** + * Tests that getRelays parses a valid JSON array of relay URLs. + */ + @Test + void shouldParseRelaysFromJson() { + // Arrange + Nip05Record record = Nip05Record.builder() + .nip05("alice@example.com") + .relaysJson("[\"wss://relay1.example.com\",\"wss://relay2.example.com\"]") + .build(); + + // Act + List relays = record.getRelays(); + + // Assert + assertThat(relays).hasSize(2); + assertThat(relays).containsExactly("wss://relay1.example.com", "wss://relay2.example.com"); + } + + /** + * Tests that getRelays returns empty list for null relaysJson. + */ + @Test + void shouldReturnEmptyListWhenRelaysJsonIsNull() { + // Arrange + Nip05Record record = Nip05Record.builder() + .nip05("alice@example.com") + .relaysJson(null) + .build(); + + // Act + List relays = record.getRelays(); + + // Assert + assertThat(relays).isEmpty(); + } + + /** + * Tests that getRelays returns empty list for empty array JSON. + */ + @Test + void shouldReturnEmptyListWhenRelaysJsonIsEmptyArray() { + // Arrange + Nip05Record record = Nip05Record.builder() + .nip05("alice@example.com") + .relaysJson("[]") + .build(); + + // Act + List relays = record.getRelays(); + + // Assert + assertThat(relays).isEmpty(); + } + + /** + * Tests that getUsername extracts the username from NIP-05 identifier. + */ + @Test + void shouldExtractUsername() { + // Arrange + Nip05Record record = Nip05Record.builder() + .nip05("alice@example.com") + .build(); + + // Act & Assert + assertThat(record.getUsername()).isEqualTo("alice"); + } + + /** + * Tests that getDomain extracts the domain from NIP-05 identifier. + */ + @Test + void shouldExtractDomain() { + // Arrange + Nip05Record record = Nip05Record.builder() + .nip05("alice@example.com") + .build(); + + // Act & Assert + assertThat(record.getDomain()).isEqualTo("example.com"); + } + + /** + * Tests that getUsername returns null for invalid NIP-05. + */ + @Test + void shouldReturnNullUsernameWhenNip05Invalid() { + // Arrange + Nip05Record record = Nip05Record.builder() + .nip05("invalid-nip05") + .build(); + + // Act & Assert + assertThat(record.getUsername()).isNull(); + } + + /** + * Tests that hasRelays returns true when relays are configured. + */ + @Test + void shouldDetectWhenHasRelays() { + // Arrange + Nip05Record withRelays = Nip05Record.builder() + .nip05("alice@example.com") + .relaysJson("[\"wss://relay.example.com\"]") + .build(); + + Nip05Record withoutRelays = Nip05Record.builder() + .nip05("bob@example.com") + .relaysJson("[]") + .build(); + + // Act & Assert + assertThat(withRelays.hasRelays()).isTrue(); + assertThat(withoutRelays.hasRelays()).isFalse(); + } + + /** + * Tests the static factory method of() creates a valid record. + */ + @Test + void shouldCreateRecordWithOfFactory() { + // Arrange & Act + Nip05Record record = Nip05Record.of("alice", "example.com", "abcdef1234"); + + // Assert + assertThat(record.getNip05()).isEqualTo("alice@example.com"); + assertThat(record.getPubkey()).isEqualTo("abcdef1234"); + assertThat(record.getRelaysJson()).isEqualTo("[]"); + } + + /** + * Tests the static factory method of() with relays. + */ + @Test + void shouldCreateRecordWithOfFactoryAndRelays() { + // Arrange + List relays = List.of("wss://relay1.com", "wss://relay2.com"); + + // Act + Nip05Record record = Nip05Record.of("bob", "test.com", "pubkey123", relays); + + // Assert + assertThat(record.getNip05()).isEqualTo("bob@test.com"); + assertThat(record.getPubkey()).isEqualTo("pubkey123"); + assertThat(record.getRelays()).containsExactly("wss://relay1.com", "wss://relay2.com"); + } + + /** + * Tests creating a record from BunkerKey. + */ + @Test + void shouldCreateRecordFromBunkerKey() { + // Arrange + BunkerKey key = BunkerKey.builder() + .name("test-key") + .pubkeyHex("79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798") + .npub("npub1...") + .build(); + + // Act + Nip05Record record = Nip05Record.fromBunkerKey(key, "alice", "example.com"); + + // Assert + assertThat(record.getNip05()).isEqualTo("alice@example.com"); + assertThat(record.getPubkey()).isEqualTo("79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"); + assertThat(record.getRelaysJson()).isEqualTo("[]"); + } + + /** + * Tests creating a record from BunkerKey with relays. + */ + @Test + void shouldCreateRecordFromBunkerKeyWithRelays() { + // Arrange + BunkerKey key = BunkerKey.builder() + .name("test-key") + .pubkeyHex("abcdef123456") + .build(); + List relays = List.of("wss://relay.nostr.info"); + + // Act + Nip05Record record = Nip05Record.fromBunkerKey(key, "bob", "nostr.info", relays); + + // Assert + assertThat(record.getNip05()).isEqualTo("bob@nostr.info"); + assertThat(record.getPubkey()).isEqualTo("abcdef123456"); + assertThat(record.getRelays()).containsExactly("wss://relay.nostr.info"); + } + + /** + * Tests that fromBunkerKey throws on null key. + */ + @Test + void shouldThrowOnNullKeyInFromBunkerKey() { + assertThatThrownBy(() -> Nip05Record.fromBunkerKey(null, "alice", "example.com")) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("key"); + } + + /** + * Tests that fromBunkerKey uses npub when pubkeyHex is null. + */ + @Test + void shouldUseNpubWhenPubkeyHexIsNull() { + // Arrange + BunkerKey key = BunkerKey.builder() + .name("test-key") + .npub("npub1abcdef...") + .build(); + + // Act + Nip05Record record = Nip05Record.fromBunkerKey(key, "alice", "example.com"); + + // Assert + assertThat(record.getPubkey()).isEqualTo("npub1abcdef..."); + } } diff --git a/nsecbunker-account/src/test/java/xyz/tcheeric/nsecbunker/account/nip05/spi/DefaultNip05ManagerProviderTest.java b/nsecbunker-account/src/test/java/xyz/tcheeric/nsecbunker/account/nip05/spi/DefaultNip05ManagerProviderTest.java new file mode 100644 index 0000000..0142a67 --- /dev/null +++ b/nsecbunker-account/src/test/java/xyz/tcheeric/nsecbunker/account/nip05/spi/DefaultNip05ManagerProviderTest.java @@ -0,0 +1,98 @@ +package xyz.tcheeric.nsecbunker.account.nip05.spi; + +import org.junit.jupiter.api.Test; +import xyz.tcheeric.nsecbunker.account.nip05.DefaultNip05Manager; +import xyz.tcheeric.nsecbunker.account.nip05.Nip05Manager; +import xyz.tcheeric.nsecbunker.account.registration.DefaultAccountManager; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link DefaultNip05ManagerProvider}. + */ +class DefaultNip05ManagerProviderTest { + + /** + * Tests that the provider creates a DefaultNip05Manager instance. + */ + @Test + void shouldCreateDefaultNip05Manager() { + // Arrange + DefaultNip05ManagerProvider provider = new DefaultNip05ManagerProvider(); + + // Act + Nip05Manager manager = provider.create(); + + // Assert + assertThat(manager).isInstanceOf(DefaultNip05Manager.class); + } + + /** + * Tests that the provider has priority 0 (default/lowest). + */ + @Test + void shouldHavePriorityZero() { + // Arrange + DefaultNip05ManagerProvider provider = new DefaultNip05ManagerProvider(); + + // Act & Assert + assertThat(provider.priority()).isEqualTo(0); + } + + /** + * Tests that the provider name is "in-memory". + */ + @Test + void shouldHaveInMemoryName() { + // Arrange + DefaultNip05ManagerProvider provider = new DefaultNip05ManagerProvider(); + + // Act & Assert + assertThat(provider.name()).isEqualTo("in-memory"); + } + + /** + * Tests that the provider is always available. + */ + @Test + void shouldBeAvailable() { + // Arrange + DefaultNip05ManagerProvider provider = new DefaultNip05ManagerProvider(); + + // Act & Assert + assertThat(provider.isAvailable()).isTrue(); + } + + /** + * Tests that the provider can be created with a custom AccountManager. + */ + @Test + void shouldAcceptCustomAccountManager() { + // Arrange + DefaultAccountManager accountManager = new DefaultAccountManager(); + DefaultNip05ManagerProvider provider = new DefaultNip05ManagerProvider(accountManager); + + // Act + Nip05Manager manager = provider.create(); + + // Assert + assertThat(manager).isNotNull(); + } + + /** + * Tests that the created manager can setup NIP-05 identifiers. + */ + @Test + void shouldCreateFunctionalManager() { + // Arrange + DefaultNip05ManagerProvider provider = new DefaultNip05ManagerProvider(); + Nip05Manager manager = provider.create(); + + // Act + var record = manager.setupNip05("alice", "example.com").join(); + + // Assert + assertThat(record.getNip05()).isEqualTo("alice@example.com"); + assertThat(manager.verifyNip05("alice@example.com").join()).isTrue(); + } +} diff --git a/nsecbunker-account/src/test/java/xyz/tcheeric/nsecbunker/account/registration/spi/DefaultAccountManagerProviderTest.java b/nsecbunker-account/src/test/java/xyz/tcheeric/nsecbunker/account/registration/spi/DefaultAccountManagerProviderTest.java new file mode 100644 index 0000000..0f04f9a --- /dev/null +++ b/nsecbunker-account/src/test/java/xyz/tcheeric/nsecbunker/account/registration/spi/DefaultAccountManagerProviderTest.java @@ -0,0 +1,99 @@ +package xyz.tcheeric.nsecbunker.account.registration.spi; + +import org.junit.jupiter.api.Test; +import xyz.tcheeric.nsecbunker.account.registration.AccountManager; +import xyz.tcheeric.nsecbunker.account.registration.DefaultAccountManager; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link DefaultAccountManagerProvider}. + */ +class DefaultAccountManagerProviderTest { + + /** + * Tests that the provider creates a DefaultAccountManager instance. + */ + @Test + void shouldCreateDefaultAccountManager() { + // Arrange + DefaultAccountManagerProvider provider = new DefaultAccountManagerProvider(); + + // Act + AccountManager manager = provider.create(); + + // Assert + assertThat(manager).isInstanceOf(DefaultAccountManager.class); + } + + /** + * Tests that the provider has priority 0 (default/lowest). + */ + @Test + void shouldHavePriorityZero() { + // Arrange + DefaultAccountManagerProvider provider = new DefaultAccountManagerProvider(); + + // Act & Assert + assertThat(provider.priority()).isEqualTo(0); + } + + /** + * Tests that the provider name is "in-memory". + */ + @Test + void shouldHaveInMemoryName() { + // Arrange + DefaultAccountManagerProvider provider = new DefaultAccountManagerProvider(); + + // Act & Assert + assertThat(provider.name()).isEqualTo("in-memory"); + } + + /** + * Tests that the provider is always available. + */ + @Test + void shouldBeAvailable() { + // Arrange + DefaultAccountManagerProvider provider = new DefaultAccountManagerProvider(); + + // Act & Assert + assertThat(provider.isAvailable()).isTrue(); + } + + /** + * Tests that the created manager can create accounts. + */ + @Test + void shouldCreateFunctionalManager() { + // Arrange + DefaultAccountManagerProvider provider = new DefaultAccountManagerProvider(); + AccountManager manager = provider.create(); + + // Act + var result = manager.createAccount("alice", "example.com").join(); + + // Assert + assertThat(result.getNip05()).isEqualTo("alice@example.com"); + assertThat(result.getUsername()).isEqualTo("alice"); + assertThat(result.getDomain()).isEqualTo("example.com"); + } + + /** + * Tests that the created manager supports the registration flow. + */ + @Test + void shouldSupportRegistrationFlow() { + // Arrange + DefaultAccountManagerProvider provider = new DefaultAccountManagerProvider(); + AccountManager manager = provider.create(); + + // Act + var result = manager.registerAccount("bob", "test.com").join(); + + // Assert + assertThat(result.getNip05()).isEqualTo("bob@test.com"); + assertThat(result.getKeyName()).isEqualTo("key-bob"); + } +} diff --git a/nsecbunker-admin/pom.xml b/nsecbunker-admin/pom.xml index e859bd8..c40c51f 100644 --- a/nsecbunker-admin/pom.xml +++ b/nsecbunker-admin/pom.xml @@ -8,7 +8,7 @@ xyz.tcheeric nsecbunker-java - 0.1.0-SNAPSHOT + 0.1.0 nsecbunker-admin diff --git a/nsecbunker-admin/src/main/java/xyz/tcheeric/nsecbunker/admin/AdminAuthenticator.java b/nsecbunker-admin/src/main/java/xyz/tcheeric/nsecbunker/admin/AdminAuthenticator.java index f1af2b1..2aff940 100644 --- a/nsecbunker-admin/src/main/java/xyz/tcheeric/nsecbunker/admin/AdminAuthenticator.java +++ b/nsecbunker-admin/src/main/java/xyz/tcheeric/nsecbunker/admin/AdminAuthenticator.java @@ -2,7 +2,6 @@ import lombok.extern.slf4j.Slf4j; import xyz.tcheeric.nsecbunker.protocol.nip46.Nip46Request; -import xyz.tcheeric.nsecbunker.protocol.nip46.Nip46Response; import java.util.List; import java.util.concurrent.CompletableFuture; diff --git a/nsecbunker-admin/src/main/java/xyz/tcheeric/nsecbunker/admin/NsecBunkerAdminClient.java b/nsecbunker-admin/src/main/java/xyz/tcheeric/nsecbunker/admin/NsecBunkerAdminClient.java index 60c1435..0e58b4a 100644 --- a/nsecbunker-admin/src/main/java/xyz/tcheeric/nsecbunker/admin/NsecBunkerAdminClient.java +++ b/nsecbunker-admin/src/main/java/xyz/tcheeric/nsecbunker/admin/NsecBunkerAdminClient.java @@ -3,20 +3,20 @@ import lombok.Getter; import lombok.extern.slf4j.Slf4j; import nostr.id.Identity; -import xyz.tcheeric.nsecbunker.connection.ConnectionListener; -import xyz.tcheeric.nsecbunker.connection.ConnectionState; -import xyz.tcheeric.nsecbunker.connection.ExponentialBackoffStrategy; -import xyz.tcheeric.nsecbunker.connection.RelayConnection; -import xyz.tcheeric.nsecbunker.connection.RelayListener; -import xyz.tcheeric.nsecbunker.connection.RelayPool; import xyz.tcheeric.nsecbunker.admin.key.DefaultKeyManager; import xyz.tcheeric.nsecbunker.admin.key.KeyManager; -import xyz.tcheeric.nsecbunker.admin.policy.DefaultPolicyManager; -import xyz.tcheeric.nsecbunker.admin.policy.PolicyManager; import xyz.tcheeric.nsecbunker.admin.permission.DefaultPermissionManager; import xyz.tcheeric.nsecbunker.admin.permission.PermissionManager; +import xyz.tcheeric.nsecbunker.admin.policy.DefaultPolicyManager; +import xyz.tcheeric.nsecbunker.admin.policy.PolicyManager; import xyz.tcheeric.nsecbunker.admin.token.DefaultTokenManager; import xyz.tcheeric.nsecbunker.admin.token.TokenManager; +import xyz.tcheeric.nsecbunker.connection.ConnectionListener; +import xyz.tcheeric.nsecbunker.connection.ConnectionState; +import xyz.tcheeric.nsecbunker.connection.ExponentialBackoffStrategy; +import xyz.tcheeric.nsecbunker.connection.RelayConnection; +import xyz.tcheeric.nsecbunker.connection.RelayListener; +import xyz.tcheeric.nsecbunker.connection.RelayPool; import xyz.tcheeric.nsecbunker.core.exception.BunkerConnectionException; import xyz.tcheeric.nsecbunker.protocol.crypto.Nip04Crypto; import xyz.tcheeric.nsecbunker.protocol.nip46.Nip46Decoder; @@ -650,7 +650,7 @@ public void onNotice(RelayConnection relay, String message) { /** * Connection listener for handling connection state changes. */ - private class AdminConnectionListener implements ConnectionListener { + private static class AdminConnectionListener implements ConnectionListener { @Override public void onConnected(String relayUrl) { log.debug("Connected to relay: {}", relayUrl); diff --git a/nsecbunker-admin/src/main/java/xyz/tcheeric/nsecbunker/admin/permission/PermissionManager.java b/nsecbunker-admin/src/main/java/xyz/tcheeric/nsecbunker/admin/permission/PermissionManager.java index c2f8ddd..231ffe5 100644 --- a/nsecbunker-admin/src/main/java/xyz/tcheeric/nsecbunker/admin/permission/PermissionManager.java +++ b/nsecbunker-admin/src/main/java/xyz/tcheeric/nsecbunker/admin/permission/PermissionManager.java @@ -1,7 +1,7 @@ package xyz.tcheeric.nsecbunker.admin.permission; -import xyz.tcheeric.nsecbunker.core.model.KeyUser; import xyz.tcheeric.nsecbunker.core.model.BunkerPolicy; +import xyz.tcheeric.nsecbunker.core.model.KeyUser; import java.util.List; import java.util.concurrent.CompletableFuture; diff --git a/nsecbunker-admin/src/test/java/xyz/tcheeric/nsecbunker/admin/AdminConfigTest.java b/nsecbunker-admin/src/test/java/xyz/tcheeric/nsecbunker/admin/AdminConfigTest.java index 8a6059c..bb99e2f 100644 --- a/nsecbunker-admin/src/test/java/xyz/tcheeric/nsecbunker/admin/AdminConfigTest.java +++ b/nsecbunker-admin/src/test/java/xyz/tcheeric/nsecbunker/admin/AdminConfigTest.java @@ -1,12 +1,13 @@ package xyz.tcheeric.nsecbunker.admin; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; import java.time.Duration; import java.util.List; -import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; /** * Tests for {@link AdminConfig}. diff --git a/nsecbunker-admin/src/test/java/xyz/tcheeric/nsecbunker/admin/AdminExceptionTest.java b/nsecbunker-admin/src/test/java/xyz/tcheeric/nsecbunker/admin/AdminExceptionTest.java index e0a11b7..66b7326 100644 --- a/nsecbunker-admin/src/test/java/xyz/tcheeric/nsecbunker/admin/AdminExceptionTest.java +++ b/nsecbunker-admin/src/test/java/xyz/tcheeric/nsecbunker/admin/AdminExceptionTest.java @@ -1,10 +1,10 @@ package xyz.tcheeric.nsecbunker.admin; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; import xyz.tcheeric.nsecbunker.protocol.nip46.Nip46Error; -import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; /** * Tests for {@link AdminException}. diff --git a/nsecbunker-admin/src/test/java/xyz/tcheeric/nsecbunker/admin/NsecBunkerAdminClientTest.java b/nsecbunker-admin/src/test/java/xyz/tcheeric/nsecbunker/admin/NsecBunkerAdminClientTest.java index ea67756..494b804 100644 --- a/nsecbunker-admin/src/test/java/xyz/tcheeric/nsecbunker/admin/NsecBunkerAdminClientTest.java +++ b/nsecbunker-admin/src/test/java/xyz/tcheeric/nsecbunker/admin/NsecBunkerAdminClientTest.java @@ -1,15 +1,15 @@ package xyz.tcheeric.nsecbunker.admin; -import org.junit.jupiter.api.Test; +import nostr.base.PublicKey; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; import xyz.tcheeric.nsecbunker.connection.ConnectionState; -import nostr.base.PublicKey; - import java.time.Duration; import java.util.List; -import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; /** * Tests for {@link NsecBunkerAdminClient}. diff --git a/nsecbunker-client/pom.xml b/nsecbunker-client/pom.xml index 3a5806f..bbc8833 100644 --- a/nsecbunker-client/pom.xml +++ b/nsecbunker-client/pom.xml @@ -8,7 +8,7 @@ xyz.tcheeric nsecbunker-java - 0.1.0-SNAPSHOT + 0.1.0 nsecbunker-client diff --git a/nsecbunker-client/src/main/java/xyz/tcheeric/nsecbunker/client/signer/NsecBunkerSigner.java b/nsecbunker-client/src/main/java/xyz/tcheeric/nsecbunker/client/signer/NsecBunkerSigner.java index 4021b1f..3518a16 100644 --- a/nsecbunker-client/src/main/java/xyz/tcheeric/nsecbunker/client/signer/NsecBunkerSigner.java +++ b/nsecbunker-client/src/main/java/xyz/tcheeric/nsecbunker/client/signer/NsecBunkerSigner.java @@ -15,8 +15,8 @@ import java.util.List; import java.util.Objects; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; /** diff --git a/nsecbunker-client/src/main/java/xyz/tcheeric/nsecbunker/client/transport/RelayNip46Transport.java b/nsecbunker-client/src/main/java/xyz/tcheeric/nsecbunker/client/transport/RelayNip46Transport.java index a8af5c5..45aa47f 100644 --- a/nsecbunker-client/src/main/java/xyz/tcheeric/nsecbunker/client/transport/RelayNip46Transport.java +++ b/nsecbunker-client/src/main/java/xyz/tcheeric/nsecbunker/client/transport/RelayNip46Transport.java @@ -1,14 +1,11 @@ package xyz.tcheeric.nsecbunker.client.transport; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.Builder; import lombok.extern.slf4j.Slf4j; import nostr.event.impl.GenericEvent; import nostr.id.Identity; import xyz.tcheeric.nsecbunker.connection.RelayConnection; -import xyz.tcheeric.nsecbunker.connection.RelayListener; import xyz.tcheeric.nsecbunker.connection.RelayPool; import xyz.tcheeric.nsecbunker.protocol.crypto.Nip04Crypto; import xyz.tcheeric.nsecbunker.protocol.nip46.Nip46Decoder; diff --git a/nsecbunker-client/src/test/java/xyz/tcheeric/nsecbunker/client/integration/SignerIntegrationTest.java b/nsecbunker-client/src/test/java/xyz/tcheeric/nsecbunker/client/integration/SignerIntegrationTest.java index 309e893..3410955 100644 --- a/nsecbunker-client/src/test/java/xyz/tcheeric/nsecbunker/client/integration/SignerIntegrationTest.java +++ b/nsecbunker-client/src/test/java/xyz/tcheeric/nsecbunker/client/integration/SignerIntegrationTest.java @@ -5,7 +5,6 @@ import xyz.tcheeric.nsecbunker.client.signer.NsecBunkerSigner; import xyz.tcheeric.nsecbunker.client.signer.SignerConfig; import xyz.tcheeric.nsecbunker.client.signer.SignerException; -import xyz.tcheeric.nsecbunker.protocol.nip46.Nip46Request; import xyz.tcheeric.nsecbunker.protocol.nip46.Nip46Response; import java.time.Duration; diff --git a/nsecbunker-connection/pom.xml b/nsecbunker-connection/pom.xml index c3cc9ff..99f1516 100644 --- a/nsecbunker-connection/pom.xml +++ b/nsecbunker-connection/pom.xml @@ -8,7 +8,7 @@ xyz.tcheeric nsecbunker-java - 0.1.0-SNAPSHOT + 0.1.0 nsecbunker-connection diff --git a/nsecbunker-connection/src/main/java/xyz/tcheeric/nsecbunker/connection/RelayConnection.java b/nsecbunker-connection/src/main/java/xyz/tcheeric/nsecbunker/connection/RelayConnection.java index 8d2d07e..a1b8bd0 100644 --- a/nsecbunker-connection/src/main/java/xyz/tcheeric/nsecbunker/connection/RelayConnection.java +++ b/nsecbunker-connection/src/main/java/xyz/tcheeric/nsecbunker/connection/RelayConnection.java @@ -50,7 +50,7 @@ * } */ @Slf4j -public class RelayConnection { +public final class RelayConnection { private static final int NORMAL_CLOSURE = 1000; private static final int GOING_AWAY = 1001; diff --git a/nsecbunker-connection/src/main/java/xyz/tcheeric/nsecbunker/connection/RelayHealthMonitor.java b/nsecbunker-connection/src/main/java/xyz/tcheeric/nsecbunker/connection/RelayHealthMonitor.java index c85e4a8..637ad4e 100644 --- a/nsecbunker-connection/src/main/java/xyz/tcheeric/nsecbunker/connection/RelayHealthMonitor.java +++ b/nsecbunker-connection/src/main/java/xyz/tcheeric/nsecbunker/connection/RelayHealthMonitor.java @@ -399,7 +399,6 @@ private ConnectionHealth buildHealthSnapshot() { } // Determine health status - long totalPings = successfulPings.get() + failedPings.get(); int consecutiveFailures = calculateConsecutiveFailures(); boolean healthy = connected && consecutiveFailures < unhealthyThreshold; diff --git a/nsecbunker-connection/src/main/java/xyz/tcheeric/nsecbunker/connection/RelayPool.java b/nsecbunker-connection/src/main/java/xyz/tcheeric/nsecbunker/connection/RelayPool.java index 8b5a268..f28395b 100644 --- a/nsecbunker-connection/src/main/java/xyz/tcheeric/nsecbunker/connection/RelayPool.java +++ b/nsecbunker-connection/src/main/java/xyz/tcheeric/nsecbunker/connection/RelayPool.java @@ -55,7 +55,7 @@ * } */ @Slf4j -public class RelayPool implements AutoCloseable { +public final class RelayPool implements AutoCloseable { /** * Map of relay URL to connection. @@ -378,13 +378,20 @@ public void connectAll(Duration timeout) throws BunkerConnectionException { } if (successCount.get() < minConnectedRelays) { + String errorSummary = errors.isEmpty() ? "no error details" + : errors.stream() + .map(Throwable::getMessage) + .distinct() + .limit(3) + .reduce((a, b) -> a + "; " + b) + .orElse("unknown"); throw new BunkerConnectionException( - String.format("Only %d of %d required relays connected", - successCount.get(), minConnectedRelays) + String.format("Only %d of %d required relays connected (%s)", + successCount.get(), minConnectedRelays, errorSummary) ); } - log.info("Connected to {}/{} relays", successCount.get(), relays.size()); + log.info("relay_pool_connected success={} total={}", successCount.get(), relays.size()); } /** @@ -425,7 +432,7 @@ public int sendTo(String message, Predicate predicate) { count++; } } catch (Exception e) { - log.warn("Failed to send to {}: {}", relay.getUrl(), e.getMessage()); + log.warn("relay_pool_broadcast_failed relay={} error={}", relay.getUrl(), e.getMessage()); } } } @@ -445,7 +452,7 @@ public boolean sendTo(String url, String message) { try { return relay.send(message); } catch (Exception e) { - log.warn("Failed to send to {}: {}", url, e.getMessage()); + log.warn("relay_pool_send_failed relay={} error={}", url, e.getMessage()); } } return false; diff --git a/nsecbunker-connection/src/test/java/xyz/tcheeric/nsecbunker/connection/CompositeConnectionListenerTest.java b/nsecbunker-connection/src/test/java/xyz/tcheeric/nsecbunker/connection/CompositeConnectionListenerTest.java index 2281381..2680e44 100644 --- a/nsecbunker-connection/src/test/java/xyz/tcheeric/nsecbunker/connection/CompositeConnectionListenerTest.java +++ b/nsecbunker-connection/src/test/java/xyz/tcheeric/nsecbunker/connection/CompositeConnectionListenerTest.java @@ -7,7 +7,11 @@ import java.util.List; import java.util.concurrent.atomic.AtomicInteger; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertTrue; /** * Tests for the {@link CompositeConnectionListener} class. diff --git a/nsecbunker-connection/src/test/java/xyz/tcheeric/nsecbunker/connection/ConnectionHealthTest.java b/nsecbunker-connection/src/test/java/xyz/tcheeric/nsecbunker/connection/ConnectionHealthTest.java index c950500..c5044d2 100644 --- a/nsecbunker-connection/src/test/java/xyz/tcheeric/nsecbunker/connection/ConnectionHealthTest.java +++ b/nsecbunker-connection/src/test/java/xyz/tcheeric/nsecbunker/connection/ConnectionHealthTest.java @@ -6,7 +6,10 @@ import java.time.Instant; import java.util.Optional; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; /** * Tests for the {@link ConnectionHealth} class. diff --git a/nsecbunker-connection/src/test/java/xyz/tcheeric/nsecbunker/connection/ConnectionListenerTest.java b/nsecbunker-connection/src/test/java/xyz/tcheeric/nsecbunker/connection/ConnectionListenerTest.java index 05ae365..439b133 100644 --- a/nsecbunker-connection/src/test/java/xyz/tcheeric/nsecbunker/connection/ConnectionListenerTest.java +++ b/nsecbunker-connection/src/test/java/xyz/tcheeric/nsecbunker/connection/ConnectionListenerTest.java @@ -2,7 +2,9 @@ import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; /** * Tests for the {@link ConnectionListener} interface. diff --git a/nsecbunker-connection/src/test/java/xyz/tcheeric/nsecbunker/connection/ExponentialBackoffStrategyTest.java b/nsecbunker-connection/src/test/java/xyz/tcheeric/nsecbunker/connection/ExponentialBackoffStrategyTest.java index cfa5d59..acd6235 100644 --- a/nsecbunker-connection/src/test/java/xyz/tcheeric/nsecbunker/connection/ExponentialBackoffStrategyTest.java +++ b/nsecbunker-connection/src/test/java/xyz/tcheeric/nsecbunker/connection/ExponentialBackoffStrategyTest.java @@ -1,12 +1,16 @@ package xyz.tcheeric.nsecbunker.connection; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.RepeatedTest; +import org.junit.jupiter.api.Test; import java.time.Duration; import java.util.Optional; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; /** * Tests for the {@link ExponentialBackoffStrategy} class. diff --git a/nsecbunker-connection/src/test/java/xyz/tcheeric/nsecbunker/connection/FixedDelayStrategyTest.java b/nsecbunker-connection/src/test/java/xyz/tcheeric/nsecbunker/connection/FixedDelayStrategyTest.java index fac4bd6..680788d 100644 --- a/nsecbunker-connection/src/test/java/xyz/tcheeric/nsecbunker/connection/FixedDelayStrategyTest.java +++ b/nsecbunker-connection/src/test/java/xyz/tcheeric/nsecbunker/connection/FixedDelayStrategyTest.java @@ -5,7 +5,11 @@ import java.time.Duration; import java.util.Optional; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; /** * Tests for the {@link FixedDelayStrategy} class. diff --git a/nsecbunker-connection/src/test/java/xyz/tcheeric/nsecbunker/connection/LoggingConnectionListenerTest.java b/nsecbunker-connection/src/test/java/xyz/tcheeric/nsecbunker/connection/LoggingConnectionListenerTest.java index fe981a2..8c83399 100644 --- a/nsecbunker-connection/src/test/java/xyz/tcheeric/nsecbunker/connection/LoggingConnectionListenerTest.java +++ b/nsecbunker-connection/src/test/java/xyz/tcheeric/nsecbunker/connection/LoggingConnectionListenerTest.java @@ -4,7 +4,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; /** * Tests for the {@link LoggingConnectionListener} class. diff --git a/nsecbunker-connection/src/test/java/xyz/tcheeric/nsecbunker/connection/NoReconnectionStrategyTest.java b/nsecbunker-connection/src/test/java/xyz/tcheeric/nsecbunker/connection/NoReconnectionStrategyTest.java index 779dcda..efb568f 100644 --- a/nsecbunker-connection/src/test/java/xyz/tcheeric/nsecbunker/connection/NoReconnectionStrategyTest.java +++ b/nsecbunker-connection/src/test/java/xyz/tcheeric/nsecbunker/connection/NoReconnectionStrategyTest.java @@ -2,10 +2,12 @@ import org.junit.jupiter.api.Test; -import java.time.Duration; import java.util.Optional; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertSame; /** * Tests for the {@link NoReconnectionStrategy} class. diff --git a/nsecbunker-connection/src/test/java/xyz/tcheeric/nsecbunker/connection/ReconnectionStrategyTest.java b/nsecbunker-connection/src/test/java/xyz/tcheeric/nsecbunker/connection/ReconnectionStrategyTest.java index 6ae1fd6..aa95229 100644 --- a/nsecbunker-connection/src/test/java/xyz/tcheeric/nsecbunker/connection/ReconnectionStrategyTest.java +++ b/nsecbunker-connection/src/test/java/xyz/tcheeric/nsecbunker/connection/ReconnectionStrategyTest.java @@ -4,7 +4,10 @@ import java.time.Duration; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; /** * Tests for the {@link ReconnectionStrategy} interface. diff --git a/nsecbunker-connection/src/test/java/xyz/tcheeric/nsecbunker/connection/RelayHealthMonitorTest.java b/nsecbunker-connection/src/test/java/xyz/tcheeric/nsecbunker/connection/RelayHealthMonitorTest.java index 3195fda..4abe073 100644 --- a/nsecbunker-connection/src/test/java/xyz/tcheeric/nsecbunker/connection/RelayHealthMonitorTest.java +++ b/nsecbunker-connection/src/test/java/xyz/tcheeric/nsecbunker/connection/RelayHealthMonitorTest.java @@ -5,11 +5,15 @@ import org.junit.jupiter.api.Test; import java.time.Duration; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; /** * Tests for the {@link RelayHealthMonitor} class. diff --git a/nsecbunker-connection/src/test/java/xyz/tcheeric/nsecbunker/connection/testing/MockRelayServer.java b/nsecbunker-connection/src/test/java/xyz/tcheeric/nsecbunker/connection/testing/MockRelayServer.java index 28f9c6c..6c92b32 100644 --- a/nsecbunker-connection/src/test/java/xyz/tcheeric/nsecbunker/connection/testing/MockRelayServer.java +++ b/nsecbunker-connection/src/test/java/xyz/tcheeric/nsecbunker/connection/testing/MockRelayServer.java @@ -3,7 +3,6 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ArrayNode; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import okhttp3.Response; @@ -13,8 +12,15 @@ import okhttp3.mockwebserver.MockWebServer; import java.io.IOException; -import java.util.*; -import java.util.concurrent.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import java.util.function.Function; diff --git a/nsecbunker-connection/src/test/java/xyz/tcheeric/nsecbunker/connection/testing/MockRelayServerTest.java b/nsecbunker-connection/src/test/java/xyz/tcheeric/nsecbunker/connection/testing/MockRelayServerTest.java index b060006..78990c1 100644 --- a/nsecbunker-connection/src/test/java/xyz/tcheeric/nsecbunker/connection/testing/MockRelayServerTest.java +++ b/nsecbunker-connection/src/test/java/xyz/tcheeric/nsecbunker/connection/testing/MockRelayServerTest.java @@ -1,6 +1,10 @@ package xyz.tcheeric.nsecbunker.connection.testing; -import okhttp3.*; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.WebSocket; +import okhttp3.WebSocketListener; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -11,7 +15,10 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; /** * Tests for {@link MockRelayServer}. diff --git a/nsecbunker-core/pom.xml b/nsecbunker-core/pom.xml index 03e4280..2484c19 100644 --- a/nsecbunker-core/pom.xml +++ b/nsecbunker-core/pom.xml @@ -8,7 +8,7 @@ xyz.tcheeric nsecbunker-java - 0.1.0-SNAPSHOT + 0.1.0 nsecbunker-core diff --git a/nsecbunker-core/src/main/java/xyz/tcheeric/nsecbunker/core/exception/BunkerAuthenticationException.java b/nsecbunker-core/src/main/java/xyz/tcheeric/nsecbunker/core/exception/BunkerAuthenticationException.java index 849991d..ddb6e49 100644 --- a/nsecbunker-core/src/main/java/xyz/tcheeric/nsecbunker/core/exception/BunkerAuthenticationException.java +++ b/nsecbunker-core/src/main/java/xyz/tcheeric/nsecbunker/core/exception/BunkerAuthenticationException.java @@ -1,5 +1,7 @@ package xyz.tcheeric.nsecbunker.core.exception; +import java.io.Serial; + /** * Exception thrown when authentication with nsecBunker fails. * @@ -8,6 +10,7 @@ */ public class BunkerAuthenticationException extends BunkerException { + @Serial private static final long serialVersionUID = 1L; /** diff --git a/nsecbunker-core/src/main/java/xyz/tcheeric/nsecbunker/core/exception/BunkerAuthorizationException.java b/nsecbunker-core/src/main/java/xyz/tcheeric/nsecbunker/core/exception/BunkerAuthorizationException.java index 1a0c867..1c5a5c0 100644 --- a/nsecbunker-core/src/main/java/xyz/tcheeric/nsecbunker/core/exception/BunkerAuthorizationException.java +++ b/nsecbunker-core/src/main/java/xyz/tcheeric/nsecbunker/core/exception/BunkerAuthorizationException.java @@ -1,5 +1,7 @@ package xyz.tcheeric.nsecbunker.core.exception; +import java.io.Serial; + /** * Exception thrown when an operation is not permitted by nsecBunker. * @@ -8,6 +10,7 @@ */ public class BunkerAuthorizationException extends BunkerException { + @Serial private static final long serialVersionUID = 1L; private final String method; diff --git a/nsecbunker-core/src/main/java/xyz/tcheeric/nsecbunker/core/exception/BunkerConnectionException.java b/nsecbunker-core/src/main/java/xyz/tcheeric/nsecbunker/core/exception/BunkerConnectionException.java index 5afdb55..020b47e 100644 --- a/nsecbunker-core/src/main/java/xyz/tcheeric/nsecbunker/core/exception/BunkerConnectionException.java +++ b/nsecbunker-core/src/main/java/xyz/tcheeric/nsecbunker/core/exception/BunkerConnectionException.java @@ -1,5 +1,7 @@ package xyz.tcheeric.nsecbunker.core.exception; +import java.io.Serial; + /** * Exception thrown when there are connection-related errors with nsecBunker. * @@ -8,6 +10,7 @@ */ public class BunkerConnectionException extends BunkerException { + @Serial private static final long serialVersionUID = 1L; private final String relay; diff --git a/nsecbunker-core/src/main/java/xyz/tcheeric/nsecbunker/core/exception/BunkerException.java b/nsecbunker-core/src/main/java/xyz/tcheeric/nsecbunker/core/exception/BunkerException.java index 3543cec..ad25105 100644 --- a/nsecbunker-core/src/main/java/xyz/tcheeric/nsecbunker/core/exception/BunkerException.java +++ b/nsecbunker-core/src/main/java/xyz/tcheeric/nsecbunker/core/exception/BunkerException.java @@ -1,5 +1,7 @@ package xyz.tcheeric.nsecbunker.core.exception; +import java.io.Serial; + /** * Base exception for all nsecBunker-related errors. * @@ -8,6 +10,7 @@ */ public class BunkerException extends Exception { + @Serial private static final long serialVersionUID = 1L; /** diff --git a/nsecbunker-core/src/main/java/xyz/tcheeric/nsecbunker/core/exception/BunkerKeyException.java b/nsecbunker-core/src/main/java/xyz/tcheeric/nsecbunker/core/exception/BunkerKeyException.java index 2763e60..0694c33 100644 --- a/nsecbunker-core/src/main/java/xyz/tcheeric/nsecbunker/core/exception/BunkerKeyException.java +++ b/nsecbunker-core/src/main/java/xyz/tcheeric/nsecbunker/core/exception/BunkerKeyException.java @@ -1,5 +1,7 @@ package xyz.tcheeric.nsecbunker.core.exception; +import java.io.Serial; + /** * Exception thrown when there are key-related errors in nsecBunker. * @@ -8,6 +10,7 @@ */ public class BunkerKeyException extends BunkerException { + @Serial private static final long serialVersionUID = 1L; /** diff --git a/nsecbunker-core/src/main/java/xyz/tcheeric/nsecbunker/core/exception/BunkerProtocolException.java b/nsecbunker-core/src/main/java/xyz/tcheeric/nsecbunker/core/exception/BunkerProtocolException.java index 38d2fc6..ebe400a 100644 --- a/nsecbunker-core/src/main/java/xyz/tcheeric/nsecbunker/core/exception/BunkerProtocolException.java +++ b/nsecbunker-core/src/main/java/xyz/tcheeric/nsecbunker/core/exception/BunkerProtocolException.java @@ -1,5 +1,7 @@ package xyz.tcheeric.nsecbunker.core.exception; +import java.io.Serial; + /** * Exception thrown when there are NIP-46 protocol-related errors. * @@ -8,6 +10,7 @@ */ public class BunkerProtocolException extends BunkerException { + @Serial private static final long serialVersionUID = 1L; private final String requestId; diff --git a/nsecbunker-core/src/main/java/xyz/tcheeric/nsecbunker/core/exception/BunkerTimeoutException.java b/nsecbunker-core/src/main/java/xyz/tcheeric/nsecbunker/core/exception/BunkerTimeoutException.java index ca7b777..b772271 100644 --- a/nsecbunker-core/src/main/java/xyz/tcheeric/nsecbunker/core/exception/BunkerTimeoutException.java +++ b/nsecbunker-core/src/main/java/xyz/tcheeric/nsecbunker/core/exception/BunkerTimeoutException.java @@ -1,5 +1,6 @@ package xyz.tcheeric.nsecbunker.core.exception; +import java.io.Serial; import java.time.Duration; /** @@ -10,6 +11,7 @@ */ public class BunkerTimeoutException extends BunkerException { + @Serial private static final long serialVersionUID = 1L; private final Duration timeout; diff --git a/nsecbunker-core/src/test/java/xyz/tcheeric/nsecbunker/core/model/BunkerPolicyTest.java b/nsecbunker-core/src/test/java/xyz/tcheeric/nsecbunker/core/model/BunkerPolicyTest.java index 7b7de20..e16d954 100644 --- a/nsecbunker-core/src/test/java/xyz/tcheeric/nsecbunker/core/model/BunkerPolicyTest.java +++ b/nsecbunker-core/src/test/java/xyz/tcheeric/nsecbunker/core/model/BunkerPolicyTest.java @@ -7,7 +7,6 @@ import java.time.Duration; import java.time.Instant; import java.time.temporal.ChronoUnit; -import java.util.List; import static org.assertj.core.api.Assertions.assertThat; diff --git a/nsecbunker-core/src/test/java/xyz/tcheeric/nsecbunker/core/testing/TestFixtures.java b/nsecbunker-core/src/test/java/xyz/tcheeric/nsecbunker/core/testing/TestFixtures.java index aa5c972..4560de5 100644 --- a/nsecbunker-core/src/test/java/xyz/tcheeric/nsecbunker/core/testing/TestFixtures.java +++ b/nsecbunker-core/src/test/java/xyz/tcheeric/nsecbunker/core/testing/TestFixtures.java @@ -1,6 +1,12 @@ package xyz.tcheeric.nsecbunker.core.testing; -import xyz.tcheeric.nsecbunker.core.model.*; +import xyz.tcheeric.nsecbunker.core.model.AccessToken; +import xyz.tcheeric.nsecbunker.core.model.BunkerConnection; +import xyz.tcheeric.nsecbunker.core.model.BunkerKey; +import xyz.tcheeric.nsecbunker.core.model.BunkerPolicy; +import xyz.tcheeric.nsecbunker.core.model.KeyUser; +import xyz.tcheeric.nsecbunker.core.model.PolicyRule; +import xyz.tcheeric.nsecbunker.core.model.SigningCondition; import java.time.Instant; import java.time.temporal.ChronoUnit; diff --git a/nsecbunker-monitoring/pom.xml b/nsecbunker-monitoring/pom.xml index cdee3b4..a7dd48b 100644 --- a/nsecbunker-monitoring/pom.xml +++ b/nsecbunker-monitoring/pom.xml @@ -8,7 +8,7 @@ xyz.tcheeric nsecbunker-java - 0.1.0-SNAPSHOT + 0.1.0 nsecbunker-monitoring diff --git a/nsecbunker-monitoring/src/main/java/xyz/tcheeric/nsecbunker/monitoring/alerting/CallbackAlertDelivery.java b/nsecbunker-monitoring/src/main/java/xyz/tcheeric/nsecbunker/monitoring/alerting/CallbackAlertDelivery.java index 64bee6d..98347e6 100644 --- a/nsecbunker-monitoring/src/main/java/xyz/tcheeric/nsecbunker/monitoring/alerting/CallbackAlertDelivery.java +++ b/nsecbunker-monitoring/src/main/java/xyz/tcheeric/nsecbunker/monitoring/alerting/CallbackAlertDelivery.java @@ -59,10 +59,11 @@ public CompletableFuture deliver(Alert alert) { try { callback.accept(alert); long latency = System.currentTimeMillis() - start; - log.debug("Alert delivered via callback: {}", alert.getType()); + log.debug("callback_delivered channel={} type={}", name, alert.getType()); return DeliveryResult.success(alert, name, latency); } catch (Exception e) { - log.warn("Failed to deliver alert via callback: {}", e.getMessage()); + log.warn("callback_delivery_failed channel={} type={} error={}", + name, alert.getType(), e.getMessage()); return DeliveryResult.failure(alert, name, e.getMessage()); } }); @@ -79,7 +80,8 @@ public CompletableFuture> deliverAll(List alerts) { long latency = System.currentTimeMillis() - start; results.add(DeliveryResult.success(alert, name, latency)); } catch (Exception e) { - log.warn("Failed to deliver alert via callback: {}", e.getMessage()); + log.warn("callback_batch_delivery_failed channel={} type={} error={}", + name, alert.getType(), e.getMessage()); results.add(DeliveryResult.failure(alert, name, e.getMessage())); } } diff --git a/nsecbunker-monitoring/src/main/java/xyz/tcheeric/nsecbunker/monitoring/alerting/WebhookAlertDelivery.java b/nsecbunker-monitoring/src/main/java/xyz/tcheeric/nsecbunker/monitoring/alerting/WebhookAlertDelivery.java index fb28027..e501adc 100644 --- a/nsecbunker-monitoring/src/main/java/xyz/tcheeric/nsecbunker/monitoring/alerting/WebhookAlertDelivery.java +++ b/nsecbunker-monitoring/src/main/java/xyz/tcheeric/nsecbunker/monitoring/alerting/WebhookAlertDelivery.java @@ -90,7 +90,7 @@ public CompletableFuture deliver(Alert alert) { client.newCall(request).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { - log.warn("Webhook delivery failed: {}", e.getMessage()); + log.warn("webhook_network_error channel={} error={}", name, e.getMessage()); future.complete(DeliveryResult.failure(alert, name, e.getMessage())); } @@ -99,18 +99,18 @@ public void onResponse(Call call, Response response) { long latency = System.currentTimeMillis() - start; try (response) { if (response.isSuccessful()) { - log.debug("Alert delivered via webhook: {} -> {}", alert.getType(), response.code()); + log.debug("webhook_delivered channel={} type={} status={}", name, alert.getType(), response.code()); future.complete(DeliveryResult.success(alert, name, latency)); } else { String error = "HTTP " + response.code() + ": " + response.message(); - log.warn("Webhook delivery failed: {}", error); + log.warn("webhook_http_error channel={} status={}", name, error); future.complete(DeliveryResult.failure(alert, name, error)); } } } }); } catch (Exception e) { - log.warn("Failed to send webhook: {}", e.getMessage()); + log.warn("webhook_request_failed channel={} error={}", name, e.getMessage()); future.complete(DeliveryResult.failure(alert, name, e.getMessage())); } @@ -157,6 +157,17 @@ private Map buildPayload(Alert alert) { public static class WebhookAlertDeliveryBuilder { private Map headers = new HashMap<>(); + /** + * Sets all headers at once. + * + * @param headers the headers map + * @return this builder + */ + public WebhookAlertDeliveryBuilder headers(Map headers) { + this.headers = headers != null ? new HashMap<>(headers) : new HashMap<>(); + return this; + } + /** * Adds a single header. * @@ -165,6 +176,9 @@ public static class WebhookAlertDeliveryBuilder { * @return this builder */ public WebhookAlertDeliveryBuilder header(String name, String value) { + if (this.headers == null) { + this.headers = new HashMap<>(); + } this.headers.put(name, value); return this; } diff --git a/nsecbunker-monitoring/src/main/java/xyz/tcheeric/nsecbunker/monitoring/health/DefaultHealthChecker.java b/nsecbunker-monitoring/src/main/java/xyz/tcheeric/nsecbunker/monitoring/health/DefaultHealthChecker.java index d99faa6..e122e83 100644 --- a/nsecbunker-monitoring/src/main/java/xyz/tcheeric/nsecbunker/monitoring/health/DefaultHealthChecker.java +++ b/nsecbunker-monitoring/src/main/java/xyz/tcheeric/nsecbunker/monitoring/health/DefaultHealthChecker.java @@ -5,9 +5,7 @@ import xyz.tcheeric.nsecbunker.connection.RelayConnection; import xyz.tcheeric.nsecbunker.connection.RelayPool; -import java.time.Duration; import java.time.Instant; -import java.util.Collection; import java.util.HashMap; import java.util.Map; import java.util.Objects; diff --git a/nsecbunker-monitoring/src/main/java/xyz/tcheeric/nsecbunker/monitoring/health/ErrorRateMonitor.java b/nsecbunker-monitoring/src/main/java/xyz/tcheeric/nsecbunker/monitoring/health/ErrorRateMonitor.java index 7ba5a96..a5e6d7d 100644 --- a/nsecbunker-monitoring/src/main/java/xyz/tcheeric/nsecbunker/monitoring/health/ErrorRateMonitor.java +++ b/nsecbunker-monitoring/src/main/java/xyz/tcheeric/nsecbunker/monitoring/health/ErrorRateMonitor.java @@ -7,7 +7,6 @@ import java.time.Duration; import java.time.Instant; import java.util.Deque; -import java.util.Objects; import java.util.concurrent.ConcurrentLinkedDeque; import java.util.concurrent.atomic.AtomicLong; diff --git a/nsecbunker-monitoring/src/test/java/xyz/tcheeric/nsecbunker/monitoring/alerting/AlertDeliveryTest.java b/nsecbunker-monitoring/src/test/java/xyz/tcheeric/nsecbunker/monitoring/alerting/AlertDeliveryTest.java index 2347ada..f618784 100644 --- a/nsecbunker-monitoring/src/test/java/xyz/tcheeric/nsecbunker/monitoring/alerting/AlertDeliveryTest.java +++ b/nsecbunker-monitoring/src/test/java/xyz/tcheeric/nsecbunker/monitoring/alerting/AlertDeliveryTest.java @@ -5,11 +5,10 @@ import okhttp3.mockwebserver.RecordedRequest; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; import java.io.IOException; -import java.time.Duration; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicReference; diff --git a/nsecbunker-monitoring/src/test/java/xyz/tcheeric/nsecbunker/monitoring/health/CircuitBreakerTest.java b/nsecbunker-monitoring/src/test/java/xyz/tcheeric/nsecbunker/monitoring/health/CircuitBreakerTest.java index 3cad7bd..32d9a33 100644 --- a/nsecbunker-monitoring/src/test/java/xyz/tcheeric/nsecbunker/monitoring/health/CircuitBreakerTest.java +++ b/nsecbunker-monitoring/src/test/java/xyz/tcheeric/nsecbunker/monitoring/health/CircuitBreakerTest.java @@ -1,7 +1,7 @@ package xyz.tcheeric.nsecbunker.monitoring.health; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; import java.time.Duration; import java.util.concurrent.atomic.AtomicInteger; diff --git a/nsecbunker-monitoring/src/test/java/xyz/tcheeric/nsecbunker/monitoring/health/ErrorRateMonitorTest.java b/nsecbunker-monitoring/src/test/java/xyz/tcheeric/nsecbunker/monitoring/health/ErrorRateMonitorTest.java index fd95200..bb848aa 100644 --- a/nsecbunker-monitoring/src/test/java/xyz/tcheeric/nsecbunker/monitoring/health/ErrorRateMonitorTest.java +++ b/nsecbunker-monitoring/src/test/java/xyz/tcheeric/nsecbunker/monitoring/health/ErrorRateMonitorTest.java @@ -1,7 +1,7 @@ package xyz.tcheeric.nsecbunker.monitoring.health; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; import java.time.Duration; diff --git a/nsecbunker-protocol/pom.xml b/nsecbunker-protocol/pom.xml index 6420f1c..ff65d98 100644 --- a/nsecbunker-protocol/pom.xml +++ b/nsecbunker-protocol/pom.xml @@ -8,7 +8,7 @@ xyz.tcheeric nsecbunker-java - 0.1.0-SNAPSHOT + 0.1.0 nsecbunker-protocol diff --git a/nsecbunker-protocol/src/main/java/xyz/tcheeric/nsecbunker/protocol/nip46/PendingRequestManager.java b/nsecbunker-protocol/src/main/java/xyz/tcheeric/nsecbunker/protocol/nip46/PendingRequestManager.java index e6dc669..07ebbed 100644 --- a/nsecbunker-protocol/src/main/java/xyz/tcheeric/nsecbunker/protocol/nip46/PendingRequestManager.java +++ b/nsecbunker-protocol/src/main/java/xyz/tcheeric/nsecbunker/protocol/nip46/PendingRequestManager.java @@ -8,7 +8,12 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; -import java.util.concurrent.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; /** * Manages pending NIP-46 requests and correlates them with responses. diff --git a/nsecbunker-protocol/src/test/java/xyz/tcheeric/nsecbunker/protocol/crypto/Nip04CryptoTest.java b/nsecbunker-protocol/src/test/java/xyz/tcheeric/nsecbunker/protocol/crypto/Nip04CryptoTest.java index eaf7a96..9539404 100644 --- a/nsecbunker-protocol/src/test/java/xyz/tcheeric/nsecbunker/protocol/crypto/Nip04CryptoTest.java +++ b/nsecbunker-protocol/src/test/java/xyz/tcheeric/nsecbunker/protocol/crypto/Nip04CryptoTest.java @@ -2,7 +2,12 @@ import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; /** * Tests for {@link Nip04Crypto}. diff --git a/nsecbunker-protocol/src/test/java/xyz/tcheeric/nsecbunker/protocol/nip46/Nip46DecoderTest.java b/nsecbunker-protocol/src/test/java/xyz/tcheeric/nsecbunker/protocol/nip46/Nip46DecoderTest.java index 94d0873..26ae7a0 100644 --- a/nsecbunker-protocol/src/test/java/xyz/tcheeric/nsecbunker/protocol/nip46/Nip46DecoderTest.java +++ b/nsecbunker-protocol/src/test/java/xyz/tcheeric/nsecbunker/protocol/nip46/Nip46DecoderTest.java @@ -5,7 +5,11 @@ import java.util.Optional; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; /** * Tests for {@link Nip46Decoder}. diff --git a/nsecbunker-protocol/src/test/java/xyz/tcheeric/nsecbunker/protocol/nip46/Nip46EncoderTest.java b/nsecbunker-protocol/src/test/java/xyz/tcheeric/nsecbunker/protocol/nip46/Nip46EncoderTest.java index 44096bf..e36e52a 100644 --- a/nsecbunker-protocol/src/test/java/xyz/tcheeric/nsecbunker/protocol/nip46/Nip46EncoderTest.java +++ b/nsecbunker-protocol/src/test/java/xyz/tcheeric/nsecbunker/protocol/nip46/Nip46EncoderTest.java @@ -6,7 +6,11 @@ import java.util.Arrays; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; /** * Tests for {@link Nip46Encoder}. diff --git a/nsecbunker-protocol/src/test/java/xyz/tcheeric/nsecbunker/protocol/nip46/Nip46MethodTest.java b/nsecbunker-protocol/src/test/java/xyz/tcheeric/nsecbunker/protocol/nip46/Nip46MethodTest.java index bfea633..6b73aa9 100644 --- a/nsecbunker-protocol/src/test/java/xyz/tcheeric/nsecbunker/protocol/nip46/Nip46MethodTest.java +++ b/nsecbunker-protocol/src/test/java/xyz/tcheeric/nsecbunker/protocol/nip46/Nip46MethodTest.java @@ -2,7 +2,9 @@ import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; /** * Tests for {@link Nip46Method}. diff --git a/nsecbunker-protocol/src/test/java/xyz/tcheeric/nsecbunker/protocol/nip46/Nip46RequestTest.java b/nsecbunker-protocol/src/test/java/xyz/tcheeric/nsecbunker/protocol/nip46/Nip46RequestTest.java index 21943e0..9b48112 100644 --- a/nsecbunker-protocol/src/test/java/xyz/tcheeric/nsecbunker/protocol/nip46/Nip46RequestTest.java +++ b/nsecbunker-protocol/src/test/java/xyz/tcheeric/nsecbunker/protocol/nip46/Nip46RequestTest.java @@ -6,7 +6,12 @@ import java.util.Collections; import java.util.List; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; /** * Tests for {@link Nip46Request}. diff --git a/nsecbunker-protocol/src/test/java/xyz/tcheeric/nsecbunker/protocol/nip46/Nip46ResponseTest.java b/nsecbunker-protocol/src/test/java/xyz/tcheeric/nsecbunker/protocol/nip46/Nip46ResponseTest.java index 1c21973..ae75b92 100644 --- a/nsecbunker-protocol/src/test/java/xyz/tcheeric/nsecbunker/protocol/nip46/Nip46ResponseTest.java +++ b/nsecbunker-protocol/src/test/java/xyz/tcheeric/nsecbunker/protocol/nip46/Nip46ResponseTest.java @@ -2,7 +2,11 @@ import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; /** * Tests for {@link Nip46Response}. diff --git a/nsecbunker-protocol/src/test/java/xyz/tcheeric/nsecbunker/protocol/nip46/PendingRequestManagerTest.java b/nsecbunker-protocol/src/test/java/xyz/tcheeric/nsecbunker/protocol/nip46/PendingRequestManagerTest.java index 26a4467..1b2f6f4 100644 --- a/nsecbunker-protocol/src/test/java/xyz/tcheeric/nsecbunker/protocol/nip46/PendingRequestManagerTest.java +++ b/nsecbunker-protocol/src/test/java/xyz/tcheeric/nsecbunker/protocol/nip46/PendingRequestManagerTest.java @@ -11,7 +11,12 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; /** * Tests for {@link PendingRequestManager}. diff --git a/nsecbunker-protocol/src/test/java/xyz/tcheeric/nsecbunker/protocol/nip46/PendingRequestTest.java b/nsecbunker-protocol/src/test/java/xyz/tcheeric/nsecbunker/protocol/nip46/PendingRequestTest.java index 021f371..5fe7b76 100644 --- a/nsecbunker-protocol/src/test/java/xyz/tcheeric/nsecbunker/protocol/nip46/PendingRequestTest.java +++ b/nsecbunker-protocol/src/test/java/xyz/tcheeric/nsecbunker/protocol/nip46/PendingRequestTest.java @@ -6,7 +6,13 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeoutException; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; /** * Tests for {@link PendingRequest}. diff --git a/nsecbunker-protocol/src/test/java/xyz/tcheeric/nsecbunker/protocol/testing/BunkerIntegrationTestBase.java b/nsecbunker-protocol/src/test/java/xyz/tcheeric/nsecbunker/protocol/testing/BunkerIntegrationTestBase.java index b85fc84..ce827e9 100644 --- a/nsecbunker-protocol/src/test/java/xyz/tcheeric/nsecbunker/protocol/testing/BunkerIntegrationTestBase.java +++ b/nsecbunker-protocol/src/test/java/xyz/tcheeric/nsecbunker/protocol/testing/BunkerIntegrationTestBase.java @@ -5,7 +5,6 @@ import xyz.tcheeric.nsecbunker.protocol.nip46.Nip46Response; import java.util.List; -import java.util.concurrent.TimeUnit; import java.util.function.Function; /** diff --git a/nsecbunker-protocol/src/test/java/xyz/tcheeric/nsecbunker/protocol/testing/MockBunkerServer.java b/nsecbunker-protocol/src/test/java/xyz/tcheeric/nsecbunker/protocol/testing/MockBunkerServer.java index 703883c..49d9d83 100644 --- a/nsecbunker-protocol/src/test/java/xyz/tcheeric/nsecbunker/protocol/testing/MockBunkerServer.java +++ b/nsecbunker-protocol/src/test/java/xyz/tcheeric/nsecbunker/protocol/testing/MockBunkerServer.java @@ -7,11 +7,20 @@ import lombok.extern.slf4j.Slf4j; import xyz.tcheeric.nsecbunker.connection.testing.MockRelayServer; import xyz.tcheeric.nsecbunker.protocol.crypto.Nip04Crypto; -import xyz.tcheeric.nsecbunker.protocol.nip46.*; +import xyz.tcheeric.nsecbunker.protocol.nip46.Nip46Decoder; +import xyz.tcheeric.nsecbunker.protocol.nip46.Nip46Encoder; +import xyz.tcheeric.nsecbunker.protocol.nip46.Nip46Request; +import xyz.tcheeric.nsecbunker.protocol.nip46.Nip46Response; import java.io.IOException; -import java.util.*; -import java.util.concurrent.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; import java.util.function.Function; /** diff --git a/nsecbunker-protocol/src/test/java/xyz/tcheeric/nsecbunker/protocol/testing/MockBunkerServerTest.java b/nsecbunker-protocol/src/test/java/xyz/tcheeric/nsecbunker/protocol/testing/MockBunkerServerTest.java index 39b1ccd..80a342a 100644 --- a/nsecbunker-protocol/src/test/java/xyz/tcheeric/nsecbunker/protocol/testing/MockBunkerServerTest.java +++ b/nsecbunker-protocol/src/test/java/xyz/tcheeric/nsecbunker/protocol/testing/MockBunkerServerTest.java @@ -1,17 +1,26 @@ package xyz.tcheeric.nsecbunker.protocol.testing; -import okhttp3.*; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.WebSocket; +import okhttp3.WebSocketListener; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import xyz.tcheeric.nsecbunker.protocol.nip46.*; +import xyz.tcheeric.nsecbunker.protocol.nip46.Nip46Decoder; +import xyz.tcheeric.nsecbunker.protocol.nip46.Nip46Encoder; +import xyz.tcheeric.nsecbunker.protocol.nip46.Nip46Request; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; /** * Tests for {@link MockBunkerServer}. diff --git a/nsecbunker-protocol/src/test/java/xyz/tcheeric/nsecbunker/protocol/testing/RelayIntegrationTestBase.java b/nsecbunker-protocol/src/test/java/xyz/tcheeric/nsecbunker/protocol/testing/RelayIntegrationTestBase.java index 2f71493..a1fbffe 100644 --- a/nsecbunker-protocol/src/test/java/xyz/tcheeric/nsecbunker/protocol/testing/RelayIntegrationTestBase.java +++ b/nsecbunker-protocol/src/test/java/xyz/tcheeric/nsecbunker/protocol/testing/RelayIntegrationTestBase.java @@ -1,9 +1,5 @@ package xyz.tcheeric.nsecbunker.protocol.testing; -import xyz.tcheeric.nsecbunker.connection.testing.MockRelayServer; - -import java.util.concurrent.TimeUnit; - /** * Base class for integration tests that only need a mock relay server. * diff --git a/nsecbunker-spring-boot-starter/pom.xml b/nsecbunker-spring-boot-starter/pom.xml index fab1b2b..eb44dc5 100644 --- a/nsecbunker-spring-boot-starter/pom.xml +++ b/nsecbunker-spring-boot-starter/pom.xml @@ -8,7 +8,7 @@ xyz.tcheeric nsecbunker-java - 0.1.0-SNAPSHOT + 0.1.0 nsecbunker-spring-boot-starter @@ -31,6 +31,10 @@ xyz.tcheeric nsecbunker-monitoring + + xyz.tcheeric + nsecbunker-account + diff --git a/nsecbunker-spring-boot-starter/src/main/java/xyz/tcheeric/nsecbunker/starter/NsecBunkerAutoConfiguration.java b/nsecbunker-spring-boot-starter/src/main/java/xyz/tcheeric/nsecbunker/starter/NsecBunkerAutoConfiguration.java index 72fb038..a30b40c 100644 --- a/nsecbunker-spring-boot-starter/src/main/java/xyz/tcheeric/nsecbunker/starter/NsecBunkerAutoConfiguration.java +++ b/nsecbunker-spring-boot-starter/src/main/java/xyz/tcheeric/nsecbunker/starter/NsecBunkerAutoConfiguration.java @@ -1,19 +1,31 @@ package xyz.tcheeric.nsecbunker.starter; +import lombok.extern.slf4j.Slf4j; import org.springframework.boot.actuate.health.HealthIndicator; import org.springframework.boot.actuate.info.InfoContributor; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; +import xyz.tcheeric.nsecbunker.account.nip05.DefaultNip05Manager; +import xyz.tcheeric.nsecbunker.account.nip05.Nip05Manager; +import xyz.tcheeric.nsecbunker.account.nip05.spi.DefaultNip05ManagerProvider; +import xyz.tcheeric.nsecbunker.account.nip05.spi.Nip05ManagerProvider; +import xyz.tcheeric.nsecbunker.account.registration.AccountManager; +import xyz.tcheeric.nsecbunker.account.registration.DefaultAccountManager; +import xyz.tcheeric.nsecbunker.account.registration.spi.AccountManagerProvider; +import xyz.tcheeric.nsecbunker.account.registration.spi.DefaultAccountManagerProvider; import xyz.tcheeric.nsecbunker.admin.NsecBunkerAdminClient; import xyz.tcheeric.nsecbunker.client.signer.NsecBunkerSigner; import xyz.tcheeric.nsecbunker.client.signer.SignerConfig; import xyz.tcheeric.nsecbunker.starter.health.BunkerHealthIndicator; import xyz.tcheeric.nsecbunker.starter.info.BunkerInfoContributor; +import java.util.Comparator; import java.util.List; +import java.util.Optional; /** * Auto-configuration for nsecBunker integration. @@ -24,10 +36,13 @@ *
  • {@link NsecBunkerSigner} - if signer properties are configured
  • *
  • {@link BunkerHealthIndicator} - for Spring Boot Actuator health checks
  • *
  • {@link BunkerInfoContributor} - for Spring Boot Actuator info endpoint
  • + *
  • {@link Nip05Manager} - for NIP-05 identity management
  • + *
  • {@link AccountManager} - for account registration
  • * * * @see NsecBunkerProperties */ +@Slf4j @AutoConfiguration @EnableConfigurationProperties(NsecBunkerProperties.class) public class NsecBunkerAutoConfiguration { @@ -129,4 +144,133 @@ private void validateSigner(NsecBunkerProperties.Signer signer) { throw new IllegalStateException("nsecbunker.signer.relays is required"); } } + + // ========== NIP-05 and Account Management ========== + + /** + * Creates the AccountManager bean using the highest priority available provider. + * + *

    If multiple {@link AccountManagerProvider} beans are registered, the one + * with the highest priority will be used. If none are registered, a default + * in-memory implementation is created.

    + * + * @param providers list of available providers (may be empty) + * @param properties the configuration properties + * @return the account manager + */ + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(name = "nsecbunker.nip05.enabled", havingValue = "true", matchIfMissing = true) + public AccountManager accountManager(List providers, + NsecBunkerProperties properties) { + String requestedProvider = properties.getNip05().getProvider(); + + // If a specific provider is requested (not "auto"), try to find it + if (!"auto".equalsIgnoreCase(requestedProvider)) { + Optional specific = providers.stream() + .filter(p -> requestedProvider.equalsIgnoreCase(p.name())) + .filter(AccountManagerProvider::isAvailable) + .findFirst(); + + if (specific.isPresent()) { + log.info("account_manager_created provider={}", specific.get().name()); + return specific.get().create(); + } + + log.warn("account_manager_provider_not_found requested={} using=default", + requestedProvider); + } + + // Auto-select highest priority available provider + Optional best = providers.stream() + .filter(AccountManagerProvider::isAvailable) + .max(Comparator.comparingInt(AccountManagerProvider::priority)); + + if (best.isPresent()) { + log.info("account_manager_created provider={} priority={}", + best.get().name(), best.get().priority()); + return best.get().create(); + } + + // Fallback to default + log.info("account_manager_created provider=in-memory (fallback)"); + return new DefaultAccountManager(); + } + + /** + * Creates the Nip05Manager bean using the highest priority available provider. + * + *

    If multiple {@link Nip05ManagerProvider} beans are registered, the one + * with the highest priority will be used. If none are registered, a default + * in-memory implementation is created.

    + * + * @param providers list of available providers (may be empty) + * @param accountManager the account manager for NIP-05 operations + * @param properties the configuration properties + * @return the NIP-05 manager + */ + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(name = "nsecbunker.nip05.enabled", havingValue = "true", matchIfMissing = true) + public Nip05Manager nip05Manager(List providers, + AccountManager accountManager, + NsecBunkerProperties properties) { + String requestedProvider = properties.getNip05().getProvider(); + + // If a specific provider is requested (not "auto"), try to find it + if (!"auto".equalsIgnoreCase(requestedProvider)) { + Optional specific = providers.stream() + .filter(p -> requestedProvider.equalsIgnoreCase(p.name())) + .filter(Nip05ManagerProvider::isAvailable) + .findFirst(); + + if (specific.isPresent()) { + log.info("nip05_manager_created provider={}", specific.get().name()); + return specific.get().create(); + } + + log.warn("nip05_manager_provider_not_found requested={} using=default", + requestedProvider); + } + + // Auto-select highest priority available provider + Optional best = providers.stream() + .filter(Nip05ManagerProvider::isAvailable) + .max(Comparator.comparingInt(Nip05ManagerProvider::priority)); + + if (best.isPresent()) { + log.info("nip05_manager_created provider={} priority={}", + best.get().name(), best.get().priority()); + return best.get().create(); + } + + // Fallback to default using the provided AccountManager + log.info("nip05_manager_created provider=in-memory (fallback)"); + return new DefaultNip05Manager(accountManager); + } + + /** + * Registers the default in-memory AccountManager provider. + * + * @return the default provider with priority 0 + */ + @Bean + @ConditionalOnMissingBean(name = "defaultAccountManagerProvider") + @ConditionalOnProperty(name = "nsecbunker.nip05.enabled", havingValue = "true", matchIfMissing = true) + public AccountManagerProvider defaultAccountManagerProvider() { + return new DefaultAccountManagerProvider(); + } + + /** + * Registers the default in-memory Nip05Manager provider. + * + * @param accountManager the account manager dependency + * @return the default provider with priority 0 + */ + @Bean + @ConditionalOnMissingBean(name = "defaultNip05ManagerProvider") + @ConditionalOnProperty(name = "nsecbunker.nip05.enabled", havingValue = "true", matchIfMissing = true) + public Nip05ManagerProvider defaultNip05ManagerProvider(AccountManager accountManager) { + return new DefaultNip05ManagerProvider(accountManager); + } } diff --git a/nsecbunker-spring-boot-starter/src/main/java/xyz/tcheeric/nsecbunker/starter/NsecBunkerProperties.java b/nsecbunker-spring-boot-starter/src/main/java/xyz/tcheeric/nsecbunker/starter/NsecBunkerProperties.java index ce4f246..9bef1d3 100644 --- a/nsecbunker-spring-boot-starter/src/main/java/xyz/tcheeric/nsecbunker/starter/NsecBunkerProperties.java +++ b/nsecbunker-spring-boot-starter/src/main/java/xyz/tcheeric/nsecbunker/starter/NsecBunkerProperties.java @@ -17,6 +17,7 @@ public class NsecBunkerProperties { private final Admin admin = new Admin(); private final Signer signer = new Signer(); private final Metrics metrics = new Metrics(); + private final Nip05 nip05 = new Nip05(); /** * Admin client configuration properties. @@ -85,4 +86,46 @@ public static class Metrics { */ private boolean perMethodMetrics = true; } + + /** + * NIP-05 configuration properties. + * + *

    These properties control how NIP-05 identity management is configured, + * including which provider implementation to use.

    + */ + @Data + public static class Nip05 { + /** + * Whether NIP-05 functionality is enabled. + */ + private boolean enabled = true; + + /** + * The provider to use for NIP-05 management. + * + *

    Options:

    + *
      + *
    • {@code auto} - Automatically select highest priority provider
    • + *
    • {@code in-memory} - Use in-memory storage (default)
    • + *
    • {@code bottin} - Use bottin persistent storage
    • + *
    • Custom provider name
    • + *
    + */ + private String provider = "auto"; + + /** + * Default relays to associate with new NIP-05 records. + */ + private List defaultRelays = new ArrayList<>(); + + /** + * Whether to auto-register NIP-05 when creating keys via admin client. + */ + private boolean autoRegister = false; + + /** + * Default domain for auto-registration (required if autoRegister is true). + */ + private String defaultDomain; + } } diff --git a/nsecbunker-spring-boot-starter/src/main/java/xyz/tcheeric/nsecbunker/starter/metrics/SigningMetrics.java b/nsecbunker-spring-boot-starter/src/main/java/xyz/tcheeric/nsecbunker/starter/metrics/SigningMetrics.java index 6e4f8c5..85e5d85 100644 --- a/nsecbunker-spring-boot-starter/src/main/java/xyz/tcheeric/nsecbunker/starter/metrics/SigningMetrics.java +++ b/nsecbunker-spring-boot-starter/src/main/java/xyz/tcheeric/nsecbunker/starter/metrics/SigningMetrics.java @@ -8,7 +8,6 @@ import lombok.extern.slf4j.Slf4j; import xyz.tcheeric.nsecbunker.client.signer.NsecBunkerSigner; -import java.time.Duration; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; diff --git a/nsecbunker-spring-boot-starter/src/test/java/xyz/tcheeric/nsecbunker/starter/NsecBunkerAutoConfigurationTest.java b/nsecbunker-spring-boot-starter/src/test/java/xyz/tcheeric/nsecbunker/starter/NsecBunkerAutoConfigurationTest.java index 1e1300b..6c4b759 100644 --- a/nsecbunker-spring-boot-starter/src/test/java/xyz/tcheeric/nsecbunker/starter/NsecBunkerAutoConfigurationTest.java +++ b/nsecbunker-spring-boot-starter/src/test/java/xyz/tcheeric/nsecbunker/starter/NsecBunkerAutoConfigurationTest.java @@ -4,6 +4,10 @@ import org.springframework.boot.actuate.health.HealthIndicator; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import xyz.tcheeric.nsecbunker.account.nip05.Nip05Manager; +import xyz.tcheeric.nsecbunker.account.nip05.spi.Nip05ManagerProvider; +import xyz.tcheeric.nsecbunker.account.registration.AccountManager; +import xyz.tcheeric.nsecbunker.account.registration.spi.AccountManagerProvider; import xyz.tcheeric.nsecbunker.admin.NsecBunkerAdminClient; import xyz.tcheeric.nsecbunker.client.signer.NsecBunkerSigner; @@ -47,4 +51,142 @@ void shouldExposeHealthIndicator() { assertThat(context).hasSingleBean(HealthIndicator.class); }); } + + // ========================================================================= + // NIP-05 Auto-Configuration Tests + // ========================================================================= + + /** + * Ensures AccountManager bean is created by default when NIP-05 is enabled (default). + */ + @Test + void shouldCreateAccountManagerBeanByDefault() { + contextRunner.run(context -> { + assertThat(context).hasSingleBean(AccountManager.class); + assertThat(context).hasSingleBean(AccountManagerProvider.class); + }); + } + + /** + * Ensures Nip05Manager bean is created by default when NIP-05 is enabled (default). + */ + @Test + void shouldCreateNip05ManagerBeanByDefault() { + contextRunner.run(context -> { + assertThat(context).hasSingleBean(Nip05Manager.class); + assertThat(context).hasSingleBean(Nip05ManagerProvider.class); + }); + } + + /** + * Ensures NIP-05 beans are not created when explicitly disabled. + */ + @Test + void shouldNotCreateNip05BeansWhenDisabled() { + contextRunner + .withPropertyValues("nsecbunker.nip05.enabled=false") + .run(context -> { + assertThat(context).doesNotHaveBean(AccountManager.class); + assertThat(context).doesNotHaveBean(Nip05Manager.class); + assertThat(context).doesNotHaveBean(AccountManagerProvider.class); + assertThat(context).doesNotHaveBean(Nip05ManagerProvider.class); + }); + } + + /** + * Ensures default in-memory provider is used when provider is set to "auto". + */ + @Test + void shouldUseDefaultProviderWhenAutoSelected() { + contextRunner + .withPropertyValues("nsecbunker.nip05.provider=auto") + .run(context -> { + assertThat(context).hasSingleBean(AccountManager.class); + assertThat(context).hasSingleBean(Nip05Manager.class); + + // Verify the default provider is registered + AccountManagerProvider accountProvider = context.getBean(AccountManagerProvider.class); + assertThat(accountProvider.name()).isEqualTo("in-memory"); + assertThat(accountProvider.priority()).isEqualTo(0); + }); + } + + /** + * Ensures fallback to default when requested provider is not available. + */ + @Test + void shouldFallbackToDefaultWhenRequestedProviderNotFound() { + contextRunner + .withPropertyValues("nsecbunker.nip05.provider=nonexistent-provider") + .run(context -> { + // Should still create beans using fallback + assertThat(context).hasSingleBean(AccountManager.class); + assertThat(context).hasSingleBean(Nip05Manager.class); + }); + } + + /** + * Ensures in-memory provider is selected when explicitly requested. + */ + @Test + void shouldSelectInMemoryProviderWhenExplicitlyRequested() { + contextRunner + .withPropertyValues("nsecbunker.nip05.provider=in-memory") + .run(context -> { + assertThat(context).hasSingleBean(AccountManager.class); + assertThat(context).hasSingleBean(Nip05Manager.class); + + AccountManagerProvider provider = context.getBean(AccountManagerProvider.class); + assertThat(provider.name()).isEqualTo("in-memory"); + }); + } + + /** + * Ensures NIP-05 beans are created with minimal configuration. + * + *

    Note: Admin/signer beans require their respective properties to be set, + * so this test provides minimal valid configuration to verify NIP-05 beans + * are created and functional independently of admin/signer usage. + */ + @Test + void shouldCreateNip05BeansWithMinimalConfiguration() { + new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(NsecBunkerAutoConfiguration.class)) + .withPropertyValues( + // Minimal admin/signer config required by auto-configuration + "nsecbunker.admin.bunker-pubkey=" + TEST_BUNKER_PUBKEY, + "nsecbunker.admin.admin-private-key=" + TEST_ADMIN_PRIVKEY, + "nsecbunker.admin.relays[0]=wss://relay.example.com", + "nsecbunker.signer.bunker-pubkey=" + TEST_BUNKER_PUBKEY, + "nsecbunker.signer.client-private-key=" + TEST_CLIENT_PRIVKEY, + "nsecbunker.signer.relays[0]=wss://relay.example.com" + ) + .run(context -> { + // NIP-05 beans should be present + assertThat(context).hasSingleBean(AccountManager.class); + assertThat(context).hasSingleBean(Nip05Manager.class); + + // Verify they are functional (can be used without external dependencies) + AccountManager accountManager = context.getBean(AccountManager.class); + assertThat(accountManager).isNotNull(); + + Nip05Manager nip05Manager = context.getBean(Nip05Manager.class); + assertThat(nip05Manager).isNotNull(); + }); + } + + /** + * Ensures provider availability check is respected. + */ + @Test + void shouldCheckProviderAvailability() { + contextRunner.run(context -> { + AccountManagerProvider provider = context.getBean(AccountManagerProvider.class); + // Default in-memory provider should always be available + assertThat(provider.isAvailable()).isTrue(); + + Nip05ManagerProvider nip05Provider = context.getBean(Nip05ManagerProvider.class); + assertThat(nip05Provider.isAvailable()).isTrue(); + }); + } } diff --git a/nsecbunker-tests/nsecbunker-chaos/pom.xml b/nsecbunker-tests/nsecbunker-chaos/pom.xml index b2dfeca..f085853 100644 --- a/nsecbunker-tests/nsecbunker-chaos/pom.xml +++ b/nsecbunker-tests/nsecbunker-chaos/pom.xml @@ -7,7 +7,7 @@ xyz.tcheeric nsecbunker-tests - 0.1.0-SNAPSHOT + 0.1.0 nsecbunker-chaos diff --git a/nsecbunker-tests/nsecbunker-chaos/src/test/java/xyz/tcheeric/nsecbunker/chaos/RelayConnectionChaosTest.java b/nsecbunker-tests/nsecbunker-chaos/src/test/java/xyz/tcheeric/nsecbunker/chaos/RelayConnectionChaosTest.java index ba1165d..c0d1c43 100644 --- a/nsecbunker-tests/nsecbunker-chaos/src/test/java/xyz/tcheeric/nsecbunker/chaos/RelayConnectionChaosTest.java +++ b/nsecbunker-tests/nsecbunker-chaos/src/test/java/xyz/tcheeric/nsecbunker/chaos/RelayConnectionChaosTest.java @@ -1,5 +1,6 @@ package xyz.tcheeric.nsecbunker.chaos; +import okhttp3.WebSocket; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import org.awaitility.Awaitility; @@ -10,12 +11,11 @@ import xyz.tcheeric.nsecbunker.connection.ConnectionState; import xyz.tcheeric.nsecbunker.connection.ReconnectionStrategy; import xyz.tcheeric.nsecbunker.connection.RelayConnection; -import okhttp3.WebSocket; import java.io.IOException; +import java.lang.reflect.Field; import java.time.Duration; import java.util.concurrent.TimeUnit; -import java.lang.reflect.Field; import static org.assertj.core.api.Assertions.assertThat; diff --git a/nsecbunker-tests/nsecbunker-e2e/pom.xml b/nsecbunker-tests/nsecbunker-e2e/pom.xml index 70f1c70..1b5071d 100644 --- a/nsecbunker-tests/nsecbunker-e2e/pom.xml +++ b/nsecbunker-tests/nsecbunker-e2e/pom.xml @@ -7,7 +7,7 @@ xyz.tcheeric nsecbunker-tests - 0.1.0-SNAPSHOT + 0.1.0 nsecbunker-e2e @@ -19,6 +19,8 @@ true + + true @@ -117,6 +119,11 @@ junit-jupiter test + + org.testcontainers + postgresql + test + @@ -141,6 +148,10 @@ 1 true + + + ${bottin.verification.skip} + @@ -153,5 +164,22 @@ false + + e2e-bottin + + false + + + + + org.apache.maven.plugins + maven-surefire-plugin + + bottin + + + + + diff --git a/nsecbunker-tests/nsecbunker-e2e/src/test/java/xyz/tcheeric/nsecbunker/e2e/BottinE2ETest.java b/nsecbunker-tests/nsecbunker-e2e/src/test/java/xyz/tcheeric/nsecbunker/e2e/BottinE2ETest.java new file mode 100644 index 0000000..f5c4491 --- /dev/null +++ b/nsecbunker-tests/nsecbunker-e2e/src/test/java/xyz/tcheeric/nsecbunker/e2e/BottinE2ETest.java @@ -0,0 +1,636 @@ +package xyz.tcheeric.nsecbunker.e2e; + +import com.fasterxml.jackson.databind.JsonNode; +import nostr.id.Identity; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import xyz.tcheeric.nsecbunker.e2e.containers.BottinContainer; + +import java.io.IOException; +import java.net.http.HttpResponse; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * End-to-end tests for Bottin NIP-05 registry service. + * + *

    These tests verify the complete NIP-05 identity management flow: + *

      + *
    • Domain registration and management
    • + *
    • NIP-05 record CRUD operations
    • + *
    • .well-known/nostr.json endpoint compliance with NIP-05 spec
    • + *
    + * + *

    The tests use Testcontainers to spin up: + *

      + *
    • PostgreSQL database for Bottin persistence
    • + *
    • Bottin application with test mode (verification skip enabled)
    • + *
    + * + *

    Prerequisites: Bottin must support the {@code BOTTIN_VERIFICATION_SKIP=true} + * environment variable to auto-verify domains in test mode. + * + *

    Run with: {@code mvn -Pe2e test -Dtest=BottinE2ETest} + */ +@Testcontainers +@Tag("e2e") +@Tag("bottin") +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class BottinE2ETest { + + private static final Logger log = LoggerFactory.getLogger(BottinE2ETest.class); + + private static final String TEST_DOMAIN = "test.example.com"; + private static final String POSTGRES_NETWORK_ALIAS = "postgres"; + private static final int POSTGRES_PORT = 5432; + + static Network network = Network.newNetwork(); + + @Container + static PostgreSQLContainer postgres = new PostgreSQLContainer<>("postgres:15-alpine") + .withNetwork(network) + .withNetworkAliases(POSTGRES_NETWORK_ALIAS) + .withDatabaseName("bottin") + .withUsername("bottin") + .withPassword("bottin"); + + @Container + static BottinContainer bottin = new BottinContainer() + .withNetwork(network) + .withNetworkAliases("bottin") + .withPostgresUrl("jdbc:postgresql://" + POSTGRES_NETWORK_ALIAS + ":" + POSTGRES_PORT + "/bottin") + .withPostgresUser("bottin") + .withPostgresPassword("bottin") + .withVerificationSkip(true) + .dependsOn(postgres); + + private static BottinTestClient client; + + // Shared state across ordered tests + private static Long domainId; + private static Long recordId; + private static String testPubkey; + + @BeforeAll + static void setup() { + client = new BottinTestClient( + bottin.getBaseUrl(), + bottin.getAdminUser(), + bottin.getAdminPassword()); + testPubkey = Identity.generateRandomIdentity().getPublicKey().toString(); + log.info("bottin_e2e_test_started base_url={} admin_user={} test_pubkey={}", + bottin.getBaseUrl(), bottin.getAdminUser(), testPubkey); + } + + /** + * Force-verifies a domain by updating the database directly. + * This bypasses normal verification flow for test purposes. + * + *

    Note: String.format is used here because psql -c does not support + * parameterized queries. This is safe because domainId is a Long (numeric type), + * not a String. Do not copy this pattern for String parameters. + */ + private static void forceVerifyDomain(Long domainId) throws IOException, InterruptedException { + // Safe: domainId is Long, not String - no SQL injection risk with %d format specifier + String sql = String.format( + "UPDATE domains SET verified = true, verified_at = NOW() WHERE id = %d", + domainId); + postgres.execInContainer("psql", "-U", "bottin", "-d", "bottin", "-c", sql); + log.info("domain_force_verified id={}", domainId); + } + + /** + * Ensures a NIP-05 record is enabled before testing. + * + *

    Checks the current state and toggles if disabled, then verifies the record + * is actually enabled to prevent test flakiness from unknown initial states. + * + * @param recordId the record ID to enable + */ + private static void ensureRecordEnabled(Long recordId) throws Exception { + HttpResponse response = client.getRecord(recordId); + JsonNode json = client.parseJson(response); + boolean isEnabled = json.get("enabled").asBoolean(); + + if (!isEnabled) { + response = client.toggleRecord(recordId); + json = client.parseJson(response); + isEnabled = json.get("enabled").asBoolean(); + assertThat(isEnabled) + .as("Record should be enabled after toggle") + .isTrue(); + log.info("record_enabled id={}", recordId); + } + } + + // ========================================================================= + // Health Check Tests + // ========================================================================= + + /** + * Verifies that Bottin's health endpoint returns healthy status. + */ + @Test + @Order(1) + void shouldReturnHealthyStatus() throws Exception { + // Act + HttpResponse response = client.healthCheck(); + + // Assert + assertThat(response.statusCode()).isEqualTo(200); + assertThat(response.body()).contains("UP"); + log.info("health_check_passed"); + } + + // ========================================================================= + // Domain Management Tests + // ========================================================================= + + /** + * Verifies that a new domain can be registered via the API. + * After creation, force-verifies the domain for subsequent tests. + */ + @Test + @Order(10) + void shouldRegisterNewDomain() throws Exception { + // Act + HttpResponse response = client.createDomain(TEST_DOMAIN); + + // Assert + assertThat(response.statusCode()).isEqualTo(201); + + JsonNode json = client.parseJson(response); + assertThat(json.has("id")).isTrue(); + assertThat(json.get("name").asText()).isEqualTo(TEST_DOMAIN); + + domainId = json.get("id").asLong(); + + // Force-verify domain for subsequent tests (NIP-05 records require verified domains) + forceVerifyDomain(domainId); + + log.info("domain_created id={} name={}", domainId, TEST_DOMAIN); + } + + /** + * Verifies that the registered domain can be retrieved by ID. + */ + @Test + @Order(11) + void shouldGetDomainById() throws Exception { + // Arrange - domain created in previous test + assertThat(domainId).isNotNull(); + + // Act + HttpResponse response = client.getDomain(domainId); + + // Assert + assertThat(response.statusCode()).isEqualTo(200); + + JsonNode json = client.parseJson(response); + assertThat(json.get("id").asLong()).isEqualTo(domainId); + assertThat(json.get("name").asText()).isEqualTo(TEST_DOMAIN); + } + + /** + * Verifies that all domains can be listed. + */ + @Test + @Order(12) + void shouldListAllDomains() throws Exception { + // Act + HttpResponse response = client.getDomains(); + + // Assert + assertThat(response.statusCode()).isEqualTo(200); + + JsonNode json = client.parseJson(response); + log.info("domains_response body={}", response.body()); + + // Handle both array and paginated response formats + JsonNode domainsArray; + if (json.isArray()) { + domainsArray = json; + } else if (json.has("content")) { + // Spring Data Page format + domainsArray = json.get("content"); + } else { + domainsArray = json; + } + + assertThat(domainsArray.isArray()).isTrue(); + assertThat(domainsArray.size()).isGreaterThanOrEqualTo(1); + + // Verify our test domain is in the list + boolean found = false; + for (JsonNode domain : domainsArray) { + if (TEST_DOMAIN.equals(domain.get("name").asText())) { + found = true; + break; + } + } + assertThat(found).isTrue(); + } + + /** + * Verifies that attempting to register a duplicate domain returns error. + * Note: Bottin returns 400 Bad Request for duplicate domains (IllegalArgumentException). + */ + @Test + @Order(13) + void shouldRejectDuplicateDomain() throws Exception { + // Act - try to create the same domain again + HttpResponse response = client.createDomain(TEST_DOMAIN); + + // Assert - Bottin returns 400 for duplicate domains + assertThat(response.statusCode()).isIn(400, 409); + log.info("duplicate_domain_rejected name={} status={}", TEST_DOMAIN, response.statusCode()); + } + + // ========================================================================= + // NIP-05 Record Management Tests + // ========================================================================= + + /** + * Verifies that a new NIP-05 record can be created. + */ + @Test + @Order(20) + void shouldCreateNip05Record() throws Exception { + // Arrange + String username = "alice"; + List relays = List.of("wss://relay1.example.com", "wss://relay2.example.com"); + + // Act + HttpResponse response = client.createRecord(username, TEST_DOMAIN, testPubkey, relays); + + // Assert + assertThat(response.statusCode()).isEqualTo(201); + + JsonNode json = client.parseJson(response); + assertThat(json.has("id")).isTrue(); + assertThat(json.get("username").asText()).isEqualTo(username); + assertThat(json.get("pubkey").asText()).isEqualTo(testPubkey); + assertThat(json.get("enabled").asBoolean()).isTrue(); + + recordId = json.get("id").asLong(); + log.info("nip05_record_created id={} nip05={}@{}", recordId, username, TEST_DOMAIN); + } + + /** + * Verifies that a NIP-05 record can be retrieved by its identifier. + */ + @Test + @Order(21) + void shouldGetRecordByNip05() throws Exception { + // Arrange + String nip05 = "alice@" + TEST_DOMAIN; + + // Act + HttpResponse response = client.getRecordByNip05(nip05); + + // Assert + assertThat(response.statusCode()).isEqualTo(200); + + JsonNode json = client.parseJson(response); + assertThat(json.get("username").asText()).isEqualTo("alice"); + assertThat(json.get("pubkey").asText()).isEqualTo(testPubkey); + } + + /** + * Verifies that a NIP-05 record can be retrieved by ID. + */ + @Test + @Order(22) + void shouldGetRecordById() throws Exception { + // Arrange - record created in previous test + assertThat(recordId).isNotNull(); + + // Act + HttpResponse response = client.getRecord(recordId); + + // Assert + assertThat(response.statusCode()).isEqualTo(200); + + JsonNode json = client.parseJson(response); + assertThat(json.get("id").asLong()).isEqualTo(recordId); + assertThat(json.get("username").asText()).isEqualTo("alice"); + } + + /** + * Verifies that records can be listed and filtered by domain. + */ + @Test + @Order(23) + void shouldListRecordsForDomain() throws Exception { + // Act + HttpResponse response = client.getRecords(TEST_DOMAIN); + + // Assert + assertThat(response.statusCode()).isEqualTo(200); + + JsonNode json = client.parseJson(response); + log.info("records_response body={}", response.body()); + + // Handle both array and paginated response formats + JsonNode recordsArray; + if (json.isArray()) { + recordsArray = json; + } else if (json.has("content")) { + // Spring Data Page format + recordsArray = json.get("content"); + } else { + recordsArray = json; + } + + assertThat(recordsArray.isArray()).isTrue(); + assertThat(recordsArray.size()).isGreaterThanOrEqualTo(1); + + // Verify alice is in the list + boolean found = false; + for (JsonNode record : recordsArray) { + if ("alice".equals(record.get("username").asText())) { + found = true; + break; + } + } + assertThat(found).isTrue(); + } + + /** + * Verifies that a NIP-05 record's relays can be updated. + */ + @Test + @Order(24) + void shouldUpdateRecordRelays() throws Exception { + // Arrange + assertThat(recordId).isNotNull(); + List newRelays = List.of("wss://updated-relay.example.com"); + + // Act + HttpResponse response = client.updateRecord(recordId, newRelays); + + // Assert + assertThat(response.statusCode()).isEqualTo(200); + + JsonNode json = client.parseJson(response); + JsonNode relays = json.get("relays"); + assertThat(relays.isArray()).isTrue(); + assertThat(relays.size()).isEqualTo(1); + assertThat(relays.get(0).asText()).isEqualTo("wss://updated-relay.example.com"); + + log.info("nip05_record_updated id={} relays={}", recordId, newRelays); + } + + /** + * Verifies that attempting to create a duplicate NIP-05 record returns conflict. + */ + @Test + @Order(25) + void shouldRejectDuplicateNip05() throws Exception { + // Act - try to create the same username@domain again + HttpResponse response = client.createRecord("alice", TEST_DOMAIN, testPubkey, List.of()); + + // Assert + assertThat(response.statusCode()).isEqualTo(409); + log.info("duplicate_nip05_rejected nip05=alice@{}", TEST_DOMAIN); + } + + /** + * Verifies that attempting to create a record with invalid pubkey is rejected. + */ + @Test + @Order(26) + void shouldRejectInvalidPubkey() throws Exception { + // Arrange - invalid pubkey (not 64 hex chars) + String invalidPubkey = "not-a-valid-pubkey"; + + // Act + HttpResponse response = client.createRecord("bob", TEST_DOMAIN, invalidPubkey, List.of()); + + // Assert + assertThat(response.statusCode()).isEqualTo(400); + log.info("invalid_pubkey_rejected pubkey={}", invalidPubkey); + } + + /** + * Verifies that a record can be toggled (enabled/disabled). + */ + @Test + @Order(27) + void shouldToggleRecordStatus() throws Exception { + // Arrange + assertThat(recordId).isNotNull(); + + // Act - toggle to disabled + HttpResponse response = client.toggleRecord(recordId); + + // Assert + assertThat(response.statusCode()).isEqualTo(200); + + JsonNode json = client.parseJson(response); + boolean enabled = json.get("enabled").asBoolean(); + + // Toggle again to re-enable + response = client.toggleRecord(recordId); + assertThat(response.statusCode()).isEqualTo(200); + + json = client.parseJson(response); + assertThat(json.get("enabled").asBoolean()).isNotEqualTo(enabled); + + log.info("nip05_record_toggled id={}", recordId); + } + + // ========================================================================= + // Well-Known Endpoint Tests (NIP-05 Spec Compliance) + // ========================================================================= + + /** + * Verifies that the .well-known/nostr.json endpoint returns valid NIP-05 JSON + * when querying for a specific name. + */ + @Test + @Order(30) + void shouldReturnValidNostrJsonForName() throws Exception { + // Arrange - ensure record is enabled + ensureRecordEnabled(recordId); + + // Act - use Host header to specify the domain (per NIP-05 spec) + HttpResponse response = client.getWellKnown("alice", TEST_DOMAIN); + + // Assert + assertThat(response.statusCode()).isEqualTo(200); + assertThat(response.headers().firstValue("Content-Type")) + .isPresent() + .get().asString().contains("application/json"); + + JsonNode json = client.parseJson(response); + log.info("well_known_response body={}", response.body()); + + // Verify NIP-05 compliant structure + assertThat(json.has("names")).isTrue(); + assertThat(json.get("names").has("alice")).isTrue(); + assertThat(json.get("names").get("alice").asText()).isEqualTo(testPubkey); + + log.info("well_known_returned name=alice pubkey={}", testPubkey); + } + + /** + * Verifies that the relays field is included in the NIP-05 response when the record has relays. + */ + @Test + @Order(31) + void shouldIncludeRelaysInNostrJson() throws Exception { + // Act - use Host header to specify the domain + HttpResponse response = client.getWellKnown("alice", TEST_DOMAIN); + + // Assert + assertThat(response.statusCode()).isEqualTo(200); + + JsonNode json = client.parseJson(response); + log.info("well_known_relays_response body={}", response.body()); + + // Verify relays field exists and contains the pubkey (only if record has relays) + if (json.has("relays") && json.get("relays").has(testPubkey)) { + JsonNode relays = json.get("relays").get(testPubkey); + assertThat(relays.isArray()).isTrue(); + log.info("well_known_relays pubkey={} relays={}", testPubkey, relays); + } else { + // Per NIP-05, relays field is optional + log.info("well_known_relays pubkey={} relays=none (optional field)", testPubkey); + } + } + + /** + * Verifies that querying for an unknown name returns 404 or empty names object. + */ + @Test + @Order(32) + void shouldHandleUnknownName() throws Exception { + // Act - use Host header to specify the domain + HttpResponse response = client.getWellKnown("nonexistent", TEST_DOMAIN); + + // Assert - either 404 or empty names object is acceptable per NIP-05 + if (response.statusCode() == 200) { + JsonNode json = client.parseJson(response); + assertThat(json.has("names")).isTrue(); + assertThat(json.get("names").has("nonexistent")).isFalse(); + } else { + assertThat(response.statusCode()).isEqualTo(404); + } + + log.info("unknown_name_handled name=nonexistent status={}", response.statusCode()); + } + + /** + * Verifies that querying without a name parameter returns all records for the domain. + */ + @Test + @Order(33) + void shouldReturnAllRecordsWithoutNameParam() throws Exception { + // Act - use Host header to specify the domain + HttpResponse response = client.getWellKnownForDomain(TEST_DOMAIN); + + // Assert + assertThat(response.statusCode()).isEqualTo(200); + + JsonNode json = client.parseJson(response); + log.info("well_known_all_response body={}", response.body()); + + assertThat(json.has("names")).isTrue(); + + // Should contain at least alice + JsonNode names = json.get("names"); + assertThat(names.size()).isGreaterThanOrEqualTo(1); + + log.info("well_known_all_records count={}", names.size()); + } + + // ========================================================================= + // Cleanup Tests (Run Last) + // ========================================================================= + + /** + * Verifies that a NIP-05 record can be deleted. + */ + @Test + @Order(90) + void shouldDeleteNip05Record() throws Exception { + // Arrange + assertThat(recordId).isNotNull(); + + // Act + HttpResponse response = client.deleteRecord(recordId); + + // Assert + assertThat(response.statusCode()).isIn(200, 204); + + // Verify deletion + response = client.getRecord(recordId); + assertThat(response.statusCode()).isEqualTo(404); + + log.info("nip05_record_deleted id={}", recordId); + } + + /** + * Verifies that a domain can be deleted. + */ + @Test + @Order(91) + void shouldDeleteDomain() throws Exception { + // Arrange + assertThat(domainId).isNotNull(); + + // Act + HttpResponse response = client.deleteDomain(domainId); + + // Assert + assertThat(response.statusCode()).isIn(200, 204); + + // Verify deletion + response = client.getDomain(domainId); + assertThat(response.statusCode()).isEqualTo(404); + + log.info("domain_deleted id={}", domainId); + } + + // ========================================================================= + // Additional Test: Create second user to verify multi-user support + // ========================================================================= + + /** + * Verifies that multiple users can be registered on the same domain. + */ + @Test + @Order(28) + void shouldSupportMultipleUsersOnSameDomain() throws Exception { + // Arrange + String bobPubkey = Identity.generateRandomIdentity().getPublicKey().toString(); + + // Act + HttpResponse response = client.createRecord("bob", TEST_DOMAIN, bobPubkey, List.of()); + + // Assert + assertThat(response.statusCode()).isEqualTo(201); + + JsonNode json = client.parseJson(response); + assertThat(json.get("username").asText()).isEqualTo("bob"); + assertThat(json.get("pubkey").asText()).isEqualTo(bobPubkey); + + // Clean up bob + Long bobId = json.get("id").asLong(); + client.deleteRecord(bobId); + + log.info("multiple_users_supported bob_id={}", bobId); + } +} diff --git a/nsecbunker-tests/nsecbunker-e2e/src/test/java/xyz/tcheeric/nsecbunker/e2e/BottinTestClient.java b/nsecbunker-tests/nsecbunker-e2e/src/test/java/xyz/tcheeric/nsecbunker/e2e/BottinTestClient.java new file mode 100644 index 0000000..09e5670 --- /dev/null +++ b/nsecbunker-tests/nsecbunker-e2e/src/test/java/xyz/tcheeric/nsecbunker/e2e/BottinTestClient.java @@ -0,0 +1,413 @@ +package xyz.tcheeric.nsecbunker.e2e; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.net.URI; +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.Base64; +import java.util.List; +import java.util.Map; + +/** + * HTTP client for interacting with the Bottin REST API during e2e tests. + * + *

    Provides convenient methods for domain management, NIP-05 record operations, + * and well-known endpoint verification. + * + *

    Supports Basic Authentication for secured API endpoints. + */ +public class BottinTestClient { + + private static final Logger log = LoggerFactory.getLogger(BottinTestClient.class); + + private final HttpClient httpClient; + private final String baseUrl; + private final String apiUrl; + private final String wellKnownUrl; + private final ObjectMapper objectMapper; + private final String authHeader; + + /** + * Creates a new BottinTestClient without authentication. + * + * @param baseUrl the base URL of the Bottin service + */ + public BottinTestClient(String baseUrl) { + this(baseUrl, null, null); + } + + /** + * Creates a new BottinTestClient with Basic Authentication. + * + * @param baseUrl the base URL of the Bottin service + * @param username the admin username + * @param password the admin password + */ + public BottinTestClient(String baseUrl, String username, String password) { + this.baseUrl = baseUrl; + this.apiUrl = baseUrl + "/api/v1"; + this.wellKnownUrl = baseUrl + "/.well-known/nostr.json"; + // Allow setting the Host header (restricted by default in Java HttpClient) + System.setProperty("jdk.httpclient.allowRestrictedHeaders", "Host"); + this.httpClient = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(10)) + .build(); + this.objectMapper = new ObjectMapper(); + + if (username != null && password != null) { + String credentials = username + ":" + password; + this.authHeader = "Basic " + Base64.getEncoder() + .encodeToString(credentials.getBytes(StandardCharsets.UTF_8)); + } else { + this.authHeader = null; + } + } + + /** + * Adds authentication header to the request builder if credentials are configured. + */ + private HttpRequest.Builder addAuth(HttpRequest.Builder builder) { + if (authHeader != null) { + builder.header("Authorization", authHeader); + } + return builder; + } + + // --- Domain Operations --- + + /** + * Creates a new domain. + * + * @param name the domain name (e.g., "example.com") + * @return the HTTP response + */ + public HttpResponse createDomain(String name) throws IOException, InterruptedException { + String json = objectMapper.writeValueAsString(Map.of("name", name)); + + HttpRequest request = addAuth(HttpRequest.newBuilder()) + .uri(URI.create(apiUrl + "/domains")) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(json)) + .build(); + + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + log.debug("create_domain name={} status={}", name, response.statusCode()); + return response; + } + + /** + * Lists all domains. + * + * @return the HTTP response with domain list + */ + public HttpResponse getDomains() throws IOException, InterruptedException { + HttpRequest request = addAuth(HttpRequest.newBuilder()) + .uri(URI.create(apiUrl + "/domains")) + .GET() + .build(); + + return httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + } + + /** + * Gets a domain by ID. + * + * @param id the domain ID + * @return the HTTP response + */ + public HttpResponse getDomain(Long id) throws IOException, InterruptedException { + HttpRequest request = addAuth(HttpRequest.newBuilder()) + .uri(URI.create(apiUrl + "/domains/" + id)) + .GET() + .build(); + + return httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + } + + /** + * Deletes a domain by ID. + * + * @param id the domain ID + * @return the HTTP response + */ + public HttpResponse deleteDomain(Long id) throws IOException, InterruptedException { + HttpRequest request = addAuth(HttpRequest.newBuilder()) + .uri(URI.create(apiUrl + "/domains/" + id)) + .DELETE() + .build(); + + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + log.debug("delete_domain id={} status={}", id, response.statusCode()); + return response; + } + + // --- NIP-05 Record Operations --- + + /** + * Creates a new NIP-05 record. + * + * @param username the username (e.g., "alice") + * @param domain the domain (e.g., "example.com") + * @param pubkey the public key in hex format (64 chars) + * @param relays optional list of relay URLs + * @return the HTTP response + */ + public HttpResponse createRecord(String username, String domain, String pubkey, List relays) + throws IOException, InterruptedException { + Map body = Map.of( + "username", username, + "domain", domain, + "pubkey", pubkey, + "relays", relays != null ? relays : List.of() + ); + String json = objectMapper.writeValueAsString(body); + + HttpRequest request = addAuth(HttpRequest.newBuilder()) + .uri(URI.create(apiUrl + "/records")) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(json)) + .build(); + + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + log.debug("create_record nip05={}@{} status={}", username, domain, response.statusCode()); + return response; + } + + /** + * Lists all NIP-05 records, optionally filtered by domain. + * + * @param domain optional domain filter + * @return the HTTP response with record list + */ + public HttpResponse getRecords(String domain) throws IOException, InterruptedException { + String url = apiUrl + "/records"; + if (domain != null && !domain.isEmpty()) { + url += "?domain=" + domain; + } + + HttpRequest request = addAuth(HttpRequest.newBuilder()) + .uri(URI.create(url)) + .GET() + .build(); + + return httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + } + + /** + * Gets a NIP-05 record by its identifier. + * + * @param nip05 the full NIP-05 identifier (e.g., "alice@example.com") + * @return the HTTP response + */ + public HttpResponse getRecordByNip05(String nip05) throws IOException, InterruptedException { + HttpRequest request = addAuth(HttpRequest.newBuilder()) + .uri(URI.create(apiUrl + "/records/by-nip05/" + nip05)) + .GET() + .build(); + + return httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + } + + /** + * Gets a NIP-05 record by ID. + * + * @param id the record ID + * @return the HTTP response + */ + public HttpResponse getRecord(Long id) throws IOException, InterruptedException { + HttpRequest request = addAuth(HttpRequest.newBuilder()) + .uri(URI.create(apiUrl + "/records/" + id)) + .GET() + .build(); + + return httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + } + + /** + * Updates a NIP-05 record's relays. + * + * @param id the record ID + * @param relays the new relay list + * @return the HTTP response + */ + public HttpResponse updateRecord(Long id, List relays) throws IOException, InterruptedException { + String json = objectMapper.writeValueAsString(Map.of("relays", relays)); + + HttpRequest request = addAuth(HttpRequest.newBuilder()) + .uri(URI.create(apiUrl + "/records/" + id)) + .header("Content-Type", "application/json") + .PUT(HttpRequest.BodyPublishers.ofString(json)) + .build(); + + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + log.debug("update_record id={} status={}", id, response.statusCode()); + return response; + } + + /** + * Deletes a NIP-05 record by ID. + * + * @param id the record ID + * @return the HTTP response + */ + public HttpResponse deleteRecord(Long id) throws IOException, InterruptedException { + HttpRequest request = addAuth(HttpRequest.newBuilder()) + .uri(URI.create(apiUrl + "/records/" + id)) + .DELETE() + .build(); + + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + log.debug("delete_record id={} status={}", id, response.statusCode()); + return response; + } + + /** + * Toggles a NIP-05 record's enabled status. + * + * @param id the record ID + * @return the HTTP response + */ + public HttpResponse toggleRecord(Long id) throws IOException, InterruptedException { + HttpRequest request = addAuth(HttpRequest.newBuilder()) + .uri(URI.create(apiUrl + "/records/" + id + "/toggle")) + .method("PATCH", HttpRequest.BodyPublishers.noBody()) + .build(); + + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + log.debug("toggle_record id={} status={}", id, response.statusCode()); + return response; + } + + // --- Well-Known Endpoint (Public - No Auth Required) --- + + /** + * Queries the .well-known/nostr.json endpoint for a specific name. + * + * @param name the username to look up + * @param domain the domain to use in Host header (determines which domain records are returned) + * @return the HTTP response with NIP-05 JSON + */ + public HttpResponse getWellKnown(String name, String domain) throws IOException, InterruptedException { + String url = wellKnownUrl; + if (name != null && !name.isEmpty()) { + url += "?name=" + name; + } + + // Well-known endpoint is public, no auth needed + // Domain is determined from Host header per NIP-05 spec + HttpRequest.Builder builder = HttpRequest.newBuilder() + .uri(URI.create(url)) + .GET(); + + if (domain != null && !domain.isEmpty()) { + builder.header("Host", domain); + } + + HttpResponse response = httpClient.send(builder.build(), HttpResponse.BodyHandlers.ofString()); + log.debug("get_well_known name={} domain={} status={}", name, domain, response.statusCode()); + return response; + } + + /** + * Queries the .well-known/nostr.json endpoint for a specific name (uses default Host). + * + * @param name the username to look up + * @return the HTTP response with NIP-05 JSON + */ + public HttpResponse getWellKnown(String name) throws IOException, InterruptedException { + return getWellKnown(name, null); + } + + /** + * Queries the .well-known/nostr.json endpoint for all records. + * + * @return the HTTP response with NIP-05 JSON + */ + public HttpResponse getWellKnown() throws IOException, InterruptedException { + return getWellKnown(null, null); + } + + /** + * Queries the .well-known/nostr.json endpoint for all records for a specific domain. + * + * @param domain the domain to query + * @return the HTTP response with NIP-05 JSON + */ + public HttpResponse getWellKnownForDomain(String domain) throws IOException, InterruptedException { + return getWellKnown(null, domain); + } + + // --- Health Check (Public - No Auth Required) --- + + /** + * Checks if Bottin is healthy. + * + * @return the HTTP response from actuator health endpoint + */ + public HttpResponse healthCheck() throws IOException, InterruptedException { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(baseUrl + "/actuator/health")) + .GET() + .build(); + + return httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + } + + // --- Utility Methods --- + + /** + * Parses a JSON response body into a JsonNode. + * + * @param response the HTTP response + * @return the parsed JSON + */ + public JsonNode parseJson(HttpResponse response) throws JsonProcessingException { + return objectMapper.readTree(response.body()); + } + + /** + * Extracts the ID from a response body containing an "id" field. + * + * @param response the HTTP response + * @return the ID value + */ + public Long extractId(HttpResponse response) throws JsonProcessingException { + return objectMapper.readTree(response.body()).get("id").asLong(); + } + + /** + * Gets the base URL. + * + * @return the base URL + */ + public String getBaseUrl() { + return baseUrl; + } + + /** + * Gets the API URL. + * + * @return the API base URL + */ + public String getApiUrl() { + return apiUrl; + } + + /** + * Gets the well-known URL. + * + * @return the well-known endpoint URL + */ + public String getWellKnownUrl() { + return wellKnownUrl; + } +} diff --git a/nsecbunker-tests/nsecbunker-e2e/src/test/java/xyz/tcheeric/nsecbunker/e2e/E2ETestBase.java b/nsecbunker-tests/nsecbunker-e2e/src/test/java/xyz/tcheeric/nsecbunker/e2e/E2ETestBase.java index bf3a104..1a19296 100644 --- a/nsecbunker-tests/nsecbunker-e2e/src/test/java/xyz/tcheeric/nsecbunker/e2e/E2ETestBase.java +++ b/nsecbunker-tests/nsecbunker-e2e/src/test/java/xyz/tcheeric/nsecbunker/e2e/E2ETestBase.java @@ -1,9 +1,9 @@ package xyz.tcheeric.nsecbunker.e2e; +import nostr.id.Identity; import org.junit.jupiter.api.BeforeAll; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import nostr.id.Identity; import java.time.Duration; diff --git a/nsecbunker-tests/nsecbunker-e2e/src/test/java/xyz/tcheeric/nsecbunker/e2e/SharedContainers.java b/nsecbunker-tests/nsecbunker-e2e/src/test/java/xyz/tcheeric/nsecbunker/e2e/SharedContainers.java index 472b88d..44e97bd 100644 --- a/nsecbunker-tests/nsecbunker-e2e/src/test/java/xyz/tcheeric/nsecbunker/e2e/SharedContainers.java +++ b/nsecbunker-tests/nsecbunker-e2e/src/test/java/xyz/tcheeric/nsecbunker/e2e/SharedContainers.java @@ -1,11 +1,11 @@ package xyz.tcheeric.nsecbunker.e2e; +import nostr.id.Identity; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.testcontainers.containers.Network; import xyz.tcheeric.nsecbunker.e2e.containers.NostrRelayContainer; import xyz.tcheeric.nsecbunker.e2e.containers.NsecBunkerdContainer; -import nostr.id.Identity; /** * Singleton container manager for E2E tests. diff --git a/nsecbunker-tests/nsecbunker-e2e/src/test/java/xyz/tcheeric/nsecbunker/e2e/containers/BottinContainer.java b/nsecbunker-tests/nsecbunker-e2e/src/test/java/xyz/tcheeric/nsecbunker/e2e/containers/BottinContainer.java new file mode 100644 index 0000000..59ea3f5 --- /dev/null +++ b/nsecbunker-tests/nsecbunker-e2e/src/test/java/xyz/tcheeric/nsecbunker/e2e/containers/BottinContainer.java @@ -0,0 +1,213 @@ +package xyz.tcheeric.nsecbunker.e2e.containers; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.utility.DockerImageName; + +import java.time.Duration; + +/** + * Testcontainer for the Bottin NIP-05 registry service. + * + *

    Bottin provides persistent NIP-05 identity management with a REST API + * and .well-known/nostr.json endpoint per the NIP-05 specification. + * + *

    This container requires a PostgreSQL database to be available and + * configured via the withPostgres* methods. + * + *

    For testing, Bottin should be started with test mode enabled + * (BOTTIN_VERIFICATION_SKIP=true) to auto-verify domains without + * requiring actual DNS or HTTP verification. + */ +public class BottinContainer extends GenericContainer { + + private static final Logger log = LoggerFactory.getLogger(BottinContainer.class); + + private static final DockerImageName DEFAULT_IMAGE = + DockerImageName.parse("docker.398ja.xyz/bottin-web:0.1.0"); + private static final int HTTP_PORT = 8080; + + private static final String DEFAULT_ADMIN_USER = "admin"; + private static final String DEFAULT_ADMIN_PASSWORD = "admin"; + + private String postgresUrl; + private String postgresUser = "bottin"; + private String postgresPassword = "bottin"; + private String adminUser = DEFAULT_ADMIN_USER; + private String adminPassword = DEFAULT_ADMIN_PASSWORD; + private boolean verificationSkip = Boolean.parseBoolean( + System.getProperty("bottin.verification.skip", "true")); + + public BottinContainer() { + this(DEFAULT_IMAGE); + } + + public BottinContainer(DockerImageName imageName) { + super(imageName); + withExposedPorts(HTTP_PORT); + withLogConsumer(new Slf4jLogConsumer(log).withPrefix("bottin")); + waitingFor(Wait.forHttp("/actuator/health") + .forPort(HTTP_PORT) + .forStatusCode(200) + .withStartupTimeout(Duration.ofSeconds(120))); + } + + /** + * Configures the PostgreSQL JDBC URL. + * + *

    For container-to-container communication, use the internal network alias: + * {@code jdbc:postgresql://postgres:5432/bottin} + * + * @param url the JDBC URL + * @return this container for method chaining + */ + public BottinContainer withPostgresUrl(String url) { + this.postgresUrl = url; + return this; + } + + /** + * Configures the PostgreSQL username. + * + * @param user the database username + * @return this container for method chaining + */ + public BottinContainer withPostgresUser(String user) { + this.postgresUser = user; + return this; + } + + /** + * Configures the PostgreSQL password. + * + * @param password the database password + * @return this container for method chaining + */ + public BottinContainer withPostgresPassword(String password) { + this.postgresPassword = password; + return this; + } + + /** + * Enables or disables domain verification skip mode. + * + *

    When enabled (default for tests), domains are auto-verified + * without requiring DNS TXT or HTTP well-known file verification. + * + * @param skip true to skip verification, false to require it + * @return this container for method chaining + */ + public BottinContainer withVerificationSkip(boolean skip) { + this.verificationSkip = skip; + return this; + } + + /** + * Configures the admin username for API authentication. + * + * @param user the admin username + * @return this container for method chaining + */ + public BottinContainer withAdminUser(String user) { + this.adminUser = user; + return this; + } + + /** + * Configures the admin password for API authentication. + * + * @param password the admin password + * @return this container for method chaining + */ + public BottinContainer withAdminPassword(String password) { + this.adminPassword = password; + return this; + } + + @Override + protected void configure() { + // Spring Boot datasource configuration + withEnv("SPRING_DATASOURCE_URL", postgresUrl); + withEnv("SPRING_DATASOURCE_USERNAME", postgresUser); + withEnv("SPRING_DATASOURCE_PASSWORD", postgresPassword); + withEnv("SPRING_DATASOURCE_DRIVER_CLASS_NAME", "org.postgresql.Driver"); + + // JPA configuration for test environment + withEnv("SPRING_JPA_HIBERNATE_DDL_AUTO", "none"); + withEnv("SPRING_JPA_SHOW_SQL", "false"); + withEnv("SPRING_JPA_DATABASE_PLATFORM", "org.hibernate.dialect.PostgreSQLDialect"); + + // Enable Flyway to run migrations and create schema + withEnv("SPRING_FLYWAY_ENABLED", "true"); + + // Admin credentials for API authentication + withEnv("BOTTIN_ADMIN_USERNAME", adminUser); + withEnv("BOTTIN_ADMIN_PASSWORD", adminPassword); + + // Bottin test mode - skip domain verification + withEnv("BOTTIN_VERIFICATION_SKIP", String.valueOf(verificationSkip)); + + // Logging configuration + withEnv("LOGGING_LEVEL_XYZ_TCHEERIC", "DEBUG"); + + log.info("bottin_container_configured postgres_url={} verification_skip={} admin_user={}", + postgresUrl, verificationSkip, adminUser); + } + + /** + * Gets the base HTTP URL for the Bottin API. + * + * @return the base URL (e.g., "http://localhost:32768") + */ + public String getBaseUrl() { + return String.format("http://%s:%d", getHost(), getMappedPort(HTTP_PORT)); + } + + /** + * Gets the URL for the .well-known/nostr.json endpoint. + * + * @return the well-known URL + */ + public String getWellKnownUrl() { + return getBaseUrl() + "/.well-known/nostr.json"; + } + + /** + * Gets the base URL for the REST API. + * + * @return the API base URL (e.g., "http://localhost:32768/api/v1") + */ + public String getApiUrl() { + return getBaseUrl() + "/api/v1"; + } + + /** + * Gets the health check endpoint URL. + * + * @return the actuator health URL + */ + public String getHealthUrl() { + return getBaseUrl() + "/actuator/health"; + } + + /** + * Gets the admin username for API authentication. + * + * @return the admin username + */ + public String getAdminUser() { + return adminUser; + } + + /** + * Gets the admin password for API authentication. + * + * @return the admin password + */ + public String getAdminPassword() { + return adminPassword; + } +} diff --git a/nsecbunker-tests/nsecbunker-e2e/src/test/java/xyz/tcheeric/nsecbunker/e2e/tests/AdminConnectionLifecycleE2ETest.java b/nsecbunker-tests/nsecbunker-e2e/src/test/java/xyz/tcheeric/nsecbunker/e2e/tests/AdminConnectionLifecycleE2ETest.java index a889c13..d080bf7 100644 --- a/nsecbunker-tests/nsecbunker-e2e/src/test/java/xyz/tcheeric/nsecbunker/e2e/tests/AdminConnectionLifecycleE2ETest.java +++ b/nsecbunker-tests/nsecbunker-e2e/src/test/java/xyz/tcheeric/nsecbunker/e2e/tests/AdminConnectionLifecycleE2ETest.java @@ -1,14 +1,13 @@ package xyz.tcheeric.nsecbunker.e2e.tests; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; -import org.junit.jupiter.api.MethodOrderer; import xyz.tcheeric.nsecbunker.admin.NsecBunkerAdminClient; import xyz.tcheeric.nsecbunker.e2e.E2ETestBase; -import java.time.Duration; import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; diff --git a/nsecbunker-tests/nsecbunker-e2e/src/test/java/xyz/tcheeric/nsecbunker/e2e/tests/BatchSigningE2ETest.java b/nsecbunker-tests/nsecbunker-e2e/src/test/java/xyz/tcheeric/nsecbunker/e2e/tests/BatchSigningE2ETest.java index 5d03671..d694362 100644 --- a/nsecbunker-tests/nsecbunker-e2e/src/test/java/xyz/tcheeric/nsecbunker/e2e/tests/BatchSigningE2ETest.java +++ b/nsecbunker-tests/nsecbunker-e2e/src/test/java/xyz/tcheeric/nsecbunker/e2e/tests/BatchSigningE2ETest.java @@ -1,12 +1,13 @@ package xyz.tcheeric.nsecbunker.e2e.tests; +import nostr.id.Identity; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; -import org.junit.jupiter.api.MethodOrderer; import xyz.tcheeric.nsecbunker.admin.NsecBunkerAdminClient; import xyz.tcheeric.nsecbunker.admin.key.KeyManager; import xyz.tcheeric.nsecbunker.admin.permission.PermissionManager; @@ -17,7 +18,6 @@ import xyz.tcheeric.nsecbunker.core.model.BunkerPolicy; import xyz.tcheeric.nsecbunker.core.model.PolicyRule; import xyz.tcheeric.nsecbunker.e2e.E2ETestBase; -import nostr.id.Identity; import java.time.Duration; import java.util.ArrayList; diff --git a/nsecbunker-tests/nsecbunker-e2e/src/test/java/xyz/tcheeric/nsecbunker/e2e/tests/KeyCrudFlowE2ETest.java b/nsecbunker-tests/nsecbunker-e2e/src/test/java/xyz/tcheeric/nsecbunker/e2e/tests/KeyCrudFlowE2ETest.java index ed568db..1d55624 100644 --- a/nsecbunker-tests/nsecbunker-e2e/src/test/java/xyz/tcheeric/nsecbunker/e2e/tests/KeyCrudFlowE2ETest.java +++ b/nsecbunker-tests/nsecbunker-e2e/src/test/java/xyz/tcheeric/nsecbunker/e2e/tests/KeyCrudFlowE2ETest.java @@ -1,22 +1,22 @@ package xyz.tcheeric.nsecbunker.e2e.tests; +import nostr.id.Identity; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; -import org.junit.jupiter.api.MethodOrderer; import xyz.tcheeric.nsecbunker.admin.NsecBunkerAdminClient; import xyz.tcheeric.nsecbunker.admin.key.KeyManager; import xyz.tcheeric.nsecbunker.core.model.BunkerKey; import xyz.tcheeric.nsecbunker.e2e.E2ETestBase; -import nostr.id.Identity; +import java.time.Duration; import java.util.List; import java.util.UUID; import java.util.concurrent.TimeUnit; -import java.time.Duration; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; diff --git a/nsecbunker-tests/nsecbunker-e2e/src/test/java/xyz/tcheeric/nsecbunker/e2e/tests/PermissionFlowE2ETest.java b/nsecbunker-tests/nsecbunker-e2e/src/test/java/xyz/tcheeric/nsecbunker/e2e/tests/PermissionFlowE2ETest.java index 5d0e3f9..6066a94 100644 --- a/nsecbunker-tests/nsecbunker-e2e/src/test/java/xyz/tcheeric/nsecbunker/e2e/tests/PermissionFlowE2ETest.java +++ b/nsecbunker-tests/nsecbunker-e2e/src/test/java/xyz/tcheeric/nsecbunker/e2e/tests/PermissionFlowE2ETest.java @@ -1,13 +1,13 @@ package xyz.tcheeric.nsecbunker.e2e.tests; +import nostr.id.Identity; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; -import org.junit.jupiter.api.MethodOrderer; -import java.time.Duration; import xyz.tcheeric.nsecbunker.admin.NsecBunkerAdminClient; import xyz.tcheeric.nsecbunker.admin.key.KeyManager; import xyz.tcheeric.nsecbunker.admin.permission.PermissionManager; @@ -17,8 +17,8 @@ import xyz.tcheeric.nsecbunker.core.model.KeyUser; import xyz.tcheeric.nsecbunker.core.model.PolicyRule; import xyz.tcheeric.nsecbunker.e2e.E2ETestBase; -import nostr.id.Identity; +import java.time.Duration; import java.util.List; import java.util.UUID; import java.util.concurrent.TimeUnit; diff --git a/nsecbunker-tests/nsecbunker-e2e/src/test/java/xyz/tcheeric/nsecbunker/e2e/tests/PolicyFlowE2ETest.java b/nsecbunker-tests/nsecbunker-e2e/src/test/java/xyz/tcheeric/nsecbunker/e2e/tests/PolicyFlowE2ETest.java index f284aa4..9e38974 100644 --- a/nsecbunker-tests/nsecbunker-e2e/src/test/java/xyz/tcheeric/nsecbunker/e2e/tests/PolicyFlowE2ETest.java +++ b/nsecbunker-tests/nsecbunker-e2e/src/test/java/xyz/tcheeric/nsecbunker/e2e/tests/PolicyFlowE2ETest.java @@ -3,10 +3,10 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; -import org.junit.jupiter.api.MethodOrderer; import xyz.tcheeric.nsecbunker.admin.NsecBunkerAdminClient; import xyz.tcheeric.nsecbunker.admin.policy.PolicyManager; import xyz.tcheeric.nsecbunker.core.model.BunkerPolicy; diff --git a/nsecbunker-tests/nsecbunker-e2e/src/test/java/xyz/tcheeric/nsecbunker/e2e/tests/SigningFlowE2ETest.java b/nsecbunker-tests/nsecbunker-e2e/src/test/java/xyz/tcheeric/nsecbunker/e2e/tests/SigningFlowE2ETest.java index c329a71..dffb9a5 100644 --- a/nsecbunker-tests/nsecbunker-e2e/src/test/java/xyz/tcheeric/nsecbunker/e2e/tests/SigningFlowE2ETest.java +++ b/nsecbunker-tests/nsecbunker-e2e/src/test/java/xyz/tcheeric/nsecbunker/e2e/tests/SigningFlowE2ETest.java @@ -1,12 +1,13 @@ package xyz.tcheeric.nsecbunker.e2e.tests; +import nostr.id.Identity; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; -import org.junit.jupiter.api.MethodOrderer; import xyz.tcheeric.nsecbunker.admin.NsecBunkerAdminClient; import xyz.tcheeric.nsecbunker.admin.key.KeyManager; import xyz.tcheeric.nsecbunker.admin.permission.PermissionManager; @@ -18,7 +19,6 @@ import xyz.tcheeric.nsecbunker.core.model.KeyUser; import xyz.tcheeric.nsecbunker.core.model.PolicyRule; import xyz.tcheeric.nsecbunker.e2e.E2ETestBase; -import nostr.id.Identity; import java.time.Duration; import java.util.UUID; diff --git a/nsecbunker-tests/nsecbunker-e2e/src/test/java/xyz/tcheeric/nsecbunker/e2e/tests/TokenFlowE2ETest.java b/nsecbunker-tests/nsecbunker-e2e/src/test/java/xyz/tcheeric/nsecbunker/e2e/tests/TokenFlowE2ETest.java index ea68105..a1bb40e 100644 --- a/nsecbunker-tests/nsecbunker-e2e/src/test/java/xyz/tcheeric/nsecbunker/e2e/tests/TokenFlowE2ETest.java +++ b/nsecbunker-tests/nsecbunker-e2e/src/test/java/xyz/tcheeric/nsecbunker/e2e/tests/TokenFlowE2ETest.java @@ -3,10 +3,10 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; -import org.junit.jupiter.api.MethodOrderer; import xyz.tcheeric.nsecbunker.admin.NsecBunkerAdminClient; import xyz.tcheeric.nsecbunker.admin.key.KeyManager; import xyz.tcheeric.nsecbunker.admin.policy.PolicyManager; diff --git a/nsecbunker-tests/nsecbunker-it/pom.xml b/nsecbunker-tests/nsecbunker-it/pom.xml index 7465688..48bc51a 100644 --- a/nsecbunker-tests/nsecbunker-it/pom.xml +++ b/nsecbunker-tests/nsecbunker-it/pom.xml @@ -7,7 +7,7 @@ xyz.tcheeric nsecbunker-tests - 0.1.0-SNAPSHOT + 0.1.0 nsecbunker-it diff --git a/nsecbunker-tests/nsecbunker-it/src/test/java/xyz/tcheeric/nsecbunker/admin/integration/AdminConnectionLifecycleTest.java b/nsecbunker-tests/nsecbunker-it/src/test/java/xyz/tcheeric/nsecbunker/admin/integration/AdminConnectionLifecycleTest.java index e3ae6df..c7ba252 100644 --- a/nsecbunker-tests/nsecbunker-it/src/test/java/xyz/tcheeric/nsecbunker/admin/integration/AdminConnectionLifecycleTest.java +++ b/nsecbunker-tests/nsecbunker-it/src/test/java/xyz/tcheeric/nsecbunker/admin/integration/AdminConnectionLifecycleTest.java @@ -21,7 +21,6 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; diff --git a/nsecbunker-tests/nsecbunker-it/src/test/java/xyz/tcheeric/nsecbunker/admin/integration/AdminIntegrationTest.java b/nsecbunker-tests/nsecbunker-it/src/test/java/xyz/tcheeric/nsecbunker/admin/integration/AdminIntegrationTest.java index e4f2217..66e9ac6 100644 --- a/nsecbunker-tests/nsecbunker-it/src/test/java/xyz/tcheeric/nsecbunker/admin/integration/AdminIntegrationTest.java +++ b/nsecbunker-tests/nsecbunker-it/src/test/java/xyz/tcheeric/nsecbunker/admin/integration/AdminIntegrationTest.java @@ -13,8 +13,8 @@ import org.slf4j.LoggerFactory; import xyz.tcheeric.nsecbunker.admin.NsecBunkerAdminClient; import xyz.tcheeric.nsecbunker.admin.key.DefaultKeyManager; -import xyz.tcheeric.nsecbunker.admin.policy.DefaultPolicyManager; import xyz.tcheeric.nsecbunker.admin.permission.DefaultPermissionManager; +import xyz.tcheeric.nsecbunker.admin.policy.DefaultPolicyManager; import xyz.tcheeric.nsecbunker.admin.token.DefaultTokenManager; import xyz.tcheeric.nsecbunker.core.model.AccessToken; import xyz.tcheeric.nsecbunker.core.model.BunkerKey; @@ -23,10 +23,10 @@ import xyz.tcheeric.nsecbunker.protocol.nip46.Nip46Request; import xyz.tcheeric.nsecbunker.protocol.nip46.Nip46Response; +import java.time.Duration; import java.time.Instant; import java.util.List; import java.util.concurrent.CompletableFuture; -import java.time.Duration; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; diff --git a/nsecbunker-tests/nsecbunker-it/src/test/java/xyz/tcheeric/nsecbunker/it/MonitoringIntegrationTest.java b/nsecbunker-tests/nsecbunker-it/src/test/java/xyz/tcheeric/nsecbunker/it/MonitoringIntegrationTest.java index 4c67deb..0681066 100644 --- a/nsecbunker-tests/nsecbunker-it/src/test/java/xyz/tcheeric/nsecbunker/it/MonitoringIntegrationTest.java +++ b/nsecbunker-tests/nsecbunker-it/src/test/java/xyz/tcheeric/nsecbunker/it/MonitoringIntegrationTest.java @@ -3,13 +3,13 @@ import org.junit.jupiter.api.Test; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import xyz.tcheeric.nsecbunker.monitoring.metrics.MetricsRecord; +import xyz.tcheeric.nsecbunker.admin.NsecBunkerAdminClient; import xyz.tcheeric.nsecbunker.monitoring.alerting.Alert; import xyz.tcheeric.nsecbunker.monitoring.alerting.AlertThresholds; import xyz.tcheeric.nsecbunker.monitoring.alerting.DefaultAlertManager; import xyz.tcheeric.nsecbunker.monitoring.health.DefaultHealthChecker; import xyz.tcheeric.nsecbunker.monitoring.health.HealthStatus; -import xyz.tcheeric.nsecbunker.admin.NsecBunkerAdminClient; +import xyz.tcheeric.nsecbunker.monitoring.metrics.MetricsRecord; import java.time.Instant; import java.util.List; diff --git a/nsecbunker-tests/nsecbunker-it/src/test/java/xyz/tcheeric/nsecbunker/it/RelayContainerIntegrationTest.java b/nsecbunker-tests/nsecbunker-it/src/test/java/xyz/tcheeric/nsecbunker/it/RelayContainerIntegrationTest.java index 9ca49a3..2dd8e91 100644 --- a/nsecbunker-tests/nsecbunker-it/src/test/java/xyz/tcheeric/nsecbunker/it/RelayContainerIntegrationTest.java +++ b/nsecbunker-tests/nsecbunker-it/src/test/java/xyz/tcheeric/nsecbunker/it/RelayContainerIntegrationTest.java @@ -1,5 +1,9 @@ package xyz.tcheeric.nsecbunker.it; +import nostr.event.BaseTag; +import nostr.event.impl.GenericEvent; +import nostr.event.tag.PubKeyTag; +import nostr.id.Identity; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; @@ -8,23 +12,19 @@ import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; -import nostr.id.Identity; -import nostr.event.impl.GenericEvent; -import nostr.event.BaseTag; -import nostr.event.tag.PubKeyTag; import xyz.tcheeric.nsecbunker.connection.RelayConnection; import xyz.tcheeric.nsecbunker.connection.RelayListener; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assumptions.assumeTrue; -import static java.util.concurrent.TimeUnit.SECONDS; - import java.time.Duration; import java.time.Instant; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicReference; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + @Testcontainers @Tag("integration") class RelayContainerIntegrationTest { diff --git a/nsecbunker-tests/nsecbunker-it/src/test/java/xyz/tcheeric/nsecbunker/it/SignerRequestExecutorIntegrationTest.java b/nsecbunker-tests/nsecbunker-it/src/test/java/xyz/tcheeric/nsecbunker/it/SignerRequestExecutorIntegrationTest.java index 56bef98..ab8a50b 100644 --- a/nsecbunker-tests/nsecbunker-it/src/test/java/xyz/tcheeric/nsecbunker/it/SignerRequestExecutorIntegrationTest.java +++ b/nsecbunker-tests/nsecbunker-it/src/test/java/xyz/tcheeric/nsecbunker/it/SignerRequestExecutorIntegrationTest.java @@ -3,7 +3,6 @@ import org.junit.jupiter.api.Test; import xyz.tcheeric.nsecbunker.client.signer.NsecBunkerSigner; import xyz.tcheeric.nsecbunker.client.signer.SignerConfig; -import xyz.tcheeric.nsecbunker.protocol.nip46.Nip46Request; import xyz.tcheeric.nsecbunker.protocol.nip46.Nip46Response; import java.time.Duration; diff --git a/nsecbunker-tests/nsecbunker-perf/pom.xml b/nsecbunker-tests/nsecbunker-perf/pom.xml index b29b1b4..a7863f6 100644 --- a/nsecbunker-tests/nsecbunker-perf/pom.xml +++ b/nsecbunker-tests/nsecbunker-perf/pom.xml @@ -7,7 +7,7 @@ xyz.tcheeric nsecbunker-tests - 0.1.0-SNAPSHOT + 0.1.0 nsecbunker-perf diff --git a/nsecbunker-tests/nsecbunker-perf/src/main/java/xyz/tcheeric/nsecbunker/perf/CryptoBenchmark.java b/nsecbunker-tests/nsecbunker-perf/src/main/java/xyz/tcheeric/nsecbunker/perf/CryptoBenchmark.java index a3c04a4..f8ed1c0 100644 --- a/nsecbunker-tests/nsecbunker-perf/src/main/java/xyz/tcheeric/nsecbunker/perf/CryptoBenchmark.java +++ b/nsecbunker-tests/nsecbunker-perf/src/main/java/xyz/tcheeric/nsecbunker/perf/CryptoBenchmark.java @@ -2,7 +2,17 @@ import nostr.crypto.schnorr.Schnorr; import nostr.id.Identity; -import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; import org.openjdk.jmh.infra.Blackhole; import java.nio.charset.StandardCharsets; diff --git a/nsecbunker-tests/nsecbunker-perf/src/main/java/xyz/tcheeric/nsecbunker/perf/Nip46SerializationBenchmark.java b/nsecbunker-tests/nsecbunker-perf/src/main/java/xyz/tcheeric/nsecbunker/perf/Nip46SerializationBenchmark.java index a6bd823..3706541 100644 --- a/nsecbunker-tests/nsecbunker-perf/src/main/java/xyz/tcheeric/nsecbunker/perf/Nip46SerializationBenchmark.java +++ b/nsecbunker-tests/nsecbunker-perf/src/main/java/xyz/tcheeric/nsecbunker/perf/Nip46SerializationBenchmark.java @@ -2,7 +2,17 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; import org.openjdk.jmh.infra.Blackhole; import xyz.tcheeric.nsecbunker.protocol.nip46.Nip46Request; import xyz.tcheeric.nsecbunker.protocol.nip46.Nip46Response; diff --git a/nsecbunker-tests/nsecbunker-perf/src/main/java/xyz/tcheeric/nsecbunker/perf/RelayPoolBenchmark.java b/nsecbunker-tests/nsecbunker-perf/src/main/java/xyz/tcheeric/nsecbunker/perf/RelayPoolBenchmark.java index 001cb24..5fee64f 100644 --- a/nsecbunker-tests/nsecbunker-perf/src/main/java/xyz/tcheeric/nsecbunker/perf/RelayPoolBenchmark.java +++ b/nsecbunker-tests/nsecbunker-perf/src/main/java/xyz/tcheeric/nsecbunker/perf/RelayPoolBenchmark.java @@ -1,6 +1,18 @@ package xyz.tcheeric.nsecbunker.perf; -import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.annotations.Warmup; import org.openjdk.jmh.infra.Blackhole; import xyz.tcheeric.nsecbunker.connection.RelayConnection; import xyz.tcheeric.nsecbunker.connection.RelayPool; diff --git a/nsecbunker-tests/nsecbunker-security/pom.xml b/nsecbunker-tests/nsecbunker-security/pom.xml index 7aa66a3..babd250 100644 --- a/nsecbunker-tests/nsecbunker-security/pom.xml +++ b/nsecbunker-tests/nsecbunker-security/pom.xml @@ -7,7 +7,7 @@ xyz.tcheeric nsecbunker-tests - 0.1.0-SNAPSHOT + 0.1.0 nsecbunker-security diff --git a/nsecbunker-tests/nsecbunker-security/src/test/java/xyz/tcheeric/nsecbunker/security/TransportSecurityTest.java b/nsecbunker-tests/nsecbunker-security/src/test/java/xyz/tcheeric/nsecbunker/security/TransportSecurityTest.java index 3bb17d6..0dd748d 100644 --- a/nsecbunker-tests/nsecbunker-security/src/test/java/xyz/tcheeric/nsecbunker/security/TransportSecurityTest.java +++ b/nsecbunker-tests/nsecbunker-security/src/test/java/xyz/tcheeric/nsecbunker/security/TransportSecurityTest.java @@ -1,5 +1,8 @@ package xyz.tcheeric.nsecbunker.security; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import org.junit.jupiter.api.AfterEach; @@ -12,9 +15,6 @@ import xyz.tcheeric.nsecbunker.core.connection.BunkerConnectionString; import xyz.tcheeric.nsecbunker.core.model.BunkerConnection; -import javax.net.ssl.SSLContext; -import javax.net.ssl.TrustManager; -import javax.net.ssl.X509TrustManager; import java.io.IOException; import java.net.URI; import java.security.cert.X509Certificate; @@ -22,7 +22,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; -import static org.assertj.core.api.Assertions.assertThatThrownBy; /** * Security tests for transport layer (WebSocket/TLS). diff --git a/nsecbunker-tests/pom.xml b/nsecbunker-tests/pom.xml index 859c251..82b6f47 100644 --- a/nsecbunker-tests/pom.xml +++ b/nsecbunker-tests/pom.xml @@ -7,7 +7,7 @@ xyz.tcheeric nsecbunker-java - 0.1.0-SNAPSHOT + 0.1.0 nsecbunker-tests diff --git a/pom.xml b/pom.xml index c3dbcf9..a3c3c22 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ xyz.tcheeric nsecbunker-java - 0.1.0-SNAPSHOT + 0.1.0 pom nsecBunker Java Library @@ -58,15 +58,15 @@ 2.17.0 4.12.0 2.0.12 - 1.5.3 - 5.10.2 + 1.5.15 + 5.12.2 5.11.0 3.25.3 4.2.1 1.19.7 1.18.32 - 1.12.4 - 3.2.4 + 1.14.3 + 3.5.9 true @@ -85,7 +85,7 @@ 10.14.2 3.3.1 4.8.3.1 - 3.21.2 + 3.26.0 1.37 9.0.9