Test the channel tree, the event handler and settings
- the tree's sibling chain: arrival order, broken links, removal, reordering and reparenting, orphans, client order, path lookup - the event handler, fed events parsed from the server's own wording: joins, moves, leaves, partial and clearing updates, server groups, and a channel's life from creation to deletion - settings: saving and loading back, unknown values, and the one-time conversion of the old dBFS voice threshold Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,74 @@
|
|||||||
|
package com.ts3client.config;
|
||||||
|
|
||||||
|
import com.ts3client.audio.InputLevel;
|
||||||
|
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.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
|
||||||
|
class SettingsTest {
|
||||||
|
|
||||||
|
@TempDir
|
||||||
|
Path dir;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void freshProfile() {
|
||||||
|
AppDirs.init(dir.toFile(), dir.resolve("chats"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void defaultProfile() {
|
||||||
|
AppDirs.init(null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void whatIsSavedLoadsBack() {
|
||||||
|
Settings saved = new Settings();
|
||||||
|
saved.nickname = "Žofia";
|
||||||
|
saved.inputMode = Settings.InputMode.PUSH_TO_TALK;
|
||||||
|
saved.vadMode = Settings.VadMode.AUTOMATIC;
|
||||||
|
saved.vadThresholdDb = -33.5;
|
||||||
|
saved.appearance = Settings.Appearance.DARK;
|
||||||
|
saved.whisperPolicy = Settings.WhisperPolicy.NOBODY;
|
||||||
|
saved.chatLogs = false;
|
||||||
|
saved.chatLogDir = "/somewhere/else";
|
||||||
|
saved.save();
|
||||||
|
|
||||||
|
Settings loaded = Settings.load();
|
||||||
|
assertEquals("Žofia", loaded.nickname);
|
||||||
|
assertEquals(Settings.InputMode.PUSH_TO_TALK, loaded.inputMode);
|
||||||
|
assertEquals(Settings.VadMode.AUTOMATIC, loaded.vadMode);
|
||||||
|
assertEquals(-33.5, loaded.vadThresholdDb);
|
||||||
|
assertEquals(Settings.Appearance.DARK, loaded.appearance);
|
||||||
|
assertEquals(Settings.WhisperPolicy.NOBODY, loaded.whisperPolicy);
|
||||||
|
assertFalse(loaded.chatLogs);
|
||||||
|
assertEquals("/somewhere/else", loaded.chatLogDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void unknownValuesFallBackToDefaults() throws Exception {
|
||||||
|
Files.writeString(dir.resolve("settings.properties"), "inputMode=TELEPATHY\nvadMode=\n");
|
||||||
|
Settings loaded = Settings.load();
|
||||||
|
assertEquals(new Settings().inputMode, loaded.inputMode);
|
||||||
|
assertEquals(new Settings().vadMode, loaded.vadMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void anOldDbfsThresholdIsConvertedOnce() throws Exception {
|
||||||
|
Files.writeString(dir.resolve("settings.properties"), "vadThresholdDb=-40.0\n");
|
||||||
|
Settings loaded = Settings.load();
|
||||||
|
double expected = InputLevel.powerToDb(Math.pow(10.0, -40.0 / 10.0));
|
||||||
|
assertEquals(expected, loaded.vadThresholdDb, 1e-9);
|
||||||
|
|
||||||
|
loaded.save();
|
||||||
|
String saved = Files.readString(dir.resolve("settings.properties"));
|
||||||
|
assertFalse(saved.contains("vadThresholdDb"), "the old key is dropped once converted");
|
||||||
|
assertEquals(expected, Settings.load().vadThresholdDb, 1e-9);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
package com.ts3client.net;
|
||||||
|
|
||||||
|
import com.github.manevolent.ts3j.command.MultiCommand;
|
||||||
|
import com.github.manevolent.ts3j.event.ChannelCreateEvent;
|
||||||
|
import com.github.manevolent.ts3j.event.ChannelDeletedEvent;
|
||||||
|
import com.github.manevolent.ts3j.event.ChannelEditedEvent;
|
||||||
|
import com.github.manevolent.ts3j.event.ChannelMovedEvent;
|
||||||
|
import com.github.manevolent.ts3j.event.ClientJoinEvent;
|
||||||
|
import com.github.manevolent.ts3j.event.ClientLeaveEvent;
|
||||||
|
import com.github.manevolent.ts3j.event.ClientMovedEvent;
|
||||||
|
import com.github.manevolent.ts3j.event.ClientUpdatedEvent;
|
||||||
|
import com.github.manevolent.ts3j.event.ServerGroupClientAddedEvent;
|
||||||
|
import com.github.manevolent.ts3j.event.ServerGroupClientDeletedEvent;
|
||||||
|
import com.github.manevolent.ts3j.protocol.ProtocolRole;
|
||||||
|
import com.ts3client.config.Settings;
|
||||||
|
import com.ts3client.contacts.ContactStore;
|
||||||
|
import com.ts3client.sound.SoundNotifier;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
/** Feeds the handler events as the server words them and checks what the model makes of them. */
|
||||||
|
class ConnectionEventHandlerTest {
|
||||||
|
|
||||||
|
private int modelChanges;
|
||||||
|
private final Settings settings = new Settings();
|
||||||
|
private final TeamspeakConnection conn = new TeamspeakConnection(settings, null, new ConnectionListener() {
|
||||||
|
@Override
|
||||||
|
public void onStatus(String status) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onConnected() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDisconnected(String reason) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onModelChanged() {
|
||||||
|
modelChanges++;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onInfoUpdated() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onIconsUpdated() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onChat(ChatScope scope, int fromClientId, String fromUniqueId, String fromName, String message) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTalkStateChanged(int clientId, boolean talking) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(String message) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPoke(String fromName, String message) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onServerLog(String message) {
|
||||||
|
}
|
||||||
|
}, new SoundNotifier(settings), ContactStore.load());
|
||||||
|
private final ConnectionEventHandler events = new ConnectionEventHandler(conn);
|
||||||
|
private final ServerModel model = conn.getModel();
|
||||||
|
|
||||||
|
/** The fields of an event, parsed from the command text the server sends. */
|
||||||
|
private static Map<String, String> fields(String command) {
|
||||||
|
return MultiCommand.parse(ProtocolRole.SERVER, command).simplifyOne().toMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void join(int clid, String nickname, int channel) {
|
||||||
|
events.onClientJoin(new ClientJoinEvent(fields("notifycliententerview cfid=0 ctid=" + channel
|
||||||
|
+ " reasonid=0 clid=" + clid + " client_unique_identifier=uid" + clid + "=="
|
||||||
|
+ " client_nickname=" + nickname + " client_input_muted=0 client_output_muted=0"
|
||||||
|
+ " client_input_hardware=1 client_output_hardware=1 client_database_id=" + (100 + clid)
|
||||||
|
+ " client_channel_group_id=8 client_servergroups=6,7 client_away=0 client_away_message"
|
||||||
|
+ " client_type=0 client_flag_avatar=abc client_talk_power=25")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void aJoiningClientEntersTheModelWithWhatItReported() {
|
||||||
|
model.putChannel(1, "Lobby", 0, 0);
|
||||||
|
join(5, "alice", 1);
|
||||||
|
|
||||||
|
ClientEntry alice = model.getClient(5);
|
||||||
|
assertEquals("alice", alice.nickname);
|
||||||
|
assertEquals(1, alice.channelId);
|
||||||
|
assertEquals(105, alice.databaseId);
|
||||||
|
assertEquals(25, alice.talkPower);
|
||||||
|
assertArrayEquals(new int[]{6, 7}, alice.serverGroupIds);
|
||||||
|
assertEquals("abc", alice.avatarFlag);
|
||||||
|
assertEquals(1, modelChanges);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void movesAndLeaves() {
|
||||||
|
model.putChannel(1, "Lobby", 0, 0);
|
||||||
|
model.putChannel(2, "Games", 0, 1);
|
||||||
|
join(5, "alice", 1);
|
||||||
|
|
||||||
|
events.onClientMoved(new ClientMovedEvent(fields("notifyclientmoved ctid=2 reasonid=0 clid=5")));
|
||||||
|
assertEquals(2, model.getClient(5).channelId);
|
||||||
|
|
||||||
|
events.onClientLeave(new ClientLeaveEvent(fields(
|
||||||
|
"notifyclientleftview cfid=2 ctid=0 reasonid=8 reasonmsg=bye clid=5")));
|
||||||
|
assertNull(model.getClient(5));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void aPartialUpdateLeavesTheOtherFieldsAlone() {
|
||||||
|
model.putChannel(1, "Lobby", 0, 0);
|
||||||
|
join(5, "alice", 1);
|
||||||
|
|
||||||
|
events.onClientChanged(new ClientUpdatedEvent(fields("notifyclientupdated clid=5 client_input_muted=1")));
|
||||||
|
|
||||||
|
ClientEntry alice = model.getClient(5);
|
||||||
|
assertTrue(alice.inputMuted);
|
||||||
|
assertEquals("alice", alice.nickname);
|
||||||
|
assertEquals("abc", alice.avatarFlag);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void clearedFieldsAreCleared() {
|
||||||
|
model.putChannel(1, "Lobby", 0, 0);
|
||||||
|
join(5, "alice", 1);
|
||||||
|
events.onClientChanged(new ClientUpdatedEvent(fields(
|
||||||
|
"notifyclientupdated clid=5 client_away=1 client_away_message=lunch")));
|
||||||
|
assertEquals("lunch", model.getClient(5).awayMessage);
|
||||||
|
|
||||||
|
events.onClientChanged(new ClientUpdatedEvent(fields(
|
||||||
|
"notifyclientupdated clid=5 client_away_message client_flag_avatar")));
|
||||||
|
|
||||||
|
ClientEntry alice = model.getClient(5);
|
||||||
|
assertTrue(alice.away);
|
||||||
|
assertEquals("", alice.awayMessage);
|
||||||
|
assertEquals("", alice.avatarFlag);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void serverGroupsComeAndGo() {
|
||||||
|
model.putChannel(1, "Lobby", 0, 0);
|
||||||
|
join(5, "alice", 1);
|
||||||
|
|
||||||
|
events.onServerGroupClientAdded(new ServerGroupClientAddedEvent(fields(
|
||||||
|
"notifyservergroupclientadded name=Admin sgid=9 cldbid=105 clid=5")));
|
||||||
|
assertTrue(model.getClient(5).inServerGroup(9));
|
||||||
|
|
||||||
|
events.onServerGroupClientDeleted(new ServerGroupClientDeletedEvent(fields(
|
||||||
|
"notifyservergroupclientdeleted name=Guest sgid=6 cldbid=105 clid=5")));
|
||||||
|
assertArrayEquals(new int[]{7, 9}, model.getClient(5).serverGroupIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void channelsAreCreatedEditedMovedAndDeleted() {
|
||||||
|
model.putChannel(1, "Lobby", 0, 0);
|
||||||
|
events.onChannelCreate(new ChannelCreateEvent(fields(
|
||||||
|
"notifychannelcreated cid=2 cpid=0 channel_name=Games channel_order=1"
|
||||||
|
+ " channel_flag_password=1 channel_maxclients=5 channel_flag_maxclients_unlimited=0")));
|
||||||
|
ChannelNode games = model.getChannel(2);
|
||||||
|
assertTrue(games.hasPassword);
|
||||||
|
assertEquals(5, games.maxClients);
|
||||||
|
assertEquals(List.of("Lobby", "Games"), model.buildTree().stream().map(c -> c.name).toList());
|
||||||
|
|
||||||
|
events.onChannelEdit(new ChannelEditedEvent(fields(
|
||||||
|
"notifychanneledited cid=2 reasonid=10 invokerid=3 invokername=bob invokeruid=bobuid="
|
||||||
|
+ " channel_flag_password=0 channel_topic")));
|
||||||
|
assertFalse(games.hasPassword);
|
||||||
|
assertEquals("", games.topic);
|
||||||
|
|
||||||
|
events.onChannelMoved(new ChannelMovedEvent(fields("notifychannelmoved cid=2 cpid=1 order=0 reasonid=1")));
|
||||||
|
assertEquals(1, games.parentId);
|
||||||
|
assertEquals(List.of("Games"), model.buildTree().get(0).children.stream().map(c -> c.name).toList());
|
||||||
|
|
||||||
|
events.onChannelDeleted(new ChannelDeletedEvent(fields("notifychanneldeleted cid=2")));
|
||||||
|
assertNull(model.getChannel(2));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
package com.ts3client.net;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||||
|
|
||||||
|
/** The channel tree: TS3 orders siblings as a chain of "sits below" links, not by a key. */
|
||||||
|
class ServerModelTreeTest {
|
||||||
|
|
||||||
|
private final ServerModel model = new ServerModel();
|
||||||
|
|
||||||
|
private static List<String> names(List<ChannelNode> channels) {
|
||||||
|
return channels.stream().map(c -> c.name).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void ordersSiblingsByTheirChainWhateverOrderTheyArrivedIn() {
|
||||||
|
model.putChannel(3, "C", 0, 2);
|
||||||
|
model.putChannel(1, "A", 0, 0);
|
||||||
|
model.putChannel(2, "B", 0, 1);
|
||||||
|
assertEquals(List.of("A", "B", "C"), names(model.buildTree()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void keepsChannelsWhoseLinksAreBroken() {
|
||||||
|
model.putChannel(1, "A", 0, 0);
|
||||||
|
model.putChannel(2, "B", 0, 1);
|
||||||
|
model.putChannel(3, "C", 0, 1); // claims the same place as B
|
||||||
|
model.putChannel(4, "D", 0, 99); // sits below a channel that does not exist
|
||||||
|
List<String> tree = names(model.buildTree());
|
||||||
|
assertEquals(4, tree.size());
|
||||||
|
assertEquals(List.of("A", "B"), tree.subList(0, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void nestsChannelsAndPutsOrphansAtTheTop() {
|
||||||
|
model.putChannel(1, "Lobby", 0, 0);
|
||||||
|
model.putChannel(2, "Games", 1, 0);
|
||||||
|
model.putChannel(3, "Lost", 42, 0);
|
||||||
|
List<ChannelNode> roots = model.buildTree();
|
||||||
|
assertEquals(List.of("Lobby", "Lost"), names(roots).stream().sorted().toList());
|
||||||
|
assertEquals(List.of("Games"), names(model.getChannel(1).children));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void aRemovedChannelsFollowerMovesUp() {
|
||||||
|
model.putChannel(1, "A", 0, 0);
|
||||||
|
model.putChannel(2, "B", 0, 1);
|
||||||
|
model.putChannel(3, "C", 0, 2);
|
||||||
|
model.removeChannel(2);
|
||||||
|
assertEquals(List.of("A", "C"), names(model.buildTree()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void reorderingRepairsTheLinksTheServerDoesNotResend() {
|
||||||
|
model.putChannel(1, "A", 0, 0);
|
||||||
|
model.putChannel(2, "B", 0, 1);
|
||||||
|
model.putChannel(3, "C", 0, 2);
|
||||||
|
model.relinkChannel(3, 0, 0); // C to the top
|
||||||
|
assertEquals(List.of("C", "A", "B"), names(model.buildTree()));
|
||||||
|
model.relinkChannel(3, 0, 2); // and back below B
|
||||||
|
assertEquals(List.of("A", "B", "C"), names(model.buildTree()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void movingUnderAnotherParentClosesTheGap() {
|
||||||
|
model.putChannel(1, "A", 0, 0);
|
||||||
|
model.putChannel(2, "B", 0, 1);
|
||||||
|
model.putChannel(3, "C", 0, 2);
|
||||||
|
model.relinkChannel(2, 1, 0);
|
||||||
|
assertEquals(List.of("A", "C"), names(model.buildTree()));
|
||||||
|
assertEquals(List.of("B"), names(model.getChannel(1).children));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void listsClientsByTalkPowerThenNameAndHidesQueryClients() {
|
||||||
|
model.putChannel(1, "Lobby", 0, 0);
|
||||||
|
model.putClient(10, "zed", 1);
|
||||||
|
model.putClient(11, "Amy", 1);
|
||||||
|
model.putClient(12, "bob", 1).talkPower = 50;
|
||||||
|
model.putClient(13, "serveradmin", 1).type = 1;
|
||||||
|
model.buildTree();
|
||||||
|
assertEquals(List.of("bob", "Amy", "zed"),
|
||||||
|
model.getChannel(1).clients.stream().map(c -> c.nickname).toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void findsChannelsByPathIdOrPlainName() {
|
||||||
|
model.putChannel(1, "Lobby", 0, 0);
|
||||||
|
ChannelNode games = model.putChannel(2, "Games", 1, 0);
|
||||||
|
assertEquals("Lobby/Games", model.channelPath(2));
|
||||||
|
assertSame(games, model.findChannelByPath("/lobby/games/"));
|
||||||
|
assertSame(games, model.findChannelByPath("/2"));
|
||||||
|
assertSame(games, model.findChannelByPath("Games"), "a moved channel is still found by name");
|
||||||
|
assertNull(model.findChannelByPath("Lobby/Music"));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user