fix: validate announcement and proxy date ranges

This commit is contained in:
Rain
2026-09-08 13:37:52 +08:00
parent 772a0382f0
commit efc6e4a816
7 changed files with 129 additions and 4 deletions
+6
View File
@@ -56,6 +56,9 @@ func (s *adminServiceImpl) GetProxiesByIDs(ctx context.Context, ids []int64) ([]
}
func (s *adminServiceImpl) CreateProxy(ctx context.Context, input *CreateProxyInput) (*Proxy, error) {
if !isJSONTimeInRange(input.ExpiresAt) {
return nil, infraerrors.BadRequest("PROXY_EXPIRY_INVALID", "proxy expiry year must be between 0 and 9999")
}
// 规范化 fallback_mode
mode := input.FallbackMode
if mode == "" {
@@ -91,6 +94,9 @@ func (s *adminServiceImpl) CreateProxy(ctx context.Context, input *CreateProxyIn
}
func (s *adminServiceImpl) UpdateProxy(ctx context.Context, id int64, input *UpdateProxyInput) (*Proxy, error) {
if !isJSONTimeInRange(input.ExpiresAt) {
return nil, infraerrors.BadRequest("PROXY_EXPIRY_INVALID", "proxy expiry year must be between 0 and 9999")
}
// 校验:backup_proxy_id 不能是自身
if input.BackupProxyID != nil && *input.BackupProxyID == id {
return nil, infraerrors.BadRequest("PROXY_BACKUP_SELF", "backup proxy cannot be itself")
@@ -0,0 +1,41 @@
//go:build unit
package service
import (
"context"
"encoding/json"
"testing"
"time"
"github.com/stretchr/testify/require"
)
func TestAdminProxyRejectsOutOfRangeExpiry(t *testing.T) {
for _, year := range []int{-1, 10000} {
expiry := time.Date(year, 1, 1, 0, 0, 0, 0, time.UTC)
t.Run(expiry.String(), func(t *testing.T) {
// A nil repository ensures rejection happens before any persistence or probe.
svc := &adminServiceImpl{}
_, err := svc.CreateProxy(context.Background(), &CreateProxyInput{ExpiresAt: &expiry})
require.Error(t, err)
require.Contains(t, err.Error(), "proxy expiry year")
_, err = svc.UpdateProxy(context.Background(), 1, &UpdateProxyInput{ExpiresAt: &expiry})
require.Error(t, err)
require.Contains(t, err.Error(), "proxy expiry year")
})
}
}
func TestAdminProxyAcceptsJSONExpiryBoundaries(t *testing.T) {
for _, year := range []int{0, 9999} {
expiry := time.Date(year, 12, 31, 23, 59, 59, 0, time.UTC)
repo := &updatingProxyRepoStub{proxyRepoStub: &proxyRepoStub{}, proxy: &Proxy{ID: 9}}
svc := &adminServiceImpl{proxyRepo: repo}
got, err := svc.UpdateProxy(context.Background(), 9, &UpdateProxyInput{ExpiresAt: &expiry})
require.NoError(t, err)
_, err = json.Marshal(got)
require.NoError(t, err)
require.Equal(t, &expiry, repo.proxy.ExpiresAt)
}
}
@@ -73,6 +73,10 @@ func (s *AnnouncementService) Create(ctx context.Context, input *CreateAnnouncem
return nil, ErrAnnouncementNilInput
}
if !isJSONTimeInRange(input.StartsAt) || !isJSONTimeInRange(input.EndsAt) {
return nil, ErrAnnouncementInvalidSchedule
}
title := strings.TrimSpace(input.Title)
content := strings.TrimSpace(input.Content)
if title == "" || len(title) > 200 {
@@ -134,6 +138,11 @@ func (s *AnnouncementService) Update(ctx context.Context, id int64, input *Updat
return nil, ErrAnnouncementNilInput
}
if (input.StartsAt != nil && !isJSONTimeInRange(*input.StartsAt)) ||
(input.EndsAt != nil && !isJSONTimeInRange(*input.EndsAt)) {
return nil, ErrAnnouncementInvalidSchedule
}
a, err := s.announcementRepo.GetByID(ctx, id)
if err != nil {
return nil, err
+9
View File
@@ -0,0 +1,9 @@
package service
import "time"
// isJSONTimeInRange rejects dates that PostgreSQL can store but time.Time cannot
// serialize in API responses. Nil represents an unset schedule or expiry.
func isJSONTimeInRange(value *time.Time) bool {
return value == nil || (value.Year() >= 0 && value.Year() <= 9999)
}
@@ -0,0 +1,60 @@
package service
import (
"context"
"encoding/json"
"testing"
"time"
"github.com/stretchr/testify/require"
)
func TestAnnouncementServiceRejectsOutOfRangeDates(t *testing.T) {
for _, year := range []int{-1, 10000} {
date := time.Date(year, 1, 1, 0, 0, 0, 0, time.UTC)
for _, field := range []string{"start", "end"} {
t.Run(date.String()+field, func(t *testing.T) {
repo := &announcementRepoStub{}
svc := NewAnnouncementService(repo, nil, nil, nil)
input := &CreateAnnouncementInput{Title: "test", Content: "test"}
if field == "start" {
input.StartsAt = &date
} else {
input.EndsAt = &date
}
_, err := svc.Create(context.Background(), input)
require.ErrorIs(t, err, ErrAnnouncementInvalidSchedule)
require.Nil(t, repo.item, "invalid dates must never reach persistence")
repo.item = &Announcement{ID: 1, Title: "test", Content: "test"}
update := &UpdateAnnouncementInput{}
ptr := &date
if field == "start" {
update.StartsAt = &ptr
} else {
update.EndsAt = &ptr
}
_, err = svc.Update(context.Background(), 1, update)
require.ErrorIs(t, err, ErrAnnouncementInvalidSchedule)
require.Nil(t, repo.item.StartsAt)
require.Nil(t, repo.item.EndsAt)
})
}
}
}
func TestAnnouncementServiceAcceptsJSONDateBoundariesAndClear(t *testing.T) {
start := time.Date(0, 1, 1, 0, 0, 0, 0, time.UTC)
end := time.Date(9999, 12, 31, 23, 59, 59, 0, time.UTC)
repo := &announcementRepoStub{}
svc := NewAnnouncementService(repo, nil, nil, nil)
a, err := svc.Create(context.Background(), &CreateAnnouncementInput{Title: "test", Content: "test", StartsAt: &start, EndsAt: &end})
require.NoError(t, err)
_, err = json.Marshal(a)
require.NoError(t, err)
var clear *time.Time
a, err = svc.Update(context.Background(), 1, &UpdateAnnouncementInput{StartsAt: &clear, EndsAt: &clear})
require.NoError(t, err)
require.Nil(t, a.StartsAt)
require.Nil(t, a.EndsAt)
}
@@ -201,12 +201,12 @@
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div>
<label class="input-label">{{ t('admin.announcements.form.startsAt') }}</label>
<input v-model="form.starts_at_str" type="datetime-local" class="input" />
<input v-model="form.starts_at_str" type="datetime-local" max="9999-12-31T23:59" class="input" />
<p class="input-hint">{{ t('admin.announcements.form.startsAtHint') }}</p>
</div>
<div>
<label class="input-label">{{ t('admin.announcements.form.endsAt') }}</label>
<input v-model="form.ends_at_str" type="datetime-local" class="input" />
<input v-model="form.ends_at_str" type="datetime-local" max="9999-12-31T23:59" class="input" />
<p class="input-hint">{{ t('admin.announcements.form.endsAtHint') }}</p>
</div>
</div>
+2 -2
View File
@@ -513,7 +513,7 @@
class="input mb-2"
:placeholder="t('admin.proxies.expiryDaysPlaceholder')"
/>
<input v-model="createForm.expires_at" type="date" class="input" />
<input v-model="createForm.expires_at" type="date" max="9999-12-31" class="input" />
</div>
<div>
<label class="input-label">{{ t('admin.proxies.fallbackMode') }}</label>
@@ -746,7 +746,7 @@
class="input mb-2"
:placeholder="t('admin.proxies.expiryDaysPlaceholder')"
/>
<input v-model="editForm.expires_at" type="date" class="input" />
<input v-model="editForm.expires_at" type="date" max="9999-12-31" class="input" />
</div>
<div>
<label class="input-label">{{ t('admin.proxies.fallbackMode') }}</label>