From 85e5bf7eac0f14f93304590284d4f2330c668907 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Anderss=C3=A9n?= Date: Fri, 13 Feb 2026 15:09:44 +0200 Subject: [PATCH] Test: More tests --- .github/workflows/badge.yml | 2 +- README.md | 2 + cmd/client/chat_test.go | 179 ++++++++++++++++++++++++++++++++++++ scripts/coverage.sh | 4 +- 4 files changed, 183 insertions(+), 4 deletions(-) diff --git a/.github/workflows/badge.yml b/.github/workflows/badge.yml index 3f391b4..6cb8163 100644 --- a/.github/workflows/badge.yml +++ b/.github/workflows/badge.yml @@ -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 diff --git a/README.md b/README.md index ea737e1..ce6c928 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/cmd/client/chat_test.go b/cmd/client/chat_test.go index f1fed37..661166d 100644 --- a/cmd/client/chat_test.go +++ b/cmd/client/chat_test.go @@ -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"} @@ -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) { @@ -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"} @@ -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 diff --git a/scripts/coverage.sh b/scripts/coverage.sh index c39adff..e9b108d 100755 --- a/scripts/coverage.sh +++ b/scripts/coverage.sh @@ -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