Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/badge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ jobs:
fi
state_top="#E05D44"
state_bottom="#CB2431"
if awk "BEGIN {exit !($total >= 80)}"; then
if awk "BEGIN {exit !($total >= 85)}"; then
state_top="#34D058"
state_bottom="#28A745"
elif awk "BEGIN {exit !($total >= 60)}"; then
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

[![CI](https://github.com/Avicted/dialtone/actions/workflows/ci.yml/badge.svg)](https://github.com/Avicted/dialtone/actions/workflows/ci.yml)
[![Coverage](https://avicted.github.io/dialtone/badges/coverage.svg)](https://github.com/Avicted/dialtone/actions/workflows/ci.yml)
![License](https://img.shields.io/badge/license-MIT-blue.svg)


Dialtone is a realtime websocket chat with end-to-end encrypted message bodies and channel names.

Expand Down
179 changes: 179 additions & 0 deletions cmd/client/chat_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -741,6 +741,38 @@ func TestBuildDirectoryKeyEnvelopesErrors(t *testing.T) {
}
}

func TestBuildDirectoryKeyEnvelopesSuccessAndValidation(t *testing.T) {
sender := newTestKeyPair(t)
recipient := newTestKeyPair(t)
key := bytes.Repeat([]byte{9}, crypto.KeySize)

envelopes, err := buildDirectoryKeyEnvelopes(sender, "dev-1", key, []DeviceKey{
{},
{DeviceID: "dev-2", PublicKey: crypto.PublicKeyToBase64(recipient.Public)},
})
if err != nil {
t.Fatalf("buildDirectoryKeyEnvelopes: %v", err)
}
if len(envelopes) != 1 {
t.Fatalf("expected exactly one envelope, got %d", len(envelopes))
}
if envelopes[0].DeviceID != "dev-2" || envelopes[0].SenderDeviceID != "dev-1" {
t.Fatalf("unexpected envelope metadata: %+v", envelopes[0])
}

decrypted, err := crypto.DecryptFromPeer(recipient.Private, sender.Public, envelopes[0].Envelope)
if err != nil {
t.Fatalf("decrypt envelope: %v", err)
}
if !bytes.Equal(decrypted, key) {
t.Fatalf("unexpected decrypted key")
}

if _, err := buildDirectoryKeyEnvelopes(sender, "dev-1", key, []DeviceKey{{DeviceID: "dev-x", PublicKey: "invalid"}}); err == nil {
t.Fatalf("expected invalid device public key error")
}
}

func TestChatModelSelectSidebarChannel(t *testing.T) {
m := newChatForTest(t, &APIClient{serverURL: "http://server", httpClient: http.DefaultClient})
m.channels["a"] = channelInfo{ID: "a", Name: "alpha"}
Expand Down Expand Up @@ -799,6 +831,27 @@ func TestChatModelVoiceStatusLabel(t *testing.T) {
if got := m.voiceStatusLabel(); got != "voice: reconnecting" {
t.Fatalf("unexpected reconnect status: %s", got)
}

m.voiceReconnectAttempt = 0
m.voiceRoom = "ch-1"
leaveCmd := ipc.Message{Cmd: ipc.CommandVoiceLeave}
m.voicePendingCmd = &leaveCmd
if got := m.voiceStatusLabel(); got != "voice: leaving general" {
t.Fatalf("unexpected leaving status: %s", got)
}

m.voicePendingCmd = &ipc.Message{Cmd: ipc.CommandMute}
if got := m.voiceStatusLabel(); got != "voice: general" {
t.Fatalf("unexpected updating status with active room: %s", got)
}

m.voicePendingCmd = nil
m.voiceRoom = ""
m.voiceAutoStarting = true
m.voicePendingRoom = "ch-2"
if got := m.voiceStatusLabel(); got != "voice: starting" {
t.Fatalf("unexpected starting status: %s", got)
}
}

func TestChatModelViewIncludesVoiceStatus(t *testing.T) {
Expand Down Expand Up @@ -930,6 +983,58 @@ func TestChatModelHandleVoiceInfoEvent(t *testing.T) {
}
}

func TestChatModelHandleVoiceEventAdditionalPaths(t *testing.T) {
m := newChatForTest(t, &APIClient{serverURL: "http://server", httpClient: http.DefaultClient})
m.channels["ch-1"] = channelInfo{ID: "ch-1", Name: "general"}
m.voiceRoom = "ch-1"
m.voiceMembers["u2"] = true

m.handleVoiceEvent(ipc.Message{Event: ipc.EventVoiceReady, Room: "ch-1"})
if m.voiceRoom != "" {
t.Fatalf("expected voice room cleared on ready event for same room")
}
if len(m.voiceMembers) != 0 {
t.Fatalf("expected voice members cleared on ready event")
}

m.handleVoiceEvent(ipc.Message{Event: ipc.EventVoiceConnected, Room: "ch-1"})
if m.voiceRoom != "ch-1" {
t.Fatalf("expected connected room to be set")
}
if !m.voiceMembers[m.auth.UserID] {
t.Fatalf("expected local user added to voice members on connect")
}

m.voiceSpeaking = nil
m.handleVoiceEvent(ipc.Message{Event: ipc.EventUserSpeaking, User: "", Active: true})
if m.voiceSpeaking != nil {
t.Fatalf("expected empty user speaking event to be ignored")
}
m.handleVoiceEvent(ipc.Message{Event: ipc.EventUserSpeaking, User: "u2", Active: true})
if m.voiceSpeaking == nil || !m.voiceSpeaking["u2"] {
t.Fatalf("expected speaking map initialized and updated")
}

before := len(m.messages)
m.voiceAutoStarting = true
m.handleVoiceEvent(ipc.Message{Event: ipc.EventError, Error: "dial unix /tmp/dialtone-voice.sock: connect: no such file or directory"})
if len(m.messages) != before {
t.Fatalf("expected IPC-not-running error to be suppressed while auto-starting")
}

m.voiceAutoStarting = false
m.handleVoiceEvent(ipc.Message{Event: ipc.EventError, Error: "boom"})
if !strings.Contains(lastSystemMessage(m), "voice error: boom") {
t.Fatalf("expected voice error message")
}

before = len(m.messages)
m.handleVoiceEvent(ipc.Message{Event: ipc.EventPong})
if len(m.messages) != before {
t.Fatalf("expected pong event to be ignored")
}
}

func TestChatModelWaitForWSMsg(t *testing.T) {
ch := make(chan ServerMessage, 1)
ch <- ServerMessage{Type: "ping"}
Expand Down Expand Up @@ -1534,6 +1639,80 @@ func TestChatModelVoiceHelperMethods(t *testing.T) {
}
}

func TestChatModelDispatchVoiceCommandAutoStartPaths(t *testing.T) {
m := newChatForTest(t, &APIClient{serverURL: "http://server", httpClient: http.DefaultClient})
m.voiceAutoStart = true
m.voiceIPCAddr = filepath.Join(t.TempDir(), "missing.sock")
m.voiceIPC = newVoiceIPC(m.voiceIPCAddr)
m.voicedPath = "/bin/true"
m.channels["ch-1"] = channelInfo{ID: "ch-1", Name: "general"}

cmd := m.dispatchVoiceCommand(
ipc.Message{Cmd: ipc.CommandVoiceJoin, Room: "ch-1"},
"ch-1",
"voice join requested",
"voice join",
)
if cmd == nil {
t.Fatalf("expected reconnect command when auto-start queues voice command")
}
if m.voicePendingCmd == nil || m.voicePendingCmd.Cmd != ipc.CommandVoiceJoin {
t.Fatalf("expected pending join command to be queued")
}
if m.voicePendingRoom != "ch-1" {
t.Fatalf("expected pending room ch-1, got %q", m.voicePendingRoom)
}
if !strings.Contains(lastSystemMessage(m), "starting voice daemon") {
t.Fatalf("expected auto-start notice")
}

m.stopVoiceDaemon()

m2 := newChatForTest(t, &APIClient{serverURL: "http://server", httpClient: http.DefaultClient})
m2.voiceAutoStart = true
m2.voiceIPCAddr = filepath.Join(t.TempDir(), "missing.sock")
m2.voiceIPC = newVoiceIPC(m2.voiceIPCAddr)
m2.voicedPath = filepath.Join(t.TempDir(), "does-not-exist")

cmd = m2.dispatchVoiceCommand(
ipc.Message{Cmd: ipc.CommandMute},
"",
"voice mute requested",
"voice mute",
)
if cmd != nil {
t.Fatalf("expected no command when auto-start fails")
}
if !strings.Contains(lastSystemMessage(m2), "voice auto-start failed") {
t.Fatalf("expected auto-start failure message")
}
}

func TestChatModelCommandClosures(t *testing.T) {
m := newChatForTest(t, &APIClient{serverURL: "http://server", httpClient: http.DefaultClient})
m.auth.IsTrusted = true
m.api = nil

cmd := m.syncDirectoryCmd(true)
if cmd == nil {
t.Fatalf("expected syncDirectoryCmd when trusted")
}
if _, ok := cmd().(directorySyncMsg); !ok {
t.Fatalf("expected directorySyncMsg from syncDirectoryCmd")
}

m.channelKeys["ch-1"] = bytes.Repeat([]byte{1}, crypto.KeySize)
m.auth = nil
m.kp = nil
cmd = m.shareKnownChannelKeysCmd()
if cmd == nil {
t.Fatalf("expected shareKnownChannelKeysCmd when channel keys are present")
}
if _, ok := cmd().(shareKeysMsg); !ok {
t.Fatalf("expected shareKeysMsg from shareKnownChannelKeysCmd")
}
}

func TestChatModelShareDirectoryKeyCmdGuards(t *testing.T) {
m := newChatForTest(t, &APIClient{serverURL: "http://server", httpClient: http.DefaultClient})
m.auth.IsTrusted = false
Expand Down
4 changes: 1 addition & 3 deletions scripts/coverage.sh
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,7 @@ while [[ $# -gt 0 ]]; do
esac
done

# EXCLUDE_REGEX='internal|cmd/server'
# mapfile -t PKGS < <(go list ./... | grep -vE "$EXCLUDE_REGEX")
mapfile -t PKGS < <(go list ./...)
mapfile -t PKGS < <(go list ./... | grep -vE '/cmd/voiced$')

# Run tests with coverage and preserve failure output for diagnosability.
if [[ "$SHOW_ZERO_ONLY" == true || "$FAIL_ON_ZERO" == true ]]; then
Expand Down
Loading