// Package miot_test provides black-box tests for common.go utility functions. package miot_test import ( "encoding/hex" "os" "path/filepath" "testing" miot "miot" ) // ============================================================================ // CalcGroupID // ============================================================================ func TestCalcGroupID(t *testing.T) { uid := "test_user" homeID := "12345" id1 := miot.CalcGroupID(uid, homeID) id2 := miot.CalcGroupID(uid, homeID) // Same inputs → same output if id1 != id2 { t.Errorf("CalcGroupID should be deterministic: %q != %q", id1, id2) } // Length should be 16 (hex encoding of first 8 bytes of SHA1) if len(id1) != 16 { t.Errorf("CalcGroupID length = %d, want 16", len(id1)) } // Should be valid hex if _, err := hex.DecodeString(id1); err != nil { t.Errorf("CalcGroupID should produce valid hex: %v", err) } // Different inputs → different output id3 := miot.CalcGroupID(uid, "99999") if id1 == id3 { t.Error("CalcGroupID should produce different results for different inputs") } } func TestCalcGroupID_EmptyInputs(t *testing.T) { // Empty inputs should still work without panic id := miot.CalcGroupID("", "") if len(id) != 16 { t.Errorf("CalcGroupID with empty inputs length = %d, want 16", len(id)) } } // ============================================================================ // GenAbsolutePath // ============================================================================ func TestGenAbsolutePath(t *testing.T) { // Save and restore RootPath origRoot := miot.RootPath miot.RootPath = "/test/root" defer func() { miot.RootPath = origRoot }() abs := miot.GenAbsolutePath("subdir/file.txt") expected := filepath.Join("/test/root", "subdir/file.txt") if abs != expected { t.Errorf("GenAbsolutePath = %q, want %q", abs, expected) } } func TestGenAbsolutePath_Empty(t *testing.T) { origRoot := miot.RootPath miot.RootPath = "/test/root" defer func() { miot.RootPath = origRoot }() abs := miot.GenAbsolutePath("") // Should return RootPath itself when empty string is given if abs != "/test/root" && abs != "/test/root"+string(filepath.Separator) { // filepath.Join behavior varies, just check it's non-empty if abs == "" { t.Error("GenAbsolutePath with empty string should not be empty") } } } // ============================================================================ // LoadJSONFile // ============================================================================ func TestLoadJSONFile_Valid(t *testing.T) { // Create a temporary JSON file tmpDir := t.TempDir() jsonPath := filepath.Join(tmpDir, "test.json") content := `{"key1": "value1", "key2": 42, "key3": true}` if err := os.WriteFile(jsonPath, []byte(content), 0644); err != nil { t.Fatal(err) } data, err := miot.LoadJSONFile(jsonPath) if err != nil { t.Fatalf("LoadJSONFile should succeed: %v", err) } if data["key1"] != "value1" { t.Errorf("key1 = %v, want 'value1'", data["key1"]) } if data["key2"].(float64) != 42 { t.Errorf("key2 = %v, want 42", data["key2"]) } if data["key3"] != true { t.Errorf("key3 = %v, want true", data["key3"]) } } func TestLoadJSONFile_NotExist(t *testing.T) { _, err := miot.LoadJSONFile("/nonexistent/path/file.json") if err == nil { t.Error("LoadJSONFile should return error for nonexistent file") } } func TestLoadJSONFile_InvalidJSON(t *testing.T) { tmpDir := t.TempDir() jsonPath := filepath.Join(tmpDir, "invalid.json") content := `{not valid json` if err := os.WriteFile(jsonPath, []byte(content), 0644); err != nil { t.Fatal(err) } _, err := miot.LoadJSONFile(jsonPath) if err == nil { t.Error("LoadJSONFile should return error for invalid JSON") } } func TestLoadJSONFile_EmptyFile(t *testing.T) { tmpDir := t.TempDir() jsonPath := filepath.Join(tmpDir, "empty.json") if err := os.WriteFile(jsonPath, []byte(""), 0644); err != nil { t.Fatal(err) } // Empty file is not valid JSON object _, err := miot.LoadJSONFile(jsonPath) if err == nil { t.Error("LoadJSONFile should return error for empty file") } } // ============================================================================ // LoadYAMLFile // ============================================================================ func TestLoadYAMLFile_Valid(t *testing.T) { tmpDir := t.TempDir() yamlPath := filepath.Join(tmpDir, "test.yaml") content := "key1: value1\nkey2: 42\nkey3: true\n" if err := os.WriteFile(yamlPath, []byte(content), 0644); err != nil { t.Fatal(err) } data, err := miot.LoadYAMLFile(yamlPath) if err != nil { t.Fatalf("LoadYAMLFile should succeed: %v", err) } if data["key1"] != "value1" { t.Errorf("key1 = %v, want 'value1'", data["key1"]) } } func TestLoadYAMLFile_NotExist(t *testing.T) { _, err := miot.LoadYAMLFile("/nonexistent/path/file.yaml") if err == nil { t.Error("LoadYAMLFile should return error for nonexistent file") } } func TestLoadYAMLFile_InvalidYAML(t *testing.T) { tmpDir := t.TempDir() yamlPath := filepath.Join(tmpDir, "invalid.yaml") // Invalid YAML that will cause parsing error content := "\tinvalid\tyaml\tbad" if err := os.WriteFile(yamlPath, []byte(content), 0644); err != nil { t.Fatal(err) } // YAML is lenient; this might parse as a string. Just ensure no panic. _, _ = miot.LoadYAMLFile(yamlPath) } // ============================================================================ // RandomizeInt // ============================================================================ func TestRandomizeInt(t *testing.T) { value := 100 ratio := 0.3 minVal := int(float64(value) * (1 - ratio)) maxVal := int(float64(value) * (1 + ratio)) // Run multiple times to cover the range for i := 0; i < 100; i++ { result := miot.RandomizeInt(value, ratio) if result < minVal || result > maxVal { t.Errorf("RandomizeInt(%d, %f) = %d, expected in [%d, %d]", value, ratio, result, minVal, maxVal) } } } func TestRandomizeInt_ZeroRatio(t *testing.T) { value := 50 result := miot.RandomizeInt(value, 0) if result != value { t.Errorf("RandomizeInt(%d, 0) = %d, want %d", value, result, value) } } // ============================================================================ // RandomizeFloat // ============================================================================ func TestRandomizeFloat(t *testing.T) { value := 10.0 ratio := 0.2 minVal := value * (1 - ratio) maxVal := value * (1 + ratio) for i := 0; i < 100; i++ { result := miot.RandomizeFloat(value, ratio) if result < minVal || result > maxVal { t.Errorf("RandomizeFloat(%f, %f) = %f, expected in [%f, %f]", value, ratio, result, minVal, maxVal) } } } func TestRandomizeFloat_ZeroRatio(t *testing.T) { value := 3.14 result := miot.RandomizeFloat(value, 0) if result != value { t.Errorf("RandomizeFloat(%f, 0) = %f, want %f", value, result, value) } } // ============================================================================ // SlugifyName // ============================================================================ func TestSlugifyName(t *testing.T) { tests := []struct { input string want string }{ {"Hello World", "hello_world"}, {"Xiaomi Home", "xiaomi_home"}, {"UPPERCASE", "uppercase"}, {"a b c", "a_b_c"}, {"already_slug", "already_slug"}, {"", ""}, {" spaces ", "spaces"}, {"special!@#chars", "special_chars"}, {"混合中文English", "english"}, // non-alpha chars become underscores, trim {"dash-separated", "dash_separated"}, // dash is non-alphanumeric {"123numbers456", "123numbers456"}, {"__leading_trailing__", "leading_trailing"}, } for _, tt := range tests { t.Run(tt.input, func(t *testing.T) { if got := miot.SlugifyName(tt.input); got != tt.want { t.Errorf("SlugifyName(%q) = %q, want %q", tt.input, got, tt.want) } }) } } // ============================================================================ // SlugifyDID // ============================================================================ func TestSlugifyDID(t *testing.T) { server := "cn" did := "device_123" result := miot.SlugifyDID(server, did) // Should equal SlugifyName("cn_device_123") expected := miot.SlugifyName(server + "_" + did) if result != expected { t.Errorf("SlugifyDID(%q, %q) = %q, want %q", server, did, result, expected) } } func TestSlugifyDID_NonASCIIDID(t *testing.T) { // Even with non-ASCII DID, should not panic result := miot.SlugifyDID("cn", "did.test") if result == "" { t.Error("SlugifyDID should not return empty string for valid inputs") } } // ============================================================================ // RootPath // ============================================================================ func TestRootPath_Initialized(t *testing.T) { // RootPath should be set during init() if miot.RootPath == "" { t.Error("RootPath should not be empty after init()") } } // ============================================================================ // Benchmark // ============================================================================ func BenchmarkCalcGroupID(b *testing.B) { for i := 0; i < b.N; i++ { miot.CalcGroupID("test_uid", "test_home") } } func BenchmarkSlugifyName(b *testing.B) { for i := 0; i < b.N; i++ { miot.SlugifyName("Xiaomi Smart Home Device") } }