Files
ttstd-infinite-canvas/backend/internal/app/appearance_updates_test.go

181 lines
7.4 KiB
Go

package app
import (
"encoding/json"
"strings"
"testing"
"infinite-canvas/backend/internal/model"
)
func updateTestValue() UpdateAnnouncement {
return UpdateAnnouncement{Enabled: true, CurrentVersion: "v2.0", Releases: []UpdateRelease{{ID: "release-1", Version: "v2.0", Title: "更新公告", Blocks: []UpdateBlock{{ID: "text-1", Type: "text", Text: "新增功能", Layout: "full"}}}}}
}
func TestUpdateAnnouncementSwitchSaveAndPublicProjection(t *testing.T) {
svc, _, _, admin := newAppearanceTestService(t)
setting := defaultAppearanceSetting()
setting.BrandName = "影绘"
setting.Updates = updateTestValue()
saved, err := svc.UpdateAppearance(admin, setting)
if err != nil {
t.Fatal(err)
}
if saved.Public.Updates.CurrentVersion != "v2.0" || len(saved.Public.Updates.Releases) != 1 {
t.Fatalf("public update: %#v", saved.Public.Updates)
}
setting = saved.AppearanceSetting
setting.Updates.Enabled = false
if _, err := svc.UpdateAppearance(admin, setting); err != nil {
t.Fatal(err)
}
public, err := svc.Appearance()
if err != nil {
t.Fatal(err)
}
if public.Updates.Enabled || public.Updates.CurrentVersion != "" || len(public.Updates.Releases) != 0 || public.BrandName != "影绘" {
t.Fatalf("disabled projection: %#v", public)
}
adminValue, err := svc.AdminAppearance(admin)
if err != nil {
t.Fatal(err)
}
if len(adminValue.Updates.Releases) != 1 || adminValue.Updates.CurrentVersion != "v2.0" {
t.Fatal("switch erased custom content")
}
adminValue.Updates.Enabled = true
if _, err := svc.UpdateAppearance(admin, adminValue.AppearanceSetting); err != nil {
t.Fatal(err)
}
if _, err := svc.UpdateAppearance(&model.User{ID: "user", Role: model.UserRoleUser, Status: model.UserStatusActive}, setting); err == nil {
t.Fatal("non-admin changed appearance")
}
}
func TestUpdateAnnouncementValidation(t *testing.T) {
cases := []struct {
name string
mutate func(*UpdateAnnouncement)
}{
{"empty", func(v *UpdateAnnouncement) { v.Releases = nil }},
{"version mismatch", func(v *UpdateAnnouncement) { v.CurrentVersion = "other" }},
{"duplicate id", func(v *UpdateAnnouncement) { v.Releases[0].Blocks[0].ID = "release-1" }},
{"duplicate version", func(v *UpdateAnnouncement) {
v.Releases = append(v.Releases, UpdateRelease{ID: "release-2", Version: "v2.0"})
}},
{"blank text", func(v *UpdateAnnouncement) { v.Releases[0].Blocks[0].Text = " " }},
{"unknown type", func(v *UpdateAnnouncement) { v.Releases[0].Blocks[0].Type = "html" }},
{"unknown layout", func(v *UpdateAnnouncement) { v.Releases[0].Blocks[0].Layout = "script" }},
{"missing media", func(v *UpdateAnnouncement) { v.Releases[0].Blocks[0].Type = "video" }},
{"invalid date", func(v *UpdateAnnouncement) { v.Releases[0].Date = "2026-02-30" }},
{"control characters", func(v *UpdateAnnouncement) { v.CurrentVersion = "v\x00bad" }},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
v := updateTestValue()
tc.mutate(&v)
if _, err := normalizeUpdateAnnouncement(v); err == nil {
t.Fatal("invalid input accepted")
}
})
}
value := updateTestValue()
value.Releases[0].Blocks[0].MediaURL = "javascript:alert(1)"
clean, err := normalizeUpdateAnnouncement(value)
if err != nil || clean.Releases[0].Blocks[0].MediaURL != "" {
t.Fatal("client media URL retained")
}
}
func TestUpdateAnnouncementMediaOwnershipPublicationAndDeletionProtection(t *testing.T) {
svc, db, dir, admin := newAppearanceTestService(t)
resources := []model.Resource{
{ID: "update-image", UserID: admin.ID, Kind: "image", Status: model.ResourceStatusReady, Provider: "local", ObjectKey: "test/image.png", MimeType: "image/png", Size: 12},
{ID: "other-image", UserID: "other-admin", Kind: "image", Status: model.ResourceStatusReady, Provider: "local", ObjectKey: "test/other.png", MimeType: "image/png", Size: 12},
{ID: "update-video", UserID: admin.ID, Kind: "video", Status: model.ResourceStatusReady, Provider: "local", ObjectKey: "test/video.mp4", MimeType: "video/mp4", Size: 12},
}
if err := db.Create(&resources).Error; err != nil {
t.Fatal(err)
}
writeAppearanceResourceFixtures(t, dir, resources)
setting := defaultAppearanceSetting()
setting.Updates = updateTestValue()
setting.Updates.Releases[0].Blocks = append(setting.Updates.Releases[0].Blocks, UpdateBlock{ID: "image-1", Type: "image", ResourceID: "other-image", Layout: "half"})
if _, err := svc.UpdateAppearance(admin, setting); err == nil {
t.Fatal("other admin's unpublished resource accepted")
}
setting.Updates.Releases[0].Blocks[1].ResourceID = "update-image"
setting.Updates.Releases[0].Blocks = append(setting.Updates.Releases[0].Blocks, UpdateBlock{ID: "video-1", Type: "video", ResourceID: "update-video", Layout: "half"})
if _, err := svc.PrepareUpdateAnnouncementDelivery(nil, "update-image", ResourceAccessOptions{}, ""); err == nil {
t.Fatal("unpublished media was public")
}
if _, err := svc.PrepareUpdateAnnouncementDelivery(admin, "other-image", ResourceAccessOptions{}, ""); err == nil {
t.Fatal("other admin draft was previewable")
}
saved, err := svc.UpdateAppearance(admin, setting)
if err != nil {
t.Fatal(err)
}
block := saved.Public.Updates.Releases[0].Blocks[1]
if !strings.Contains(block.MediaURL, "/api/public/appearance/updates/update-image?v=") {
t.Fatalf("media URL: %s", block.MediaURL)
}
if saved.Updates.Releases[0].Blocks[1].MediaURL != "" {
t.Fatal("public projection modified private setting")
}
refs := svc.appearanceResourceReferences([]string{"update-image", "update-video"})
if len(refs["update-image"]) == 0 || len(refs["update-video"]) == 0 {
t.Fatal("announcement resources not protected")
}
for _, id := range []string{"update-image", "update-video"} {
delivery, err := svc.PrepareUpdateAnnouncementDelivery(nil, id, ResourceAccessOptions{}, "")
if err != nil {
t.Fatal(err)
}
if delivery.Stream != nil {
delivery.Stream.Body.Close()
}
}
ranged, err := svc.PrepareUpdateAnnouncementDelivery(nil, "update-video", ResourceAccessOptions{}, "bytes=0-3")
if err != nil {
t.Fatal(err)
}
if ranged.Stream == nil || ranged.Stream.AcceptRanges != "bytes" {
t.Fatalf("video Range stream: %#v", ranged)
}
ranged.Stream.Body.Close()
setting = saved.AppearanceSetting
setting.Updates.Enabled = false
if _, err := svc.UpdateAppearance(admin, setting); err != nil {
t.Fatal(err)
}
if _, err := svc.PrepareUpdateAnnouncementDelivery(nil, "update-image", ResourceAccessOptions{}, ""); err == nil {
t.Fatal("disabled media remains public")
}
if len(svc.appearanceResourceReferences([]string{"update-image"})["update-image"]) == 0 {
t.Fatal("disabled draft lost deletion protection")
}
private, _ := json.Marshal(setting.Updates)
public := publicUpdateAnnouncement(setting.Updates, "rev")
projected, _ := json.Marshal(public)
if strings.Contains(string(projected), "新增功能") || !strings.Contains(string(private), "新增功能") {
t.Fatal("disabled draft exposure")
}
}
func TestUpdateAnnouncementUploadTypesAndSizes(t *testing.T) {
for _, slot := range []string{AppearanceAssetUpdateImage, AppearanceAssetUpdateVideo} {
max, err := AppearanceAssetMaxBytes(slot)
if err != nil {
t.Fatal(err)
}
if slot == AppearanceAssetUpdateImage && max != 10<<20 || slot == AppearanceAssetUpdateVideo && max != 256<<20 {
t.Fatalf("limit %s %d", slot, max)
}
fake := multipartFileHeader(t, "fake.png", "image/png", []byte("not media"))
if _, err := validateAppearanceUpload(slot, fake); err == nil {
t.Fatal("fake media accepted")
}
}
}