Add per-user item ownership and isolation
Items are now private to their owner. Add items.user_id (migrated in place, existing items backfilled to the first admin) and scope every item, results, and dashboard view to the signed-in user via owner-scoped queries plus an ownedItem 404 guard. Admins get strict isolation too; elevation stays limited to settings and user management. Alert routing follows ownership: deal emails go only to the item owner and the weekly digest is built per-recipient from their own items. The scheduler still polls every active item; the Apify/eBay budget stays a shared pool visible to all. Add TestItemsArePrivatePerUser and seed owners in db tests.
This commit is contained in:
@@ -23,15 +23,27 @@ func newTestStore(t *testing.T) *Store {
|
||||
return NewStore(conn, key)
|
||||
}
|
||||
|
||||
// seedUser creates an owner so item rows satisfy the items.user_id foreign key.
|
||||
func seedUser(t *testing.T, s *Store) int64 {
|
||||
t.Helper()
|
||||
id, err := s.CreateUser(context.Background(), "tester", "!x", models.RoleAdmin)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
func TestDedupByItemAndURL(t *testing.T) {
|
||||
if os.Getenv("CI_SKIP_SQLITE") != "" {
|
||||
t.Skip()
|
||||
}
|
||||
s := newTestStore(t)
|
||||
ctx := context.Background()
|
||||
uid := seedUser(t, s)
|
||||
|
||||
id, err := s.CreateItem(ctx, &models.Item{
|
||||
Name: "TwinBee", NtfyTopic: "veola", Active: true,
|
||||
UserID: uid,
|
||||
Name: "TwinBee", NtfyTopic: "veola", Active: true,
|
||||
PollIntervalMinutes: 60, NtfyPriority: "default",
|
||||
})
|
||||
if err != nil {
|
||||
@@ -62,7 +74,7 @@ func TestDedupByItemAndURL(t *testing.T) {
|
||||
}
|
||||
|
||||
// Different item, same URL should not collide.
|
||||
id2, _ := s.CreateItem(ctx, &models.Item{Name: "Other", NtfyTopic: "veola", PollIntervalMinutes: 60, NtfyPriority: "default"})
|
||||
id2, _ := s.CreateItem(ctx, &models.Item{UserID: uid, Name: "Other", NtfyTopic: "veola", PollIntervalMinutes: 60, NtfyPriority: "default"})
|
||||
other, _ := s.ResultExists(ctx, id2, "https://example.com/listing/1")
|
||||
if other {
|
||||
t.Error("dedup should be scoped to item_id")
|
||||
@@ -81,10 +93,12 @@ func TestDashboardMoneySavedWindow(t *testing.T) {
|
||||
}
|
||||
s := newTestStore(t)
|
||||
ctx := context.Background()
|
||||
uid := seedUser(t, s)
|
||||
|
||||
bp := 100.0
|
||||
id, err := s.CreateItem(ctx, &models.Item{
|
||||
Name: "Windowed", NtfyTopic: "veola", Active: true,
|
||||
UserID: uid,
|
||||
Name: "Windowed", NtfyTopic: "veola", Active: true,
|
||||
PollIntervalMinutes: 60, NtfyPriority: "default",
|
||||
BestPrice: &bp,
|
||||
})
|
||||
@@ -123,7 +137,9 @@ func TestDashboardMoneySavedWindow(t *testing.T) {
|
||||
func TestCJKRoundTrip(t *testing.T) {
|
||||
s := newTestStore(t)
|
||||
ctx := context.Background()
|
||||
uid := seedUser(t, s)
|
||||
id, err := s.CreateItem(ctx, &models.Item{
|
||||
UserID: uid,
|
||||
Name: "ツインビー",
|
||||
SearchQuery: "ツインビー グラディウス パロディウス",
|
||||
NtfyTopic: "veola",
|
||||
|
||||
Reference in New Issue
Block a user