Add higher-level bridge code for account_exists()
This commit is contained in:
parent
f758cf9794
commit
a6edef3ad0
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,4 +1,4 @@
|
||||
/.idea
|
||||
.idea
|
||||
*.iml
|
||||
/target
|
||||
/swift/**/.build
|
||||
|
||||
@ -1,2 +1,4 @@
|
||||
v0.86.17
|
||||
|
||||
- Expose accountExists() API to client libraries
|
||||
|
||||
|
||||
1
java/.gitignore
vendored
1
java/.gitignore
vendored
@ -5,3 +5,4 @@ out
|
||||
*.ipr
|
||||
*.iws
|
||||
*.iml
|
||||
.kotlin
|
||||
|
||||
@ -0,0 +1,38 @@
|
||||
//
|
||||
// Copyright 2026 Signal Messenger, LLC.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
package org.signal.libsignal.net
|
||||
|
||||
import org.signal.libsignal.internal.CompletableFuture
|
||||
import org.signal.libsignal.internal.Native
|
||||
import org.signal.libsignal.internal.mapWithCancellation
|
||||
import org.signal.libsignal.protocol.ServiceId
|
||||
|
||||
public class UnauthProfilesService(
|
||||
private val connection: UnauthenticatedChatConnection,
|
||||
) {
|
||||
/**
|
||||
* Does an account with the given ACI or PNI exist?
|
||||
*
|
||||
* All exceptions are mapped into [RequestResult]; unexpected ones will be treated as
|
||||
* [RequestResult.ApplicationError].
|
||||
*/
|
||||
public fun accountExists(account: ServiceId): CompletableFuture<RequestResult<Boolean, Nothing>> =
|
||||
try {
|
||||
connection.runWithContextAndConnectionHandles { asyncCtx, conn ->
|
||||
Native
|
||||
.UnauthenticatedChatConnection_account_exists(
|
||||
asyncCtx,
|
||||
conn,
|
||||
account.toServiceIdFixedWidthBinary(),
|
||||
).mapWithCancellation(
|
||||
onSuccess = { RequestResult.Success(it) },
|
||||
onError = { err -> err.toRequestResult() },
|
||||
)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
CompletableFuture.completedFuture(RequestResult.ApplicationError(e))
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,60 @@
|
||||
//
|
||||
// Copyright 2026 Signal Messenger, LLC.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
package org.signal.libsignal.net
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.signal.libsignal.internal.TokioAsyncContext
|
||||
import org.signal.libsignal.protocol.ServiceId
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.arrayOf
|
||||
import kotlin.test.assertIs
|
||||
|
||||
class UnauthProfilesServiceTest {
|
||||
@Test
|
||||
fun testAccountExists() {
|
||||
data class TestCase(
|
||||
val serviceId: ServiceId,
|
||||
val found: Boolean,
|
||||
)
|
||||
|
||||
val aci = ServiceId.Aci(UUID.fromString("9d0652a3-dcc3-4d11-975f-74d61598733f"))
|
||||
val pni = ServiceId.Pni(UUID.fromString("796abedb-ca4e-4f18-8803-1fde5b921f9f"))
|
||||
|
||||
val tokioAsyncContext = TokioAsyncContext()
|
||||
val (chat, fakeRemote) =
|
||||
UnauthenticatedChatConnection.fakeConnect(
|
||||
tokioAsyncContext,
|
||||
NoOpListener(),
|
||||
Network.Environment.STAGING,
|
||||
)
|
||||
|
||||
val profilesService = UnauthProfilesService(chat)
|
||||
|
||||
for (testCase in listOf(
|
||||
TestCase(aci, true),
|
||||
TestCase(pni, true),
|
||||
TestCase(aci, false),
|
||||
TestCase(pni, false),
|
||||
)) {
|
||||
val responseFuture = profilesService.accountExists(testCase.serviceId)
|
||||
val (request, requestId) = fakeRemote.getNextIncomingRequest().get(1, TimeUnit.SECONDS)
|
||||
assertEquals("HEAD", request.method)
|
||||
assertEquals("/v1/accounts/account/${testCase.serviceId.toServiceIdString()}", request.pathAndQuery)
|
||||
fakeRemote.sendResponse(
|
||||
requestId,
|
||||
if (testCase.found) 200 else 404,
|
||||
if (testCase.found) "OK" else "Not Found",
|
||||
arrayOf(),
|
||||
ByteArray(0),
|
||||
)
|
||||
val result = responseFuture.get()
|
||||
val successResult = assertIs<RequestResult.Success<Boolean>>(result)
|
||||
assertEquals(testCase.found, successResult.result)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -25,6 +25,7 @@ import { BridgedStringMap, newNativeHandle } from './internal.js';
|
||||
export * from './net/CDSI.js';
|
||||
export * from './net/Chat.js';
|
||||
export * from './net/chat/UnauthMessagesService.js';
|
||||
export * from './net/chat/UnauthProfilesService.js';
|
||||
export * from './net/chat/UnauthUsernamesService.js';
|
||||
export * from './net/Registration.js';
|
||||
export * from './net/SvrB.js';
|
||||
|
||||
45
node/ts/net/chat/UnauthProfilesService.ts
Normal file
45
node/ts/net/chat/UnauthProfilesService.ts
Normal file
@ -0,0 +1,45 @@
|
||||
//
|
||||
// Copyright 2026 Signal Messenger, LLC.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
import { ServiceId } from '../../Address.js';
|
||||
import { RequestOptions, UnauthenticatedChatConnection } from '../Chat.js';
|
||||
import * as Native from '../../Native.js';
|
||||
|
||||
declare module '../Chat' {
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
interface UnauthenticatedChatConnection extends UnauthProfilesService {}
|
||||
}
|
||||
|
||||
export interface UnauthProfilesService {
|
||||
/**
|
||||
* Does an account with the given ACI or PNI exist?
|
||||
*
|
||||
* Throws / completes with failure only if the request can't be completed.
|
||||
*/
|
||||
accountExists: (
|
||||
request: {
|
||||
account: ServiceId;
|
||||
},
|
||||
options?: RequestOptions
|
||||
) => Promise<boolean>;
|
||||
}
|
||||
|
||||
UnauthenticatedChatConnection.prototype.accountExists = async function (
|
||||
{
|
||||
account,
|
||||
}: {
|
||||
account: ServiceId;
|
||||
},
|
||||
options?: RequestOptions
|
||||
): Promise<boolean> {
|
||||
return await this._asyncContext.makeCancellable(
|
||||
options?.abortSignal,
|
||||
Native.UnauthenticatedChatConnection_account_exists(
|
||||
this._asyncContext,
|
||||
this._chatService,
|
||||
account.getServiceIdFixedWidthBinary()
|
||||
)
|
||||
);
|
||||
};
|
||||
51
node/ts/test/chat/UnauthProfilesServiceTest.ts
Normal file
51
node/ts/test/chat/UnauthProfilesServiceTest.ts
Normal file
@ -0,0 +1,51 @@
|
||||
//
|
||||
// Copyright 2026 Signal Messenger, LLC.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
import { config, expect, use } from 'chai';
|
||||
import chaiAsPromised from 'chai-as-promised';
|
||||
|
||||
import * as Native from '../../Native.js';
|
||||
import * as util from '../util.js';
|
||||
import { TokioAsyncContext, UnauthProfilesService } from '../../net.js';
|
||||
import { connectUnauth } from './ServiceTestUtils.js';
|
||||
import { Aci, Pni } from '../../Address.js';
|
||||
|
||||
use(chaiAsPromised);
|
||||
|
||||
util.initLogger();
|
||||
config.truncateThreshold = 0;
|
||||
|
||||
describe('UnauthProfilesService', () => {
|
||||
describe('accountExists', () => {
|
||||
it('faithfully returns true or false', async () => {
|
||||
const tokio = new TokioAsyncContext(Native.TokioAsyncContext_new());
|
||||
const [chat, fakeRemote] = connectUnauth<UnauthProfilesService>(tokio);
|
||||
|
||||
const aci = Aci.fromUuid('9d0652a3-dcc3-4d11-975f-74d61598733f');
|
||||
const pni = Pni.fromUuid('796abedb-ca4e-4f18-8803-1fde5b921f9f');
|
||||
|
||||
for (const testCase of [
|
||||
{ serviceId: aci, found: true },
|
||||
{ serviceId: pni, found: true },
|
||||
{ serviceId: aci, found: false },
|
||||
{ serviceId: pni, found: false },
|
||||
]) {
|
||||
const responseFuture = chat.accountExists({
|
||||
account: testCase.serviceId,
|
||||
});
|
||||
const request = await fakeRemote.assertReceiveIncomingRequest();
|
||||
expect(request.verb).to.eq('HEAD');
|
||||
expect(request.path).to.eq(
|
||||
`/v1/accounts/account/${testCase.serviceId.getServiceIdString()}`
|
||||
);
|
||||
fakeRemote.sendReplyTo(request, {
|
||||
status: testCase.found ? 200 : 404,
|
||||
message: testCase.found ? 'OK' : 'Not Found',
|
||||
});
|
||||
expect(await responseFuture).to.eq(testCase.found);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,36 @@
|
||||
//
|
||||
// Copyright 2026 Signal Messenger, LLC.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SignalFfi
|
||||
|
||||
public protocol UnauthProfilesService: Sendable {
|
||||
/// Does an account with the given ACI or PNI exist?
|
||||
///
|
||||
/// Throws only if the request can't be completed.
|
||||
func accountExists(_ account: ServiceId) async throws -> Bool
|
||||
}
|
||||
|
||||
extension UnauthenticatedChatConnection: UnauthProfilesService {
|
||||
public func accountExists(_ account: ServiceId) async throws -> Bool {
|
||||
return try await self.tokioAsyncContext
|
||||
.invokeAsyncFunction { promise, tokioAsyncContext in
|
||||
withNativeHandle { chatService in
|
||||
account.withPointerToFixedWidthBinary { account in
|
||||
signal_unauthenticated_chat_connection_account_exists(
|
||||
promise,
|
||||
tokioAsyncContext.const(),
|
||||
chatService.const(),
|
||||
account
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension UnauthServiceSelector where Self == UnauthServiceSelectorHelper<any UnauthProfilesService> {
|
||||
public static var profiles: Self { .init() }
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
//
|
||||
// Copyright 2026 Signal Messenger, LLC.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
@testable import LibSignalClient
|
||||
|
||||
// These testing endpoints aren't generated in device builds, to save on code size.
|
||||
#if !os(iOS) || targetEnvironment(simulator)
|
||||
|
||||
class UnauthProfilesServiceTests: UnauthChatServiceTestBase<any UnauthProfilesService> {
|
||||
override class var selector: SelectorCheck { .profiles }
|
||||
|
||||
func testAccountExists() async throws {
|
||||
let ACI = Aci(fromUUID: UUID(uuidString: "9d0652a3-dcc3-4d11-975f-74d61598733f")!)
|
||||
let PNI = Pni(fromUUID: UUID(uuidString: "796abedb-ca4e-4f18-8803-1fde5b921f9f")!)
|
||||
let api = self.api
|
||||
struct TestCase {
|
||||
var serviceId: ServiceId
|
||||
var found: Bool
|
||||
}
|
||||
for testCase in [
|
||||
TestCase(serviceId: ACI, found: true),
|
||||
TestCase(serviceId: PNI, found: true),
|
||||
TestCase(serviceId: ACI, found: false),
|
||||
TestCase(serviceId: PNI, found: false),
|
||||
] {
|
||||
async let responseFuture = api.accountExists(testCase.serviceId)
|
||||
let (request, id) = try await fakeRemote.getNextIncomingRequest()
|
||||
XCTAssertEqual(request.method, "HEAD")
|
||||
XCTAssertEqual(request.pathAndQuery, "/v1/accounts/account/\(testCase.serviceId.serviceIdString)")
|
||||
try fakeRemote.sendResponse(requestId: id, ChatResponse(status: testCase.found ? 200 : 404))
|
||||
let resp = try await responseFuture
|
||||
XCTAssertEqual(resp, testCase.found)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
Loading…
Reference in New Issue
Block a user