store/memo_attachment_test.go
Rama main · réplica de lectura del repositorio original.
package store_test
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"github.com/usememos/memos/store"
)
func TestMemoMutationRejectsInvalidInputBeforeWriting(t *testing.T) {
creation := func() *store.MemoMutation {
return &store.MemoMutation{MemoCreate: &store.Memo{UID: "new-memo", CreatorID: 1, Visibility: store.Private}}
}
references := func(relations ...*store.MemoRelation) *store.MemoMutation {
return &store.MemoMutation{MemoID: 1, MemoCreatorID: 1, ReplaceReferenceRelations: true, ReferenceRelations: relations}
}
tests := []struct {
name string
mutation func() *store.MemoMutation
message string
}{
{"nil mutation", func() *store.MemoMutation { return nil }, "memo mutation is required"},
{"create and update", func() *store.MemoMutation {
m := creation()
m.MemoUpdate = &store.UpdateMemo{ID: 1}
return m
}, "cannot update or remove existing state"},
{"create with update policy", func() *store.MemoMutation {
m := creation()
m.Policy = &store.MemoWritePolicy{ActorUserID: 1}
return m
}, "cannot update or remove existing state"},
{"create and delete attachment", func() *store.MemoMutation {
m := creation()
m.RemovedAttachmentIDs = []int32{1}
return m
}, "cannot update or remove existing state"},
{"missing creator", func() *store.MemoMutation {
m := creation()
m.MemoCreate.CreatorID = 0
return m
}, "requires creator"},
{"invalid creation UID", func() *store.MemoMutation {
m := creation()
m.MemoCreate.UID = "invalid uid"
return m
}, "invalid uid"},
{"invalid creation visibility", func() *store.MemoMutation {
m := creation()
m.MemoCreate.Visibility = "INVALID"
return m
}, "invalid visibility"},
{"space audience without placement", func() *store.MemoMutation {
m := creation()
m.MemoCreate.Visibility = store.SpaceAudience
return m
}, "SPACE visibility requires a space"},
{"invalid comment context", func() *store.MemoMutation {
m := creation()
m.CommentContextMemoID = new(int32(0))
return m
}, "valid context memo"},
{"missing policy actor", func() *store.MemoMutation {
return &store.MemoMutation{Policy: &store.MemoWritePolicy{}}
}, "write policy requires actor"},
{"missing fallback policy actor", func() *store.MemoMutation {
return &store.MemoMutation{MemoUpdate: &store.UpdateMemo{ID: 1, Policy: &store.MemoWritePolicy{}}}
}, "write policy requires actor"},
{"conflicting policy actions", func() *store.MemoMutation {
return &store.MemoMutation{Policy: &store.MemoWritePolicy{ActorUserID: 1, LifecycleOnly: true, CreatingShare: true}}
}, "conflicting actions"},
{"conflicting fallback policy actions", func() *store.MemoMutation {
return &store.MemoMutation{MemoUpdate: &store.UpdateMemo{ID: 1, Policy: &store.MemoWritePolicy{
ActorUserID: 1, LifecycleOnly: true, CreatingShare: true,
}}}
}, "conflicting actions"},
{"zero removed ID", func() *store.MemoMutation {
return &store.MemoMutation{RemovedAttachmentIDs: []int32{0}}
}, "IDs must be positive"},
{"nil attachment binding", func() *store.MemoMutation {
return &store.MemoMutation{Bindings: []*store.MemoAttachmentBinding{nil}}
}, "attachment binding is required"},
{"negative removed ID", func() *store.MemoMutation {
return &store.MemoMutation{RemovedAttachmentIDs: []int32{-1}}
}, "IDs must be positive"},
{"nonadjacent duplicate removed IDs", func() *store.MemoMutation {
return &store.MemoMutation{RemovedAttachmentIDs: []int32{2, 1, 2}}
}, "duplicate removed attachment ID"},
{"bind and remove same attachment", func() *store.MemoMutation {
return &store.MemoMutation{RemovedAttachmentIDs: []int32{1}, Bindings: []*store.MemoAttachmentBinding{{ID: 1}}}
}, "both bound and removed"},
{"require and remove same attachment", func() *store.MemoMutation {
return &store.MemoMutation{RemovedAttachmentIDs: []int32{1}, RequiredAttachmentIDs: []int32{1}}
}, "both required and removed"},
{"reference without actor", func() *store.MemoMutation {
m := references()
m.MemoCreatorID = 0
return m
}, "requires an actor"},
{"nil reference", func() *store.MemoMutation { return references(nil) }, "only REFERENCE"},
{"invalid reference target", func() *store.MemoMutation {
return references(&store.MemoRelation{MemoID: 1, Type: store.MemoRelationReference})
}, "only REFERENCE"},
{"comment relation replacement", func() *store.MemoMutation {
return references(&store.MemoRelation{MemoID: 1, RelatedMemoID: 2, Type: store.MemoRelationComment})
}, "only REFERENCE"},
{"wrong reference source", func() *store.MemoMutation {
return references(&store.MemoRelation{MemoID: 3, RelatedMemoID: 2, Type: store.MemoRelationReference})
}, "source does not match"},
{"self reference", func() *store.MemoMutation {
return references(&store.MemoRelation{MemoID: 1, RelatedMemoID: 1, Type: store.MemoRelationReference})
}, "reflexive memo relations"},
{"duplicate references", func() *store.MemoMutation {
return references(
&store.MemoRelation{MemoID: 1, RelatedMemoID: 2, Type: store.MemoRelationReference},
&store.MemoRelation{MemoID: 1, RelatedMemoID: 2, Type: store.MemoRelationReference},
)
}, "duplicate memo reference"},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// No driver is installed: invalid input must fail before database access.
s := &store.Store{}
require.ErrorContains(t, s.ApplyMemoMutation(context.Background(), tc.mutation()), tc.message)
})
}
}
func TestMemoLifecycleMutationRejectsDependentWrites(t *testing.T) {
for _, tc := range []struct {
name string
change func(*store.MemoMutation)
}{
{"bindings", func(m *store.MemoMutation) { m.Bindings = []*store.MemoAttachmentBinding{{ID: 1}} }},
{"removals", func(m *store.MemoMutation) { m.RemovedAttachmentIDs = []int32{1} }},
{"required attachments", func(m *store.MemoMutation) { m.RequiredAttachmentIDs = []int32{1} }},
{"reference replacement", func(m *store.MemoMutation) { m.ReplaceReferenceRelations = true }},
} {
t.Run(tc.name, func(t *testing.T) {
for _, source := range []string{"mutation policy", "update policy fallback"} {
t.Run(source, func(t *testing.T) {
mutation := &store.MemoMutation{
MemoID: 1, MemoCreatorID: 1,
MemoUpdate: &store.UpdateMemo{ID: 1, ClearSpace: true},
}
policy := &store.MemoWritePolicy{ActorUserID: 1, LifecycleOnly: true}
if source == "mutation policy" {
mutation.Policy = policy
} else {
mutation.MemoUpdate.Policy = policy
}
tc.change(mutation)
require.ErrorIs(t, (&store.Store{}).ApplyMemoMutation(context.Background(), mutation), store.ErrMemoSpaceMembershipRequired)
})
}
})
}
}