Manage identities on Android, pick one per bookmark, and give identities their own nicknames
Settings -> Identities lists them and creates, imports (an identity file or a TeamSpeak 3 settings.db), exports, renames and removes them, picks the default and improves an identity's security level; the search runs on until reached or stopped, whichever page is on screen. The Connect screen and each bookmark choose the identity to connect with. An identity keeps a nickname and phonetic nickname, in TeamSpeak's own INI keys, on both clients. A connect goes by the bookmark's nickname, else the identity's, else the global one; the phonetic one is sent in clientinit. Imported bookmarks no longer copy their identity's nickname. IdentityStore is now safe to share with the thread a connect resolves its identity on, and imports and exports through streams for Android's document picker. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -30,6 +30,16 @@ public final class Bookmark {
|
||||
this.password = password == null ? "" : password;
|
||||
}
|
||||
|
||||
/** A bookmark of its own with the same fields, to change without touching this one. */
|
||||
public Bookmark copy() {
|
||||
Bookmark b = new Bookmark(label, address, port, nickname, password);
|
||||
b.identityId = identityId;
|
||||
b.channel = channel;
|
||||
b.channelPassword = channelPassword;
|
||||
b.connectOnStartup = connectOnStartup;
|
||||
return b;
|
||||
}
|
||||
|
||||
/** Display name, falling back to "address:port" when no label is set. */
|
||||
public String displayName() {
|
||||
if (label != null && !label.isBlank()) return label;
|
||||
|
||||
@@ -14,6 +14,8 @@ public final class IdentityEntry {
|
||||
private final File file;
|
||||
private final LocalIdentity identity;
|
||||
private String name;
|
||||
private String nickname = "";
|
||||
private String phoneticNickname = "";
|
||||
|
||||
IdentityEntry(String id, String name, File file, LocalIdentity identity) {
|
||||
this.id = id;
|
||||
@@ -35,6 +37,20 @@ public final class IdentityEntry {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
/** Nickname to connect with, unless a bookmark names its own; empty for the global one. */
|
||||
public String getNickname() {
|
||||
return nickname;
|
||||
}
|
||||
|
||||
public String getPhoneticNickname() {
|
||||
return phoneticNickname;
|
||||
}
|
||||
|
||||
void setNicknames(String nickname, String phoneticNickname) {
|
||||
this.nickname = nickname == null ? "" : nickname.trim();
|
||||
this.phoneticNickname = phoneticNickname == null ? "" : phoneticNickname.trim();
|
||||
}
|
||||
|
||||
public File getFile() {
|
||||
return file;
|
||||
}
|
||||
|
||||
@@ -5,8 +5,10 @@ import org.ini4j.Ini;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
@@ -26,12 +28,20 @@ import java.util.Map;
|
||||
* file in TeamSpeak's own format, so files can be exchanged with the official
|
||||
* client verbatim. The display name is kept in the INI's {@code id} key, again
|
||||
* as TeamSpeak does.
|
||||
*
|
||||
* <p>Safe to share between threads: a connection resolves its identity on its own thread
|
||||
* while the UI may be managing them.
|
||||
*/
|
||||
public final class IdentityStore {
|
||||
|
||||
/** Security level new identities are generated with, matching the TS3 client's default. */
|
||||
public static final int DEFAULT_SECURITY_LEVEL = 8;
|
||||
|
||||
/** Keys TeamSpeak keeps next to the key in an identity INI. */
|
||||
private static final String KEY_NAME = "id";
|
||||
private static final String KEY_NICKNAME = "nickname";
|
||||
private static final String KEY_PHONETIC_NICKNAME = "phonetic_nickname";
|
||||
|
||||
|
||||
private final List<IdentityEntry> entries = new ArrayList<>();
|
||||
|
||||
@@ -73,15 +83,16 @@ public final class IdentityStore {
|
||||
}
|
||||
}
|
||||
|
||||
public List<IdentityEntry> all() {
|
||||
return entries;
|
||||
/** A snapshot of the identities, sorted by name. */
|
||||
public synchronized List<IdentityEntry> all() {
|
||||
return new ArrayList<>(entries);
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
public synchronized boolean isEmpty() {
|
||||
return entries.isEmpty();
|
||||
}
|
||||
|
||||
public IdentityEntry byId(String id) {
|
||||
public synchronized IdentityEntry byId(String id) {
|
||||
if (id == null || id.isBlank()) return null;
|
||||
for (IdentityEntry e : entries) {
|
||||
if (e.getId().equals(id)) return e;
|
||||
@@ -90,7 +101,7 @@ public final class IdentityStore {
|
||||
}
|
||||
|
||||
/** The identity with this TeamSpeak unique ID, or {@code null}. */
|
||||
public IdentityEntry byUniqueId(String uniqueId) {
|
||||
public synchronized IdentityEntry byUniqueId(String uniqueId) {
|
||||
if (uniqueId == null || uniqueId.isBlank()) return null;
|
||||
for (IdentityEntry e : entries) {
|
||||
if (e.getUniqueId().equals(uniqueId)) return e;
|
||||
@@ -102,7 +113,7 @@ public final class IdentityStore {
|
||||
* Resolves the identity to connect with: the requested one, else the configured
|
||||
* default, else the first available — generating one when the store is empty.
|
||||
*/
|
||||
public IdentityEntry resolve(Settings settings, String preferredId) throws Exception {
|
||||
public synchronized IdentityEntry resolve(Settings settings, String preferredId) throws Exception {
|
||||
IdentityEntry e = byId(preferredId);
|
||||
if (e != null) return e;
|
||||
|
||||
@@ -122,16 +133,23 @@ public final class IdentityStore {
|
||||
|
||||
/** Imports a TeamSpeak identity INI file, keeping its own name unless one is given. */
|
||||
public IdentityEntry importFile(File source, String name) throws IOException {
|
||||
LocalIdentity identity = LocalIdentity.read(source);
|
||||
String label = (name == null || name.isBlank()) ? readName(source, null) : name;
|
||||
if (label == null || label.isBlank()) label = stripExtension(source.getName());
|
||||
return create(label, identity);
|
||||
try (InputStream in = new FileInputStream(source)) {
|
||||
return importStream(in, name, stripExtension(source.getName()));
|
||||
}
|
||||
}
|
||||
|
||||
/** Imports an identity from a raw TeamSpeak identity INI stream. */
|
||||
/** Imports a TeamSpeak identity INI from a stream, keeping its own name unless one is given. */
|
||||
public IdentityEntry importStream(InputStream in, String name) throws IOException {
|
||||
LocalIdentity identity = LocalIdentity.read(in);
|
||||
return create((name == null || name.isBlank()) ? "Imported identity" : name, identity);
|
||||
return importStream(in, name, "Imported identity");
|
||||
}
|
||||
|
||||
private IdentityEntry importStream(InputStream in, String name, String fallbackName) throws IOException {
|
||||
byte[] ini = in.readAllBytes();
|
||||
LocalIdentity identity = LocalIdentity.read(new ByteArrayInputStream(ini));
|
||||
Map<String, String> props = readProperties(new ByteArrayInputStream(ini));
|
||||
String label = (name == null || name.isBlank()) ? props.getOrDefault(KEY_NAME, "") : name;
|
||||
return create(label.isBlank() ? fallbackName : label, identity,
|
||||
props.getOrDefault(KEY_NICKNAME, ""), props.getOrDefault(KEY_PHONETIC_NICKNAME, ""));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -144,24 +162,37 @@ public final class IdentityStore {
|
||||
}
|
||||
|
||||
/** Files an existing key pair as a managed identity under the given name. */
|
||||
public IdentityEntry add(String name, LocalIdentity identity) throws IOException {
|
||||
return create(name, identity);
|
||||
public IdentityEntry add(String name, LocalIdentity identity, String nickname, String phoneticNickname)
|
||||
throws IOException {
|
||||
return create(name, identity, nickname, phoneticNickname);
|
||||
}
|
||||
|
||||
/** Writes an identity out in TeamSpeak's own format, ready to import elsewhere. */
|
||||
public void exportTo(IdentityEntry entry, File target) throws IOException {
|
||||
write(entry.getIdentity(), entry.getName(), target);
|
||||
write(entry, target);
|
||||
}
|
||||
|
||||
public void rename(IdentityEntry entry, String name) throws IOException {
|
||||
/** Writes an identity out in TeamSpeak's own format; closes {@code out}. */
|
||||
public void exportTo(IdentityEntry entry, OutputStream out) throws IOException {
|
||||
entry.getIdentity().save(out, properties(entry));
|
||||
}
|
||||
|
||||
public synchronized void rename(IdentityEntry entry, String name) throws IOException {
|
||||
if (name == null || name.isBlank()) return;
|
||||
entry.setName(name.trim());
|
||||
write(entry.getIdentity(), entry.getName(), entry.getFile());
|
||||
write(entry, entry.getFile());
|
||||
sort();
|
||||
}
|
||||
|
||||
/** Sets the nicknames the identity connects with; empty ones fall back to the global nickname. */
|
||||
public synchronized void setNicknames(IdentityEntry entry, String nickname, String phoneticNickname)
|
||||
throws IOException {
|
||||
entry.setNicknames(nickname, phoneticNickname);
|
||||
write(entry, entry.getFile());
|
||||
}
|
||||
|
||||
/** Deletes an identity, clearing any settings or bookmarks that referenced it. */
|
||||
public void remove(IdentityEntry entry, Settings settings, Bookmarks bookmarks) {
|
||||
public synchronized void remove(IdentityEntry entry, Settings settings, Bookmarks bookmarks) {
|
||||
entries.remove(entry);
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
entry.getFile().delete();
|
||||
@@ -207,7 +238,7 @@ public final class IdentityStore {
|
||||
}
|
||||
} finally {
|
||||
identity.setLastCheckedKeyOffset(offset);
|
||||
write(identity, entry.getName(), entry.getFile());
|
||||
write(entry, entry.getFile());
|
||||
}
|
||||
return best;
|
||||
}
|
||||
@@ -215,45 +246,65 @@ public final class IdentityStore {
|
||||
// ---- persistence ----
|
||||
|
||||
private IdentityEntry create(String name, LocalIdentity identity) throws IOException {
|
||||
return create(name, identity, "", "");
|
||||
}
|
||||
|
||||
private synchronized IdentityEntry create(String name, LocalIdentity identity, String nickname,
|
||||
String phoneticNickname) throws IOException {
|
||||
String id = uniqueId(name);
|
||||
File file = new File(directory(), id + ".ini");
|
||||
write(identity, name, file);
|
||||
IdentityEntry entry = new IdentityEntry(id, name, file, identity);
|
||||
IdentityEntry entry = new IdentityEntry(id, name, new File(directory(), id + ".ini"), identity);
|
||||
entry.setNicknames(nickname, phoneticNickname);
|
||||
write(entry, entry.getFile());
|
||||
entries.add(entry);
|
||||
sort();
|
||||
return entry;
|
||||
}
|
||||
|
||||
private static void write(LocalIdentity identity, String name, File file) throws IOException {
|
||||
private static void write(IdentityEntry entry, File file) throws IOException {
|
||||
File dir = file.getParentFile();
|
||||
if (dir != null && !dir.isDirectory() && !dir.mkdirs()) {
|
||||
throw new IOException("Cannot create " + dir);
|
||||
}
|
||||
ProfileFiles.write(file, out -> entry.getIdentity().save(out, properties(entry)));
|
||||
}
|
||||
|
||||
private static Map<String, String> properties(IdentityEntry entry) {
|
||||
Map<String, String> props = new HashMap<>();
|
||||
props.put("id", name == null ? "" : name);
|
||||
ProfileFiles.write(file, out -> identity.save(out, props));
|
||||
props.put(KEY_NAME, entry.getName() == null ? "" : entry.getName());
|
||||
props.put(KEY_NICKNAME, entry.getNickname());
|
||||
props.put(KEY_PHONETIC_NICKNAME, entry.getPhoneticNickname());
|
||||
return props;
|
||||
}
|
||||
|
||||
private static IdentityEntry read(File file) {
|
||||
try {
|
||||
LocalIdentity identity = LocalIdentity.read(file);
|
||||
String id = stripExtension(file.getName());
|
||||
String name = readName(file, id);
|
||||
return new IdentityEntry(id, name, file, identity);
|
||||
Map<String, String> props;
|
||||
try (InputStream in = new FileInputStream(file)) {
|
||||
props = readProperties(in);
|
||||
}
|
||||
String name = props.getOrDefault(KEY_NAME, "");
|
||||
IdentityEntry entry = new IdentityEntry(id, name.isBlank() ? id : name, file, identity);
|
||||
entry.setNicknames(props.get(KEY_NICKNAME), props.get(KEY_PHONETIC_NICKNAME));
|
||||
return entry;
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Reads the display name from an identity INI's {@code id} key. */
|
||||
private static String readName(File file, String fallback) {
|
||||
/** The name and nicknames an identity INI carries besides the key; missing ones are left out. */
|
||||
private static Map<String, String> readProperties(InputStream in) {
|
||||
Map<String, String> props = new HashMap<>();
|
||||
try {
|
||||
Ini ini = new Ini(file);
|
||||
String name = unquote(ini.get("Identity", "id"));
|
||||
if (name != null && !name.isBlank()) return name;
|
||||
Ini ini = new Ini(in);
|
||||
for (String key : new String[]{KEY_NAME, KEY_NICKNAME, KEY_PHONETIC_NICKNAME}) {
|
||||
String value = unquote(ini.get("Identity", key));
|
||||
if (value != null) props.put(key, value);
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
return fallback;
|
||||
return props;
|
||||
}
|
||||
|
||||
/** TeamSpeak quotes INI values; ini4j hands them back verbatim. */
|
||||
|
||||
@@ -240,31 +240,23 @@ public final class TeamspeakConnection implements TS3Listener {
|
||||
|
||||
// ---- connection lifecycle ----
|
||||
|
||||
/**
|
||||
* Connects in the background.
|
||||
*
|
||||
* @param identity identity to authenticate with; see {@link com.ts3client.config.IdentityStore}
|
||||
*/
|
||||
public void connect(String address, int port, String nickname, String password, LocalIdentity identity) {
|
||||
connect(address, port, nickname, password, identity, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Connects in the background, joining a specific channel instead of the server's
|
||||
* default one.
|
||||
*
|
||||
* @param phoneticNickname how text-to-speech should say the nickname; may be empty
|
||||
* @param identity identity to authenticate with; see {@link com.ts3client.config.IdentityStore}
|
||||
* @param channel channel path ("Lobby/Games") or "/<channelId>"; null/empty for the default channel
|
||||
* @param channelPassword password for that channel, or null if it has none
|
||||
*/
|
||||
public void connect(String address, int port, String nickname, String password, LocalIdentity identity,
|
||||
String channel, String channelPassword) {
|
||||
new Thread(() -> doConnect(address, port, nickname, password, identity, channel, channelPassword),
|
||||
"ts3j-connect").start();
|
||||
public void connect(String address, int port, String nickname, String phoneticNickname, String password,
|
||||
LocalIdentity identity, String channel, String channelPassword) {
|
||||
new Thread(() -> doConnect(address, port, nickname, phoneticNickname, password, identity, channel,
|
||||
channelPassword), "ts3j-connect").start();
|
||||
}
|
||||
|
||||
private void doConnect(String address, int port, String nickname, String password, LocalIdentity withIdentity,
|
||||
String channel, String channelPassword) {
|
||||
private void doConnect(String address, int port, String nickname, String phoneticNickname, String password,
|
||||
LocalIdentity withIdentity, String channel, String channelPassword) {
|
||||
Object attempt = new Object();
|
||||
connectAttempt = attempt;
|
||||
connectErrorId = 0;
|
||||
@@ -315,6 +307,7 @@ public final class TeamspeakConnection implements TS3Listener {
|
||||
});
|
||||
client.setIdentity(identity);
|
||||
client.setNickname(nickname);
|
||||
setIfPresent("client.nickname_phonetic", phoneticNickname);
|
||||
client.setHWID("ts3jswing-" + Integer.toHexString(nickname.hashCode()));
|
||||
client.addListener(this);
|
||||
client.addListener(events);
|
||||
|
||||
@@ -117,6 +117,8 @@ public final class ServerSession {
|
||||
// ---- connection lifecycle ----
|
||||
|
||||
/**
|
||||
* @param nickname this server's own nickname, as a bookmark may name; empty for the
|
||||
* identity's, else the global one
|
||||
* @param identityId identity to use, or empty for the default one
|
||||
* @param channel channel path to join on connect, or empty for the default channel
|
||||
* @param channelPassword password for that channel, if any
|
||||
@@ -146,10 +148,17 @@ public final class ServerSession {
|
||||
}
|
||||
this.identityId = entry.getId();
|
||||
listener.onSystemMessage("Using identity \"" + entry.getName() + "\".");
|
||||
conn.connect(address, port, nickname, password, entry.getIdentity(), channel, channelPassword);
|
||||
conn.connect(address, port, nicknameFor(nickname, entry), entry.getPhoneticNickname(), password,
|
||||
entry.getIdentity(), channel, channelPassword);
|
||||
}, "identity-resolve").start();
|
||||
}
|
||||
|
||||
private String nicknameFor(String serverNickname, IdentityEntry identity) {
|
||||
if (serverNickname != null && !serverNickname.isBlank()) return serverNickname.trim();
|
||||
if (!identity.getNickname().isBlank()) return identity.getNickname();
|
||||
return settings.nickname;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces this session's connection: leaves the server it is on, then connects to the
|
||||
* new one in its place — what left-clicking a bookmark does.
|
||||
|
||||
@@ -144,7 +144,7 @@ public final class TeamSpeakImporter {
|
||||
if (entry != null) {
|
||||
identitiesKnown++;
|
||||
} else {
|
||||
entry = identities.add(nameOf(identity), key);
|
||||
entry = identities.add(nameOf(identity), key, identity.nickname(), identity.phoneticNickname());
|
||||
identitiesAdded++;
|
||||
}
|
||||
imported.put(identity.uuid(), entry);
|
||||
@@ -168,19 +168,16 @@ public final class TeamSpeakImporter {
|
||||
SyncItem.Identity syncIdentity = identityOf(bookmark, syncIdentities);
|
||||
IdentityEntry identity = bookmark.identityUuid().isEmpty() || syncIdentity == null
|
||||
? null : imported.get(syncIdentity.uuid());
|
||||
bookmarks.add(toBookmark(bookmark, syncIdentity, identity));
|
||||
bookmarks.add(toBookmark(bookmark, identity));
|
||||
bookmarksAdded++;
|
||||
}
|
||||
if (bookmarksAdded > 0) bookmarks.save();
|
||||
return new Result(identitiesAdded, identitiesKnown, bookmarksAdded, bookmarksKnown);
|
||||
}
|
||||
|
||||
private Bookmark toBookmark(SyncItem.Bookmark sync, SyncItem.Identity syncIdentity, IdentityEntry identity) {
|
||||
String nickname = sync.nickname();
|
||||
if (nickname.isBlank() && syncIdentity != null) nickname = syncIdentity.nickname();
|
||||
if (nickname.isBlank()) nickname = settings.nickname;
|
||||
|
||||
Bookmark b = new Bookmark(sync.name(), sync.address(), sync.port(), nickname, sync.serverPassword());
|
||||
private Bookmark toBookmark(SyncItem.Bookmark sync, IdentityEntry identity) {
|
||||
// Without a nickname of its own, the bookmark takes the identity's as it connects.
|
||||
Bookmark b = new Bookmark(sync.name(), sync.address(), sync.port(), sync.nickname(), sync.serverPassword());
|
||||
b.identityId = identity == null ? "" : identity.getId();
|
||||
b.channel = sync.defaultChannel();
|
||||
if (b.channel.isBlank() && sync.defaultChannelId() != 0) b.channel = "/" + sync.defaultChannelId();
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.ts3client.config;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
|
||||
class IdentityStoreTest {
|
||||
|
||||
@TempDir
|
||||
Path dir;
|
||||
|
||||
@BeforeEach
|
||||
void freshProfile() {
|
||||
AppDirs.init(dir.toFile(), dir.resolve("chats"));
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void defaultProfile() {
|
||||
AppDirs.init(null, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
void exportedIdentityImportsWithItsName() throws Exception {
|
||||
IdentityStore store = IdentityStore.load(new Settings());
|
||||
IdentityEntry original = store.generate("Žofia's key", 0);
|
||||
|
||||
ByteArrayOutputStream exported = new ByteArrayOutputStream();
|
||||
store.exportTo(original, exported);
|
||||
IdentityEntry imported = store.importStream(new ByteArrayInputStream(exported.toByteArray()), null);
|
||||
|
||||
assertEquals("Žofia's key", imported.getName());
|
||||
assertEquals(original.getUniqueId(), imported.getUniqueId());
|
||||
assertNotEquals(original.getId(), imported.getId());
|
||||
assertEquals(2, IdentityStore.load(new Settings()).all().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void nicknamesTravelWithTheIdentity() throws Exception {
|
||||
IdentityStore store = IdentityStore.load(new Settings());
|
||||
IdentityEntry original = store.generate("Work", 0);
|
||||
store.setNicknames(original, " Žofia ", "Zofia");
|
||||
|
||||
IdentityEntry reloaded = IdentityStore.load(new Settings()).byId(original.getId());
|
||||
assertEquals("Žofia", reloaded.getNickname());
|
||||
assertEquals("Zofia", reloaded.getPhoneticNickname());
|
||||
|
||||
ByteArrayOutputStream exported = new ByteArrayOutputStream();
|
||||
store.exportTo(original, exported);
|
||||
IdentityEntry imported = store.importStream(new ByteArrayInputStream(exported.toByteArray()), null);
|
||||
assertEquals("Žofia", imported.getNickname());
|
||||
assertEquals("Zofia", imported.getPhoneticNickname());
|
||||
}
|
||||
|
||||
@Test
|
||||
void givenNameWinsOverTheFilesOwn() throws Exception {
|
||||
IdentityStore store = IdentityStore.load(new Settings());
|
||||
ByteArrayOutputStream exported = new ByteArrayOutputStream();
|
||||
store.exportTo(store.generate("First", 0), exported);
|
||||
|
||||
IdentityEntry imported = store.importStream(new ByteArrayInputStream(exported.toByteArray()), "Second");
|
||||
|
||||
assertEquals("Second", imported.getName());
|
||||
}
|
||||
}
|
||||
@@ -64,7 +64,8 @@ class TeamSpeakImporterTest {
|
||||
|
||||
Bookmark byId = all.get(2);
|
||||
assertEquals("/42", byId.channel);
|
||||
assertEquals("SyncTester", byId.nickname, "the default identity's nickname");
|
||||
assertEquals("", byId.nickname, "takes the identity's nickname as it connects");
|
||||
assertEquals("SyncTester", imported.getNickname());
|
||||
assertEquals("", byId.identityId, "connects with whatever the default identity is");
|
||||
assertEquals("SyncTester", settings.nickname);
|
||||
|
||||
@@ -88,7 +89,9 @@ class TeamSpeakImporterTest {
|
||||
|
||||
new TeamSpeakImporter(identities, bookmarks, settings).importAll(List.of(bookmark, identity));
|
||||
assertEquals(identities.all().get(0).getId(), bookmarks.all().get(0).identityId);
|
||||
assertEquals(identity.nickname(), bookmarks.all().get(0).nickname);
|
||||
assertEquals("", bookmarks.all().get(0).nickname);
|
||||
assertEquals(identity.nickname(), identities.all().get(0).getNickname());
|
||||
assertEquals(identity.phoneticNickname(), identities.all().get(0).getPhoneticNickname());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user