nut-shell 0.1.2

A lightweight command-line interface library for embedded systems
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
//! Core shell functionality tests.
//!
//! Tests command execution, directory navigation, path resolution,
//! argument validation, and access control.

#[allow(clippy::duplicate_mod)]
#[path = "fixtures/mod.rs"]
mod fixtures;

#[allow(clippy::duplicate_mod)]
#[path = "helpers.rs"]
mod helpers;

// ============================================================================
// Command Execution Tests
// ============================================================================

#[test]
#[cfg(not(feature = "authentication"))]
fn test_command_with_arguments() {
    let mut shell = helpers::create_test_shell();

    let output = helpers::execute_command(&mut shell, "echo arg1 arg2 arg3");

    helpers::assert_contains_all(&output, &["arg1", "arg2", "arg3"]);
}

#[test]
#[cfg(not(feature = "authentication"))]
fn test_path_based_command_execution() {
    // Table-driven test for path-based execution scenarios
    let test_cases = [
        // (command, expected_output, description)
        (
            "system/status",
            "System OK",
            "simple path without navigation",
        ),
        ("system/network/status", "Network OK", "deeply nested path"),
        ("system/hardware/led on", "LED: on", "path with arguments"),
    ];

    for (cmd, expected, description) in test_cases {
        let mut shell = helpers::create_test_shell();
        let output = helpers::execute_command(&mut shell, cmd);

        assert!(
            output.contains(expected),
            "Failed '{}': expected '{}' in output: {}",
            description,
            expected,
            output
        );
    }
}

#[test]
#[cfg(not(feature = "authentication"))]
fn test_command_with_leading_spaces() {
    let mut shell = helpers::create_test_shell();

    // Leading spaces should be trimmed
    let output = helpers::execute_command(&mut shell, "   echo test");
    assert!(output.contains("test"), "Leading spaces should be trimmed");
}

#[test]
#[cfg(not(feature = "authentication"))]
fn test_command_with_trailing_spaces() {
    let mut shell = helpers::create_test_shell();

    // Trailing spaces should be trimmed
    let output = helpers::execute_command(&mut shell, "echo test   ");
    assert!(output.contains("test"), "Trailing spaces should be trimmed");
}

#[test]
#[cfg(not(feature = "authentication"))]
fn test_command_with_multiple_spaces_between_args() {
    let mut shell = helpers::create_test_shell();

    // Multiple spaces between args should be normalized to single space
    let output = helpers::execute_command(&mut shell, "echo arg1    arg2    arg3");
    helpers::assert_contains_all(&output, &["arg1", "arg2", "arg3"]);
}

#[test]
#[cfg(not(feature = "authentication"))]
fn test_command_with_exact_buffer_size() {
    // Command exactly at buffer limit should work
    let mut shell = helpers::create_test_shell();

    // Create command that's close to buffer size (128 chars)
    // "echo " + 122 chars of args = 127 chars (leave 1 for null/safety)
    let args = "a".repeat(120);
    let cmd = format!("echo {}", args);

    let output = helpers::execute_command(&mut shell, &cmd);
    assert!(
        output.contains(&args),
        "Should handle command at buffer limit"
    );
}

#[test]
#[cfg(not(feature = "authentication"))]
fn test_repeated_command_execution() {
    // Executing same command multiple times should work consistently
    let mut shell = helpers::create_test_shell();

    for i in 0..5 {
        let output = helpers::execute_command(&mut shell, "echo test");
        assert!(
            output.contains("test"),
            "Repeated execution #{} should work",
            i + 1
        );
    }
}

#[test]
#[cfg(not(feature = "authentication"))]
fn test_rapid_state_changes() {
    // Rapidly navigating and executing commands should maintain state correctly
    let mut shell = helpers::create_test_shell();

    helpers::execute_command(&mut shell, "system");
    helpers::execute_command(&mut shell, "status");
    helpers::execute_command(&mut shell, "..");
    helpers::execute_command(&mut shell, "debug");
    helpers::execute_command(&mut shell, "memory");
    helpers::execute_command(&mut shell, "/");

    let output = helpers::execute_command(&mut shell, "echo final");
    helpers::assert_contains_all(&output, &["@/>", "final"]);
}

// ============================================================================
// Directory Navigation Tests
// ============================================================================

