|
4 | 4 | "bytes"
|
5 | 5 | "context"
|
6 | 6 | "strconv"
|
| 7 | + "strings" |
7 | 8 | "testing"
|
| 9 | + "time" |
8 | 10 |
|
9 | 11 | "github.com/planetscale/cli/internal/cmdutil"
|
10 | 12 | "github.com/planetscale/cli/internal/config"
|
@@ -116,3 +118,131 @@ func TestDeployRequest_ShowBranchName(t *testing.T) {
|
116 | 118 | res := &ps.DeployRequest{Number: number}
|
117 | 119 | c.Assert(buf.String(), qt.JSONEquals, res)
|
118 | 120 | }
|
| 121 | + |
| 122 | +func TestDeployRequest_ShowTimestampBug(t *testing.T) { |
| 123 | + c := qt.New(t) |
| 124 | + |
| 125 | + var buf bytes.Buffer |
| 126 | + format := printer.Human // Use human format to test table output |
| 127 | + p := printer.NewPrinter(&format) |
| 128 | + p.SetResourceOutput(&buf) |
| 129 | + |
| 130 | + org := "planetscale" |
| 131 | + db := "testdb" |
| 132 | + var number uint64 = 47 |
| 133 | + |
| 134 | + // Create timestamps for testing - make them different to verify correct field assignment |
| 135 | + createdAt := time.Date(2025, 6, 23, 22, 46, 42, 348000000, time.UTC) // Base timestamp |
| 136 | + updatedAt := time.Date(2025, 6, 23, 22, 52, 34, 76000000, time.UTC) // 6 minutes later |
| 137 | + startedAt := time.Date(2025, 6, 23, 22, 48, 52, 553000000, time.UTC) // 2 minutes after created |
| 138 | + queuedAt := time.Date(2025, 6, 23, 22, 48, 38, 809000000, time.UTC) // Just before started |
| 139 | + |
| 140 | + svc := &mock.DeployRequestsService{ |
| 141 | + GetFn: func(ctx context.Context, req *ps.GetDeployRequestRequest) (*ps.DeployRequest, error) { |
| 142 | + c.Assert(req.Organization, qt.Equals, org) |
| 143 | + c.Assert(req.Database, qt.Equals, db) |
| 144 | + c.Assert(req.Number, qt.Equals, number) |
| 145 | + |
| 146 | + return &ps.DeployRequest{ |
| 147 | + ID: "abcd1234efgh", |
| 148 | + Number: number, |
| 149 | + Branch: "feature-branch-2025", // String, not timestamp |
| 150 | + IntoBranch: "main", // String, not timestamp |
| 151 | + Approved: true, |
| 152 | + State: "open", |
| 153 | + CreatedAt: createdAt, |
| 154 | + UpdatedAt: updatedAt, |
| 155 | + Deployment: &ps.Deployment{ |
| 156 | + ID: "deploy5678wxyz", |
| 157 | + State: "in_progress", |
| 158 | + Deployable: true, |
| 159 | + InstantDDLEligible: false, |
| 160 | + StartedAt: &startedAt, |
| 161 | + QueuedAt: &queuedAt, |
| 162 | + FinishedAt: nil, // This should show as empty, not showing incorrect timestamp |
| 163 | + }, |
| 164 | + }, nil |
| 165 | + }, |
| 166 | + } |
| 167 | + |
| 168 | + ch := &cmdutil.Helper{ |
| 169 | + Printer: p, |
| 170 | + Config: &config.Config{ |
| 171 | + Organization: org, |
| 172 | + }, |
| 173 | + Client: func() (*ps.Client, error) { |
| 174 | + return &ps.Client{ |
| 175 | + DeployRequests: svc, |
| 176 | + }, nil |
| 177 | + }, |
| 178 | + } |
| 179 | + |
| 180 | + cmd := ShowCmd(ch) |
| 181 | + cmd.SetArgs([]string{db, strconv.FormatUint(number, 10)}) |
| 182 | + err := cmd.Execute() |
| 183 | + |
| 184 | + c.Assert(err, qt.IsNil) |
| 185 | + c.Assert(svc.GetFnInvoked, qt.IsTrue) |
| 186 | + |
| 187 | + output := buf.String() |
| 188 | + |
| 189 | + // Debug: Print the actual output to see what we get |
| 190 | + t.Logf("Table output:\n%s", output) |
| 191 | + |
| 192 | + // Debug: Print the actual struct values being passed to tableprinter |
| 193 | + dr, _ := svc.GetFn(context.Background(), &ps.GetDeployRequestRequest{ |
| 194 | + Organization: org, |
| 195 | + Database: db, |
| 196 | + Number: number, |
| 197 | + }) |
| 198 | + converted := toDeployRequest(dr) |
| 199 | + t.Logf("Struct values - CreatedAt: %v, UpdatedAt: %v, FinishedAt: %v, StartedAt: %v, QueuedAt: %v", |
| 200 | + converted.CreatedAt, converted.UpdatedAt, converted.Deployment.FinishedAt, converted.Deployment.StartedAt, converted.Deployment.QueuedAt) |
| 201 | + |
| 202 | + // Test the specific bug: FINISHED AT should be empty when deployment.finished_at is nil |
| 203 | + // Look for the FINISHED AT column in the table output |
| 204 | + lines := strings.Split(output, "\n") |
| 205 | + headerLine := "" |
| 206 | + dataLine := "" |
| 207 | + |
| 208 | + for _, line := range lines { |
| 209 | + trimmed := strings.TrimSpace(line) |
| 210 | + if trimmed != "" && !strings.Contains(line, "---") { |
| 211 | + if headerLine == "" { |
| 212 | + headerLine = line |
| 213 | + } else if dataLine == "" { |
| 214 | + dataLine = line |
| 215 | + break |
| 216 | + } |
| 217 | + } |
| 218 | + } |
| 219 | + |
| 220 | + c.Assert(headerLine, qt.Not(qt.Equals), "", qt.Commentf("Could not find header line")) |
| 221 | + c.Assert(dataLine, qt.Not(qt.Equals), "", qt.Commentf("Could not find data line")) |
| 222 | + |
| 223 | + // Find the FINISHED AT column position |
| 224 | + finishedAtPos := strings.Index(headerLine, "FINISHED AT") |
| 225 | + c.Assert(finishedAtPos, qt.Not(qt.Equals), -1, qt.Commentf("Could not find FINISHED AT column in header")) |
| 226 | + |
| 227 | + // Find the next column after FINISHED AT to know where this column ends |
| 228 | + remainingHeader := headerLine[finishedAtPos+len("FINISHED AT"):] |
| 229 | + nextColumnMatch := strings.Fields(remainingHeader) |
| 230 | + var finishedAtEndPos int |
| 231 | + if len(nextColumnMatch) > 0 { |
| 232 | + nextColumnPos := strings.Index(remainingHeader, nextColumnMatch[0]) |
| 233 | + finishedAtEndPos = finishedAtPos + len("FINISHED AT") + nextColumnPos |
| 234 | + } else { |
| 235 | + finishedAtEndPos = len(headerLine) |
| 236 | + } |
| 237 | + |
| 238 | + // Extract the FINISHED AT column value from the data line |
| 239 | + if finishedAtEndPos <= len(dataLine) { |
| 240 | + finishedAtValue := strings.TrimSpace(dataLine[finishedAtPos:finishedAtEndPos]) |
| 241 | + |
| 242 | + // The bug: FINISHED AT should be empty since deployment.finished_at is nil |
| 243 | + // If it contains "ago" or any timestamp value, that's the bug |
| 244 | + if finishedAtValue != "" && strings.Contains(finishedAtValue, "ago") { |
| 245 | + c.Errorf("FINISHED AT column shows '%s' but deployment.finished_at is nil - should be empty", finishedAtValue) |
| 246 | + } |
| 247 | + } |
| 248 | +} |
0 commit comments