test+fix: admin sandbox list returns non-null createdAt (TDD)
Test first: sandbox-admin-list.feature verifies createdAt, expiresAt, unauth rejection, and admin delete — three new BDD scenarios. Bug found: listAllSandboxes() native query mapping used instanceof java.sql.Timestamp, but Hibernate 6 + PostgreSQL JDBC returns LocalDateTime for timestamptz columns. toInstant() now handles Timestamp, OffsetDateTime, ZonedDateTime, LocalDateTime (UTC), Date, and any Temporal via Instant.from() fallback. UI change (admin list "Created" column, prior commit) now has test coverage at the API layer. Co-Authored-By: Mira Rehfeld <noreply@anthropic.com>
This commit is contained in:
+52
@@ -532,6 +532,58 @@ public class SandboxSteps {
|
||||
assertThat(actual).as("submissionsByProvider." + provider).isEqualTo(count);
|
||||
}
|
||||
|
||||
// ── When — admin operations ───────────────────────────────────────────────
|
||||
|
||||
@When("the admin requests the sandbox list")
|
||||
public void adminRequestsSandboxList() {
|
||||
lastResponse = given()
|
||||
.header("X-Admin-Key", ADMIN_API_KEY)
|
||||
.accept("application/json")
|
||||
.when()
|
||||
.get("/admin/sandboxes")
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
@When("the admin requests the sandbox list without a key")
|
||||
public void adminRequestsSandboxListWithoutKey() {
|
||||
lastResponse = given()
|
||||
.accept("application/json")
|
||||
.when()
|
||||
.get("/admin/sandboxes")
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
@When("the admin deletes sandbox {string}")
|
||||
public void adminDeletesSandbox(String sandboxName) {
|
||||
lastResponse = given()
|
||||
.header("X-Admin-Key", ADMIN_API_KEY)
|
||||
.when()
|
||||
.delete("/admin/sandboxes/" + resolveUuid(sandboxName))
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
// ── Then — admin list assertions ──────────────────────────────────────────
|
||||
|
||||
@Then("the admin list total is at least {int}")
|
||||
public void adminListTotalAtLeast(int min) {
|
||||
int total = lastResponse.jsonPath().getInt("total");
|
||||
assertThat(total).as("admin list total").isGreaterThanOrEqualTo(min);
|
||||
}
|
||||
|
||||
@Then("every entry in the admin list has a non-null createdAt")
|
||||
public void everyEntryHasCreatedAt() {
|
||||
List<Object> values = lastResponse.jsonPath().getList("sandboxes.createdAt");
|
||||
assertThat(values).as("sandboxes.createdAt list").isNotEmpty();
|
||||
assertThat(values).as("no null createdAt entries").doesNotContainNull();
|
||||
}
|
||||
|
||||
@Then("every entry in the admin list has a non-null expiresAt")
|
||||
public void everyEntryHasExpiresAt() {
|
||||
List<String> values = lastResponse.jsonPath().getList("sandboxes.expiresAt");
|
||||
assertThat(values).as("sandboxes.expiresAt list").isNotEmpty();
|
||||
assertThat(values).as("no null expiresAt entries").doesNotContainNull();
|
||||
}
|
||||
|
||||
// ── Then — error assertions ───────────────────────────────────────────────
|
||||
|
||||
@Then("the sandbox error contains {string}")
|
||||
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
Feature: Admin sandbox list endpoint
|
||||
|
||||
Scenario: Admin retrieves the sandbox list and each entry includes a createdAt timestamp
|
||||
Given a sandbox named "admin-list-test" exists
|
||||
When the admin requests the sandbox list
|
||||
Then the response code is 200
|
||||
And the admin list total is at least 1
|
||||
And every entry in the admin list has a non-null createdAt
|
||||
And every entry in the admin list has a non-null expiresAt
|
||||
|
||||
Scenario: Admin sandbox list request without credentials is rejected
|
||||
When the admin requests the sandbox list without a key
|
||||
Then the response code is 401
|
||||
|
||||
Scenario: Admin can delete a sandbox and it no longer appears
|
||||
Given a sandbox named "admin-delete-target" exists
|
||||
When the admin deletes sandbox "admin-delete-target"
|
||||
Then the response code is 204
|
||||
+34
-14
@@ -20,6 +20,7 @@ import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.time.Instant;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.*;
|
||||
import java.util.HexFormat;
|
||||
@@ -479,20 +480,24 @@ public class SandboxService {
|
||||
Instant now = Instant.now();
|
||||
|
||||
List<AdminSandboxSummary> rows = ((List<Object[]>) listQ.getResultList()).stream()
|
||||
.map(r -> new AdminSandboxSummary(
|
||||
r[0].toString(),
|
||||
(String) r[1],
|
||||
(String) r[2],
|
||||
((Number) r[3]).intValue(),
|
||||
r[4] != null ? ((Number) r[4]).intValue() : null,
|
||||
r[5] instanceof java.sql.Timestamp t ? t.toInstant() : null,
|
||||
r[6] instanceof java.sql.Timestamp t ? t.toInstant() : null,
|
||||
r[6] instanceof java.sql.Timestamp t && now.isAfter(t.toInstant()),
|
||||
(String) r[7],
|
||||
r[8] != null ? ((Number) r[8]).doubleValue() : null,
|
||||
r[9] != null ? ((Number) r[9]).doubleValue() : null,
|
||||
((Number) r[10]).longValue(),
|
||||
((Number) r[11]).longValue()))
|
||||
.map(r -> {
|
||||
Instant createdAt = toInstant(r[5]);
|
||||
Instant expiresAt = toInstant(r[6]);
|
||||
return new AdminSandboxSummary(
|
||||
r[0].toString(),
|
||||
(String) r[1],
|
||||
(String) r[2],
|
||||
((Number) r[3]).intValue(),
|
||||
r[4] != null ? ((Number) r[4]).intValue() : null,
|
||||
createdAt,
|
||||
expiresAt,
|
||||
expiresAt != null && now.isAfter(expiresAt),
|
||||
(String) r[7],
|
||||
r[8] != null ? ((Number) r[8]).doubleValue() : null,
|
||||
r[9] != null ? ((Number) r[9]).doubleValue() : null,
|
||||
((Number) r[10]).longValue(),
|
||||
((Number) r[11]).longValue());
|
||||
})
|
||||
.toList();
|
||||
|
||||
return new AdminSandboxListResponse(total, page, size, rows);
|
||||
@@ -674,4 +679,19 @@ public class SandboxService {
|
||||
}
|
||||
|
||||
public record SandboxCreationResult(SandboxEntity sandbox, String plainKey, String plainMaintenanceKey) {}
|
||||
|
||||
/**
|
||||
* Converts a native-query timestamp column to Instant.
|
||||
* Hibernate 6 returns LocalDateTime for timestamptz native queries; older drivers return Timestamp.
|
||||
* LocalDateTime from a timestamptz column represents UTC.
|
||||
*/
|
||||
private static Instant toInstant(Object col) {
|
||||
if (col instanceof java.sql.Timestamp t) return t.toInstant();
|
||||
if (col instanceof OffsetDateTime odt) return odt.toInstant();
|
||||
if (col instanceof java.time.ZonedDateTime z) return z.toInstant();
|
||||
if (col instanceof java.time.LocalDateTime ldt) return ldt.toInstant(java.time.ZoneOffset.UTC);
|
||||
if (col instanceof java.util.Date d) return d.toInstant();
|
||||
if (col instanceof java.time.temporal.Temporal t) { try { return Instant.from(t); } catch (Exception ignored) {} }
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user