#[test]
#[cfg(not(feature = "authentication"))]
fn test_navigate_to_directory() {
    let mut shell = helpers::create_test_shell();

    // Navigate to system directory
    helpers::execute_command(&mut shell, "system");

    // Execute command in navigated directory
    let output = helpers::execute_command(&mut shell, "status");

    helpers::assert_contains_all(&output, &["@/system>", "System OK"]);
}

#[test]
#[cfg(not(feature = "authentication"))]
fn test_navigate_with_relative_path() {
    let mut shell = helpers::create_test_shell();

    // Navigate using multi-segment relative path
    helpers::execute_command(&mut shell, "system/network");

    let output = helpers::execute_command(&mut shell, "status");

    helpers::assert_contains_all(&output, &["@/system/network>", "Network OK"]);
}

#[test]
#[cfg(not(feature = "authentication"))]
fn test_navigate_parent_directory() {
    let mut shell = helpers::create_test_shell();

    // Navigate to system/network
    helpers::execute_command(&mut shell, "system/network");

    // Navigate up one level using ..
    helpers::execute_command(&mut shell, "..");

    // Should be in system/ now
    let output = helpers::execute_command(&mut shell, "status");

    helpers::assert_contains_all(&output, &["@/system>", "System OK"]);
}

#[test]
#[cfg(not(feature = "authentication"))]
fn test_navigate_absolute_path() {
    let mut shell = helpers::create_test_shell();

    // Navigate to system first
    helpers::execute_command(&mut shell, "system");

    // Navigate to debug using absolute path
    helpers::execute_command(&mut shell, "/debug");

    // Should be in debug/
    let output = helpers::execute_command(&mut shell, "memory");

    helpers::assert_contains_all(&output, &["@/debug>", "Memory"]);
}

#[test]
#[cfg(not(feature = "authentication"))]
fn test_navigate_invalid_directory() {
    let mut shell = helpers::create_test_shell();

    let output = helpers::execute_command(&mut shell, "nonexistent");

    assert!(output.contains("Error: Command not found"));
}

#[test]
#[cfg(not(feature = "authentication"))]
fn test_navigate_parent_beyond_root() {
    let mut shell = helpers::create_test_shell();

    // Try to navigate above root
    helpers::execute_command(&mut shell, "..");

    // Should still be at root
    let output = helpers::execute_command(&mut shell, "echo still at root");

    helpers::assert_contains_all(&output, &["@/>", "still at root"]);
}

#[test]
#[cfg(not(feature = "authentication"))]
fn test_navigate_to_current_directory() {
    // Using "." should stay in current directory
    let mut shell = helpers::create_test_shell();

    helpers::execute_command(&mut shell, "system");

    let output = helpers::execute_command(&mut shell, ".");

    helpers::assert_prompt(&output, "@/system>");
}

#[test]
#[cfg(not(feature = "authentication"))]
fn test_multiple_parent_navigation() {
    // Multiple ".." should navigate up multiple levels
    let mut shell = helpers::create_test_shell();

    helpers::execute_command(&mut shell, "system/network");

    let output = helpers::execute_command(&mut shell, "../..");

    helpers::assert_prompt(&output, "@/>");
}

#[test]
#[cfg(not(feature = "authentication"))]
fn test_navigate_with_mixed_dots() {
    // Mix of "." and ".." should work correctly
    let mut shell = helpers::create_test_shell();

    helpers::execute_command(&mut shell, "system");

    let output = helpers::execute_command(&mut shell, "./network/../hardware");

    helpers::assert_prompt(&output, "@/system/hardware>");
}

#[test]
#[cfg(not(feature = "authentication"))]
fn test_absolute_path_from_subdirectory() {
    // Absolute paths should work from any directory
    let mut shell = helpers::create_test_shell();

    helpers::execute_command(&mut shell, "system/network");

    let output = helpers::execute_command(&mut shell, "/debug");

    helpers::assert_prompt(&output, "@/debug>");
}

#[test]
#[cfg(not(feature = "authentication"))]
fn test_navigate_to_root_explicitly() {
    // "/" should navigate to root
    let mut shell = helpers::create_test_shell();

    helpers::execute_command(&mut shell, "system/network");

    let output = helpers::execute_command(&mut shell, "/");

    helpers::assert_prompt(&output, "@/>");
}

