Skip to content

Commit ec4e2d5

Browse files
Merge pull request #998 from TowyTowy/fix/plugin-messenger-application-id
fix: create internal application when plugin adds Messenger after init
2 parents e2b6a2e + a9cec79 commit ec4e2d5

4 files changed

Lines changed: 171 additions & 9 deletions

File tree

plugin/manager.go

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,20 @@ func (m *Manager) initializeSingleUserPlugin(userCtx compat.UserContext, p compa
323323
m.instances[pluginConf.ID] = instance
324324

325325
if compat.HasSupport(instance, compat.Messenger) {
326+
if pluginConf.ApplicationID == 0 {
327+
// The Messenger capability was added after this plugin was first
328+
// initialized for the user, so no internal application exists yet.
329+
// Create one now, otherwise messages would be stored with
330+
// application_id = 0 and become orphaned (not shown, not deletable).
331+
app, err := m.createInternalApplication(info, userID)
332+
if err != nil {
333+
return err
334+
}
335+
pluginConf.ApplicationID = app.ID
336+
if err := m.db.UpdatePluginConf(pluginConf); err != nil {
337+
return err
338+
}
339+
}
326340
instance.SetMessageHandler(redirectToChannel{
327341
ApplicationID: pluginConf.ApplicationID,
328342
UserID: pluginConf.UserID,
@@ -399,15 +413,8 @@ func (m *Manager) createPluginConf(instance compat.PluginInstance, info compat.I
399413
pluginConf.Config, _ = yaml.Marshal(instance.DefaultConfig())
400414
}
401415
if compat.HasSupport(instance, compat.Messenger) {
402-
tokenPublic, _ := auth.GenerateApplicationToken()
403-
app := &model.Application{
404-
Token: tokenPublic,
405-
Name: info.String(),
406-
UserID: userID,
407-
Internal: true,
408-
Description: fmt.Sprintf("auto generated application for %s", info.ModulePath),
409-
}
410-
if err := m.db.CreateApplication(app); err != nil {
416+
app, err := m.createInternalApplication(info, userID)
417+
if err != nil {
411418
return nil, err
412419
}
413420
pluginConf.ApplicationID = app.ID
@@ -417,3 +424,20 @@ func (m *Manager) createPluginConf(instance compat.PluginInstance, info compat.I
417424
}
418425
return pluginConf, nil
419426
}
427+
428+
// createInternalApplication creates the auto generated internal application a
429+
// Messenger plugin uses to publish its messages.
430+
func (m *Manager) createInternalApplication(info compat.Info, userID uint) (*model.Application, error) {
431+
tokenPublic, _ := auth.GenerateApplicationToken()
432+
app := &model.Application{
433+
Token: tokenPublic,
434+
Name: info.String(),
435+
UserID: userID,
436+
Internal: true,
437+
Description: fmt.Sprintf("auto generated application for %s", info.ModulePath),
438+
}
439+
if err := m.db.CreateApplication(app); err != nil {
440+
return nil, err
441+
}
442+
return app, nil
443+
}

plugin/manager_test.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,105 @@ func TestNewManager_InternalApplicationManagement(t *testing.T) {
443443
}
444444
}
445445

446+
func TestNewManager_MessengerAddedAfterInit_createsApplication(t *testing.T) {
447+
db := testdb.NewDBWithDefaultUser(t)
448+
449+
// Simulate a plugin conf that was created on a previous startup when the
450+
// plugin did not yet implement the Messenger interface: it has no
451+
// associated internal application (ApplicationID == 0).
452+
assert.NoError(t, db.CreatePluginConf(&model.PluginConf{
453+
UserID: 1,
454+
ModulePath: mock.ModulePath,
455+
Enabled: true,
456+
Token: auth.GeneratePluginToken(),
457+
}))
458+
459+
manager, err := NewManager(db, "", nil, nil)
460+
assert.Nil(t, err)
461+
assert.Nil(t, manager.LoadPlugin(new(mock.Plugin)))
462+
// The mock plugin supports Messenger, so re-initializing must back-fill the
463+
// missing internal application instead of leaving ApplicationID at 0.
464+
assert.Nil(t, manager.InitializeForUserID(1))
465+
466+
conf, err := db.GetPluginConfByUserAndPath(1, mock.ModulePath)
467+
assert.NoError(t, err)
468+
if assert.NotNil(t, conf) {
469+
assert.NotZero(t, conf.ApplicationID, "an internal application should have been created for the messenger plugin")
470+
471+
app, err := db.GetApplicationByID(conf.ApplicationID)
472+
assert.NoError(t, err)
473+
if assert.NotNil(t, app) {
474+
assert.True(t, app.Internal)
475+
assert.Equal(t, uint(1), app.UserID)
476+
}
477+
}
478+
}
479+
480+
// failingDB wraps a real test database so that individual operations can be
481+
// forced to fail, allowing the error branches around internal-application
482+
// creation to be exercised.
483+
type failingDB struct {
484+
*testdb.Database
485+
failCreateApplication error
486+
failUpdatePluginConf error
487+
}
488+
489+
func (d *failingDB) CreateApplication(app *model.Application) error {
490+
if d.failCreateApplication != nil {
491+
return d.failCreateApplication
492+
}
493+
return d.Database.CreateApplication(app)
494+
}
495+
496+
func (d *failingDB) UpdatePluginConf(conf *model.PluginConf) error {
497+
if d.failUpdatePluginConf != nil {
498+
return d.failUpdatePluginConf
499+
}
500+
return d.Database.UpdatePluginConf(conf)
501+
}
502+
503+
func seedMessengerConfWithoutApplication(t *testing.T, db Database) {
504+
t.Helper()
505+
assert.NoError(t, db.CreatePluginConf(&model.PluginConf{
506+
UserID: 1,
507+
ModulePath: mock.ModulePath,
508+
Enabled: true,
509+
Token: auth.GeneratePluginToken(),
510+
}))
511+
}
512+
513+
func TestNewManager_MessengerAddedAfterInit_createApplicationError(t *testing.T) {
514+
db := &failingDB{
515+
Database: testdb.NewDBWithDefaultUser(t),
516+
failCreateApplication: errors.New("create application failed"),
517+
}
518+
seedMessengerConfWithoutApplication(t, db)
519+
520+
manager, err := NewManager(db, "", nil, nil)
521+
assert.Nil(t, err)
522+
assert.Nil(t, manager.LoadPlugin(new(mock.Plugin)))
523+
524+
// Back-filling the missing internal application must surface the database
525+
// error instead of silently continuing with ApplicationID == 0.
526+
assert.EqualError(t, manager.InitializeForUserID(1), "create application failed")
527+
}
528+
529+
func TestNewManager_MessengerAddedAfterInit_updatePluginConfError(t *testing.T) {
530+
db := &failingDB{
531+
Database: testdb.NewDBWithDefaultUser(t),
532+
failUpdatePluginConf: errors.New("update plugin conf failed"),
533+
}
534+
seedMessengerConfWithoutApplication(t, db)
535+
536+
manager, err := NewManager(db, "", nil, nil)
537+
assert.Nil(t, err)
538+
assert.Nil(t, manager.LoadPlugin(new(mock.Plugin)))
539+
540+
// Persisting the back-filled ApplicationID may fail; that error must be
541+
// propagated as well.
542+
assert.EqualError(t, manager.InitializeForUserID(1), "update plugin conf failed")
543+
}
544+
446545
func TestPluginFileLoadError(t *testing.T) {
447546
err := pluginFileLoadError{Filename: "test.so", UnderlyingError: errors.New("test error")}
448547
assert.Error(t, err)

plugin/messagehandler.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package plugin
22

33
import (
4+
"errors"
45
"time"
56

67
"github.com/gotify/server/v2/model"
@@ -21,6 +22,13 @@ type MessageWithUserID struct {
2122

2223
// SendMessage sends a message to the underlying message channel.
2324
func (c redirectToChannel) SendMessage(msg compat.Message) error {
25+
if c.ApplicationID == 0 {
26+
// Final safety net: the internal application should always be set up by
27+
// Manager.initializeSingleUserPlugin. If it somehow isn't, refuse the
28+
// message instead of storing it with application_id = 0, where it would
29+
// be orphaned (not shown in the UI and not deletable).
30+
return errors.New("plugin messenger has no associated internal application")
31+
}
2432
c.Messages <- MessageWithUserID{
2533
Message: model.MessageExternal{
2634
ApplicationID: c.ApplicationID,

plugin/messagehandler_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package plugin
2+
3+
import (
4+
"testing"
5+
6+
"github.com/gotify/server/v2/plugin/compat"
7+
"github.com/stretchr/testify/assert"
8+
)
9+
10+
func TestRedirectToChannel_SendMessage_rejectsMissingApplication(t *testing.T) {
11+
messages := make(chan MessageWithUserID, 1)
12+
handler := redirectToChannel{ApplicationID: 0, UserID: 1, Messages: messages}
13+
14+
err := handler.SendMessage(compat.Message{Message: "orphan"})
15+
16+
assert.Error(t, err)
17+
assert.Empty(t, messages, "no message should be queued when the internal application is missing")
18+
}
19+
20+
func TestRedirectToChannel_SendMessage_forwardsWithApplication(t *testing.T) {
21+
messages := make(chan MessageWithUserID, 1)
22+
handler := redirectToChannel{ApplicationID: 7, UserID: 3, Messages: messages}
23+
24+
assert.NoError(t, handler.SendMessage(compat.Message{Message: "hi", Title: "t"}))
25+
26+
got := <-messages
27+
assert.Equal(t, uint(7), got.Message.ApplicationID)
28+
assert.Equal(t, uint(3), got.UserID)
29+
assert.Equal(t, "hi", got.Message.Message)
30+
assert.Equal(t, "t", got.Message.Title)
31+
}

0 commit comments

Comments
 (0)