fix: harden platform audit coverage

This commit is contained in:
2026-07-27 11:13:55 +08:00
parent 48fc80b003
commit 0ba4136e1a
7 changed files with 125 additions and 13 deletions

View File

@@ -276,6 +276,26 @@ func TestResourceResponseDoesNotExposeAutoIncrementRelationIDs(t *testing.T) {
}
}
func TestListSafetyEventDisposalsReturnsOnlyTheRequestedEventHistory(t *testing.T) {
_, mock := setupPlatformRoleDatabase(t)
mock.ExpectQuery(regexp.QuoteMeta(`SELECT count(*) FROM "saf_event_disposal" WHERE saf_event_identity = $1`)).
WithArgs("event-a").
WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(1))
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "saf_event_disposal" WHERE saf_event_identity = $1 ORDER BY created_at asc LIMIT $2`)).
WithArgs("event-a", 20).
WillReturnRows(sqlmock.NewRows([]string{"id", "identity", "created_at", "updated_at", "status", "version", "saf_event_identity", "action", "reason", "operator_identity"}).
AddRow(uint64(9), "disposal-a", nil, nil, "enabled", 1, "event-a", "close", "resolved", "operator-a"))
ctx, recorder := updateContext(http.MethodGet, "/safety/saf_event/event-a/disposals", "event-a", nil)
ListSafetyEventDisposals(ctx)
assertResponseCode(t, recorder, 0)
if !strings.Contains(recorder.Body.String(), `"saf_event_identity":"event-a"`) || strings.Contains(recorder.Body.String(), `"id":`) {
t.Fatalf("disposal history did not keep the event identity-only shape: %s", recorder.Body.String())
}
assertMockExpectations(t, mock)
}
func TestListGasAccountProjectsGasBasicIdentityAndNeverReturnsRelationID(t *testing.T) {
_, mock := setupPlatformRoleDatabase(t)
mock.ExpectQuery(regexp.QuoteMeta(`SELECT count(*) FROM "gas_account"`)).

View File

@@ -423,6 +423,22 @@ func stripInternalIDs(value any) any {
// DisposeSafetyEvent atomically updates an event and appends its operator-owned
// action record. Disposal records deliberately have no update or delete route.
func ListSafetyEventDisposals(ctx *gin.Context) {
page, size := pageSize(ctx)
var list []models.SafEventDisposal
query := impl.DBService.Model(&models.SafEventDisposal{}).Where("saf_event_identity = ?", ctx.Param("identity"))
var total int64
if err := query.Count(&total).Error; err != nil {
infra.Response.Error(ctx, err)
return
}
if err := query.Order("created_at asc").Offset((page - 1) * size).Limit(size).Find(&list).Error; err != nil {
infra.Response.Error(ctx, err)
return
}
infra.Response.Success(ctx, gin.H{"total": total, "list": resourceResponse(list)})
}
func DisposeSafetyEvent(ctx *gin.Context) {
claims, err := middleware.ParseAuth(ctx)
if err != nil {

View File

@@ -64,6 +64,7 @@ func registerSafetyRoute(group *gin.RouterGroup) {
registerRestrictedWritableResource(group, "/safety/saf_rule", &models.SafRule{}, []string{"rule_code", "version_no", "threshold", "action", "gray_scope"})
registerRestrictedWritableResource(group, "/safety/saf_event", &models.SafEvent{}, []string{"event_code", "level", "title", "smart_cylinder_valve_identity", "sla_at"})
registerRestrictedWritableResource(group, "/safety/saf_inspection", &models.SafInspection{}, []string{"result", "evidence_uri"}, requiredRelation("user_account_identity", "user_account_id", &models.UserAccount{}), requiredRelation("staff_account_identity", "staff_account_id", &models.StaffAccount{}))
group.GET("/safety/saf_event/:identity/disposals", platform.ListSafetyEventDisposals)
group.POST("/safety/saf_event/:identity/disposals", platform.DisposeSafetyEvent)
}

View File

@@ -24,8 +24,11 @@ func TestEveryContractHasRegisteredRoute(t *testing.T) {
case platform.ReadOnly:
assertRouteMethods(t, routes, path, http.MethodGet)
assertRouteMethods(t, routes, path+"/:identity", http.MethodGet)
assertNoRouteMethods(t, routes, path, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete)
assertNoRouteMethods(t, routes, path+"/:identity", http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete)
case platform.AppendOnly:
assertRouteMethods(t, routes, path, http.MethodPost)
assertRouteMethods(t, routes, path, http.MethodGet, http.MethodPost)
assertNoRouteMethods(t, routes, path, http.MethodPut, http.MethodPatch, http.MethodDelete)
default:
assertRouteMethods(t, routes, path, http.MethodGet, http.MethodPost)
assertRouteMethods(t, routes, path+"/:identity", http.MethodGet, http.MethodPut, http.MethodDelete)
@@ -169,3 +172,12 @@ func assertRouteMethods(t *testing.T, routes map[string]map[string]bool, path st
}
}
}
func assertNoRouteMethods(t *testing.T, routes map[string]map[string]bool, path string, methods ...string) {
t.Helper()
for _, method := range methods {
if routes[path][method] {
t.Errorf("route %s %s must not be registered", method, path)
}
}
}