Add Keyring Java API (incomplete) with tests

keyringListIdentities()
keyringSetDidName()
This commit is contained in:
Andrew Bettison 2015-08-31 19:12:43 +09:30
parent 0e783c6b73
commit 7635e6b71b
8 changed files with 681 additions and 2 deletions

View File

@ -25,6 +25,8 @@ import org.servalproject.servaldna.meshms.MeshMSConversationList;
import org.servalproject.servaldna.meshms.MeshMSException;
import org.servalproject.servaldna.meshms.MeshMSMessageList;
import java.lang.Iterable;
import java.util.Vector;
import java.io.InputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
@ -37,6 +39,9 @@ import org.servalproject.servaldna.BundleId;
import org.servalproject.servaldna.BundleSecret;
import org.servalproject.servaldna.ServalDCommand;
import org.servalproject.servaldna.ServalDInterfaceException;
import org.servalproject.servaldna.keyring.KeyringCommon;
import org.servalproject.servaldna.keyring.KeyringIdentity;
import org.servalproject.servaldna.keyring.KeyringIdentityList;
import org.servalproject.servaldna.rhizome.RhizomeCommon;
import org.servalproject.servaldna.rhizome.RhizomeIncompleteManifest;
import org.servalproject.servaldna.rhizome.RhizomeBundleList;
@ -74,6 +79,18 @@ public class ServalDClient implements ServalDHttpConnectionFactory
this.restfulPassword = restfulPassword;
}
public KeyringIdentityList keyringListIdentities(String pin) throws ServalDInterfaceException, IOException
{
KeyringIdentityList list = new KeyringIdentityList(this);
list.connect(pin);
return list;
}
public KeyringIdentity keyringSetDidName(SubscriberId sid, String did, String name, String pin) throws ServalDInterfaceException, IOException
{
return KeyringCommon.setDidName(this, sid, did, name, pin);
}
public RhizomeBundleList rhizomeListBundles() throws ServalDInterfaceException, IOException
{
RhizomeBundleList list = new RhizomeBundleList(this);
@ -171,7 +188,20 @@ public class ServalDClient implements ServalDHttpConnectionFactory
// interface ServalDHttpConnectionFactory
public HttpURLConnection newServalDHttpConnection(String path) throws ServalDInterfaceException, IOException
{
URL url = new URL("http", "localhost", httpPort, path);
return newServalDHttpConnection(path, new Vector<QueryParam>());
}
// interface ServalDHttpConnectionFactory
public HttpURLConnection newServalDHttpConnection(String path, Iterable<QueryParam> query_params) throws ServalDInterfaceException, IOException
{
StringBuilder str = new StringBuilder();
char sep = '?';
for (QueryParam param : query_params) {
str.append(sep);
param.uri_encode(str);
sep = '&';
}
URL url = new URL("http://localhost:" + httpPort + path + str.toString());
URLConnection uconn = url.openConnection();
HttpURLConnection conn;
try {

View File

@ -20,11 +20,49 @@
package org.servalproject.servaldna;
import java.lang.Iterable;
import java.lang.StringBuilder;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
public interface ServalDHttpConnectionFactory {
public static class QueryParam
{
public final String key;
public final String value;
public QueryParam(String key, String value) {
this.key = key;
this.value = value;
}
public void uri_encode(StringBuilder str) throws UnsupportedEncodingException {
uri_encode_string(str, this.key);
if (this.value != null) {
str.append('=');
uri_encode_string(str, this.value);
}
}
static private void uri_encode_string(StringBuilder str, String text) throws UnsupportedEncodingException {
for (byte b : text.getBytes("UTF-8")) {
if ( (b >= '0' && b <= '9')
|| (b >= 'A' && b <= 'Z')
|| (b >= 'a' && b <= 'z')
|| b == '_' || b == '.' || b == '-' || b == '~') {
str.appendCodePoint(b);
} else {
str.append('%');
str.append(Character.forDigit((b >> 4) % 16, 16));
str.append(Character.forDigit(b % 16, 16));
}
}
}
}
public HttpURLConnection newServalDHttpConnection(String path) throws ServalDInterfaceException, IOException;
public HttpURLConnection newServalDHttpConnection(String path, Iterable<QueryParam> query_params) throws ServalDInterfaceException, IOException;
}

View File

@ -0,0 +1,231 @@
/**
* Copyright (C) 2015 Serval Project Inc.
*
* This file is part of Serval Software (http://www.servalproject.org)
*
* Serval Software is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This source code is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this source code; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package org.servalproject.servaldna.keyring;
import java.lang.StringBuilder;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.InvocationTargetException;
import java.util.Map;
import java.util.List;
import java.util.Vector;
import java.io.IOException;
import java.io.PrintStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.URL;
import java.net.HttpURLConnection;
import org.servalproject.json.JSONTokeniser;
import org.servalproject.json.JSONInputException;
import org.servalproject.servaldna.SubscriberId;
import org.servalproject.servaldna.ServalDHttpConnectionFactory;
import org.servalproject.servaldna.ServalDInterfaceException;
import org.servalproject.servaldna.ServalDFailureException;
import org.servalproject.servaldna.ServalDNotImplementedException;
public class KeyringCommon
{
public static class Status {
InputStream input_stream;
JSONTokeniser json;
public int http_status_code;
public String http_status_message;
public KeyringIdentity identity;
}
private static void dumpStatus(Status status, PrintStream out)
{
out.println("input_stream=" + status.input_stream);
out.println("http_status_code=" + status.http_status_code);
out.println("http_status_message=" + status.http_status_message);
if (status.identity == null) {
out.println("identity=null");
} else {
out.println("identity.sid=" + status.identity.sid);
out.println("identity.did=" + status.identity.did);
out.println("identity.name=" + status.identity.name);
}
}
protected static Status receiveResponse(HttpURLConnection conn, int expected_response_code) throws IOException, ServalDInterfaceException
{
int[] expected_response_codes = { expected_response_code };
return receiveResponse(conn, expected_response_codes);
}
protected static Status receiveResponse(HttpURLConnection conn, int[] expected_response_codes) throws IOException, ServalDInterfaceException
{
Status status = new Status();
status.http_status_code = conn.getResponseCode();
status.http_status_message = conn.getResponseMessage();
for (int code: expected_response_codes) {
if (status.http_status_code == code) {
status.input_stream = conn.getInputStream();
return status;
}
}
if (!conn.getContentType().equals("application/json"))
throw new ServalDInterfaceException("unexpected HTTP Content-Type: " + conn.getContentType());
if (status.http_status_code >= 300) {
status.json = new JSONTokeniser(new InputStreamReader(conn.getErrorStream(), "UTF-8"));
decodeRestfulStatus(status);
}
if (status.http_status_code == HttpURLConnection.HTTP_FORBIDDEN)
return status;
if (status.http_status_code == HttpURLConnection.HTTP_NOT_IMPLEMENTED)
throw new ServalDNotImplementedException(status.http_status_message);
throw new ServalDInterfaceException("unexpected HTTP response: " + status.http_status_code + " " + status.http_status_message);
}
protected static ServalDInterfaceException unexpectedResponse(HttpURLConnection conn, Status status)
{
return new ServalDInterfaceException(
"unexpected Keyring failure, " + quoteString(status.http_status_message)
+ " from " + conn.getURL()
);
}
protected static Status receiveRestfulResponse(HttpURLConnection conn, int expected_response_code) throws IOException, ServalDInterfaceException
{
int[] expected_response_codes = { expected_response_code };
return receiveRestfulResponse(conn, expected_response_codes);
}
protected static Status receiveRestfulResponse(HttpURLConnection conn, int[] expected_response_codes) throws IOException, ServalDInterfaceException
{
Status status = receiveResponse(conn, expected_response_codes);
if (!conn.getContentType().equals("application/json"))
throw new ServalDInterfaceException("unexpected HTTP Content-Type: " + conn.getContentType());
status.json = new JSONTokeniser(new InputStreamReader(status.input_stream, "UTF-8"));
return status;
}
protected static void decodeRestfulStatus(Status status) throws IOException, ServalDInterfaceException
{
JSONTokeniser json = status.json;
try {
json.consume(JSONTokeniser.Token.START_OBJECT);
json.consume("http_status_code");
json.consume(JSONTokeniser.Token.COLON);
int hs = json.consume(Integer.class);
json.consume(JSONTokeniser.Token.COMMA);
if (status.http_status_code == 0)
status.http_status_code = json.consume(Integer.class);
else if (hs != status.http_status_code)
throw new ServalDInterfaceException("JSON/header conflict"
+ ", http_status_code=" + hs
+ " but HTTP response code is " + status.http_status_code);
json.consume("http_status_message");
json.consume(JSONTokeniser.Token.COLON);
status.http_status_message = json.consume(String.class);
Object tok = json.nextToken();
if (tok == JSONTokeniser.Token.COMMA) {
json.consume("identity");
json.consume(JSONTokeniser.Token.COLON);
json.consume(JSONTokeniser.Token.START_OBJECT);
json.consume("sid");
json.consume(JSONTokeniser.Token.COLON);
String sid_hex = json.consume(String.class);
SubscriberId sid = new SubscriberId(sid_hex);
String did = null;
String name = null;
tok = json.nextToken();
if (tok == JSONTokeniser.Token.COMMA) {
json.consume("did");
json.consume(JSONTokeniser.Token.COLON);
did = json.consume(String.class);
tok = json.nextToken();
}
if (tok == JSONTokeniser.Token.COMMA) {
json.consume("name");
json.consume(JSONTokeniser.Token.COLON);
name = json.consume(String.class);
tok = json.nextToken();
}
json.match(tok, JSONTokeniser.Token.END_OBJECT);
tok = json.nextToken();
status.identity = new KeyringIdentity(0, sid, did, name);
}
json.match(tok, JSONTokeniser.Token.END_OBJECT);
json.consume(JSONTokeniser.Token.EOF);
}
catch (SubscriberId.InvalidHexException e) {
throw new ServalDInterfaceException("malformed JSON status response", e);
}
catch (JSONInputException e) {
throw new ServalDInterfaceException("malformed JSON status response", e);
}
}
private static void dumpHeaders(HttpURLConnection conn, PrintStream out)
{
for (Map.Entry<String,List<String>> e: conn.getHeaderFields().entrySet())
for (String v: e.getValue())
out.println("received header " + e.getKey() + ": " + v);
}
private static String quoteString(String unquoted)
{
if (unquoted == null)
return "null";
StringBuilder b = new StringBuilder(unquoted.length() + 2);
b.append('"');
for (int i = 0; i < unquoted.length(); ++i) {
char c = unquoted.charAt(i);
if (c == '"' || c == '\\')
b.append('\\');
b.append(c);
}
b.append('"');
return b.toString();
}
public static KeyringIdentity setDidName(ServalDHttpConnectionFactory connector, SubscriberId sid, String did, String name, String pin)
throws IOException, ServalDInterfaceException
{
Vector<ServalDHttpConnectionFactory.QueryParam> query_params = new Vector<ServalDHttpConnectionFactory.QueryParam>();
if (did != null)
query_params.add(new ServalDHttpConnectionFactory.QueryParam("did", did));
if (name != null)
query_params.add(new ServalDHttpConnectionFactory.QueryParam("name", name));
if (pin != null)
query_params.add(new ServalDHttpConnectionFactory.QueryParam("pin", pin));
HttpURLConnection conn = connector.newServalDHttpConnection("/restful/keyring/" + sid.toHex() + "/set", query_params);
conn.connect();
Status status = receiveRestfulResponse(conn, HttpURLConnection.HTTP_OK);
try {
decodeRestfulStatus(status);
dumpStatus(status, System.err);
if (status.identity == null)
throw new ServalDInterfaceException("invalid JSON response; missing identity");
return status.identity;
}
finally {
if (status.input_stream != null)
status.input_stream.close();
}
}
}

View File

@ -0,0 +1,43 @@
/**
* Copyright (C) 2015 Serval Project Inc.
*
* This file is part of Serval Software (http://www.servalproject.org)
*
* Serval Software is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This source code is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this source code; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package org.servalproject.servaldna.keyring;
import org.servalproject.servaldna.SubscriberId;
public class KeyringIdentity {
public final int rowNumber;
public final SubscriberId sid;
public final String did;
public final String name;
protected KeyringIdentity(int rowNumber,
SubscriberId sid,
String did,
String name)
{
this.rowNumber = rowNumber;
this.sid = sid;
this.did = did;
this.name = name;
}
}

View File

@ -0,0 +1,118 @@
/**
* Copyright (C) 2015 Serval Project Inc.
*
* This file is part of Serval Software (http://www.servalproject.org)
*
* Serval Software is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This source code is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this source code; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package org.servalproject.servaldna.keyring;
import org.servalproject.json.JSONInputException;
import org.servalproject.json.JSONTableScanner;
import org.servalproject.json.JSONTokeniser;
import org.servalproject.servaldna.ServalDHttpConnectionFactory;
import org.servalproject.servaldna.ServalDInterfaceException;
import org.servalproject.servaldna.SubscriberId;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.util.Vector;
import java.util.Map;
public class KeyringIdentityList {
private ServalDHttpConnectionFactory httpConnector;
private HttpURLConnection httpConnection;
private JSONTokeniser json;
private JSONTableScanner table;
int rowCount;
public KeyringIdentityList(ServalDHttpConnectionFactory connector)
{
this.httpConnector = connector;
this.table = new JSONTableScanner()
.addColumn("sid", SubscriberId.class)
.addColumn("did", String.class, JSONTokeniser.Narrow.ALLOW_NULL)
.addColumn("name", String.class, JSONTokeniser.Narrow.ALLOW_NULL);
}
public boolean isConnected()
{
return this.json != null;
}
public void connect(String pin) throws IOException, ServalDInterfaceException
{
try {
rowCount = 0;
Vector<ServalDHttpConnectionFactory.QueryParam> query_params = new Vector<ServalDHttpConnectionFactory.QueryParam>();
if (pin != null) {
query_params.add(new ServalDHttpConnectionFactory.QueryParam("pin", pin));
}
httpConnection = httpConnector.newServalDHttpConnection("/restful/keyring/identities.json", query_params);
httpConnection.connect();
KeyringCommon.Status status = KeyringCommon.receiveRestfulResponse(httpConnection, HttpURLConnection.HTTP_OK);
json = status.json;
json.consume(JSONTokeniser.Token.START_OBJECT);
json.consume("header");
json.consume(JSONTokeniser.Token.COLON);
table.consumeHeaderArray(json);
json.consume(JSONTokeniser.Token.COMMA);
json.consume("rows");
json.consume(JSONTokeniser.Token.COLON);
json.consume(JSONTokeniser.Token.START_ARRAY);
}
catch (JSONInputException e) {
throw new ServalDInterfaceException(e);
}
}
public KeyringIdentity nextIdentity() throws ServalDInterfaceException, IOException
{
try {
Object tok = json.nextToken();
if (tok == JSONTokeniser.Token.END_ARRAY) {
json.consume(JSONTokeniser.Token.END_OBJECT);
json.consume(JSONTokeniser.Token.EOF);
return null;
}
if (rowCount != 0)
JSONTokeniser.match(tok, JSONTokeniser.Token.COMMA);
else
json.pushToken(tok);
Map<String,Object> row = table.consumeRowArray(json);
return new KeyringIdentity(
rowCount++,
(SubscriberId)row.get("sid"),
(String)row.get("did"),
(String)row.get("name")
);
}
catch (JSONInputException e) {
throw new ServalDInterfaceException(e);
}
}
public void close() throws IOException
{
httpConnection = null;
if (json != null) {
json.close();
json = null;
}
}
}

View File

@ -0,0 +1,90 @@
/**
* Copyright (C) 2015 Serval Project Inc.
*
* This file is part of Serval Software (http://www.servalproject.org)
*
* Serval Software is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This source code is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this source code; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package org.servalproject.test;
import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import org.servalproject.servaldna.ServalDClient;
import org.servalproject.servaldna.ServalDInterfaceException;
import org.servalproject.servaldna.ServalDNotImplementedException;
import org.servalproject.servaldna.ServerControl;
import org.servalproject.servaldna.BundleId;
import org.servalproject.servaldna.BundleSecret;
import org.servalproject.servaldna.SubscriberId;
import org.servalproject.servaldna.keyring.KeyringIdentityList;
import org.servalproject.servaldna.keyring.KeyringIdentity;
public class Keyring {
static void keyring_list(String pin) throws ServalDInterfaceException, IOException, InterruptedException
{
ServalDClient client = new ServerControl().getRestfulClient();
KeyringIdentityList list = null;
try {
list = client.keyringListIdentities(pin);
KeyringIdentity id;
while ((id = list.nextIdentity()) != null) {
System.out.println("sid=" + id.sid +
", did=" + id.did +
", name=" + id.name
);
}
}
finally {
if (list != null)
list.close();
}
System.exit(0);
}
static void set(SubscriberId sid, String did, String name, String pin) throws ServalDInterfaceException, IOException, InterruptedException
{
ServalDClient client = new ServerControl().getRestfulClient();
KeyringIdentity id = client.keyringSetDidName(sid, did, name, pin);
System.out.println("sid=" + id.sid +
", did=" + id.did +
", name=" + id.name
);
System.exit(0);
}
public static void main(String... args)
{
if (args.length < 1)
return;
String methodName = args[0];
try {
if (methodName.equals("list-identities"))
keyring_list(args.length >= 2 ? args[1] : null);
else if (methodName.equals("set"))
set(new SubscriberId(args[1]), args[2], args[3], args.length >= 5 ? args[4] : null);
} catch (Exception e) {
e.printStackTrace();
System.exit(1);
}
System.err.println("No such command: " + methodName);
System.exit(1);
}
}

View File

@ -35,11 +35,12 @@ includeTests rhizomeprotocol
includeTests meshms
includeTests directory_service
includeTests vomp
includeTests rhizomerestful
includeTests keyringrestful
includeTests rhizomerestful
includeTests meshmsrestful
if type -p "$JAVAC" >/dev/null; then
includeTests jni
includeTests keyringjava
includeTests rhizomejava
includeTests meshmsjava
fi

128
tests/keyringjava Executable file
View File

@ -0,0 +1,128 @@
#!/bin/bash
# Tests for Keyring Java API.
#
# Copyright 2015 Serval Project Inc.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
source "${0%/*}/../testframework.sh"
source "${0%/*}/../testdefs.sh"
source "${0%/*}/../testdefs_java.sh"
setup() {
setup_servald
setup_servald_so
compile_java_classes
set_instance +A
set_keyring_config
set_extra_config
if [ -z "$IDENTITY_COUNT" ]; then
create_single_identity
else
create_identities $IDENTITY_COUNT
fi
export SERVALD_RHIZOME_DB_RETRY_LIMIT_MS=60000
start_servald_server
wait_until servald_restful_http_server_started +A
get_servald_restful_http_server_port PORTA +A
}
teardown() {
stop_all_servald_servers
kill_all_servald_processes
assert_no_servald_processes
report_all_servald_servers
}
set_keyring_config() {
executeOk_servald config \
set log.console.level debug \
set debug.httpd on \
set debug.keyring on \
set debug.verbose on
}
set_extra_config() {
:
}
doc_keyringList="Java API list keyring identities"
setup_keyringList() {
IDENTITY_COUNT=10
DIDA1=123123123
NAMEA1='Joe Bloggs'
DIDA5=567567567
setup
}
test_keyringList() {
executeJavaOk org.servalproject.test.Keyring list-identities
tfw_cat --stdout --stderr
assertStdoutLineCount == $IDENTITY_COUNT
# TODO: these tests only work because the listed order of identities is the
# order of creation, which makes locked identities easy to attack. When the
# random search TODO in keyring.c:find_free_slot() is done, then these tests
# should fail.
for ((n = 1; n != IDENTITY_COUNT + 1; ++n)); do
line="$(sed -n -e ${n}p "$TFWSTDOUT")"
unset_vars_with_prefix XX_
unpack_vars XX_ "$line"
local sidvar=SIDA$n
local didvar=DIDA$n
local namevar=NAMEA$n
assert [ "$XX_sid" = "${!sidvar}" ]
assert [ "$XX_did" = "${!didvar-null}" ]
assert [ "$XX_name" = "${!namevar-null}" ]
done
}
doc_keyringListPin="Java API list keyring identities, with PIN"
setup_keyringListPin() {
IDENTITY_COUNT=3
PINA1='wif waf'
setup
}
test_keyringListPin() {
# First, list without supplying the PIN
executeJavaOk org.servalproject.test.Keyring list-identities
tfw_cat --stdout --stderr
assertStdoutLineCount == $((IDENTITY_COUNT - 1))
assertStdoutGrep --matches=0 "sid=$SIDA1"
assertStdoutGrep --matches=1 "sid=$SIDA2"
assertStdoutGrep --matches=1 "sid=$SIDA3"
# Then, list supplying the PIN
executeJavaOk org.servalproject.test.Keyring list-identities "$PINA1"
tfw_cat --stdout --stderr
assertStdoutLineCount == $IDENTITY_COUNT
assertStdoutGrep --matches=1 "sid=$SIDA1"
assertStdoutGrep --matches=1 "sid=$SIDA2"
assertStdoutGrep --matches=1 "sid=$SIDA3"
}
doc_keyringSetDidName="Java API set DID and name"
setup_keyringSetDidName() {
IDENTITY_COUNT=2
setup
}
test_keyringSetDidName() {
executeJavaOk org.servalproject.test.Keyring set "$SIDA1" 987654321 'Joe Bloggs'
tfw_cat --stdout --stderr
assertStdoutGrep --matches=1 "sid=$SIDA1, did=987654321, name=Joe Bloggs$"
executeOk_servald keyring list
assert_keyring_list 2
assertStdoutGrep --stderr --matches=1 "^$SIDA1:987654321:Joe Bloggs\$"
}
runTests "$@"