Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,10 @@ $ git checkout <your_chosen_branch>
</details>

<details>
<summary>integration-tested build (requires a nostr-relay for testing)</summary>
<summary>integration-tested build (requires Docker)</summary>

valid relay(s) must **_first_** be defined using the `relays.<name>=<uri>` format in [relays.properties](nostr-java-api/src/main/resources/relays.properties) file, then
Integration tests automatically start a `nostr-rs-relay` container using [Testcontainers](https://testcontainers.com/). Ensure Docker is installed and running before executing the build. The relay image can be overridden in `src/test/resources/relay-container.properties`.
Specify your own container image by setting `relay.container.image=<image>` in that file.

###### maven
(unix)
Expand Down
6 changes: 6 additions & 0 deletions nostr-java-api/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,11 @@
<artifactId>nostr-java-encryption</artifactId>
<version>${nostr-java.version}</version>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ public void testNIP01SendTextNoteEventRecipientGenericTag() {
public void testNIP01SendTextNoteEventUrlTag() {
System.out.println("testNIP01SendTextNoteEventUrlTag");

String targetString = "ws://localhost:5555";
String targetString = getRelayUri();
BaseTag genericTag = BaseTag.create("u", targetString);

NIP01 nip01 = new NIP01(Identity.generateRandomIdentity());
Expand All @@ -251,7 +251,7 @@ public void testNIP01SendTextNoteEventUrlTag() {
public void testFilterUrlTag() {
System.out.println("testFilterUrlTag");

String targetString = "https://localhost:5555";
String targetString = getRelayUri().replace("ws://", "https://");
//UrlTag urlTag = new UrlTag(targetString);
BaseTag urlTag = BaseTag.create("u", targetString);

Expand Down Expand Up @@ -560,7 +560,7 @@ public void testNIP52CalendarTimeBasedEventEvent() throws IOException {

List<BaseTag> tags = new ArrayList<>();
tags.add(new PubKeyTag(Identity.generateRandomIdentity().getPublicKey(),
"ws://localhost:5555",
getRelayUri(),
"ISSUER"));
tags.add(new PubKeyTag(Identity.generateRandomIdentity().getPublicKey(),
"",
Expand All @@ -583,7 +583,7 @@ void testNIP57CreateZapRequestEvent() throws Exception {
final String ZAP_REQUEST_CONTENT = "zap request content";
final Long AMOUNT = 1232456L;
final String LNURL = "lnUrl";
final String RELAYS_TAG = "ws://localhost:5555";
final String RELAYS_TAG = getRelayUri();

var instance = nip57.createZapRequestEvent(
AMOUNT,
Expand Down Expand Up @@ -630,7 +630,7 @@ void testNIP57CreateZapReceiptEvent() throws Exception {
String zapSender = Identity.generateRandomIdentity().getPublicKey().toString();
PublicKey zapRecipient = Identity.generateRandomIdentity().getPublicKey();
final String ZAP_RECEIPT_IDENTIFIER = "ipsum";
final String ZAP_RECEIPT_RELAY_URI = "ws://localhost:5555";
final String ZAP_RECEIPT_RELAY_URI = getRelayUri();
final String BOLT_11 = "bolt11";
final String DESCRIPTION_SHA256 = "descriptionSha256";
final String PRE_IMAGE = "preimage";
Expand All @@ -652,7 +652,7 @@ void testNIP57CreateZapReceiptEvent() throws Exception {
final String ZAP_REQUEST_CONTENT = "zap request content";
final Long AMOUNT = 1232456L;
final String LNURL = "lnUrl";
final String RELAYS_TAG = "ws://localhost:5555";
final String RELAYS_TAG = getRelayUri();

var zapRequestEvent = nip57.createZapRequestEvent(
AMOUNT,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,10 @@

@ActiveProfiles("test")
class ApiNIP52EventIT extends BaseRelayIntegrationTest {
private static final String RELAY_URI = "ws://localhost:5555";
private final SpringWebSocketClient springWebSocketClient;

public ApiNIP52EventIT() {
springWebSocketClient = new SpringWebSocketClient(RELAY_URI);
springWebSocketClient = new SpringWebSocketClient(getRelayUri());
Copy link

Copilot AI Jul 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Constructor initialization calling getRelayUri() will fail because the method depends on the relayUri field which is not initialized during object construction. Move this initialization to a @beforeeach method or use a lazy initialization pattern.

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Jul 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to ApiNIP99EventIT, the constructor calls getRelayUri() which may return null if called before container initialization.

Copilot uses AI. Check for mistakes.
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
@ActiveProfiles("test")
class ApiNIP52RequestIT extends BaseRelayIntegrationTest {
private static final String PRV_KEY_VALUE = "23c011c4c02de9aa98d48c3646c70bb0e7ae30bdae1dfed4d251cbceadaeeb7b";
private static final String RELAY_URI = "ws://localhost:5555";
private static final String UUID_CALENDAR_TIME_BASED_EVENT_TEST = "UUID-CalendarTimeBasedEventTest";

public static final String ID = "299ab85049a7923e9cd82329c0fa489ca6fd6d21feeeac33543b1237e14a9e07";
Expand All @@ -59,11 +58,8 @@ class ApiNIP52RequestIT extends BaseRelayIntegrationTest {
public static final String LOCATION = "calendar location";

public static final EventTag E_TAG = new EventTag(E_TAG_HEX);
public static final PubKeyTag P1_TAG = new PubKeyTag(new PublicKey(P1_TAG_HEX), RELAY_URI, P1_ROLE);
public static final PubKeyTag P2_TAG = new PubKeyTag(new PublicKey(P2_TAG_HEX), RELAY_URI, P2_ROLE);
public static final GeohashTag G_TAG = new GeohashTag(G_TAG_VALUE);
public static final HashtagTag T_TAG = new HashtagTag(T_TAG_VALUE);
public static final ReferenceTag R_TAG = new ReferenceTag(URI.create(RELAY_URI));

public static final String LABEL_NAMESPACE = "audiospace";
public static final String LABEL_1 = "calendar label 1 of 2";
Expand All @@ -86,8 +82,10 @@ void testNIP99CalendarContentPreRequest() throws IOException {

List<BaseTag> tags = new ArrayList<>();
tags.add(E_TAG);
tags.add(P1_TAG);
tags.add(P2_TAG);
PubKeyTag p1Tag = new PubKeyTag(new PublicKey(P1_TAG_HEX), getRelayUri(), P1_ROLE);
PubKeyTag p2Tag = new PubKeyTag(new PublicKey(P2_TAG_HEX), getRelayUri(), P2_ROLE);
tags.add(p1Tag);
tags.add(p2Tag);
tags.add(BaseTag.create(START_TZID_CODE, START_TZID));
tags.add(BaseTag.create(END_TZID_CODE, END_TZID));
tags.add(BaseTag.create(SUMMARY_CODE, SUMMARY));
Expand All @@ -97,7 +95,7 @@ void testNIP99CalendarContentPreRequest() throws IOException {
tags.add(BaseTag.create(END_CODE, END));
tags.add(G_TAG);
tags.add(T_TAG);
tags.add(R_TAG);
tags.add(new ReferenceTag(URI.create(getRelayUri())));

CalendarContent<BaseTag> calendarContent = new CalendarContent<>(
new IdentifierTag(UUID_CALENDAR_TIME_BASED_EVENT_TEST),
Expand All @@ -113,7 +111,7 @@ void testNIP99CalendarContentPreRequest() throws IOException {
eventPubKey = event.getPubKey().toString();
EventMessage eventMessage = new EventMessage(event);

SpringWebSocketClient springWebSocketEventClient = new SpringWebSocketClient(RELAY_URI);
SpringWebSocketClient springWebSocketEventClient = new SpringWebSocketClient(getRelayUri());
String eventResponse = springWebSocketEventClient.send(eventMessage).stream().findFirst().orElseThrow();

// Extract and compare only first 3 elements of the JSON array
Expand All @@ -134,7 +132,7 @@ void testNIP99CalendarContentPreRequest() throws IOException {

// TODO - This assertion fails with superdonductor and nostr-rs-relay

SpringWebSocketClient springWebSocketRequestClient = new SpringWebSocketClient(RELAY_URI);
SpringWebSocketClient springWebSocketRequestClient = new SpringWebSocketClient(getRelayUri());
String subscriberId = UUID.randomUUID().toString();
String reqJson = createReqJson(subscriberId, eventId);
String reqResponse = springWebSocketRequestClient.send(reqJson).stream().findFirst().orElseThrow();
Expand Down Expand Up @@ -171,15 +169,15 @@ private String expectedRequestResponseJson(String subscriberId) {
" [ \"g\", \"" + G_TAG.getLocation() + "\" ],\n" +
" [ \"t\", \"" + T_TAG.getHashTag() + "\" ],\n" +
" [ \"d\", \"" + UUID_CALENDAR_TIME_BASED_EVENT_TEST + "\" ],\n" +
" [ \"p\", \"" + P1_TAG.getPublicKey() + "\", \"" + RELAY_URI + "\", \"" + P1_ROLE + "\" ],\n" +
" [ \"p\", \"" + P2_TAG.getPublicKey() + "\", \"" + RELAY_URI + "\", \"" + P2_ROLE + "\" ],\n" +
" [ \"p\", \"" + P1_TAG_HEX + "\", \"" + getRelayUri() + "\", \"" + P1_ROLE + "\" ],\n" +
" [ \"p\", \"" + P2_TAG_HEX + "\", \"" + getRelayUri() + "\", \"" + P2_ROLE + "\" ],\n" +
" [ \"start_tzid\", \"" + START_TZID + "\" ],\n" +
" [ \"end_tzid\", \"" + END_TZID + "\" ],\n" +
" [ \"summary\", \"" + SUMMARY + "\" ],\n" +
" [ \"l\", \"" + LABEL_1 + "\", \"" + LABEL_NAMESPACE + "\" ],\n" +
" [ \"l\", \"" + LABEL_2 + "\", \"" + LABEL_NAMESPACE + "\" ],\n" +
" [ \"location\", \"" + LOCATION + "\" ],\n" +
" [ \"r\", \"" + URI.create(RELAY_URI) + "\" ],\n" +
" [ \"r\", \"" + URI.create(getRelayUri()) + "\" ],\n" +
" [ \"title\", \"" + TITLE + "\" ],\n" +
" [ \"start\", \"" + START + "\" ],\n" +
" [ \"end\", \"" + END + "\" ]\n" +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@

@ActiveProfiles("test")
class ApiNIP99EventIT extends BaseRelayIntegrationTest {
private static final String RELAY_URI = "ws://localhost:5555";
public static final String CLASSIFIED_LISTING_CONTENT = "classified listing content";

public static final String PTAG_HEX = "2bed79f81439ff794cf5ac5f7bff9121e257f399829e472c7a14d3e86fe76985";
Expand All @@ -58,7 +57,7 @@ class ApiNIP99EventIT extends BaseRelayIntegrationTest {
private final SpringWebSocketClient springWebSocketClient;

public ApiNIP99EventIT() {
springWebSocketClient = new SpringWebSocketClient(RELAY_URI);
springWebSocketClient = new SpringWebSocketClient(getRelayUri());
Copy link

Copilot AI Jul 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Constructor initialization calling getRelayUri() will fail because the method depends on the relayUri field which is not initialized during object construction. Move this initialization to a @beforeeach method or use a lazy initialization pattern.

Copilot uses AI. Check for mistakes.
}

Comment on lines 57 to 62
Copy link

Copilot AI Jul 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The constructor calls getRelayUri() which may return null if called before the container is fully initialized, potentially causing the WebSocket client to fail.

Copilot uses AI. Check for mistakes.
@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@
@ActiveProfiles("test")
class ApiNIP99RequestIT extends BaseRelayIntegrationTest {
private static final String PRV_KEY_VALUE = "23c011c4c02de9aa98d48c3646c70bb0e7ae30bdae1dfed4d251cbceadaeeb7b";
private static final String RELAY_URI = "ws://localhost:5555";
public static final String PUBLISHED_AT_CODE = "published_at";
public static final String LOCATION_CODE = "location";

Expand Down Expand Up @@ -99,7 +98,7 @@ void testNIP99ClassifiedListingPreRequest() throws IOException {
eventPubKey = event.getPubKey().toString();
EventMessage eventMessage = new EventMessage(event);

SpringWebSocketClient springWebSocketEventClient = new SpringWebSocketClient(RELAY_URI);
SpringWebSocketClient springWebSocketEventClient = new SpringWebSocketClient(getRelayUri());
List<String> eventResponses = springWebSocketEventClient.send(eventMessage);

assertEquals(1, eventResponses.size(), "Expected 1 event response, but got " + eventResponses.size());
Expand All @@ -122,7 +121,7 @@ void testNIP99ClassifiedListingPreRequest() throws IOException {
// TODO - Investigate why EOSE, instead of EVENT, is returned from nostr-rs-relay, and not superconductor

///*
SpringWebSocketClient springWebSocketRequestClient = new SpringWebSocketClient(RELAY_URI);
SpringWebSocketClient springWebSocketRequestClient = new SpringWebSocketClient(getRelayUri());
String reqJson = createReqJson(UUID.randomUUID().toString(), eventId);
List<String> reqResponses = springWebSocketRequestClient.send(reqJson).stream().toList();
// springWebSocketRequestClient.closeSocket();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,53 @@

import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.BeforeAll;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
Comment on lines +5 to +6
Copy link

Copilot AI Jul 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Spring Framework imports are being used in a class that doesn't appear to be a Spring test. Consider using pure Testcontainers approach without Spring dependencies if Spring context is not required.

Suggested change
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;

Copilot uses AI. Check for mistakes.
import org.testcontainers.DockerClientFactory;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.containers.wait.strategy.Wait;

import java.util.ResourceBundle;

@Testcontainers
public abstract class BaseRelayIntegrationTest {

private static final int RELAY_PORT = 8080;

private static final String RESOURCE_BUNDLE = "relay-container";
private static final String IMAGE_KEY = "relay.container.image";

@Container
private static final GenericContainer<?> RELAY;

static {
ResourceBundle bundle = ResourceBundle.getBundle(RESOURCE_BUNDLE);
Copy link

Copilot AI Jul 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ResourceBundle.getBundle() can throw MissingResourceException if the properties file is not found. This should be handled gracefully or the file existence should be guaranteed.

Copilot uses AI. Check for mistakes.
String image = bundle.getString(IMAGE_KEY);
Copy link

Copilot AI Jul 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ResourceBundle.getString() can throw MissingResourceException if the key is not found in the properties file. This should be handled or a default value should be provided.

Suggested change
String image = bundle.getString(IMAGE_KEY);
String image;
try {
image = bundle.getString(IMAGE_KEY);
} catch (MissingResourceException e) {
image = "default/image:latest"; // Provide a default image value
System.err.println("Warning: Missing key '" + IMAGE_KEY + "' in resource bundle. Using default image: " + image);
}

Copilot uses AI. Check for mistakes.
RELAY = new GenericContainer<>(image)
.withExposedPorts(RELAY_PORT)
.waitingFor(Wait.forListeningPort());
}

private static String relayUri;

@BeforeAll
static void ensureRelayAvailable() {
Assumptions.assumeTrue(RelayAvailability.areRelaysAvailable(),
"Requires running relay defined in relays.properties");
static void ensureDockerAvailable() {
Assumptions.assumeTrue(DockerClientFactory.instance().isDockerAvailable(),
"Docker is required to run nostr-rs-relay container");
String host = RELAY.getHost(); // Use the instance of RELAY to call getHost()
relayUri = String.format("ws://%s:%d", host, RELAY.getMappedPort(RELAY_PORT));
}

@DynamicPropertySource
static void registerRelayProperties(DynamicPropertyRegistry registry) {
String host = RELAY.getHost(); // Use the instance of RELAY to call getHost()
relayUri = String.format("ws://%s:%d", host, RELAY.getMappedPort(RELAY_PORT));
registry.add("relays.nostr_rs_relay", () -> relayUri);
}

static String getRelayUri() {
return relayUri;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ public void deleteEvent() throws IOException {

@Test
public void deleteEventWithRef() throws IOException {
final String RELAY_URI = "ws://localhost:5555";
final String RELAY_URI = getRelayUri();
Identity identity = Identity.generateRandomIdentity();

NIP01 nip011 = new NIP01(identity);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,11 @@ void testNIP99CreateClassifiedListingEventWithAllOptionalParameters() {
GenericEvent instance2 = nip99.createClassifiedListingEvent(baseTags, CONTENT, classifiedListing).getEvent();
//instance.update();

assertEquals(instance, instance2);
assertNotNull(instance2.getId());
assertEquals(instance.getPubKey(), instance2.getPubKey());
assertEquals(instance.getKind(), instance2.getKind());
assertEquals(instance.getTags(), instance2.getTags());
assertEquals(instance.getContent(), instance2.getContent());
}

@Test
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
relay.container.image=scsibug/nostr-rs-relay:latest
7 changes: 7 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@
<!-- Test Dependency Versions -->
<junit-jupiter.version>5.10.2</junit-jupiter.version>
<guava.version>33.4.0-jre</guava.version>
<testcontainers.version>1.21.3</testcontainers.version>

<!-- Maven Plugin Versions -->
<maven-compiler-plugin.version>3.13.0</maven-compiler-plugin.version>
Expand Down Expand Up @@ -155,6 +156,12 @@
<version>${guava.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down