mirror of
https://github.com/go-vikunja/vikunja.git
synced 2026-08-22 12:12:18 -05:00
The capability interface carrying a notification's project was optional, and ProjectIDOf defaulted a missing implementation to 0 — which means account-scoped, which means always visible. So a new project-scoped type whose author forgot the method would have leaked task titles, project names and comment bodies to users with no access to the project, with no compile error and no test failure. Register now takes a factory returning PersistedNotification, which requires the method. Registering is what makes a notification persist, so a stored row that cannot be permission-checked no longer compiles. The three account-scoped types say so by returning 0 explicitly instead of by omission.
274 lines
8.3 KiB
Go
274 lines
8.3 KiB
Go
// Vikunja is a to-do list application to facilitate your life.
|
|
// Copyright 2018-present Vikunja and contributors. All rights reserved.
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
package notifications
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"code.vikunja.io/api/pkg/db"
|
|
"code.vikunja.io/api/pkg/mail"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"xorm.io/xorm"
|
|
"xorm.io/xorm/schemas"
|
|
)
|
|
|
|
type testNotification struct {
|
|
Test string
|
|
OtherValue int64
|
|
}
|
|
|
|
// ToMail returns the mail notification for testNotification
|
|
func (n *testNotification) ToMail(_ string) *Mail {
|
|
return NewMail().
|
|
Subject("Test Notification").
|
|
Line(n.Test)
|
|
}
|
|
|
|
// ToDB returns the testNotification notification in a format which can be saved in the db
|
|
func (n *testNotification) ToDB() interface{} {
|
|
data := make(map[string]interface{}, 2)
|
|
data["test"] = n.Test
|
|
data["other_value"] = n.OtherValue
|
|
return data
|
|
}
|
|
|
|
// Name returns the name of the notification
|
|
func (n *testNotification) Name() string {
|
|
return "test.notification"
|
|
}
|
|
|
|
type testNotifiable struct {
|
|
ShouldSendNotification bool
|
|
Language string
|
|
}
|
|
|
|
// RouteForMail routes a test notification for mail
|
|
func (t *testNotifiable) RouteForMail() (string, error) {
|
|
return "some@email.com", nil
|
|
}
|
|
|
|
// RouteForDB routes a test notification for db
|
|
func (t *testNotifiable) RouteForDB() int64 {
|
|
return 42
|
|
}
|
|
|
|
func (t *testNotifiable) ShouldNotify(_ ...*xorm.Session) (should bool, err error) {
|
|
return t.ShouldSendNotification, nil
|
|
}
|
|
|
|
func (t *testNotifiable) Lang() string {
|
|
return t.Language
|
|
}
|
|
|
|
// titlerNoSubjectNotification implements Titler and intentionally omits a
|
|
// Subject from ToMail so the fallback path is exercised.
|
|
type titlerNoSubjectNotification struct {
|
|
title string
|
|
}
|
|
|
|
func (n *titlerNoSubjectNotification) ToMail(_ string) *Mail {
|
|
return NewMail().Line("body")
|
|
}
|
|
func (n *titlerNoSubjectNotification) ToDB() interface{} { return nil }
|
|
func (n *titlerNoSubjectNotification) Name() string { return "test.titler.no.subject" }
|
|
func (n *titlerNoSubjectNotification) ToTitle(_ string) string { return n.title }
|
|
|
|
// titlerWithExplicitSubjectNotification implements Titler but also sets an
|
|
// explicit subject in ToMail; that explicit subject must win.
|
|
type titlerWithExplicitSubjectNotification struct {
|
|
title string
|
|
subject string
|
|
}
|
|
|
|
func (n *titlerWithExplicitSubjectNotification) ToMail(_ string) *Mail {
|
|
return NewMail().Subject(n.subject).Line("body")
|
|
}
|
|
func (n *titlerWithExplicitSubjectNotification) ToDB() interface{} { return nil }
|
|
func (n *titlerWithExplicitSubjectNotification) Name() string { return "test.titler.with.subject" }
|
|
func (n *titlerWithExplicitSubjectNotification) ToTitle(_ string) string { return n.title }
|
|
|
|
// noTitlerNotification is the control: no Titler, no Subject, fallback must
|
|
// leave subject empty without panicking.
|
|
type noTitlerNotification struct{}
|
|
|
|
func (n *noTitlerNotification) ToMail(_ string) *Mail { return NewMail().Line("body") }
|
|
func (n *noTitlerNotification) ToDB() interface{} { return nil }
|
|
func (n *noTitlerNotification) Name() string { return "test.no.titler" }
|
|
|
|
// titlerRegisteredNotification is used to exercise Register/Lookup.
|
|
type titlerRegisteredNotification struct {
|
|
Title string `json:"title"`
|
|
}
|
|
|
|
func (n *titlerRegisteredNotification) ToMail(_ string) *Mail { return NewMail().Line("body") }
|
|
func (n *titlerRegisteredNotification) ToDB() interface{} { return n }
|
|
func (n *titlerRegisteredNotification) Name() string { return "test.registry.titler" }
|
|
func (n *titlerRegisteredNotification) ToTitle(_ string) string { return n.Title }
|
|
func (n *titlerRegisteredNotification) ProjectID() int64 { return 0 }
|
|
|
|
func TestRegistry(t *testing.T) {
|
|
Register(func() PersistedNotification { return &titlerRegisteredNotification{} })
|
|
|
|
t.Run("known name returns fresh instance", func(t *testing.T) {
|
|
n, ok := Lookup("test.registry.titler")
|
|
require.True(t, ok)
|
|
require.NotNil(t, n)
|
|
_, ok = n.(*titlerRegisteredNotification)
|
|
assert.True(t, ok)
|
|
})
|
|
|
|
t.Run("unknown name returns false", func(t *testing.T) {
|
|
_, ok := Lookup("does.not.exist")
|
|
assert.False(t, ok)
|
|
})
|
|
}
|
|
|
|
func TestNotify(t *testing.T) {
|
|
t.Run("normal", func(t *testing.T) {
|
|
|
|
s := db.NewSession()
|
|
_, err := s.Exec("delete from notifications")
|
|
require.NoError(t, err)
|
|
require.NoError(t, s.Commit())
|
|
s.Close()
|
|
mail.ResetSent()
|
|
|
|
tn := &testNotification{
|
|
Test: "somethingsomething",
|
|
OtherValue: 42,
|
|
}
|
|
tnf := &testNotifiable{
|
|
ShouldSendNotification: true,
|
|
Language: "en",
|
|
}
|
|
|
|
err = Notify(tnf, tn)
|
|
require.NoError(t, err)
|
|
vals := map[string]interface{}{
|
|
"notifiable_id": 42,
|
|
"notification": "'{\"other_value\":42,\"test\":\"somethingsomething\"}'",
|
|
}
|
|
|
|
if db.Type() == schemas.POSTGRES {
|
|
vals["notification::jsonb"] = vals["notification"].(string) + "::jsonb"
|
|
delete(vals, "notification")
|
|
}
|
|
|
|
if db.Type() == schemas.SQLITE {
|
|
vals["CAST(notification AS BLOB)"] = "CAST(" + vals["notification"].(string) + " AS BLOB)"
|
|
delete(vals, "notification")
|
|
}
|
|
|
|
db.AssertExists(t, "notifications", vals, true)
|
|
|
|
sent := mail.LastSent()
|
|
require.NotNil(t, sent)
|
|
assert.Equal(t, "Test Notification", sent.Subject)
|
|
})
|
|
|
|
t.Run("mail queued only after the session commits", func(t *testing.T) {
|
|
mail.ResetSent()
|
|
tn := &testNotification{Test: "deferred", OtherValue: 1}
|
|
tnf := &testNotifiable{ShouldSendNotification: true, Language: "en"}
|
|
|
|
s := db.NewSession()
|
|
defer s.Close()
|
|
|
|
require.NoError(t, Notify(tnf, tn, s))
|
|
assert.Nil(t, mail.LastSent(), "mail must not be queued before the transaction commits")
|
|
|
|
require.NoError(t, s.Commit())
|
|
sent := mail.LastSent()
|
|
require.NotNil(t, sent)
|
|
assert.Equal(t, "Test Notification", sent.Subject)
|
|
})
|
|
|
|
t.Run("no mail when the session rolls back", func(t *testing.T) {
|
|
mail.ResetSent()
|
|
tn := &testNotification{Test: "rolledback", OtherValue: 2}
|
|
tnf := &testNotifiable{ShouldSendNotification: true, Language: "en"}
|
|
|
|
s := db.NewSession()
|
|
defer s.Close()
|
|
|
|
require.NoError(t, Notify(tnf, tn, s))
|
|
require.NoError(t, s.Rollback())
|
|
assert.Nil(t, mail.LastSent())
|
|
})
|
|
t.Run("subject fallback uses ToTitle when ToMail omits Subject", func(t *testing.T) {
|
|
mail.ResetSent()
|
|
tnf := &testNotifiable{ShouldSendNotification: true, Language: "en"}
|
|
|
|
err := notifyMail(tnf, &titlerNoSubjectNotification{title: "From ToTitle"})
|
|
require.NoError(t, err)
|
|
|
|
sent := mail.LastSent()
|
|
require.NotNil(t, sent)
|
|
assert.Equal(t, "From ToTitle", sent.Subject)
|
|
})
|
|
|
|
t.Run("explicit Subject in ToMail wins over ToTitle", func(t *testing.T) {
|
|
mail.ResetSent()
|
|
tnf := &testNotifiable{ShouldSendNotification: true, Language: "en"}
|
|
|
|
err := notifyMail(tnf, &titlerWithExplicitSubjectNotification{title: "From ToTitle", subject: "Explicit"})
|
|
require.NoError(t, err)
|
|
|
|
sent := mail.LastSent()
|
|
require.NotNil(t, sent)
|
|
assert.Equal(t, "Explicit", sent.Subject)
|
|
})
|
|
|
|
t.Run("no fallback when notification does not implement Titler", func(t *testing.T) {
|
|
mail.ResetSent()
|
|
tnf := &testNotifiable{ShouldSendNotification: true, Language: "en"}
|
|
|
|
err := notifyMail(tnf, &noTitlerNotification{})
|
|
require.NoError(t, err)
|
|
|
|
sent := mail.LastSent()
|
|
require.NotNil(t, sent)
|
|
assert.Empty(t, sent.Subject)
|
|
})
|
|
|
|
t.Run("disabled notifiable", func(t *testing.T) {
|
|
|
|
s := db.NewSession()
|
|
_, err := s.Exec("delete from notifications")
|
|
require.NoError(t, err)
|
|
require.NoError(t, s.Commit())
|
|
s.Close()
|
|
|
|
tn := &testNotification{
|
|
Test: "somethingsomething",
|
|
OtherValue: 42,
|
|
}
|
|
tnf := &testNotifiable{
|
|
ShouldSendNotification: false,
|
|
Language: "en",
|
|
}
|
|
|
|
err = Notify(tnf, tn)
|
|
require.NoError(t, err)
|
|
db.AssertMissing(t, "notifications", map[string]interface{}{
|
|
"notifiable_id": 42,
|
|
})
|
|
})
|
|
}
|