| 1 | package commands |
| 2 | |
| 3 | import ( |
| 4 | "os" |
| 5 | "testing" |
| 6 | "time" |
| 7 | |
| 8 | "github.com/stretchr/testify/assert" |
| 9 | ) |
| 10 | |
| 11 | func TestFormatMode(t *testing.T) { |
| 12 | t.Parallel() |
| 13 | |
| 14 | tests := []struct { |
| 15 | name string |
| 16 | mode os.FileMode |
| 17 | expected string |
| 18 | }{ |
| 19 | // File types |
| 20 | { |
| 21 | name: "regular file with rw-r--r--", |
| 22 | mode: 0644, |
| 23 | expected: "-rw-r--r--", |
| 24 | }, |
| 25 | { |
| 26 | name: "regular file with rwxr-xr-x", |
| 27 | mode: 0755, |
| 28 | expected: "-rwxr-xr-x", |
| 29 | }, |
| 30 | { |
| 31 | name: "regular file with no permissions", |
| 32 | mode: 0, |
| 33 | expected: "----------", |
| 34 | }, |
| 35 | { |
| 36 | name: "regular file with full permissions", |
| 37 | mode: 0777, |
| 38 | expected: "-rwxrwxrwx", |
| 39 | }, |
| 40 | { |
| 41 | name: "directory with rwxr-xr-x", |
| 42 | mode: os.ModeDir | 0755, |
| 43 | expected: "drwxr-xr-x", |
| 44 | }, |
| 45 | { |
| 46 | name: "directory with rwx------", |
| 47 | mode: os.ModeDir | 0700, |
| 48 | expected: "drwx------", |
| 49 | }, |
| 50 | { |
| 51 | name: "symlink with rwxrwxrwx", |
| 52 | mode: os.ModeSymlink | 0777, |
| 53 | expected: "lrwxrwxrwx", |
| 54 | }, |
| 55 | { |
| 56 | name: "named pipe with rw-r--r--", |
| 57 | mode: os.ModeNamedPipe | 0644, |
| 58 | expected: "prw-r--r--", |
| 59 | }, |
| 60 | { |
| 61 | name: "socket with rw-rw-rw-", |
| 62 | mode: os.ModeSocket | 0666, |
| 63 | expected: "srw-rw-rw-", |
| 64 | }, |
| 65 | { |
| 66 | name: "block device with rw-rw----", |
| 67 | mode: os.ModeDevice | 0660, |
| 68 | expected: "brw-rw----", |
| 69 | }, |
| 70 | { |
| 71 | name: "character device with rw-rw-rw-", |
| 72 | mode: os.ModeDevice | os.ModeCharDevice | 0666, |
| 73 | expected: "crw-rw-rw-", |
| 74 | }, |
| 75 | |
| 76 | // Special permission bits - setuid |
| 77 | { |
| 78 | name: "setuid with execute", |
| 79 | mode: os.ModeSetuid | 0755, |
| 80 | expected: "-rwsr-xr-x", |
| 81 | }, |
| 82 | { |
| 83 | name: "setuid without execute", |
| 84 | mode: os.ModeSetuid | 0644, |
| 85 | expected: "-rwSr--r--", |
| 86 | }, |
| 87 | |
| 88 | // Special permission bits - setgid |
| 89 | { |
| 90 | name: "setgid with execute", |
| 91 | mode: os.ModeSetgid | 0755, |
| 92 | expected: "-rwxr-sr-x", |
| 93 | }, |
| 94 | { |
| 95 | name: "setgid without execute", |
| 96 | mode: os.ModeSetgid | 0745, |
| 97 | expected: "-rwxr-Sr-x", |
| 98 | }, |
| 99 | |
| 100 | // Special permission bits - sticky |
| 101 | { |
| 102 | name: "sticky with execute", |
| 103 | mode: os.ModeSticky | 0755, |
| 104 | expected: "-rwxr-xr-t", |
| 105 | }, |
| 106 | { |
| 107 | name: "sticky without execute", |
| 108 | mode: os.ModeSticky | 0754, |
| 109 | expected: "-rwxr-xr-T", |
| 110 | }, |
| 111 | |
| 112 | // Combined special bits |
| 113 | { |
| 114 | name: "setuid + setgid + sticky all with execute", |
| 115 | mode: os.ModeSetuid | os.ModeSetgid | os.ModeSticky | 0777, |
| 116 | expected: "-rwsrwsrwt", |
| 117 | }, |
| 118 | { |
| 119 | name: "setuid + setgid + sticky none with execute", |
| 120 | mode: os.ModeSetuid | os.ModeSetgid | os.ModeSticky | 0666, |
| 121 | expected: "-rwSrwSrwT", |
| 122 | }, |
| 123 | |
| 124 | // Directory with special bits |
| 125 | { |
| 126 | name: "directory with sticky bit", |
| 127 | mode: os.ModeDir | os.ModeSticky | 0755, |
| 128 | expected: "drwxr-xr-t", |
| 129 | }, |
| 130 | } |
| 131 | |
| 132 | for _, tc := range tests { |
| 133 | t.Run(tc.name, func(t *testing.T) { |
| 134 | t.Parallel() |
| 135 | result := formatMode(tc.mode) |
| 136 | assert.Equal(t, tc.expected, result) |
| 137 | }) |
| 138 | } |
| 139 | } |
| 140 | |
| 141 | func TestFormatModTime(t *testing.T) { |
| 142 | t.Parallel() |
| 143 | |
| 144 | t.Run("zero time returns dash", func(t *testing.T) { |
| 145 | t.Parallel() |
| 146 | result := formatModTime(time.Time{}) |
| 147 | assert.Equal(t, "-", result) |
| 148 | }) |
| 149 | |
| 150 | t.Run("old time shows year format", func(t *testing.T) { |
| 151 | t.Parallel() |
| 152 | // Use a time clearly in the past (more than 6 months ago) |
| 153 | oldTime := time.Date(2020, time.March, 15, 10, 30, 0, 0, time.UTC) |
| 154 | result := formatModTime(oldTime) |
| 155 | // Format: "Jan 02 2006" (note: two spaces before year) |
| 156 | assert.Equal(t, "Mar 15 2020", result) |
| 157 | }) |
| 158 | |
| 159 | t.Run("very old time shows year format", func(t *testing.T) { |
| 160 | t.Parallel() |
| 161 | veryOldTime := time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC) |
| 162 | result := formatModTime(veryOldTime) |
| 163 | assert.Equal(t, "Jan 01 2000", result) |
| 164 | }) |
| 165 | |
| 166 | t.Run("future time shows year format", func(t *testing.T) { |
| 167 | t.Parallel() |
| 168 | // Times more than 24h in the future should show year format |
| 169 | futureTime := time.Now().AddDate(1, 0, 0) |
| 170 | result := formatModTime(futureTime) |
| 171 | // Should contain the future year |
| 172 | assert.Contains(t, result, " ") // two spaces before year |
| 173 | assert.Regexp(t, `^[A-Z][a-z]{2} \d{2} \d{4}$`, result) // matches "Mon DD YYYY" |
| 174 | assert.Contains(t, result, futureTime.Format("2006")) // contains the year |
| 175 | }) |
| 176 | |
| 177 | t.Run("format lengths are consistent", func(t *testing.T) { |
| 178 | t.Parallel() |
| 179 | // Both formats should produce 12-character strings for alignment |
| 180 | oldTime := time.Date(2020, time.March, 15, 10, 30, 0, 0, time.UTC) |
| 181 | oldResult := formatModTime(oldTime) |
| 182 | assert.Len(t, oldResult, 12, "old time format should be 12 chars") |
| 183 | |
| 184 | // Recent time: use 1 month ago to ensure it's always within the 6-month window |
| 185 | recentTime := time.Now().AddDate(0, -1, 0) |
| 186 | recentResult := formatModTime(recentTime) |
| 187 | assert.Len(t, recentResult, 12, "recent time format should be 12 chars") |
| 188 | }) |
| 189 | } |