Skip to content

Commit

Permalink
minor cleanup, docs, and integration tests for username API
Browse files Browse the repository at this point in the history
  • Loading branch information
sergey-signal committed Jun 2, 2023
1 parent 47cc7fd commit e6917d8
Show file tree
Hide file tree
Showing 7 changed files with 306 additions and 183 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,15 @@ public <T> T executeExpectSuccess(final Class<T> expectedType) {
return requireNonNull(execute.getRight());
}

public void executeExpectStatusCode(final int expectedStatusCode) {
final Pair<Integer, Void> execute = execute(Void.class);
Validate.isTrue(
execute.getLeft() == expectedStatusCode,
"Unexpected response code: %d",
execute.getLeft()
);
}

public <T> Pair<Integer, T> execute(final Class<T> expectedType) {
builder.uri(serverUri(endpoint, queryParams))
.header(HttpHeaders.USER_AGENT, USER_AGENT);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/

package org.signal.integration;

import static org.junit.jupiter.api.Assertions.assertEquals;

import java.util.Arrays;
import java.util.Base64;
import java.util.List;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.http.HttpStatus;
import org.junit.jupiter.api.Test;
import org.signal.libsignal.usernames.BaseUsernameException;
import org.signal.libsignal.usernames.Username;
import org.whispersystems.textsecuregcm.entities.AccountIdentifierResponse;
import org.whispersystems.textsecuregcm.entities.AccountIdentityResponse;
import org.whispersystems.textsecuregcm.entities.ConfirmUsernameHashRequest;
import org.whispersystems.textsecuregcm.entities.ReserveUsernameHashRequest;
import org.whispersystems.textsecuregcm.entities.ReserveUsernameHashResponse;
import org.whispersystems.textsecuregcm.entities.UsernameHashResponse;

public class AccountTest {

@Test
public void testCreateAccount() throws Exception {
final TestUser user = Operations.newRegisteredUser("+19995550101");
try {
final Pair<Integer, AccountIdentityResponse> execute = Operations.apiGet("/v1/accounts/whoami")
.authorized(user)
.execute(AccountIdentityResponse.class);
assertEquals(HttpStatus.SC_OK, execute.getLeft());
} finally {
Operations.deleteUser(user);
}
}

@Test
public void testCreateAccountAtomic() throws Exception {
final TestUser user = Operations.newRegisteredUserAtomic("+19995550201");
try {
final Pair<Integer, AccountIdentityResponse> execute = Operations.apiGet("/v1/accounts/whoami")
.authorized(user)
.execute(AccountIdentityResponse.class);
assertEquals(HttpStatus.SC_OK, execute.getLeft());
} finally {
Operations.deleteUser(user);
}
}

@Test
public void testUsernameOperations() throws Exception {
final TestUser user = Operations.newRegisteredUser("+19995550102");
try {
verifyFullUsernameLifecycle(user);
// no do it again to check changing usernames
verifyFullUsernameLifecycle(user);
} finally {
Operations.deleteUser(user);
}
}

private static void verifyFullUsernameLifecycle(final TestUser user) throws BaseUsernameException {
final String preferred = "test";
final List<Username> candidates = Username.candidatesFrom(preferred, preferred.length(), preferred.length() + 1);

// reserve a username
final ReserveUsernameHashRequest reserveUsernameHashRequest = new ReserveUsernameHashRequest(
candidates.stream().map(Username::getHash).toList());
// try unauthorized
Operations
.apiPut("/v1/accounts/username_hash/reserve", reserveUsernameHashRequest)
.executeExpectStatusCode(HttpStatus.SC_UNAUTHORIZED);

final ReserveUsernameHashResponse reserveUsernameHashResponse = Operations
.apiPut("/v1/accounts/username_hash/reserve", reserveUsernameHashRequest)
.authorized(user)
.executeExpectSuccess(ReserveUsernameHashResponse.class);

// find which one is the reserved username
final byte[] reservedHash = reserveUsernameHashResponse.usernameHash();
final Username reservedUsername = candidates.stream()
.filter(u -> Arrays.equals(u.getHash(), reservedHash))
.findAny()
.orElseThrow();

// confirm a username
final ConfirmUsernameHashRequest confirmUsernameHashRequest = new ConfirmUsernameHashRequest(
reservedUsername.getHash(),
reservedUsername.generateProof()
);
// try unauthorized
Operations
.apiPut("/v1/accounts/username_hash/confirm", confirmUsernameHashRequest)
.executeExpectStatusCode(HttpStatus.SC_UNAUTHORIZED);
Operations
.apiPut("/v1/accounts/username_hash/confirm", confirmUsernameHashRequest)
.authorized(user)
.executeExpectSuccess(UsernameHashResponse.class);


// lookup username
final AccountIdentifierResponse accountIdentifierResponse = Operations
.apiGet("/v1/accounts/username_hash/" + Base64.getUrlEncoder().encodeToString(reservedHash))
.executeExpectSuccess(AccountIdentifierResponse.class);
assertEquals(user.aciUuid(), accountIdentifierResponse.uuid());
// try authorized
Operations
.apiGet("/v1/accounts/username_hash/" + Base64.getUrlEncoder().encodeToString(reservedHash))
.authorized(user)
.executeExpectStatusCode(HttpStatus.SC_BAD_REQUEST);

// delete username
Operations
.apiDelete("/v1/accounts/username_hash")
.authorized(user)
.executeExpectSuccess();
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/

package org.signal.integration;

import static org.junit.jupiter.api.Assertions.assertArrayEquals;

import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.List;
import org.apache.commons.lang3.tuple.Pair;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.whispersystems.textsecuregcm.entities.IncomingMessage;
import org.whispersystems.textsecuregcm.entities.IncomingMessageList;
import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntityList;
import org.whispersystems.textsecuregcm.entities.SendMessageResponse;
import org.whispersystems.textsecuregcm.storage.Device;

public class MessagingTest {

@ParameterizedTest
@ValueSource(booleans = {true, false})
public void testSendMessageUnsealed(final boolean atomicAccountCreation) throws Exception {
final TestUser userA;
final TestUser userB;

if (atomicAccountCreation) {
userA = Operations.newRegisteredUserAtomic("+19995550102");
userB = Operations.newRegisteredUserAtomic("+19995550103");
} else {
userA = Operations.newRegisteredUser("+19995550104");
userB = Operations.newRegisteredUser("+19995550105");
}

try {
final byte[] expectedContent = "Hello, World!".getBytes(StandardCharsets.UTF_8);
final String contentBase64 = Base64.getEncoder().encodeToString(expectedContent);
final IncomingMessage message = new IncomingMessage(1, Device.MASTER_ID, userB.registrationId(), contentBase64);
final IncomingMessageList messages = new IncomingMessageList(List.of(message), false, true, System.currentTimeMillis());

final Pair<Integer, SendMessageResponse> sendMessage = Operations
.apiPut("/v1/messages/%s".formatted(userB.aciUuid().toString()), messages)
.authorized(userA)
.execute(SendMessageResponse.class);

final Pair<Integer, OutgoingMessageEntityList> receiveMessages = Operations.apiGet("/v1/messages")
.authorized(userB)
.execute(OutgoingMessageEntityList.class);

final byte[] actualContent = receiveMessages.getRight().messages().get(0).content();
assertArrayEquals(expectedContent, actualContent);
} finally {
Operations.deleteUser(userA);
Operations.deleteUser(userB);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/

package org.signal.integration;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.whispersystems.textsecuregcm.entities.CreateVerificationSessionRequest;
import org.whispersystems.textsecuregcm.entities.SubmitVerificationCodeRequest;
import org.whispersystems.textsecuregcm.entities.UpdateVerificationSessionRequest;
import org.whispersystems.textsecuregcm.entities.VerificationCodeRequest;
import org.whispersystems.textsecuregcm.entities.VerificationSessionResponse;

public class RegistrationTest {

@Test
public void testRegistration() throws Exception {
final UpdateVerificationSessionRequest originalRequest = new UpdateVerificationSessionRequest(
"test", UpdateVerificationSessionRequest.PushTokenType.FCM, null, null, null, null);
final CreateVerificationSessionRequest input = new CreateVerificationSessionRequest("+19995550102", originalRequest);

final VerificationSessionResponse verificationSessionResponse = Operations
.apiPost("/v1/verification/session", input)
.executeExpectSuccess(VerificationSessionResponse.class);

final String sessionId = verificationSessionResponse.id();
final String pushChallenge = Operations.peekVerificationSessionPushChallenge(sessionId);

// supply push challenge
final UpdateVerificationSessionRequest updatedRequest = new UpdateVerificationSessionRequest(
"test", UpdateVerificationSessionRequest.PushTokenType.FCM, pushChallenge, null, null, null);
final VerificationSessionResponse pushChallengeSupplied = Operations
.apiPatch("/v1/verification/session/%s".formatted(sessionId), updatedRequest)
.executeExpectSuccess(VerificationSessionResponse.class);

Assertions.assertTrue(pushChallengeSupplied.allowedToRequestCode());

// request code
final VerificationCodeRequest verificationCodeRequest = new VerificationCodeRequest(
VerificationCodeRequest.Transport.SMS, "android-ng");

final VerificationSessionResponse codeRequested = Operations
.apiPost("/v1/verification/session/%s/code".formatted(sessionId), verificationCodeRequest)
.executeExpectSuccess(VerificationSessionResponse.class);

// verify code
final SubmitVerificationCodeRequest submitVerificationCodeRequest = new SubmitVerificationCodeRequest("265402");
final VerificationSessionResponse codeVerified = Operations
.apiPut("/v1/verification/session/%s/code".formatted(sessionId), submitVerificationCodeRequest)
.executeExpectSuccess(VerificationSessionResponse.class);
}
}
Loading

0 comments on commit e6917d8

Please sign in to comment.