mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 03:52:26 +08:00
fix: validate announcement and proxy date ranges
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user