#[test]
#[cfg(not(feature = "authentication"))]
fn test_directory_with_args_returns_error() {
    // Table-driven: args passed to a directory path should return InvalidArgumentCount
    let test_cases = [
        ("system arg", "system"),
        ("system arg1 arg2", "system"),
        ("system/network arg", "system/network"),
    ];

    for (input, path) in &test_cases {
        let mut shell = helpers::create_test_shell();
        let output = helpers::execute_command(&mut shell, input);
        assert!(
            output.contains("Expected 0 arguments"),
            "Input {:?} (path {:?}): expected 'Expected 0 arguments' in output, got: {:?}",
            input,
            path,
            output
        );
    }
}

// ============================================================================
// Global Commands Tests
// ============================================================================

#[test]
#[cfg(not(feature = "authentication"))]
fn test_global_commands() {
    // Table-driven test for global commands
    let test_cases = [
        (
            "?",
            &["ls", "clear"] as &[&str],
            "help command shows available commands",
        ),
        ("ls", &["echo", "system"], "ls lists root contents"),
        ("clear", &["\x1b[2J"], "clear outputs ANSI escape sequence"),
    ];

    for (cmd, expected_fragments, description) in test_cases {
        let mut shell = helpers::create_test_shell();
        let output = helpers::execute_command(&mut shell, cmd);

        for fragment in expected_fragments {
            assert!(
                output.contains(fragment),
                "Failed '{}': expected '{}' in output",
                description,
                fragment
            );
        }
    }
}

// ============================================================================
// Command Argument Validation Tests
// ============================================================================

#[test]
#[cfg(not(feature = "authentication"))]
fn test_argument_validation() {
    // Table-driven test for argument validation scenarios
    let test_cases = [
        // (command, expected_error, description)
        (
            "system/hardware/led",
            "Expected 1 arguments, got 0",
            "command requires exactly 1 arg but got 0",
        ),
        (
            "system/hardware/led on",
            "LED: on",
            "command with correct argument count",
        ),
        (
            "system/reboot now",
            "Expected 0 arguments, got 1",
            "command accepts 0 args but got 1",
        ),
    ];

    for (cmd, expected, description) in test_cases {
        let mut shell = helpers::create_test_shell();
        let output = helpers::execute_command(&mut shell, cmd);

        assert!(
            output.contains(expected),
            "Failed '{}': expected '{}' in output: {}",
            description,
            expected,
            output
        );
    }
}

// ============================================================================
// Access Level Enforcement Tests
// ============================================================================
//
// NOTE: Access control works by hiding inaccessible nodes (security feature).
// When a user tries to access a command/directory they don't have permission for,
// the system returns "Command not found" rather than "access denied" to prevent
// information disclosure about the system structure.

#[test]
#[cfg(feature = "authentication")]
fn test_guest_access_control() {
    let mut shell = helpers::create_auth_shell();

    // Login as guest
    helpers::execute_command_auth(&mut shell, "guest:guest123");

    let test_cases = [
        ("echo hello", "hello", true), // Guest can execute Guest-level commands
        ("system/reboot", "Command not found", false), // Guest cannot execute Admin commands
        ("debug/memory", "Command not found", false), // Guest cannot access Admin directories
    ];

    for (cmd, expected, should_succeed) in test_cases {
        let output = helpers::execute_command_auth(&mut shell, cmd);

        if should_succeed {
            assert!(
                output.contains(expected),
                "Guest should be able to: '{}', got: {}",
                cmd,
                output
            );
        } else {
            assert!(
                output.contains(expected) || output.contains("Invalid path"),
                "Guest should not access: '{}', got: {}",
                cmd,
                output
            );
        }
    }
}

#[test]
#[cfg(feature = "authentication")]
fn test_admin_access_control() {
    let mut shell = helpers::create_auth_shell();

    // Login as admin
    helpers::execute_command_auth(&mut shell, "admin:admin123");

    let test_cases = [
        ("system/reboot", "Reboot"), // Admin can execute Admin commands
        ("debug/memory", "Memory"),  // Admin can access Admin directories
        ("echo test", "test"),       // Admin can also execute Guest-level commands
    ];

    for (cmd, expected) in test_cases {
        let output = helpers::execute_command_auth(&mut shell, cmd);
        assert!(
            output.contains(expected),
            "Admin should access '{}', got: {}",
            cmd,
            output
        );
    }
}

#[test]
fn test_minimal_features() {
    // Test works even with no optional features
    #[cfg(not(feature = "authentication"))]
    {
        let mut shell = helpers::create_test_shell();
        let output = helpers::execute_command(&mut shell, "echo minimal");
        assert!(output.contains("minimal"));
    }
}