Skip to content

Commit 967d93b

Browse files
committed
Add IPC contract tests for SMB connection commands
Covers connect_to_server, list_shares_on_host, mount_network_share. The mount_network_share signature has 6 positional args and AGENTS.md specifically calls out positional-soup as fragile — pinning the wire order here catches the kind of drift that would otherwise surface as a runtime 'not allowed'. Each command tests both the happy path and a typed error variant (ShareListError::auth_required, MountError::auth_failed, string error for connect_to_server).
1 parent baa977e commit 967d93b

1 file changed

Lines changed: 141 additions & 0 deletions

File tree

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
/**
2+
* IPC contract tests for the network/SMB connection surface: `connect_to_server`,
3+
* `list_shares_on_host`, `mount_network_share`.
4+
*
5+
* MTP and SMB stood out in the coverage report as nearly-zero IPC coverage. The
6+
* underlying smb2 / mDNS logic is tested in its own crate; what this group catches
7+
* is the **wire format**, especially the many-positional-arg shapes that AGENTS.md
8+
* specifically calls out as fragile (the `mountNetworkShare(server, share, username,
9+
* password, port, timeoutMs)` signature has 6 positional args; getting the order
10+
* wrong silently breaks at runtime).
11+
*/
12+
13+
import { afterEach, describe, expect, it } from 'vitest'
14+
15+
import { commands } from '$lib/ipc/bindings'
16+
import type { ManualConnectResult, MountResult, ShareListResult } from '$lib/ipc/bindings'
17+
import { clearIpcMocks, installIpcMock } from '$lib/ipc/test-helpers'
18+
19+
afterEach(() => {
20+
clearIpcMocks()
21+
})
22+
23+
describe('commands.connectToServer', () => {
24+
it('forwards address as a single payload key', async () => {
25+
const ipc = installIpcMock()
26+
const result: ManualConnectResult = {
27+
host: {
28+
id: 'manual-1',
29+
name: 'storage.local',
30+
hostname: 'storage.local',
31+
ipAddress: '192.168.1.42',
32+
port: 445,
33+
},
34+
sharePath: null,
35+
}
36+
ipc.mock('connect_to_server', () => result)
37+
38+
const out = await commands.connectToServer('smb://storage.local/share')
39+
40+
expect(out).toEqual({ status: 'ok', data: result })
41+
expect(ipc.lastCall('connect_to_server')?.payload).toEqual({
42+
address: 'smb://storage.local/share',
43+
})
44+
})
45+
46+
it('surfaces a string error on unreachable hosts', async () => {
47+
const ipc = installIpcMock()
48+
ipc.mock('connect_to_server', () => {
49+
throw 'host unreachable'
50+
})
51+
52+
const out = await commands.connectToServer('smb://nonexistent.invalid')
53+
54+
expect(out.status).toBe('error')
55+
if (out.status === 'error') expect(out.error).toBe('host unreachable')
56+
})
57+
})
58+
59+
describe('commands.listSharesOnHost', () => {
60+
it('sends all six positional args as camelCase payload keys', async () => {
61+
const ipc = installIpcMock()
62+
const result: ShareListResult = {
63+
shares: [{ name: 'Public', isDisk: true, comment: null }],
64+
authMode: 'guest_allowed',
65+
fromCache: false,
66+
}
67+
ipc.mock('list_shares_on_host', () => result)
68+
69+
const hostId = 'host-1'
70+
const hostname = 'TEST_SERVER.local'
71+
const ipAddress: string | null = '192.168.1.42'
72+
const port = 4450
73+
const timeoutMs: number | null = 15000
74+
const cacheTtlMs: number | null = 30000
75+
76+
await commands.listSharesOnHost(hostId, hostname, ipAddress, port, timeoutMs, cacheTtlMs)
77+
78+
expect(ipc.lastCall('list_shares_on_host')?.payload).toEqual({
79+
hostId,
80+
hostname,
81+
ipAddress,
82+
port,
83+
timeoutMs,
84+
cacheTtlMs,
85+
})
86+
})
87+
88+
it('surfaces the typed ShareListError discriminator (e.g. auth_required)', async () => {
89+
const ipc = installIpcMock()
90+
ipc.mock('list_shares_on_host', () => {
91+
throw { type: 'auth_required', message: 'Server requires credentials' }
92+
})
93+
94+
const out = await commands.listSharesOnHost('h', 'x.local', null, 445, null, null)
95+
96+
expect(out.status).toBe('error')
97+
if (out.status === 'error') {
98+
expect(out.error).toEqual({ type: 'auth_required', message: 'Server requires credentials' })
99+
}
100+
})
101+
})
102+
103+
describe('commands.mountNetworkShare', () => {
104+
it('sends the 6 positional args in declared order (server, share, username, password, port, timeoutMs)', async () => {
105+
const ipc = installIpcMock()
106+
const result: MountResult = { mountPath: '/Volumes/Public', alreadyMounted: false }
107+
ipc.mock('mount_network_share', () => result)
108+
109+
const server = 'storage.local'
110+
const share = 'Public'
111+
const username: string | null = 'dave'
112+
const password: string | null = 'hunter2'
113+
const port: number | null = 445
114+
const timeoutMs: number | null = 20000
115+
116+
await commands.mountNetworkShare(server, share, username, password, port, timeoutMs)
117+
118+
expect(ipc.lastCall('mount_network_share')?.payload).toEqual({
119+
server,
120+
share,
121+
username,
122+
password,
123+
port,
124+
timeoutMs,
125+
})
126+
})
127+
128+
it('surfaces typed MountError variants (auth_failed) on the error branch', async () => {
129+
const ipc = installIpcMock()
130+
ipc.mock('mount_network_share', () => {
131+
throw { type: 'auth_failed', message: 'bad credentials' }
132+
})
133+
134+
const out = await commands.mountNetworkShare('s', 'sh', 'u', 'p', null, null)
135+
136+
expect(out.status).toBe('error')
137+
if (out.status === 'error') {
138+
expect(out.error).toEqual({ type: 'auth_failed', message: 'bad credentials' })
139+
}
140+
})
141+
})

0 commit comments

Comments
 (0)