diff --git a/pkg/server/app/app.go b/pkg/server/app/app.go
new file mode 100644
index 00000000..5b725079
--- /dev/null
+++ b/pkg/server/app/app.go
@@ -0,0 +1,72 @@
+/* Copyright (C) 2019 Monomax Software Pty Ltd
+ *
+ * This file is part of Dnote.
+ *
+ * Dnote 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.
+ *
+ * Dnote 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 Dnote. If not, see .
+ */
+
+package app
+
+import (
+ "github.com/dnote/dnote/pkg/clock"
+ "github.com/dnote/dnote/pkg/server/mailer"
+ "github.com/jinzhu/gorm"
+ "github.com/pkg/errors"
+ "github.com/stripe/stripe-go"
+)
+
+var (
+ // ErrEmptyDB is an error for missing database connection in the app configuration
+ ErrEmptyDB = errors.New("No database connection was provided")
+ // ErrEmptyClock is an error for missing clock in the app configuration
+ ErrEmptyClock = errors.New("No clock was provided")
+ // ErrEmptyWebURL is an error for missing WebURL content in the app configuration
+ ErrEmptyWebURL = errors.New("No WebURL was provided")
+ // ErrEmptyEmailTemplates is an error for missing EmailTemplates content in the app configuration
+ ErrEmptyEmailTemplates = errors.New("No EmailTemplate store was provided")
+ // ErrEmptyEmailBackend is an error for missing EmailBackend content in the app configuration
+ ErrEmptyEmailBackend = errors.New("No EmailBackend was provided")
+)
+
+// App is an application configuration
+type App struct {
+ DB *gorm.DB
+ Clock clock.Clock
+ StripeAPIBackend stripe.Backend
+ EmailTemplates mailer.Templates
+ EmailBackend mailer.Backend
+ WebURL string
+ OnPremise bool
+}
+
+// Validate validates the app configuration
+func (a *App) Validate() error {
+ if a.WebURL == "" {
+ return ErrEmptyWebURL
+ }
+ if a.Clock == nil {
+ return ErrEmptyClock
+ }
+ if a.EmailTemplates == nil {
+ return ErrEmptyEmailTemplates
+ }
+ if a.EmailBackend == nil {
+ return ErrEmptyEmailBackend
+ }
+ if a.DB == nil {
+ return ErrEmptyDB
+ }
+
+ return nil
+}
diff --git a/pkg/server/app/app_test.go b/pkg/server/app/app_test.go
new file mode 100644
index 00000000..5a2936f1
--- /dev/null
+++ b/pkg/server/app/app_test.go
@@ -0,0 +1,113 @@
+/* Copyright (C) 2019 Monomax Software Pty Ltd
+ *
+ * This file is part of Dnote.
+ *
+ * Dnote 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.
+ *
+ * Dnote 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 Dnote. If not, see .
+ */
+
+package app
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/dnote/dnote/pkg/assert"
+ "github.com/dnote/dnote/pkg/clock"
+ "github.com/dnote/dnote/pkg/server/mailer"
+ "github.com/dnote/dnote/pkg/server/testutils"
+ "github.com/jinzhu/gorm"
+ "github.com/pkg/errors"
+)
+
+func TestValidate(t *testing.T) {
+ testCases := []struct {
+ app App
+ expectedErr error
+ }{
+ {
+ app: App{
+ DB: &gorm.DB{},
+ Clock: clock.NewMock(),
+ StripeAPIBackend: nil,
+ EmailTemplates: mailer.Templates{},
+ EmailBackend: &testutils.MockEmailbackendImplementation{},
+ WebURL: "http://mock.url",
+ },
+ expectedErr: nil,
+ },
+ {
+ app: App{
+ DB: nil,
+ Clock: clock.NewMock(),
+ StripeAPIBackend: nil,
+ EmailTemplates: mailer.Templates{},
+ EmailBackend: &testutils.MockEmailbackendImplementation{},
+ WebURL: "http://mock.url",
+ },
+ expectedErr: ErrEmptyDB,
+ },
+ {
+ app: App{
+ DB: &gorm.DB{},
+ Clock: nil,
+ StripeAPIBackend: nil,
+ EmailTemplates: mailer.Templates{},
+ EmailBackend: &testutils.MockEmailbackendImplementation{},
+ WebURL: "http://mock.url",
+ },
+ expectedErr: ErrEmptyClock,
+ },
+ {
+ app: App{
+ DB: &gorm.DB{},
+ Clock: clock.NewMock(),
+ StripeAPIBackend: nil,
+ EmailTemplates: nil,
+ EmailBackend: &testutils.MockEmailbackendImplementation{},
+ WebURL: "http://mock.url",
+ },
+ expectedErr: ErrEmptyEmailTemplates,
+ },
+ {
+ app: App{
+ DB: &gorm.DB{},
+ Clock: clock.NewMock(),
+ StripeAPIBackend: nil,
+ EmailTemplates: mailer.Templates{},
+ EmailBackend: nil,
+ WebURL: "http://mock.url",
+ },
+ expectedErr: ErrEmptyEmailBackend,
+ },
+ {
+ app: App{
+ DB: &gorm.DB{},
+ Clock: clock.NewMock(),
+ StripeAPIBackend: nil,
+ EmailTemplates: mailer.Templates{},
+ EmailBackend: &testutils.MockEmailbackendImplementation{},
+ WebURL: "",
+ },
+ expectedErr: ErrEmptyWebURL,
+ },
+ }
+
+ for idx, tc := range testCases {
+ t.Run(fmt.Sprintf("test case %d", idx), func(t *testing.T) {
+ err := tc.app.Validate()
+
+ assert.Equal(t, errors.Cause(err), tc.expectedErr, "error mismatch")
+ })
+ }
+}
diff --git a/pkg/server/operations/books.go b/pkg/server/app/books.go
similarity index 84%
rename from pkg/server/operations/books.go
rename to pkg/server/app/books.go
index 6408368b..8462960a 100644
--- a/pkg/server/operations/books.go
+++ b/pkg/server/app/books.go
@@ -16,10 +16,9 @@
* along with Dnote. If not, see .
*/
-package operations
+package app
import (
- "github.com/dnote/dnote/pkg/clock"
"github.com/dnote/dnote/pkg/server/database"
"github.com/dnote/dnote/pkg/server/helpers"
"github.com/jinzhu/gorm"
@@ -27,8 +26,8 @@ import (
)
// CreateBook creates a book with the next usn and updates the user's max_usn
-func CreateBook(db *gorm.DB, user database.User, clock clock.Clock, name string) (database.Book, error) {
- tx := db.Begin()
+func (a *App) CreateBook(user database.User, name string) (database.Book, error) {
+ tx := a.DB.Begin()
nextUSN, err := incrementUserUSN(tx, user.ID)
if err != nil {
@@ -45,7 +44,7 @@ func CreateBook(db *gorm.DB, user database.User, clock clock.Clock, name string)
UUID: uuid,
UserID: user.ID,
Label: name,
- AddedOn: clock.Now().UnixNano(),
+ AddedOn: a.Clock.Now().UnixNano(),
USN: nextUSN,
Encrypted: false,
}
@@ -60,7 +59,7 @@ func CreateBook(db *gorm.DB, user database.User, clock clock.Clock, name string)
}
// DeleteBook marks a book deleted with the next usn and updates the user's max_usn
-func DeleteBook(tx *gorm.DB, user database.User, book database.Book) (database.Book, error) {
+func (a *App) DeleteBook(tx *gorm.DB, user database.User, book database.Book) (database.Book, error) {
if user.ID != book.UserID {
return book, errors.New("Not allowed")
}
@@ -83,7 +82,7 @@ func DeleteBook(tx *gorm.DB, user database.User, book database.Book) (database.B
}
// UpdateBook updaates the book, the usn and the user's max_usn
-func UpdateBook(tx *gorm.DB, c clock.Clock, user database.User, book database.Book, label *string) (database.Book, error) {
+func (a *App) UpdateBook(tx *gorm.DB, user database.User, book database.Book, label *string) (database.Book, error) {
if user.ID != book.UserID {
return book, errors.New("Not allowed")
}
@@ -98,7 +97,7 @@ func UpdateBook(tx *gorm.DB, c clock.Clock, user database.User, book database.Bo
}
book.USN = nextUSN
- book.EditedOn = c.Now().UnixNano()
+ book.EditedOn = a.Clock.Now().UnixNano()
book.Deleted = false
// TODO: remove after all users have been migrated
book.Encrypted = false
diff --git a/pkg/server/operations/books_test.go b/pkg/server/app/books_test.go
similarity index 96%
rename from pkg/server/operations/books_test.go
rename to pkg/server/app/books_test.go
index fba28276..ac8d739c 100644
--- a/pkg/server/operations/books_test.go
+++ b/pkg/server/app/books_test.go
@@ -16,7 +16,7 @@
* along with Dnote. If not, see .
*/
-package operations
+package app
import (
"fmt"
@@ -62,9 +62,11 @@ func TestCreateBook(t *testing.T) {
anotherUser := testutils.SetupUserData()
testutils.MustExec(t, testutils.DB.Model(&anotherUser).Update("max_usn", 55), fmt.Sprintf("preparing user max_usn for test case %d", idx))
- c := clock.NewMock()
+ a := NewTest(&App{
+ Clock: clock.NewMock(),
+ })
- book, err := CreateBook(testutils.DB, user, c, tc.label)
+ book, err := a.CreateBook(user, tc.label)
if err != nil {
t.Fatal(errors.Wrap(err, "creating book"))
}
@@ -130,7 +132,8 @@ func TestDeleteBook(t *testing.T) {
testutils.MustExec(t, testutils.DB.Save(&book), fmt.Sprintf("preparing book for test case %d", idx))
tx := testutils.DB.Begin()
- ret, err := DeleteBook(tx, user, book)
+ a := NewTest(nil)
+ ret, err := a.DeleteBook(tx, user, book)
if err != nil {
tx.Rollback()
t.Fatal(errors.Wrap(err, "deleting book"))
@@ -203,14 +206,16 @@ func TestUpdateBook(t *testing.T) {
anotherUser := testutils.SetupUserData()
testutils.MustExec(t, testutils.DB.Model(&anotherUser).Update("max_usn", 55), fmt.Sprintf("preparing user max_usn for test case %d", idx))
- c := clock.NewMock()
-
b := database.Book{UserID: user.ID, Deleted: false, Label: tc.expectedLabel}
testutils.MustExec(t, testutils.DB.Save(&b), fmt.Sprintf("preparing book for test case %d", idx))
- tx := testutils.DB.Begin()
+ c := clock.NewMock()
+ a := NewTest(&App{
+ Clock: c,
+ })
- book, err := UpdateBook(tx, c, user, b, tc.payloadLabel)
+ tx := testutils.DB.Begin()
+ book, err := a.UpdateBook(tx, user, b, tc.payloadLabel)
if err != nil {
tx.Rollback()
t.Fatal(errors.Wrap(err, "updating book"))
diff --git a/pkg/server/app/email.go b/pkg/server/app/email.go
new file mode 100644
index 00000000..e043e5ea
--- /dev/null
+++ b/pkg/server/app/email.go
@@ -0,0 +1,137 @@
+/* Copyright (C) 2019 Monomax Software Pty Ltd
+ *
+ * This file is part of Dnote.
+ *
+ * Dnote 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.
+ *
+ * Dnote 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 Dnote. If not, see .
+ */
+
+package app
+
+import (
+ "fmt"
+ "net/url"
+ "strings"
+
+ "github.com/dnote/dnote/pkg/server/mailer"
+ "github.com/pkg/errors"
+)
+
+var defaultSender = "sung@getdnote.com"
+
+// getSenderEmail returns the sender email
+func (a *App) getSenderEmail(want string) (string, error) {
+ if !a.OnPremise {
+ return want, nil
+ }
+
+ addr, err := a.getNoreplySender()
+ if err != nil {
+ return "", errors.Wrap(err, "getting sender email address")
+ }
+
+ return addr, nil
+}
+
+func getDomainFromURL(rawURL string) (string, error) {
+ u, err := url.Parse(rawURL)
+ if err != nil {
+ return "", errors.Wrap(err, "parsing url")
+ }
+
+ host := u.Hostname()
+ parts := strings.Split(host, ".")
+ if len(parts) < 2 {
+ return host, nil
+ }
+ domain := parts[len(parts)-2] + "." + parts[len(parts)-1]
+
+ return domain, nil
+}
+
+func (a *App) getNoreplySender() (string, error) {
+ domain, err := getDomainFromURL(a.WebURL)
+ if err != nil {
+ return "", errors.Wrap(err, "parsing web url")
+ }
+
+ addr := fmt.Sprintf("noreply@%s", domain)
+ return addr, nil
+}
+
+// SendVerificationEmail sends verification email
+func (a *App) SendVerificationEmail(email, tokenValue string) error {
+ body, err := a.EmailTemplates.Execute(mailer.EmailTypeEmailVerification, mailer.EmailKindText, mailer.EmailVerificationTmplData{
+ Token: tokenValue,
+ WebURL: a.WebURL,
+ })
+ if err != nil {
+ return errors.Wrapf(err, "executing reset verification template for %s", email)
+ }
+
+ from, err := a.getSenderEmail(defaultSender)
+ if err != nil {
+ return errors.Wrap(err, "getting the sender email")
+ }
+
+ if err := a.EmailBackend.Queue("Verify your Dnote email address", from, []string{email}, "text/plain", body); err != nil {
+ return errors.Wrapf(err, "queueing email for %s", email)
+ }
+
+ return nil
+}
+
+// SendWelcomeEmail sends welcome email
+func (a *App) SendWelcomeEmail(email string) error {
+ body, err := a.EmailTemplates.Execute(mailer.EmailTypeWelcome, mailer.EmailKindText, mailer.WelcomeTmplData{
+ AccountEmail: email,
+ WebURL: a.WebURL,
+ })
+ if err != nil {
+ return errors.Wrapf(err, "executing reset verification template for %s", email)
+ }
+
+ from, err := a.getSenderEmail(defaultSender)
+ if err != nil {
+ return errors.Wrap(err, "getting the sender email")
+ }
+
+ if err := a.EmailBackend.Queue("Welcome to Dnote!", from, []string{email}, "text/plain", body); err != nil {
+ return errors.Wrapf(err, "queueing email for %s", email)
+ }
+
+ return nil
+}
+
+// SendPasswordResetEmail sends verification email
+func (a *App) SendPasswordResetEmail(email, tokenValue string) error {
+ body, err := a.EmailTemplates.Execute(mailer.EmailTypeResetPassword, mailer.EmailKindText, mailer.EmailResetPasswordTmplData{
+ AccountEmail: email,
+ Token: tokenValue,
+ WebURL: a.WebURL,
+ })
+ if err != nil {
+ return errors.Wrapf(err, "executing reset verification template for %s", email)
+ }
+
+ from, err := a.getSenderEmail(defaultSender)
+ if err != nil {
+ return errors.Wrap(err, "getting the sender email")
+ }
+
+ if err := a.EmailBackend.Queue("Reset your password", from, []string{email}, "text/plain", body); err != nil {
+ return errors.Wrapf(err, "queueing email for %s", email)
+ }
+
+ return nil
+}
diff --git a/pkg/server/app/email_test.go b/pkg/server/app/email_test.go
new file mode 100644
index 00000000..b6de47df
--- /dev/null
+++ b/pkg/server/app/email_test.go
@@ -0,0 +1,132 @@
+/* Copyright (C) 2019 Monomax Software Pty Ltd
+ *
+ * This file is part of Dnote.
+ *
+ * Dnote 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.
+ *
+ * Dnote 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 Dnote. If not, see .
+ */
+
+package app
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/dnote/dnote/pkg/assert"
+ "github.com/dnote/dnote/pkg/server/testutils"
+)
+
+func TestSendVerificationEmail(t *testing.T) {
+ testCases := []struct {
+ onPremise bool
+ expectedSender string
+ }{
+ {
+ onPremise: false,
+ expectedSender: "sung@getdnote.com",
+ },
+ {
+ onPremise: true,
+ expectedSender: "noreply@example.com",
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(fmt.Sprintf("self hosted %t", tc.onPremise), func(t *testing.T) {
+ emailBackend := testutils.MockEmailbackendImplementation{}
+ a := NewTest(&App{
+ OnPremise: tc.onPremise,
+ WebURL: "http://example.com",
+ EmailBackend: &emailBackend,
+ })
+
+ if err := a.SendVerificationEmail("alice@example.com", "mockTokenValue"); err != nil {
+ t.Fatal(err, "failed to perform")
+ }
+
+ assert.Equalf(t, len(emailBackend.Emails), 1, "email queue count mismatch")
+ assert.Equal(t, emailBackend.Emails[0].From, tc.expectedSender, "email sender mismatch")
+ assert.DeepEqual(t, emailBackend.Emails[0].To, []string{"alice@example.com"}, "email sender mismatch")
+ })
+ }
+}
+
+func TestSendWelcomeEmail(t *testing.T) {
+ testCases := []struct {
+ onPremise bool
+ expectedSender string
+ }{
+ {
+ onPremise: false,
+ expectedSender: "sung@getdnote.com",
+ },
+ {
+ onPremise: true,
+ expectedSender: "noreply@example.com",
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(fmt.Sprintf("self hosted %t", tc.onPremise), func(t *testing.T) {
+ emailBackend := testutils.MockEmailbackendImplementation{}
+ a := NewTest(&App{
+ OnPremise: tc.onPremise,
+ WebURL: "http://example.com",
+ EmailBackend: &emailBackend,
+ })
+
+ if err := a.SendWelcomeEmail("alice@example.com"); err != nil {
+ t.Fatal(err, "failed to perform")
+ }
+
+ assert.Equalf(t, len(emailBackend.Emails), 1, "email queue count mismatch")
+ assert.Equal(t, emailBackend.Emails[0].From, tc.expectedSender, "email sender mismatch")
+ assert.DeepEqual(t, emailBackend.Emails[0].To, []string{"alice@example.com"}, "email sender mismatch")
+ })
+ }
+}
+
+func TestSendPasswordResetEmail(t *testing.T) {
+ testCases := []struct {
+ onPremise bool
+ expectedSender string
+ }{
+ {
+ onPremise: false,
+ expectedSender: "sung@getdnote.com",
+ },
+ {
+ onPremise: true,
+ expectedSender: "noreply@example.com",
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(fmt.Sprintf("self hosted %t", tc.onPremise), func(t *testing.T) {
+ emailBackend := testutils.MockEmailbackendImplementation{}
+ a := NewTest(&App{
+ OnPremise: tc.onPremise,
+ WebURL: "http://example.com",
+ EmailBackend: &emailBackend,
+ })
+
+ if err := a.SendPasswordResetEmail("alice@example.com", "mockTokenValue"); err != nil {
+ t.Fatal(err, "failed to perform")
+ }
+
+ assert.Equalf(t, len(emailBackend.Emails), 1, "email queue count mismatch")
+ assert.Equal(t, emailBackend.Emails[0].From, tc.expectedSender, "email sender mismatch")
+ assert.DeepEqual(t, emailBackend.Emails[0].To, []string{"alice@example.com"}, "email sender mismatch")
+ })
+ }
+}
diff --git a/pkg/server/operations/helpers.go b/pkg/server/app/helpers.go
similarity index 98%
rename from pkg/server/operations/helpers.go
rename to pkg/server/app/helpers.go
index 7f748191..d39c48bc 100644
--- a/pkg/server/operations/helpers.go
+++ b/pkg/server/app/helpers.go
@@ -16,7 +16,7 @@
* along with Dnote. If not, see .
*/
-package operations
+package app
import (
"github.com/dnote/dnote/pkg/server/database"
diff --git a/pkg/server/operations/helpers_test.go b/pkg/server/app/helpers_test.go
similarity index 99%
rename from pkg/server/operations/helpers_test.go
rename to pkg/server/app/helpers_test.go
index 68e137e6..c2274744 100644
--- a/pkg/server/operations/helpers_test.go
+++ b/pkg/server/app/helpers_test.go
@@ -16,7 +16,7 @@
* along with Dnote. If not, see .
*/
-package operations
+package app
import (
"fmt"
diff --git a/pkg/server/app/main_test.go b/pkg/server/app/main_test.go
new file mode 100644
index 00000000..5bd96a61
--- /dev/null
+++ b/pkg/server/app/main_test.go
@@ -0,0 +1,35 @@
+/* Copyright (C) 2019 Monomax Software Pty Ltd
+ *
+ * This file is part of Dnote.
+ *
+ * Dnote 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.
+ *
+ * Dnote 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 Dnote. If not, see .
+ */
+
+package app
+
+import (
+ "os"
+ "testing"
+
+ "github.com/dnote/dnote/pkg/server/testutils"
+)
+
+func TestMain(m *testing.M) {
+ testutils.InitTestDB()
+
+ code := m.Run()
+ testutils.ClearData()
+
+ os.Exit(code)
+}
diff --git a/pkg/server/operations/notes.go b/pkg/server/app/notes.go
similarity index 85%
rename from pkg/server/operations/notes.go
rename to pkg/server/app/notes.go
index ba1a9a39..00b683fd 100644
--- a/pkg/server/operations/notes.go
+++ b/pkg/server/app/notes.go
@@ -16,10 +16,9 @@
* along with Dnote. If not, see .
*/
-package operations
+package app
import (
- "github.com/dnote/dnote/pkg/clock"
"github.com/dnote/dnote/pkg/server/database"
"github.com/dnote/dnote/pkg/server/helpers"
"github.com/dnote/dnote/pkg/server/permissions"
@@ -29,8 +28,8 @@ import (
// CreateNote creates a note with the next usn and updates the user's max_usn.
// It returns the created note.
-func CreateNote(db *gorm.DB, user database.User, clock clock.Clock, bookUUID, content string, addedOn *int64, editedOn *int64, public bool) (database.Note, error) {
- tx := db.Begin()
+func (a *App) CreateNote(user database.User, bookUUID, content string, addedOn *int64, editedOn *int64, public bool) (database.Note, error) {
+ tx := a.DB.Begin()
nextUSN, err := incrementUserUSN(tx, user.ID)
if err != nil {
@@ -40,7 +39,7 @@ func CreateNote(db *gorm.DB, user database.User, clock clock.Clock, bookUUID, co
var noteAddedOn int64
if addedOn == nil {
- noteAddedOn = clock.Now().UnixNano()
+ noteAddedOn = a.Clock.Now().UnixNano()
} else {
noteAddedOn = *addedOn
}
@@ -113,7 +112,7 @@ func (r UpdateNoteParams) GetPublic() bool {
}
// UpdateNote creates a note with the next usn and updates the user's max_usn
-func UpdateNote(tx *gorm.DB, user database.User, clock clock.Clock, note database.Note, p *UpdateNoteParams) (database.Note, error) {
+func (a *App) UpdateNote(tx *gorm.DB, user database.User, note database.Note, p *UpdateNoteParams) (database.Note, error) {
nextUSN, err := incrementUserUSN(tx, user.ID)
if err != nil {
return note, errors.Wrap(err, "incrementing user max_usn")
@@ -130,7 +129,7 @@ func UpdateNote(tx *gorm.DB, user database.User, clock clock.Clock, note databas
}
note.USN = nextUSN
- note.EditedOn = clock.Now().UnixNano()
+ note.EditedOn = a.Clock.Now().UnixNano()
note.Deleted = false
// TODO: remove after all users are migrated
note.Encrypted = false
@@ -143,7 +142,7 @@ func UpdateNote(tx *gorm.DB, user database.User, clock clock.Clock, note databas
}
// DeleteNote marks a note deleted with the next usn and updates the user's max_usn
-func DeleteNote(tx *gorm.DB, user database.User, note database.Note) (database.Note, error) {
+func (a *App) DeleteNote(tx *gorm.DB, user database.User, note database.Note) (database.Note, error) {
nextUSN, err := incrementUserUSN(tx, user.ID)
if err != nil {
return note, errors.Wrap(err, "incrementing user max_usn")
@@ -162,13 +161,13 @@ func DeleteNote(tx *gorm.DB, user database.User, note database.Note) (database.N
}
// GetNote retrieves a note for the given user
-func GetNote(db *gorm.DB, uuid string, user database.User) (database.Note, bool, error) {
+func (a *App) GetNote(uuid string, user database.User) (database.Note, bool, error) {
zeroNote := database.Note{}
if !helpers.ValidateUUID(uuid) {
return zeroNote, false, nil
}
- conn := db.Where("notes.uuid = ? AND deleted = ?", uuid, false)
+ conn := a.DB.Where("notes.uuid = ? AND deleted = ?", uuid, false)
conn = database.PreloadNote(conn)
var note database.Note
diff --git a/pkg/server/operations/notes_test.go b/pkg/server/app/notes_test.go
similarity index 96%
rename from pkg/server/operations/notes_test.go
rename to pkg/server/app/notes_test.go
index f30aba96..e8af2e7c 100644
--- a/pkg/server/operations/notes_test.go
+++ b/pkg/server/app/notes_test.go
@@ -16,7 +16,7 @@
* along with Dnote. If not, see .
*/
-package operations
+package app
import (
"fmt"
@@ -85,8 +85,12 @@ func TestCreateNote(t *testing.T) {
b1 := database.Book{UserID: user.ID, Label: "js", Deleted: false}
testutils.MustExec(t, testutils.DB.Save(&b1), fmt.Sprintf("preparing b1 for test case %d", idx))
+ a := NewTest(&App{
+ Clock: mockClock,
+ })
+
tx := testutils.DB.Begin()
- if _, err := CreateNote(testutils.DB, user, mockClock, b1.UUID, "note content", tc.addedOn, tc.editedOn, false); err != nil {
+ if _, err := a.CreateNote(user, b1.UUID, "note content", tc.addedOn, tc.editedOn, false); err != nil {
tx.Rollback()
t.Fatal(errors.Wrap(err, "deleting note"))
}
@@ -151,8 +155,12 @@ func TestUpdateNote(t *testing.T) {
content := "updated test content"
public := true
+ a := NewTest(&App{
+ Clock: c,
+ })
+
tx := testutils.DB.Begin()
- if _, err := UpdateNote(tx, user, c, note, &UpdateNoteParams{
+ if _, err := a.UpdateNote(tx, user, note, &UpdateNoteParams{
Content: &content,
Public: &public,
}); err != nil {
@@ -218,8 +226,10 @@ func TestDeleteNote(t *testing.T) {
note := database.Note{UserID: user.ID, Deleted: false, Body: "test content", BookUUID: b1.UUID}
testutils.MustExec(t, testutils.DB.Save(¬e), fmt.Sprintf("preparing note for test case %d", idx))
+ a := NewTest(nil)
+
tx := testutils.DB.Begin()
- ret, err := DeleteNote(tx, user, note)
+ ret, err := a.DeleteNote(tx, user, note)
if err != nil {
tx.Rollback()
t.Fatal(errors.Wrap(err, "deleting note"))
@@ -330,7 +340,8 @@ func TestGetNote(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
- note, ok, err := GetNote(testutils.DB, tc.note.UUID, tc.user)
+ a := NewTest(nil)
+ note, ok, err := a.GetNote(tc.note.UUID, tc.user)
if err != nil {
t.Fatal(errors.Wrap(err, "executing"))
}
@@ -363,8 +374,9 @@ func TestGetNote_nonexistent(t *testing.T) {
}
testutils.MustExec(t, testutils.DB.Save(&n1), "preparing n1")
+ a := NewTest(nil)
nonexistentUUID := "4fd19336-671e-4ff3-8f22-662b80e22edd"
- note, ok, err := GetNote(testutils.DB, nonexistentUUID, user)
+ note, ok, err := a.GetNote(nonexistentUUID, user)
if err != nil {
t.Fatal(errors.Wrap(err, "executing"))
}
diff --git a/pkg/server/operations/sessions.go b/pkg/server/app/sessions.go
similarity index 77%
rename from pkg/server/operations/sessions.go
rename to pkg/server/app/sessions.go
index 980d5c7c..0fc909e0 100644
--- a/pkg/server/operations/sessions.go
+++ b/pkg/server/app/sessions.go
@@ -16,19 +16,18 @@
* along with Dnote. If not, see .
*/
-package operations
+package app
import (
"time"
"github.com/dnote/dnote/pkg/server/crypt"
"github.com/dnote/dnote/pkg/server/database"
- "github.com/jinzhu/gorm"
"github.com/pkg/errors"
)
// CreateSession returns a new session for the user of the given id
-func CreateSession(db *gorm.DB, userID int) (database.Session, error) {
+func (a *App) CreateSession(userID int) (database.Session, error) {
key, err := crypt.GetRandomStr(32)
if err != nil {
return database.Session{}, errors.Wrap(err, "generating key")
@@ -41,7 +40,7 @@ func CreateSession(db *gorm.DB, userID int) (database.Session, error) {
ExpiresAt: time.Now().Add(24 * 100 * time.Hour),
}
- if err := db.Save(&session).Error; err != nil {
+ if err := a.DB.Save(&session).Error; err != nil {
return database.Session{}, errors.Wrap(err, "saving session")
}
@@ -50,8 +49,8 @@ func CreateSession(db *gorm.DB, userID int) (database.Session, error) {
// DeleteUserSessions deletes all existing sessions for the given user. It effectively
// invalidates all existing sessions.
-func DeleteUserSessions(db *gorm.DB, userID int) error {
- if err := db.Where("user_id = ?", userID).Delete(&database.Session{}).Error; err != nil {
+func (a *App) DeleteUserSessions(userID int) error {
+ if err := a.DB.Where("user_id = ?", userID).Delete(&database.Session{}).Error; err != nil {
return errors.Wrap(err, "deleting sessions")
}
@@ -59,8 +58,8 @@ func DeleteUserSessions(db *gorm.DB, userID int) error {
}
// DeleteSession deletes the session that match the given info
-func DeleteSession(db *gorm.DB, sessionKey string) error {
- if err := db.Where("key = ?", sessionKey).Delete(&database.Session{}).Error; err != nil {
+func (a *App) DeleteSession(sessionKey string) error {
+ if err := a.DB.Where("key = ?", sessionKey).Delete(&database.Session{}).Error; err != nil {
return errors.Wrap(err, "deleting the session")
}
diff --git a/pkg/server/operations/subscriptions.go b/pkg/server/app/subscriptions.go
similarity index 83%
rename from pkg/server/operations/subscriptions.go
rename to pkg/server/app/subscriptions.go
index 6d51db2e..0da08239 100644
--- a/pkg/server/operations/subscriptions.go
+++ b/pkg/server/app/subscriptions.go
@@ -16,13 +16,12 @@
* along with Dnote. If not, see .
*/
-package operations
+package app
import (
"github.com/dnote/dnote/pkg/server/database"
"github.com/pkg/errors"
- "github.com/jinzhu/gorm"
"github.com/stripe/stripe-go"
"github.com/stripe/stripe-go/sub"
)
@@ -32,7 +31,7 @@ import (
var ErrSubscriptionActive = errors.New("The subscription is currently active")
// CancelSub cancels the subscription of the given user
-func CancelSub(subscriptionID string, user database.User) error {
+func (a *App) CancelSub(subscriptionID string, user database.User) error {
updateParams := &stripe.SubscriptionParams{
CancelAtPeriodEnd: stripe.Bool(true),
}
@@ -46,7 +45,7 @@ func CancelSub(subscriptionID string, user database.User) error {
}
// ReactivateSub reactivates the subscription of the given user
-func ReactivateSub(subscriptionID string, user database.User) error {
+func (a *App) ReactivateSub(subscriptionID string, user database.User) error {
s, err := sub.Get(subscriptionID, nil)
if err != nil {
return errors.Wrap(err, "fetching subscription")
@@ -67,13 +66,13 @@ func ReactivateSub(subscriptionID string, user database.User) error {
}
// MarkUnsubscribed marks the user unsubscribed
-func MarkUnsubscribed(db *gorm.DB, stripeCustomerID string) error {
+func (a *App) MarkUnsubscribed(stripeCustomerID string) error {
var user database.User
- if err := db.Where("stripe_customer_id = ?", stripeCustomerID).First(&user).Error; err != nil {
+ if err := a.DB.Where("stripe_customer_id = ?", stripeCustomerID).First(&user).Error; err != nil {
return errors.Wrap(err, "finding user")
}
- if err := db.Model(&user).Update("cloud", false).Error; err != nil {
+ if err := a.DB.Model(&user).Update("cloud", false).Error; err != nil {
return errors.Wrap(err, "updating user")
}
diff --git a/pkg/server/app/testutils.go b/pkg/server/app/testutils.go
new file mode 100644
index 00000000..55078180
--- /dev/null
+++ b/pkg/server/app/testutils.go
@@ -0,0 +1,64 @@
+/* Copyright (C) 2019 Monomax Software Pty Ltd
+ *
+ * This file is part of Dnote.
+ *
+ * Dnote 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.
+ *
+ * Dnote 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 Dnote. If not, see .
+ */
+
+package app
+
+import (
+ "os"
+
+ "github.com/dnote/dnote/pkg/clock"
+ "github.com/dnote/dnote/pkg/server/mailer"
+ "github.com/dnote/dnote/pkg/server/testutils"
+)
+
+// NewTest returns an app for a testing environment
+func NewTest(appParams *App) App {
+ emailTmplDir := os.Getenv("DNOTE_TEST_EMAIL_TEMPLATE_DIR")
+
+ a := App{
+ DB: testutils.DB,
+ WebURL: os.Getenv("WebURL"),
+ Clock: clock.NewMock(),
+ EmailTemplates: mailer.NewTemplates(&emailTmplDir),
+ EmailBackend: &testutils.MockEmailbackendImplementation{},
+ StripeAPIBackend: nil,
+ OnPremise: false,
+ }
+
+ // Allow to override with appParams
+ if appParams != nil && appParams.EmailBackend != nil {
+ a.EmailBackend = appParams.EmailBackend
+ }
+ if appParams != nil && appParams.Clock != nil {
+ a.Clock = appParams.Clock
+ }
+ if appParams != nil && appParams.EmailTemplates != nil {
+ a.EmailTemplates = appParams.EmailTemplates
+ }
+ if appParams != nil && appParams.StripeAPIBackend != nil {
+ a.StripeAPIBackend = appParams.StripeAPIBackend
+ }
+ if appParams != nil && appParams.OnPremise {
+ a.OnPremise = appParams.OnPremise
+ }
+ if appParams != nil && appParams.WebURL != "" {
+ a.WebURL = appParams.WebURL
+ }
+
+ return a
+}
diff --git a/pkg/server/operations/users.go b/pkg/server/app/users.go
similarity index 90%
rename from pkg/server/operations/users.go
rename to pkg/server/app/users.go
index b616d524..081ec2fe 100644
--- a/pkg/server/operations/users.go
+++ b/pkg/server/app/users.go
@@ -16,7 +16,7 @@
* along with Dnote. If not, see .
*/
-package operations
+package app
import (
"time"
@@ -47,7 +47,7 @@ func generateVerificationCode() (string, error) {
}
// TouchLastLoginAt updates the last login timestamp
-func TouchLastLoginAt(user database.User, tx *gorm.DB) error {
+func (a *App) TouchLastLoginAt(user database.User, tx *gorm.DB) error {
t := time.Now()
if err := tx.Model(&user).Update(database.User{LastLoginAt: &t}).Error; err != nil {
return errors.Wrap(err, "updating last_login_at")
@@ -104,8 +104,8 @@ func createDefaultRepetitionRule(user database.User, tx *gorm.DB) error {
}
// CreateUser creates a user
-func CreateUser(db *gorm.DB, email, password string) (database.User, error) {
- tx := db.Begin()
+func (a *App) CreateUser(email, password string) (database.User, error) {
+ tx := a.DB.Begin()
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
@@ -113,7 +113,17 @@ func CreateUser(db *gorm.DB, email, password string) (database.User, error) {
return database.User{}, errors.Wrap(err, "hashing password")
}
- user := database.User{}
+ // Grant all privileges if self-hosting
+ var pro bool
+ if a.OnPremise {
+ pro = true
+ } else {
+ pro = false
+ }
+
+ user := database.User{
+ Cloud: pro,
+ }
if err = tx.Save(&user).Error; err != nil {
tx.Rollback()
return database.User{}, errors.Wrap(err, "saving user")
@@ -140,7 +150,7 @@ func CreateUser(db *gorm.DB, email, password string) (database.User, error) {
tx.Rollback()
return database.User{}, errors.Wrap(err, "creating default repetition rule")
}
- if err := TouchLastLoginAt(user, tx); err != nil {
+ if err := a.TouchLastLoginAt(user, tx); err != nil {
tx.Rollback()
return database.User{}, errors.Wrap(err, "updating last login")
}
diff --git a/pkg/server/app/users_test.go b/pkg/server/app/users_test.go
new file mode 100644
index 00000000..4a75c571
--- /dev/null
+++ b/pkg/server/app/users_test.go
@@ -0,0 +1,66 @@
+/* Copyright (C) 2019 Monomax Software Pty Ltd
+ *
+ * This file is part of Dnote.
+ *
+ * Dnote 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.
+ *
+ * Dnote 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 Dnote. If not, see .
+ */
+
+package app
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/dnote/dnote/pkg/assert"
+ "github.com/dnote/dnote/pkg/server/database"
+ "github.com/dnote/dnote/pkg/server/testutils"
+ "github.com/pkg/errors"
+)
+
+func TestCreateUser(t *testing.T) {
+ testCases := []struct {
+ onPremise bool
+ expectedPro bool
+ }{
+ {
+ onPremise: true,
+ expectedPro: true,
+ },
+ {
+ onPremise: false,
+ expectedPro: false,
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(fmt.Sprintf("self hosting %t", tc.onPremise), func(t *testing.T) {
+ defer testutils.ClearData()
+
+ a := NewTest(&App{
+ OnPremise: tc.onPremise,
+ })
+ if _, err := a.CreateUser("alice@example.com", "pass1234"); err != nil {
+ t.Fatal(errors.Wrap(err, "executing"))
+ }
+
+ var userCount int
+ var userRecord database.User
+ testutils.MustExec(t, testutils.DB.Model(&database.User{}).Count(&userCount), "counting user")
+ testutils.MustExec(t, testutils.DB.First(&userRecord), "finding user")
+
+ assert.Equal(t, userCount, 1, "book count mismatch")
+ assert.Equal(t, userRecord.Cloud, tc.expectedPro, "user pro mismatch")
+ })
+ }
+}
diff --git a/pkg/server/dbconn/dbconn.go b/pkg/server/dbconn/dbconn.go
index d55d1400..85f3194e 100644
--- a/pkg/server/dbconn/dbconn.go
+++ b/pkg/server/dbconn/dbconn.go
@@ -1,3 +1,21 @@
+/* Copyright (C) 2019 Monomax Software Pty Ltd
+ *
+ * This file is part of Dnote.
+ *
+ * Dnote 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.
+ *
+ * Dnote 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 Dnote. If not, see .
+ */
+
package dbconn
import (
diff --git a/pkg/server/handlers/auth.go b/pkg/server/handlers/auth.go
index d6b00a7b..965d0fcd 100644
--- a/pkg/server/handlers/auth.go
+++ b/pkg/server/handlers/auth.go
@@ -27,7 +27,6 @@ import (
"github.com/dnote/dnote/pkg/server/database"
"github.com/dnote/dnote/pkg/server/helpers"
"github.com/dnote/dnote/pkg/server/mailer"
- "github.com/dnote/dnote/pkg/server/operations"
"github.com/pkg/errors"
"golang.org/x/crypto/bcrypt"
)
@@ -53,7 +52,7 @@ func makeSession(user database.User, account database.Account) Session {
}
}
-func (a *App) getMe(w http.ResponseWriter, r *http.Request) {
+func (a *API) getMe(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
if !ok {
HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
@@ -61,7 +60,7 @@ func (a *App) getMe(w http.ResponseWriter, r *http.Request) {
}
var account database.Account
- if err := a.DB.Where("user_id = ?", user.ID).First(&account).Error; err != nil {
+ if err := a.App.DB.Where("user_id = ?", user.ID).First(&account).Error; err != nil {
HandleError(w, "finding account", err, http.StatusInternalServerError)
return
}
@@ -74,8 +73,8 @@ func (a *App) getMe(w http.ResponseWriter, r *http.Request) {
User: session,
}
- tx := a.DB.Begin()
- if err := operations.TouchLastLoginAt(user, tx); err != nil {
+ tx := a.App.DB.Begin()
+ if err := a.App.TouchLastLoginAt(user, tx); err != nil {
tx.Rollback()
// In case of an error, gracefully continue to avoid disturbing the service
log.Println("error touching last_login_at", err.Error())
@@ -89,7 +88,7 @@ type createResetTokenPayload struct {
Email string `json:"email"`
}
-func (a *App) createResetToken(w http.ResponseWriter, r *http.Request) {
+func (a *API) createResetToken(w http.ResponseWriter, r *http.Request) {
var params createResetTokenPayload
if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
http.Error(w, "invalid payload", http.StatusBadRequest)
@@ -97,7 +96,7 @@ func (a *App) createResetToken(w http.ResponseWriter, r *http.Request) {
}
var account database.Account
- conn := a.DB.Where("email = ?", params.Email).First(&account)
+ conn := a.App.DB.Where("email = ?", params.Email).First(&account)
if conn.RecordNotFound() {
return
}
@@ -123,22 +122,19 @@ func (a *App) createResetToken(w http.ResponseWriter, r *http.Request) {
Type: database.TokenTypeResetPassword,
}
- if err := a.DB.Save(&token).Error; err != nil {
+ if err := a.App.DB.Save(&token).Error; err != nil {
HandleError(w, errors.Wrap(err, "saving token").Error(), nil, http.StatusInternalServerError)
return
}
- body, err := a.EmailTemplates.Execute(mailer.EmailTypeResetPassword, mailer.EmailKindText, mailer.EmailResetPasswordTmplData{
- AccountEmail: account.Email.String,
- Token: resetToken,
- WebURL: a.WebURL,
- })
- if err != nil {
- HandleError(w, errors.Wrap(err, "executing reset password email template").Error(), nil, http.StatusInternalServerError)
- }
+ if err := a.App.SendPasswordResetEmail(account.Email.String, resetToken); err != nil {
+ if errors.Cause(err) == mailer.ErrSMTPNotConfigured {
+ respondInvalidSMTPConfig(w)
+ } else {
+ HandleError(w, errors.Wrap(err, "sending password reset email").Error(), nil, http.StatusInternalServerError)
+ }
- if err := a.EmailBackend.Queue("Reset your password", "sung@getdnote.com", []string{params.Email}, "text/plain", body); err != nil {
- HandleError(w, errors.Wrap(err, "queueing email").Error(), nil, http.StatusInternalServerError)
+ return
}
}
@@ -147,7 +143,7 @@ type resetPasswordPayload struct {
Token string `json:"token"`
}
-func (a *App) resetPassword(w http.ResponseWriter, r *http.Request) {
+func (a *API) resetPassword(w http.ResponseWriter, r *http.Request) {
var params resetPasswordPayload
if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
http.Error(w, "invalid payload", http.StatusBadRequest)
@@ -155,7 +151,7 @@ func (a *App) resetPassword(w http.ResponseWriter, r *http.Request) {
}
var token database.Token
- conn := a.DB.Where("value = ? AND type =? AND used_at IS NULL", params.Token, database.TokenTypeResetPassword).First(&token)
+ conn := a.App.DB.Where("value = ? AND type =? AND used_at IS NULL", params.Token, database.TokenTypeResetPassword).First(&token)
if conn.RecordNotFound() {
http.Error(w, "invalid token", http.StatusBadRequest)
return
@@ -176,7 +172,7 @@ func (a *App) resetPassword(w http.ResponseWriter, r *http.Request) {
return
}
- tx := a.DB.Begin()
+ tx := a.App.DB.Begin()
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(params.Password), bcrypt.DefaultCost)
if err != nil {
@@ -186,7 +182,7 @@ func (a *App) resetPassword(w http.ResponseWriter, r *http.Request) {
}
var account database.Account
- if err := a.DB.Where("user_id = ?", token.UserID).First(&account).Error; err != nil {
+ if err := a.App.DB.Where("user_id = ?", token.UserID).First(&account).Error; err != nil {
tx.Rollback()
HandleError(w, errors.Wrap(err, "finding user").Error(), nil, http.StatusInternalServerError)
return
@@ -206,10 +202,10 @@ func (a *App) resetPassword(w http.ResponseWriter, r *http.Request) {
tx.Commit()
var user database.User
- if err := a.DB.Where("id = ?", account.UserID).First(&user).Error; err != nil {
+ if err := a.App.DB.Where("id = ?", account.UserID).First(&user).Error; err != nil {
HandleError(w, errors.Wrap(err, "finding user").Error(), nil, http.StatusInternalServerError)
return
}
- respondWithSession(a.DB, w, user.ID, http.StatusOK)
+ a.respondWithSession(a.App.DB, w, user.ID, http.StatusOK)
}
diff --git a/pkg/server/handlers/auth_test.go b/pkg/server/handlers/auth_test.go
index ff3d8784..dcdc6b79 100644
--- a/pkg/server/handlers/auth_test.go
+++ b/pkg/server/handlers/auth_test.go
@@ -25,6 +25,7 @@ import (
"github.com/dnote/dnote/pkg/assert"
"github.com/dnote/dnote/pkg/clock"
+ "github.com/dnote/dnote/pkg/server/app"
"github.com/dnote/dnote/pkg/server/database"
"github.com/dnote/dnote/pkg/server/testutils"
"golang.org/x/crypto/bcrypt"
@@ -35,7 +36,7 @@ func TestGetMe(t *testing.T) {
defer testutils.ClearData()
// Setup
- server := MustNewServer(t, &App{
+ server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
defer server.Close()
@@ -62,7 +63,7 @@ func TestCreateResetToken(t *testing.T) {
defer testutils.ClearData()
// Setup
- server := MustNewServer(t, &App{
+ server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
@@ -96,7 +97,7 @@ func TestCreateResetToken(t *testing.T) {
defer testutils.ClearData()
// Setup
- server := MustNewServer(t, &App{
+ server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
@@ -126,7 +127,7 @@ func TestResetPassword(t *testing.T) {
defer testutils.ClearData()
// Setup
- server := MustNewServer(t, &App{
+ server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
@@ -173,7 +174,7 @@ func TestResetPassword(t *testing.T) {
defer testutils.ClearData()
// Setup
- server := MustNewServer(t, &App{
+ server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
@@ -212,7 +213,7 @@ func TestResetPassword(t *testing.T) {
defer testutils.ClearData()
// Setup
- server := MustNewServer(t, &App{
+ server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
@@ -250,7 +251,7 @@ func TestResetPassword(t *testing.T) {
defer testutils.ClearData()
// Setup
- server := MustNewServer(t, &App{
+ server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
@@ -299,7 +300,7 @@ func TestResetPassword(t *testing.T) {
defer testutils.ClearData()
// Setup
- server := MustNewServer(t, &App{
+ server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
diff --git a/pkg/server/handlers/classic.go b/pkg/server/handlers/classic.go
index 0f1b0c98..aaf8ffb4 100644
--- a/pkg/server/handlers/classic.go
+++ b/pkg/server/handlers/classic.go
@@ -26,13 +26,12 @@ import (
"github.com/dnote/dnote/pkg/server/database"
"github.com/dnote/dnote/pkg/server/helpers"
"github.com/dnote/dnote/pkg/server/log"
- "github.com/dnote/dnote/pkg/server/operations"
"github.com/dnote/dnote/pkg/server/presenters"
"github.com/pkg/errors"
"golang.org/x/crypto/bcrypt"
)
-func (a *App) classicMigrate(w http.ResponseWriter, r *http.Request) {
+func (a *API) classicMigrate(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
if !ok {
HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
@@ -40,12 +39,12 @@ func (a *App) classicMigrate(w http.ResponseWriter, r *http.Request) {
}
var account database.Account
- if err := a.DB.Where("user_id = ?", user.ID).First(&account).Error; err != nil {
+ if err := a.App.DB.Where("user_id = ?", user.ID).First(&account).Error; err != nil {
HandleError(w, "finding account", err, http.StatusInternalServerError)
return
}
- if err := a.DB.Model(&account).
+ if err := a.App.DB.Model(&account).
Update(map[string]interface{}{
"salt": "",
"auth_key_hash": "",
@@ -63,7 +62,7 @@ type PresigninResponse struct {
Iteration int `json:"iteration"`
}
-func (a *App) classicPresignin(w http.ResponseWriter, r *http.Request) {
+func (a *API) classicPresignin(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
email := q.Get("email")
if email == "" {
@@ -72,7 +71,7 @@ func (a *App) classicPresignin(w http.ResponseWriter, r *http.Request) {
}
var account database.Account
- conn := a.DB.Where("email = ?", email).First(&account)
+ conn := a.App.DB.Where("email = ?", email).First(&account)
if !conn.RecordNotFound() && conn.Error != nil {
HandleError(w, "getting user", conn.Error, http.StatusInternalServerError)
return
@@ -101,7 +100,7 @@ type classicSigninPayload struct {
AuthKey string `json:"auth_key"`
}
-func (a *App) classicSignin(w http.ResponseWriter, r *http.Request) {
+func (a *API) classicSignin(w http.ResponseWriter, r *http.Request) {
var params classicSigninPayload
if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
HandleError(w, "decoding payload", err, http.StatusInternalServerError)
@@ -114,7 +113,7 @@ func (a *App) classicSignin(w http.ResponseWriter, r *http.Request) {
}
var account database.Account
- conn := a.DB.Where("email = ?", params.Email).First(&account)
+ conn := a.App.DB.Where("email = ?", params.Email).First(&account)
if conn.RecordNotFound() {
http.Error(w, ErrLoginFailure.Error(), http.StatusUnauthorized)
return
@@ -132,7 +131,7 @@ func (a *App) classicSignin(w http.ResponseWriter, r *http.Request) {
return
}
- session, err := operations.CreateSession(a.DB, account.UserID)
+ session, err := a.App.CreateSession(account.UserID)
if err != nil {
HandleError(w, "creating session", nil, http.StatusBadRequest)
return
@@ -156,7 +155,7 @@ func (a *App) classicSignin(w http.ResponseWriter, r *http.Request) {
}
}
-func (a *App) classicGetMe(w http.ResponseWriter, r *http.Request) {
+func (a *API) classicGetMe(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
if !ok {
HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
@@ -164,7 +163,7 @@ func (a *App) classicGetMe(w http.ResponseWriter, r *http.Request) {
}
var account database.Account
- if err := a.DB.Where("user_id = ?", user.ID).First(&account).Error; err != nil {
+ if err := a.App.DB.Where("user_id = ?", user.ID).First(&account).Error; err != nil {
HandleError(w, "finding account", err, http.StatusInternalServerError)
return
}
@@ -214,7 +213,7 @@ type classicSetPasswordPayload struct {
Password string
}
-func (a *App) classicSetPassword(w http.ResponseWriter, r *http.Request) {
+func (a *API) classicSetPassword(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
if !ok {
HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
@@ -228,7 +227,7 @@ func (a *App) classicSetPassword(w http.ResponseWriter, r *http.Request) {
}
var account database.Account
- if err := a.DB.Where("user_id = ?", user.ID).First(&account).Error; err != nil {
+ if err := a.App.DB.Where("user_id = ?", user.ID).First(&account).Error; err != nil {
HandleError(w, "getting user", nil, http.StatusInternalServerError)
return
}
@@ -239,7 +238,7 @@ func (a *App) classicSetPassword(w http.ResponseWriter, r *http.Request) {
return
}
- if err := a.DB.Model(&account).Update("password", string(hashedNewPassword)).Error; err != nil {
+ if err := a.App.DB.Model(&account).Update("password", string(hashedNewPassword)).Error; err != nil {
http.Error(w, errors.Wrap(err, "updating password").Error(), http.StatusInternalServerError)
return
}
@@ -247,7 +246,7 @@ func (a *App) classicSetPassword(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}
-func (a *App) classicGetNotes(w http.ResponseWriter, r *http.Request) {
+func (a *API) classicGetNotes(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
if !ok {
HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
@@ -255,7 +254,7 @@ func (a *App) classicGetNotes(w http.ResponseWriter, r *http.Request) {
}
var notes []database.Note
- if err := a.DB.Where("user_id = ? AND encrypted = true", user.ID).Find(¬es).Error; err != nil {
+ if err := a.App.DB.Where("user_id = ? AND encrypted = true", user.ID).Find(¬es).Error; err != nil {
HandleError(w, "finding notes", err, http.StatusInternalServerError)
return
}
diff --git a/pkg/server/handlers/classic_test.go b/pkg/server/handlers/classic_test.go
index 0a01fc59..4c92712f 100644
--- a/pkg/server/handlers/classic_test.go
+++ b/pkg/server/handlers/classic_test.go
@@ -26,6 +26,7 @@ import (
"github.com/dnote/dnote/pkg/assert"
"github.com/dnote/dnote/pkg/clock"
+ "github.com/dnote/dnote/pkg/server/app"
"github.com/dnote/dnote/pkg/server/database"
"github.com/dnote/dnote/pkg/server/testutils"
"github.com/pkg/errors"
@@ -68,7 +69,7 @@ func TestClassicPresignin(t *testing.T) {
t.Run(fmt.Sprintf("presignin %s", tc.email), func(t *testing.T) {
// Setup
- server := MustNewServer(t, &App{
+ server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
defer server.Close()
@@ -96,7 +97,7 @@ func TestClassicPresignin_MissingParams(t *testing.T) {
defer testutils.ClearData()
// Setup
- server := MustNewServer(t, &App{
+ server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
defer server.Close()
@@ -118,7 +119,7 @@ func TestClassicSignin(t *testing.T) {
testutils.MustExec(t, testutils.DB.Save(&alice), "saving alice")
// Setup
- server := MustNewServer(t, &App{
+ server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
defer server.Close()
@@ -216,7 +217,7 @@ func TestClassicSignin_Failure(t *testing.T) {
t.Run(fmt.Sprintf("signin %s %s", tc.email, tc.authKey), func(t *testing.T) {
// Setup
- server := MustNewServer(t, &App{
+ server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
defer server.Close()
diff --git a/pkg/server/handlers/health.go b/pkg/server/handlers/health.go
index 1cdc03ef..29e12020 100644
--- a/pkg/server/handlers/health.go
+++ b/pkg/server/handlers/health.go
@@ -22,7 +22,7 @@ import (
"net/http"
)
-func (a *App) checkHealth(w http.ResponseWriter, r *http.Request) {
+func (a *API) checkHealth(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
}
diff --git a/pkg/server/handlers/health_test.go b/pkg/server/handlers/health_test.go
index 78067e82..c2f07027 100644
--- a/pkg/server/handlers/health_test.go
+++ b/pkg/server/handlers/health_test.go
@@ -24,13 +24,14 @@ import (
"github.com/dnote/dnote/pkg/assert"
"github.com/dnote/dnote/pkg/clock"
+ "github.com/dnote/dnote/pkg/server/app"
"github.com/dnote/dnote/pkg/server/testutils"
"github.com/jinzhu/gorm"
)
func TestCheckHealth(t *testing.T) {
// Setup
- server := MustNewServer(t, &App{
+ server := MustNewServer(t, &app.App{
DB: &gorm.DB{},
Clock: clock.NewMock(),
})
diff --git a/pkg/server/handlers/helpers.go b/pkg/server/handlers/helpers.go
index 1fd4dcb7..87ddfabe 100644
--- a/pkg/server/handlers/helpers.go
+++ b/pkg/server/handlers/helpers.go
@@ -136,7 +136,7 @@ func respondJSON(w http.ResponseWriter, statusCode int, payload interface{}) {
}
// notSupported is the handler for the route that is no longer supported
-func (a *App) notSupported(w http.ResponseWriter, r *http.Request) {
+func (a *API) notSupported(w http.ResponseWriter, r *http.Request) {
http.Error(w, "API version is not supported. Please upgrade your client.", http.StatusGone)
return
}
@@ -155,3 +155,7 @@ func respondUnauthorized(w http.ResponseWriter) {
func RespondNotFound(w http.ResponseWriter) {
http.Error(w, "not found", http.StatusNotFound)
}
+
+func respondInvalidSMTPConfig(w http.ResponseWriter) {
+ http.Error(w, "SMTP is not configured", http.StatusInternalServerError)
+}
diff --git a/pkg/server/handlers/main_test.go b/pkg/server/handlers/main_test.go
index c090f138..a3d68bcc 100644
--- a/pkg/server/handlers/main_test.go
+++ b/pkg/server/handlers/main_test.go
@@ -1,3 +1,21 @@
+/* Copyright (C) 2019 Monomax Software Pty Ltd
+ *
+ * This file is part of Dnote.
+ *
+ * Dnote 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.
+ *
+ * Dnote 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 Dnote. If not, see .
+ */
+
package handlers
import (
diff --git a/pkg/server/handlers/notes.go b/pkg/server/handlers/notes.go
index a850a82d..753718b6 100644
--- a/pkg/server/handlers/notes.go
+++ b/pkg/server/handlers/notes.go
@@ -28,7 +28,6 @@ import (
"github.com/dnote/dnote/pkg/server/database"
"github.com/dnote/dnote/pkg/server/helpers"
- "github.com/dnote/dnote/pkg/server/operations"
"github.com/dnote/dnote/pkg/server/presenters"
"github.com/gorilla/mux"
"github.com/jinzhu/gorm"
@@ -99,8 +98,8 @@ func getNoteBaseQuery(db *gorm.DB, noteUUID string, search string) *gorm.DB {
return conn
}
-func (a *App) getNote(w http.ResponseWriter, r *http.Request) {
- user, _, err := AuthWithSession(a.DB, r, nil)
+func (a *API) getNote(w http.ResponseWriter, r *http.Request) {
+ user, _, err := AuthWithSession(a.App.DB, r, nil)
if err != nil {
HandleError(w, "authenticating", err, http.StatusInternalServerError)
return
@@ -109,7 +108,7 @@ func (a *App) getNote(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
noteUUID := vars["noteUUID"]
- note, ok, err := operations.GetNote(a.DB, noteUUID, user)
+ note, ok, err := a.App.GetNote(noteUUID, user)
if !ok {
RespondNotFound(w)
return
@@ -135,7 +134,7 @@ type dateRange struct {
upper int64
}
-func (a *App) getNotes(w http.ResponseWriter, r *http.Request) {
+func (a *API) getNotes(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
if !ok {
HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
@@ -143,7 +142,7 @@ func (a *App) getNotes(w http.ResponseWriter, r *http.Request) {
}
query := r.URL.Query()
- respondGetNotes(a.DB, user.ID, query, w)
+ respondGetNotes(a.App.DB, user.ID, query, w)
}
func respondGetNotes(db *gorm.DB, userID int, query url.Values, w http.ResponseWriter) {
@@ -305,7 +304,7 @@ func escapeSearchQuery(searchQuery string) string {
return strings.Join(strings.Fields(searchQuery), "&")
}
-func (a *App) legacyGetNotes(w http.ResponseWriter, r *http.Request) {
+func (a *API) legacyGetNotes(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
if !ok {
HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
@@ -313,7 +312,7 @@ func (a *App) legacyGetNotes(w http.ResponseWriter, r *http.Request) {
}
var notes []database.Note
- if err := a.DB.Where("user_id = ? AND encrypted = true", user.ID).Find(¬es).Error; err != nil {
+ if err := a.App.DB.Where("user_id = ? AND encrypted = true", user.ID).Find(¬es).Error; err != nil {
HandleError(w, "finding notes", err, http.StatusInternalServerError)
return
}
diff --git a/pkg/server/handlers/notes_test.go b/pkg/server/handlers/notes_test.go
index 22ec8918..0cfca30d 100644
--- a/pkg/server/handlers/notes_test.go
+++ b/pkg/server/handlers/notes_test.go
@@ -28,6 +28,7 @@ import (
"github.com/dnote/dnote/pkg/assert"
"github.com/dnote/dnote/pkg/clock"
+ "github.com/dnote/dnote/pkg/server/app"
"github.com/dnote/dnote/pkg/server/database"
"github.com/dnote/dnote/pkg/server/presenters"
"github.com/dnote/dnote/pkg/server/testutils"
@@ -59,7 +60,7 @@ func TestGetNotes(t *testing.T) {
defer testutils.ClearData()
// Setup
- server := MustNewServer(t, &App{
+ server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
@@ -171,7 +172,7 @@ func TestGetNote(t *testing.T) {
defer testutils.ClearData()
// Setup
- server := MustNewServer(t, &App{
+ server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
diff --git a/pkg/server/handlers/repetition_rules.go b/pkg/server/handlers/repetition_rules.go
index f363f557..72535bfd 100644
--- a/pkg/server/handlers/repetition_rules.go
+++ b/pkg/server/handlers/repetition_rules.go
@@ -30,7 +30,7 @@ import (
"github.com/pkg/errors"
)
-func (a *App) getRepetitionRule(w http.ResponseWriter, r *http.Request) {
+func (a *API) getRepetitionRule(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
if !ok {
HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
@@ -46,7 +46,7 @@ func (a *App) getRepetitionRule(w http.ResponseWriter, r *http.Request) {
}
var repetitionRule database.RepetitionRule
- if err := a.DB.Where("user_id = ? AND uuid = ?", user.ID, repetitionRuleUUID).Preload("Books").Find(&repetitionRule).Error; err != nil {
+ if err := a.App.DB.Where("user_id = ? AND uuid = ?", user.ID, repetitionRuleUUID).Preload("Books").Find(&repetitionRule).Error; err != nil {
HandleError(w, "getting repetition rules", err, http.StatusInternalServerError)
return
}
@@ -55,7 +55,7 @@ func (a *App) getRepetitionRule(w http.ResponseWriter, r *http.Request) {
respondJSON(w, http.StatusOK, resp)
}
-func (a *App) getRepetitionRules(w http.ResponseWriter, r *http.Request) {
+func (a *API) getRepetitionRules(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
if !ok {
HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
@@ -63,7 +63,7 @@ func (a *App) getRepetitionRules(w http.ResponseWriter, r *http.Request) {
}
var repetitionRules []database.RepetitionRule
- if err := a.DB.Where("user_id = ?", user.ID).Preload("Books").Order("last_active DESC").Find(&repetitionRules).Error; err != nil {
+ if err := a.App.DB.Where("user_id = ?", user.ID).Preload("Books").Order("last_active DESC").Find(&repetitionRules).Error; err != nil {
HandleError(w, "getting repetition rules", err, http.StatusInternalServerError)
return
}
@@ -273,7 +273,7 @@ func calcNextActive(now time.Time, p calcNextActiveParams) int64 {
return t0 + p.Frequency
}
-func (a *App) createRepetitionRule(w http.ResponseWriter, r *http.Request) {
+func (a *API) createRepetitionRule(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
if !ok {
HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
@@ -287,12 +287,12 @@ func (a *App) createRepetitionRule(w http.ResponseWriter, r *http.Request) {
}
var books []database.Book
- if err := a.DB.Where("user_id = ? AND uuid IN (?)", user.ID, params.GetBookUUIDs()).Find(&books).Error; err != nil {
+ if err := a.App.DB.Where("user_id = ? AND uuid IN (?)", user.ID, params.GetBookUUIDs()).Find(&books).Error; err != nil {
HandleError(w, "finding books", nil, http.StatusInternalServerError)
return
}
- nextActive := calcNextActive(a.Clock.Now(), calcNextActiveParams{
+ nextActive := calcNextActive(a.App.Clock.Now(), calcNextActiveParams{
Hour: params.GetHour(),
Minute: params.GetMinute(),
Frequency: params.GetFrequency(),
@@ -310,7 +310,7 @@ func (a *App) createRepetitionRule(w http.ResponseWriter, r *http.Request) {
NoteCount: params.GetNoteCount(),
Enabled: params.GetEnabled(),
}
- if err := a.DB.Create(&record).Error; err != nil {
+ if err := a.App.DB.Create(&record).Error; err != nil {
HandleError(w, "creating a repetition rule", err, http.StatusInternalServerError)
return
}
@@ -333,7 +333,7 @@ func parseUpdateDigestParams(r *http.Request) (repetitionRuleParams, error) {
return ret, nil
}
-func (a *App) deleteRepetitionRule(w http.ResponseWriter, r *http.Request) {
+func (a *API) deleteRepetitionRule(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
if !ok {
HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
@@ -344,7 +344,7 @@ func (a *App) deleteRepetitionRule(w http.ResponseWriter, r *http.Request) {
repetitionRuleUUID := vars["repetitionRuleUUID"]
var rule database.RepetitionRule
- conn := a.DB.Where("uuid = ? AND user_id = ?", repetitionRuleUUID, user.ID).First(&rule)
+ conn := a.App.DB.Where("uuid = ? AND user_id = ?", repetitionRuleUUID, user.ID).First(&rule)
if conn.RecordNotFound() {
http.Error(w, "Not found", http.StatusNotFound)
@@ -354,14 +354,14 @@ func (a *App) deleteRepetitionRule(w http.ResponseWriter, r *http.Request) {
return
}
- if err := a.DB.Exec("DELETE from repetition_rules WHERE uuid = ?", rule.UUID).Error; err != nil {
+ if err := a.App.DB.Exec("DELETE from repetition_rules WHERE uuid = ?", rule.UUID).Error; err != nil {
HandleError(w, "deleting the repetition rule", err, http.StatusInternalServerError)
}
w.WriteHeader(http.StatusOK)
}
-func (a *App) updateRepetitionRule(w http.ResponseWriter, r *http.Request) {
+func (a *API) updateRepetitionRule(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
if !ok {
HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
@@ -377,7 +377,7 @@ func (a *App) updateRepetitionRule(w http.ResponseWriter, r *http.Request) {
return
}
- tx := a.DB.Begin()
+ tx := a.App.DB.Begin()
var repetitionRule database.RepetitionRule
if err := tx.Where("user_id = ? AND uuid = ?", user.ID, repetitionRuleUUID).Preload("Books").First(&repetitionRule).Error; err != nil {
@@ -393,7 +393,7 @@ func (a *App) updateRepetitionRule(w http.ResponseWriter, r *http.Request) {
repetitionRule.Enabled = enabled
if enabled && !repetitionRule.Enabled {
- repetitionRule.NextActive = calcNextActive(a.Clock.Now(), calcNextActiveParams{
+ repetitionRule.NextActive = calcNextActive(a.App.Clock.Now(), calcNextActiveParams{
Hour: repetitionRule.Hour,
Minute: repetitionRule.Minute,
Frequency: repetitionRule.Frequency,
@@ -412,7 +412,7 @@ func (a *App) updateRepetitionRule(w http.ResponseWriter, r *http.Request) {
frequency := params.GetFrequency()
repetitionRule.Frequency = frequency
- repetitionRule.NextActive = calcNextActive(a.Clock.Now(), calcNextActiveParams{
+ repetitionRule.NextActive = calcNextActive(a.App.Clock.Now(), calcNextActiveParams{
Hour: repetitionRule.Hour,
Minute: repetitionRule.Minute,
Frequency: frequency,
diff --git a/pkg/server/handlers/repetition_rules_test.go b/pkg/server/handlers/repetition_rules_test.go
index 97c9eadd..b977a453 100644
--- a/pkg/server/handlers/repetition_rules_test.go
+++ b/pkg/server/handlers/repetition_rules_test.go
@@ -27,6 +27,7 @@ import (
"github.com/dnote/dnote/pkg/assert"
"github.com/dnote/dnote/pkg/clock"
+ "github.com/dnote/dnote/pkg/server/app"
"github.com/dnote/dnote/pkg/server/database"
"github.com/dnote/dnote/pkg/server/presenters"
"github.com/dnote/dnote/pkg/server/testutils"
@@ -34,12 +35,12 @@ import (
)
func TestGetRepetitionRule(t *testing.T) {
-
+
defer testutils.ClearData()
// Setup
- server := MustNewServer(t, &App{
-
+ server := MustNewServer(t, &app.App{
+
Clock: clock.NewMock(),
})
defer server.Close()
@@ -109,12 +110,12 @@ func TestGetRepetitionRule(t *testing.T) {
}
func TestGetRepetitionRules(t *testing.T) {
-
+
defer testutils.ClearData()
// Setup
- server := MustNewServer(t, &App{
-
+ server := MustNewServer(t, &app.App{
+
Clock: clock.NewMock(),
})
defer server.Close()
@@ -215,7 +216,7 @@ func TestGetRepetitionRules(t *testing.T) {
func TestCreateRepetitionRules(t *testing.T) {
t.Run("all books", func(t *testing.T) {
-
+
defer testutils.ClearData()
// Setup
@@ -223,8 +224,8 @@ func TestCreateRepetitionRules(t *testing.T) {
t0 := time.Date(2009, time.November, 1, 2, 3, 4, 5, time.UTC)
c.SetNow(t0)
- server := MustNewServer(t, &App{
-
+ server := MustNewServer(t, &app.App{
+
Clock: c,
})
defer server.Close()
@@ -274,7 +275,7 @@ func TestCreateRepetitionRules(t *testing.T) {
}
for _, tc := range bookDomainTestCases {
t.Run(tc, func(t *testing.T) {
-
+
defer testutils.ClearData()
// Setup
@@ -282,8 +283,8 @@ func TestCreateRepetitionRules(t *testing.T) {
t0 := time.Date(2009, time.November, 1, 2, 3, 4, 5, time.UTC)
c.SetNow(t0)
- server := MustNewServer(t, &App{
-
+ server := MustNewServer(t, &app.App{
+
Clock: c,
})
defer server.Close()
@@ -339,15 +340,15 @@ func TestCreateRepetitionRules(t *testing.T) {
}
func TestUpdateRepetitionRules(t *testing.T) {
-
+
defer testutils.ClearData()
// Setup
c := clock.NewMock()
t0 := time.Date(2009, time.November, 1, 2, 3, 4, 5, time.UTC)
c.SetNow(t0)
- server := MustNewServer(t, &App{
-
+ server := MustNewServer(t, &app.App{
+
Clock: c,
})
defer server.Close()
@@ -417,12 +418,12 @@ func TestUpdateRepetitionRules(t *testing.T) {
}
func TestDeleteRepetitionRules(t *testing.T) {
-
+
defer testutils.ClearData()
// Setup
- server := MustNewServer(t, &App{
-
+ server := MustNewServer(t, &app.App{
+
Clock: clock.NewMock(),
})
defer server.Close()
@@ -543,12 +544,12 @@ func TestCreateUpdateRepetitionRules_BadRequest(t *testing.T) {
for idx, tc := range testCases {
t.Run(fmt.Sprintf("test case - create %d", idx), func(t *testing.T) {
-
+
defer testutils.ClearData()
// Setup
- server := MustNewServer(t, &App{
-
+ server := MustNewServer(t, &app.App{
+
Clock: clock.NewMock(),
})
defer server.Close()
@@ -568,7 +569,7 @@ func TestCreateUpdateRepetitionRules_BadRequest(t *testing.T) {
})
t.Run(fmt.Sprintf("test case %d - update", idx), func(t *testing.T) {
-
+
defer testutils.ClearData()
// Setup
@@ -592,8 +593,8 @@ func TestCreateUpdateRepetitionRules_BadRequest(t *testing.T) {
}
testutils.MustExec(t, testutils.DB.Save(&b1), "preparing book1")
- server := MustNewServer(t, &App{
-
+ server := MustNewServer(t, &app.App{
+
Clock: clock.NewMock(),
})
defer server.Close()
@@ -628,12 +629,12 @@ func TestCreateRepetitionRules_BadRequest(t *testing.T) {
for idx, tc := range testCases {
t.Run(fmt.Sprintf("test case %d", idx), func(t *testing.T) {
-
+
defer testutils.ClearData()
// Setup
- server := MustNewServer(t, &App{
-
+ server := MustNewServer(t, &app.App{
+
Clock: clock.NewMock(),
})
defer server.Close()
diff --git a/pkg/server/handlers/routes.go b/pkg/server/handlers/routes.go
index 5981cc0a..1417677c 100644
--- a/pkg/server/handlers/routes.go
+++ b/pkg/server/handlers/routes.go
@@ -26,30 +26,16 @@ import (
"strings"
"time"
- "github.com/dnote/dnote/pkg/clock"
+ "github.com/dnote/dnote/pkg/server/app"
"github.com/dnote/dnote/pkg/server/database"
"github.com/dnote/dnote/pkg/server/helpers"
"github.com/dnote/dnote/pkg/server/log"
- "github.com/dnote/dnote/pkg/server/mailer"
"github.com/gorilla/mux"
"github.com/jinzhu/gorm"
"github.com/pkg/errors"
"github.com/stripe/stripe-go"
)
-var (
- // ErrEmptyDB is an error for missing database connection in the app configuration
- ErrEmptyDB = errors.New("No database connection was provided")
- // ErrEmptyClock is an error for missing clock in the app configuration
- ErrEmptyClock = errors.New("No clock was provided")
- // ErrEmptyWebURL is an error for missing WebURL content in the app configuration
- ErrEmptyWebURL = errors.New("No WebURL was provided")
- // ErrEmptyEmailTemplates is an error for missing EmailTemplates content in the app configuration
- ErrEmptyEmailTemplates = errors.New("No EmailTemplate store was provided")
- // ErrEmptyEmailBackend is an error for missing EmailBackend content in the app configuration
- ErrEmptyEmailBackend = errors.New("No EmailBackend was provided")
-)
-
// Route represents a single route
type Route struct {
Method string
@@ -199,9 +185,9 @@ type AuthMiddlewareParams struct {
ProOnly bool
}
-func (a *App) auth(next http.HandlerFunc, p *AuthMiddlewareParams) http.HandlerFunc {
+func (a *API) auth(next http.HandlerFunc, p *AuthMiddlewareParams) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- user, ok, err := AuthWithSession(a.DB, r, p)
+ user, ok, err := AuthWithSession(a.App.DB, r, p)
if !ok {
respondUnauthorized(w)
return
@@ -223,9 +209,9 @@ func (a *App) auth(next http.HandlerFunc, p *AuthMiddlewareParams) http.HandlerF
})
}
-func (a *App) tokenAuth(next http.HandlerFunc, tokenType string, p *AuthMiddlewareParams) http.HandlerFunc {
+func (a *API) tokenAuth(next http.HandlerFunc, tokenType string, p *AuthMiddlewareParams) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- user, token, ok, err := authWithToken(a.DB, r, tokenType, p)
+ user, token, ok, err := authWithToken(a.App.DB, r, tokenType, p)
if err != nil {
// log the error and continue
log.ErrorWrap(err, "authenticating with token")
@@ -237,7 +223,7 @@ func (a *App) tokenAuth(next http.HandlerFunc, tokenType string, p *AuthMiddlewa
ctx = context.WithValue(ctx, helpers.KeyToken, token)
} else {
// If token-based auth fails, fall back to session-based auth
- user, ok, err = AuthWithSession(a.DB, r, p)
+ user, ok, err = AuthWithSession(a.App.DB, r, p)
if err != nil {
HandleError(w, "authenticating with session", err, http.StatusInternalServerError)
return
@@ -316,54 +302,29 @@ func applyMiddleware(h http.HandlerFunc, rateLimit bool) http.Handler {
return ret
}
-// App is an application configuration
-type App struct {
- DB *gorm.DB
- Clock clock.Clock
- StripeAPIBackend stripe.Backend
- EmailTemplates mailer.Templates
- EmailBackend mailer.Backend
- WebURL string
-}
-
-func (a *App) validate() error {
- if a.WebURL == "" {
- return ErrEmptyWebURL
- }
- if a.Clock == nil {
- return ErrEmptyClock
- }
- if a.EmailTemplates == nil {
- return ErrEmptyEmailTemplates
- }
- if a.EmailBackend == nil {
- return ErrEmptyEmailBackend
- }
- if a.DB == nil {
- return ErrEmptyDB
- }
-
- return nil
+// API is a web API configuration
+type API struct {
+ App *app.App
}
// init sets up the application based on the configuration
-func (a *App) init() error {
- if err := a.validate(); err != nil {
+func (a *API) init() error {
+ if err := a.App.Validate(); err != nil {
return errors.Wrap(err, "validating the app parameters")
}
stripe.Key = os.Getenv("StripeSecretKey")
- if a.StripeAPIBackend != nil {
- stripe.SetBackend(stripe.APIBackend, a.StripeAPIBackend)
+ if a.App.StripeAPIBackend != nil {
+ stripe.SetBackend(stripe.APIBackend, a.App.StripeAPIBackend)
}
return nil
}
// NewRouter creates and returns a new router
-func NewRouter(app *App) (*mux.Router, error) {
- if err := app.init(); err != nil {
+func (a *API) NewRouter() (*mux.Router, error) {
+ if err := a.init(); err != nil {
return nil, errors.Wrap(err, "initializing app")
}
@@ -371,61 +332,61 @@ func NewRouter(app *App) (*mux.Router, error) {
var routes = []Route{
// internal
- {"GET", "/health", app.checkHealth, false},
- {"GET", "/me", app.auth(app.getMe, nil), true},
- {"POST", "/verification-token", app.auth(app.createVerificationToken, nil), true},
- {"PATCH", "/verify-email", app.verifyEmail, true},
- {"POST", "/reset-token", app.createResetToken, true},
- {"PATCH", "/reset-password", app.resetPassword, true},
- {"PATCH", "/account/profile", app.auth(app.updateProfile, nil), true},
- {"PATCH", "/account/password", app.auth(app.updatePassword, nil), true},
- {"GET", "/account/email-preference", app.tokenAuth(app.getEmailPreference, database.TokenTypeEmailPreference, nil), true},
- {"PATCH", "/account/email-preference", app.tokenAuth(app.updateEmailPreference, database.TokenTypeEmailPreference, nil), true},
- {"POST", "/subscriptions", app.auth(app.createSub, nil), true},
- {"PATCH", "/subscriptions", app.auth(app.updateSub, nil), true},
- {"POST", "/webhooks/stripe", app.stripeWebhook, true},
- {"GET", "/subscriptions", app.auth(app.getSub, nil), true},
- {"GET", "/stripe_source", app.auth(app.getStripeSource, nil), true},
- {"PATCH", "/stripe_source", app.auth(app.updateStripeSource, nil), true},
- {"GET", "/notes", app.auth(app.getNotes, nil), false},
- {"GET", "/notes/{noteUUID}", app.getNote, true},
- {"GET", "/calendar", app.auth(app.getCalendar, nil), true},
- {"GET", "/repetition_rules", app.auth(app.getRepetitionRules, nil), true},
- {"GET", "/repetition_rules/{repetitionRuleUUID}", app.tokenAuth(app.getRepetitionRule, database.TokenTypeRepetition, &proOnly), true},
- {"POST", "/repetition_rules", app.auth(app.createRepetitionRule, &proOnly), true},
- {"PATCH", "/repetition_rules/{repetitionRuleUUID}", app.tokenAuth(app.updateRepetitionRule, database.TokenTypeRepetition, &proOnly), true},
- {"DELETE", "/repetition_rules/{repetitionRuleUUID}", app.auth(app.deleteRepetitionRule, &proOnly), true},
+ {"GET", "/health", a.checkHealth, false},
+ {"GET", "/me", a.auth(a.getMe, nil), true},
+ {"POST", "/verification-token", a.auth(a.createVerificationToken, nil), true},
+ {"PATCH", "/verify-email", a.verifyEmail, true},
+ {"POST", "/reset-token", a.createResetToken, true},
+ {"PATCH", "/reset-password", a.resetPassword, true},
+ {"PATCH", "/account/profile", a.auth(a.updateProfile, nil), true},
+ {"PATCH", "/account/password", a.auth(a.updatePassword, nil), true},
+ {"GET", "/account/email-preference", a.tokenAuth(a.getEmailPreference, database.TokenTypeEmailPreference, nil), true},
+ {"PATCH", "/account/email-preference", a.tokenAuth(a.updateEmailPreference, database.TokenTypeEmailPreference, nil), true},
+ {"POST", "/subscriptions", a.auth(a.createSub, nil), true},
+ {"PATCH", "/subscriptions", a.auth(a.updateSub, nil), true},
+ {"POST", "/webhooks/stripe", a.stripeWebhook, true},
+ {"GET", "/subscriptions", a.auth(a.getSub, nil), true},
+ {"GET", "/stripe_source", a.auth(a.getStripeSource, nil), true},
+ {"PATCH", "/stripe_source", a.auth(a.updateStripeSource, nil), true},
+ {"GET", "/notes", a.auth(a.getNotes, nil), false},
+ {"GET", "/notes/{noteUUID}", a.getNote, true},
+ {"GET", "/calendar", a.auth(a.getCalendar, nil), true},
+ {"GET", "/repetition_rules", a.auth(a.getRepetitionRules, nil), true},
+ {"GET", "/repetition_rules/{repetitionRuleUUID}", a.tokenAuth(a.getRepetitionRule, database.TokenTypeRepetition, &proOnly), true},
+ {"POST", "/repetition_rules", a.auth(a.createRepetitionRule, &proOnly), true},
+ {"PATCH", "/repetition_rules/{repetitionRuleUUID}", a.tokenAuth(a.updateRepetitionRule, database.TokenTypeRepetition, &proOnly), true},
+ {"DELETE", "/repetition_rules/{repetitionRuleUUID}", a.auth(a.deleteRepetitionRule, &proOnly), true},
// migration of classic users
- {"GET", "/classic/presignin", cors(app.classicPresignin), true},
- {"POST", "/classic/signin", cors(app.classicSignin), true},
- {"PATCH", "/classic/migrate", app.auth(app.classicMigrate, &proOnly), true},
- {"GET", "/classic/notes", app.auth(app.classicGetNotes, nil), true},
- {"PATCH", "/classic/set-password", app.auth(app.classicSetPassword, nil), true},
+ {"GET", "/classic/presignin", cors(a.classicPresignin), true},
+ {"POST", "/classic/signin", cors(a.classicSignin), true},
+ {"PATCH", "/classic/migrate", a.auth(a.classicMigrate, &proOnly), true},
+ {"GET", "/classic/notes", a.auth(a.classicGetNotes, nil), true},
+ {"PATCH", "/classic/set-password", a.auth(a.classicSetPassword, nil), true},
// v3
- {"GET", "/v3/sync/fragment", cors(app.auth(app.GetSyncFragment, nil)), false},
- {"GET", "/v3/sync/state", cors(app.auth(app.GetSyncState, nil)), false},
- {"OPTIONS", "/v3/books", cors(app.BooksOptions), true},
- {"GET", "/v3/books", cors(app.auth(app.GetBooks, nil)), true},
- {"GET", "/v3/books/{bookUUID}", cors(app.auth(app.GetBook, nil)), true},
- {"POST", "/v3/books", cors(app.auth(app.CreateBook, nil)), false},
- {"PATCH", "/v3/books/{bookUUID}", cors(app.auth(app.UpdateBook, nil)), false},
- {"DELETE", "/v3/books/{bookUUID}", cors(app.auth(app.DeleteBook, nil)), false},
- {"OPTIONS", "/v3/notes", cors(app.NotesOptions), true},
- {"POST", "/v3/notes", cors(app.auth(app.CreateNote, nil)), false},
- {"PATCH", "/v3/notes/{noteUUID}", app.auth(app.UpdateNote, nil), false},
- {"DELETE", "/v3/notes/{noteUUID}", app.auth(app.DeleteNote, nil), false},
- {"POST", "/v3/signin", cors(app.signin), true},
- {"OPTIONS", "/v3/signout", cors(app.signoutOptions), true},
- {"POST", "/v3/signout", cors(app.signout), true},
- {"POST", "/v3/register", app.register, true},
+ {"GET", "/v3/sync/fragment", cors(a.auth(a.GetSyncFragment, nil)), false},
+ {"GET", "/v3/sync/state", cors(a.auth(a.GetSyncState, nil)), false},
+ {"OPTIONS", "/v3/books", cors(a.BooksOptions), true},
+ {"GET", "/v3/books", cors(a.auth(a.GetBooks, nil)), true},
+ {"GET", "/v3/books/{bookUUID}", cors(a.auth(a.GetBook, nil)), true},
+ {"POST", "/v3/books", cors(a.auth(a.CreateBook, nil)), false},
+ {"PATCH", "/v3/books/{bookUUID}", cors(a.auth(a.UpdateBook, nil)), false},
+ {"DELETE", "/v3/books/{bookUUID}", cors(a.auth(a.DeleteBook, nil)), false},
+ {"OPTIONS", "/v3/notes", cors(a.NotesOptions), true},
+ {"POST", "/v3/notes", cors(a.auth(a.CreateNote, nil)), false},
+ {"PATCH", "/v3/notes/{noteUUID}", a.auth(a.UpdateNote, nil), false},
+ {"DELETE", "/v3/notes/{noteUUID}", a.auth(a.DeleteNote, nil), false},
+ {"POST", "/v3/signin", cors(a.signin), true},
+ {"OPTIONS", "/v3/signout", cors(a.signoutOptions), true},
+ {"POST", "/v3/signout", cors(a.signout), true},
+ {"POST", "/v3/register", a.register, true},
}
router := mux.NewRouter().StrictSlash(true)
- router.PathPrefix("/v1").Handler(applyMiddleware(app.notSupported, true))
- router.PathPrefix("/v2").Handler(applyMiddleware(app.notSupported, true))
+ router.PathPrefix("/v1").Handler(applyMiddleware(a.notSupported, true))
+ router.PathPrefix("/v2").Handler(applyMiddleware(a.notSupported, true))
for _, route := range routes {
handler := route.HandlerFunc
diff --git a/pkg/server/handlers/routes_test.go b/pkg/server/handlers/routes_test.go
index 15e2da71..f1e252a4 100644
--- a/pkg/server/handlers/routes_test.go
+++ b/pkg/server/handlers/routes_test.go
@@ -27,6 +27,7 @@ import (
"github.com/dnote/dnote/pkg/assert"
"github.com/dnote/dnote/pkg/clock"
+ "github.com/dnote/dnote/pkg/server/app"
"github.com/dnote/dnote/pkg/server/database"
"github.com/dnote/dnote/pkg/server/mailer"
"github.com/dnote/dnote/pkg/server/testutils"
@@ -202,8 +203,8 @@ func TestAuthMiddleware(t *testing.T) {
handler := func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}
- app := App{DB: testutils.DB}
- server := httptest.NewServer(app.auth(handler, nil))
+ api := API{App: &app.App{DB: testutils.DB}}
+ server := httptest.NewServer(api.auth(handler, nil))
defer server.Close()
t.Run("with header", func(t *testing.T) {
@@ -310,8 +311,8 @@ func TestAuthMiddleware_ProOnly(t *testing.T) {
handler := func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}
- app := App{DB: testutils.DB}
- server := httptest.NewServer(app.auth(handler, &AuthMiddlewareParams{
+ api := API{App: &app.App{DB: testutils.DB}}
+ server := httptest.NewServer(api.auth(handler, &AuthMiddlewareParams{
ProOnly: true,
}))
defer server.Close()
@@ -403,8 +404,8 @@ func TestTokenAuthMiddleWare(t *testing.T) {
handler := func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}
- app := App{DB: testutils.DB}
- server := httptest.NewServer(app.tokenAuth(handler, database.TokenTypeEmailPreference, nil))
+ api := API{App: &app.App{DB: testutils.DB}}
+ server := httptest.NewServer(api.tokenAuth(handler, database.TokenTypeEmailPreference, nil))
defer server.Close()
t.Run("with token", func(t *testing.T) {
@@ -533,8 +534,8 @@ func TestTokenAuthMiddleWare_ProOnly(t *testing.T) {
handler := func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}
- app := App{DB: testutils.DB}
- server := httptest.NewServer(app.tokenAuth(handler, database.TokenTypeEmailPreference, &AuthMiddlewareParams{
+ api := API{App: &app.App{DB: testutils.DB}}
+ server := httptest.NewServer(api.tokenAuth(handler, database.TokenTypeEmailPreference, &AuthMiddlewareParams{
ProOnly: true,
}))
defer server.Close()
@@ -671,7 +672,7 @@ func TestNotSupportedVersions(t *testing.T) {
}
// setup
- server := MustNewServer(t, &App{
+ server := MustNewServer(t, &app.App{
DB: &gorm.DB{},
Clock: clock.NewMock(),
})
@@ -691,11 +692,11 @@ func TestNotSupportedVersions(t *testing.T) {
func TestNewRouter_AppValidate(t *testing.T) {
testCases := []struct {
- app App
+ app app.App
expectedErr error
}{
{
- app: App{
+ app: app.App{
DB: &gorm.DB{},
Clock: clock.NewMock(),
StripeAPIBackend: nil,
@@ -706,7 +707,7 @@ func TestNewRouter_AppValidate(t *testing.T) {
expectedErr: nil,
},
{
- app: App{
+ app: app.App{
DB: nil,
Clock: clock.NewMock(),
StripeAPIBackend: nil,
@@ -714,10 +715,10 @@ func TestNewRouter_AppValidate(t *testing.T) {
EmailBackend: &testutils.MockEmailbackendImplementation{},
WebURL: "http://mock.url",
},
- expectedErr: ErrEmptyDB,
+ expectedErr: app.ErrEmptyDB,
},
{
- app: App{
+ app: app.App{
DB: &gorm.DB{},
Clock: nil,
StripeAPIBackend: nil,
@@ -725,10 +726,10 @@ func TestNewRouter_AppValidate(t *testing.T) {
EmailBackend: &testutils.MockEmailbackendImplementation{},
WebURL: "http://mock.url",
},
- expectedErr: ErrEmptyClock,
+ expectedErr: app.ErrEmptyClock,
},
{
- app: App{
+ app: app.App{
DB: &gorm.DB{},
Clock: clock.NewMock(),
StripeAPIBackend: nil,
@@ -736,10 +737,10 @@ func TestNewRouter_AppValidate(t *testing.T) {
EmailBackend: &testutils.MockEmailbackendImplementation{},
WebURL: "http://mock.url",
},
- expectedErr: ErrEmptyEmailTemplates,
+ expectedErr: app.ErrEmptyEmailTemplates,
},
{
- app: App{
+ app: app.App{
DB: &gorm.DB{},
Clock: clock.NewMock(),
StripeAPIBackend: nil,
@@ -747,10 +748,10 @@ func TestNewRouter_AppValidate(t *testing.T) {
EmailBackend: nil,
WebURL: "http://mock.url",
},
- expectedErr: ErrEmptyEmailBackend,
+ expectedErr: app.ErrEmptyEmailBackend,
},
{
- app: App{
+ app: app.App{
DB: &gorm.DB{},
Clock: clock.NewMock(),
StripeAPIBackend: nil,
@@ -758,13 +759,14 @@ func TestNewRouter_AppValidate(t *testing.T) {
EmailBackend: &testutils.MockEmailbackendImplementation{},
WebURL: "",
},
- expectedErr: ErrEmptyWebURL,
+ expectedErr: app.ErrEmptyWebURL,
},
}
for idx, tc := range testCases {
t.Run(fmt.Sprintf("test case %d", idx), func(t *testing.T) {
- _, err := NewRouter(&tc.app)
+ api := API{App: &tc.app}
+ _, err := api.NewRouter()
assert.Equal(t, errors.Cause(err), tc.expectedErr, "error mismatch")
})
diff --git a/pkg/server/handlers/subscription.go b/pkg/server/handlers/subscription.go
index 1f727ca8..5024070b 100644
--- a/pkg/server/handlers/subscription.go
+++ b/pkg/server/handlers/subscription.go
@@ -26,9 +26,9 @@ import (
"os"
"strings"
+ "github.com/dnote/dnote/pkg/server/app"
"github.com/dnote/dnote/pkg/server/database"
"github.com/dnote/dnote/pkg/server/helpers"
- "github.com/dnote/dnote/pkg/server/operations"
"github.com/jinzhu/gorm"
"github.com/pkg/errors"
"github.com/stripe/stripe-go"
@@ -125,7 +125,7 @@ type createSubPayload struct {
}
// createSub creates a subscription for a the current user
-func (a *App) createSub(w http.ResponseWriter, r *http.Request) {
+func (a *API) createSub(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
if !ok {
HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
@@ -138,7 +138,7 @@ func (a *App) createSub(w http.ResponseWriter, r *http.Request) {
return
}
- tx := a.DB.Begin()
+ tx := a.App.DB.Begin()
if err := tx.Model(&user).
Update(map[string]interface{}{
@@ -214,7 +214,7 @@ func validateUpdateSubPayload(p updateSubPayload) error {
return nil
}
-func (a *App) updateSub(w http.ResponseWriter, r *http.Request) {
+func (a *API) updateSub(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
if !ok {
HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
@@ -237,14 +237,14 @@ func (a *App) updateSub(w http.ResponseWriter, r *http.Request) {
var err error
if payload.Op == updateSubOpCancel {
- err = operations.CancelSub(payload.StripeSubcriptionID, user)
+ err = a.App.CancelSub(payload.StripeSubcriptionID, user)
} else if payload.Op == updateSubOpReactivate {
- err = operations.ReactivateSub(payload.StripeSubcriptionID, user)
+ err = a.App.ReactivateSub(payload.StripeSubcriptionID, user)
}
if err != nil {
var statusCode int
- if err == operations.ErrSubscriptionActive {
+ if err == app.ErrSubscriptionActive {
statusCode = http.StatusBadRequest
} else {
statusCode = http.StatusInternalServerError
@@ -285,7 +285,7 @@ func respondWithEmptySub(w http.ResponseWriter) {
}
}
-func (a *App) getSub(w http.ResponseWriter, r *http.Request) {
+func (a *API) getSub(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
if !ok {
HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
@@ -413,7 +413,7 @@ func validateUpdateStripeSourcePayload(p updateStripeSourcePayload) error {
return nil
}
-func (a *App) updateStripeSource(w http.ResponseWriter, r *http.Request) {
+func (a *API) updateStripeSource(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
if !ok {
HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
@@ -430,7 +430,7 @@ func (a *App) updateStripeSource(w http.ResponseWriter, r *http.Request) {
return
}
- tx := a.DB.Begin()
+ tx := a.App.DB.Begin()
if err := tx.Model(&user).
Update(map[string]interface{}{
@@ -469,7 +469,7 @@ func (a *App) updateStripeSource(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}
-func (a *App) getStripeSource(w http.ResponseWriter, r *http.Request) {
+func (a *API) getStripeSource(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
if !ok {
HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
@@ -507,7 +507,7 @@ func (a *App) getStripeSource(w http.ResponseWriter, r *http.Request) {
respondJSON(w, http.StatusOK, resp)
}
-func (a *App) stripeWebhook(w http.ResponseWriter, req *http.Request) {
+func (a *API) stripeWebhook(w http.ResponseWriter, req *http.Request) {
body, err := ioutil.ReadAll(req.Body)
if err != nil {
HandleError(w, "reading body", err, http.StatusServiceUnavailable)
@@ -530,7 +530,7 @@ func (a *App) stripeWebhook(w http.ResponseWriter, req *http.Request) {
return
}
- operations.MarkUnsubscribed(a.DB, subscription.Customer.ID)
+ a.App.MarkUnsubscribed(subscription.Customer.ID)
}
default:
{
diff --git a/pkg/server/handlers/testutils.go b/pkg/server/handlers/testutils.go
index d0cb8e21..9bcd71cc 100644
--- a/pkg/server/handlers/testutils.go
+++ b/pkg/server/handlers/testutils.go
@@ -20,29 +20,18 @@ package handlers
import (
"net/http/httptest"
- "os"
"testing"
- "github.com/dnote/dnote/pkg/server/mailer"
- "github.com/dnote/dnote/pkg/server/testutils"
+ "github.com/dnote/dnote/pkg/server/app"
"github.com/pkg/errors"
)
// MustNewServer is a test utility function to initialize a new server
// with the given app paratmers
-func MustNewServer(t *testing.T, app *App) *httptest.Server {
- app.WebURL = os.Getenv("WebURL")
- app.DB = testutils.DB
+func MustNewServer(t *testing.T, appParams *app.App) *httptest.Server {
+ api := NewTestAPI(appParams)
- // If email backend was not provided, use the default mock backend
- if app.EmailBackend == nil {
- app.EmailBackend = &testutils.MockEmailbackendImplementation{}
- }
-
- emailTmplDir := os.Getenv("DNOTE_TEST_EMAIL_TEMPLATE_DIR")
- app.EmailTemplates = mailer.NewTemplates(&emailTmplDir)
-
- r, err := NewRouter(app)
+ r, err := api.NewRouter()
if err != nil {
t.Fatal(errors.Wrap(err, "initializing server"))
}
@@ -51,3 +40,10 @@ func MustNewServer(t *testing.T, app *App) *httptest.Server {
return server
}
+
+// NewTestAPI returns a new API for test
+func NewTestAPI(appParams *app.App) API {
+ a := app.NewTest(appParams)
+
+ return API{App: &a}
+}
diff --git a/pkg/server/handlers/user.go b/pkg/server/handlers/user.go
index 95684163..f32f23a7 100644
--- a/pkg/server/handlers/user.go
+++ b/pkg/server/handlers/user.go
@@ -38,7 +38,7 @@ type updateProfilePayload struct {
}
// updateProfile updates user
-func (a *App) updateProfile(w http.ResponseWriter, r *http.Request) {
+func (a *API) updateProfile(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
if !ok {
HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
@@ -59,13 +59,13 @@ func (a *App) updateProfile(w http.ResponseWriter, r *http.Request) {
}
var account database.Account
- err = a.DB.Where("user_id = ?", user.ID).First(&account).Error
+ err = a.App.DB.Where("user_id = ?", user.ID).First(&account).Error
if err != nil {
HandleError(w, "finding account", err, http.StatusInternalServerError)
return
}
- tx := a.DB.Begin()
+ tx := a.App.DB.Begin()
if err := tx.Save(&user).Error; err != nil {
tx.Rollback()
HandleError(w, "saving user", err, http.StatusInternalServerError)
@@ -86,7 +86,7 @@ func (a *App) updateProfile(w http.ResponseWriter, r *http.Request) {
tx.Commit()
- respondWithSession(a.DB, w, user.ID, http.StatusOK)
+ a.respondWithSession(a.App.DB, w, user.ID, http.StatusOK)
}
type updateEmailPayload struct {
@@ -122,17 +122,17 @@ func respondWithCalendar(db *gorm.DB, w http.ResponseWriter, userID int) {
respondJSON(w, http.StatusOK, payload)
}
-func (a *App) getCalendar(w http.ResponseWriter, r *http.Request) {
+func (a *API) getCalendar(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
if !ok {
HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
return
}
- respondWithCalendar(a.DB, w, user.ID)
+ respondWithCalendar(a.App.DB, w, user.ID)
}
-func (a *App) createVerificationToken(w http.ResponseWriter, r *http.Request) {
+func (a *API) createVerificationToken(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
if !ok {
HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
@@ -140,7 +140,7 @@ func (a *App) createVerificationToken(w http.ResponseWriter, r *http.Request) {
}
var account database.Account
- err := a.DB.Where("user_id = ?", user.ID).First(&account).Error
+ err := a.App.DB.Where("user_id = ?", user.ID).First(&account).Error
if err != nil {
HandleError(w, "finding account", err, http.StatusInternalServerError)
return
@@ -167,20 +167,19 @@ func (a *App) createVerificationToken(w http.ResponseWriter, r *http.Request) {
Type: database.TokenTypeEmailVerification,
}
- if err := a.DB.Save(&token).Error; err != nil {
+ if err := a.App.DB.Save(&token).Error; err != nil {
HandleError(w, "saving token", err, http.StatusInternalServerError)
return
}
- body, err := a.EmailTemplates.Execute(mailer.EmailTypeEmailVerification, mailer.EmailKindText, mailer.EmailVerificationTmplData{
- Token: tokenValue,
- WebURL: a.WebURL,
- })
- if err != nil {
- HandleError(w, errors.Wrap(err, "executing reset verification template").Error(), nil, http.StatusInternalServerError)
- }
- if err := a.EmailBackend.Queue("Verify your Dnote email address", "sung@getdnote.com", []string{account.Email.String}, "text/plain", body); err != nil {
- HandleError(w, errors.Wrap(err, "queueing email").Error(), nil, http.StatusInternalServerError)
+ if err := a.App.SendVerificationEmail(account.Email.String, tokenValue); err != nil {
+ if errors.Cause(err) == mailer.ErrSMTPNotConfigured {
+ respondInvalidSMTPConfig(w)
+ } else {
+ HandleError(w, errors.Wrap(err, "sending verification email").Error(), nil, http.StatusInternalServerError)
+ }
+
+ return
}
w.WriteHeader(http.StatusCreated)
@@ -190,7 +189,7 @@ type verifyEmailPayload struct {
Token string `json:"token"`
}
-func (a *App) verifyEmail(w http.ResponseWriter, r *http.Request) {
+func (a *API) verifyEmail(w http.ResponseWriter, r *http.Request) {
var params verifyEmailPayload
if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
HandleError(w, "decoding payload", err, http.StatusInternalServerError)
@@ -198,7 +197,7 @@ func (a *App) verifyEmail(w http.ResponseWriter, r *http.Request) {
}
var token database.Token
- if err := a.DB.
+ if err := a.App.DB.
Where("value = ? AND type = ?", params.Token, database.TokenTypeEmailVerification).
First(&token).Error; err != nil {
http.Error(w, "invalid token", http.StatusBadRequest)
@@ -217,7 +216,7 @@ func (a *App) verifyEmail(w http.ResponseWriter, r *http.Request) {
}
var account database.Account
- if err := a.DB.Where("user_id = ?", token.UserID).First(&account).Error; err != nil {
+ if err := a.App.DB.Where("user_id = ?", token.UserID).First(&account).Error; err != nil {
HandleError(w, "finding account", err, http.StatusInternalServerError)
return
}
@@ -226,7 +225,7 @@ func (a *App) verifyEmail(w http.ResponseWriter, r *http.Request) {
return
}
- tx := a.DB.Begin()
+ tx := a.App.DB.Begin()
account.EmailVerified = true
if err := tx.Save(&account).Error; err != nil {
tx.Rollback()
@@ -241,7 +240,7 @@ func (a *App) verifyEmail(w http.ResponseWriter, r *http.Request) {
tx.Commit()
var user database.User
- if err := a.DB.Where("id = ?", token.UserID).First(&user).Error; err != nil {
+ if err := a.App.DB.Where("id = ?", token.UserID).First(&user).Error; err != nil {
HandleError(w, "finding user", err, http.StatusInternalServerError)
return
}
@@ -254,7 +253,7 @@ type updateEmailPreferencePayload struct {
DigestWeekly bool `json:"digest_weekly"`
}
-func (a *App) updateEmailPreference(w http.ResponseWriter, r *http.Request) {
+func (a *API) updateEmailPreference(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
if !ok {
HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
@@ -268,12 +267,12 @@ func (a *App) updateEmailPreference(w http.ResponseWriter, r *http.Request) {
}
var frequency database.EmailPreference
- if err := a.DB.Where(database.EmailPreference{UserID: user.ID}).FirstOrCreate(&frequency).Error; err != nil {
+ if err := a.App.DB.Where(database.EmailPreference{UserID: user.ID}).FirstOrCreate(&frequency).Error; err != nil {
HandleError(w, "finding frequency", err, http.StatusInternalServerError)
return
}
- tx := a.DB.Begin()
+ tx := a.App.DB.Begin()
frequency.DigestWeekly = params.DigestWeekly
if err := tx.Save(&frequency).Error; err != nil {
@@ -297,7 +296,7 @@ func (a *App) updateEmailPreference(w http.ResponseWriter, r *http.Request) {
respondJSON(w, http.StatusOK, frequency)
}
-func (a *App) getEmailPreference(w http.ResponseWriter, r *http.Request) {
+func (a *API) getEmailPreference(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
if !ok {
HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
@@ -305,7 +304,7 @@ func (a *App) getEmailPreference(w http.ResponseWriter, r *http.Request) {
}
var pref database.EmailPreference
- if err := a.DB.Where(database.EmailPreference{UserID: user.ID}).First(&pref).Error; err != nil {
+ if err := a.App.DB.Where(database.EmailPreference{UserID: user.ID}).First(&pref).Error; err != nil {
HandleError(w, "finding pref", err, http.StatusInternalServerError)
return
}
@@ -319,7 +318,7 @@ type updatePasswordPayload struct {
NewPassword string `json:"new_password"`
}
-func (a *App) updatePassword(w http.ResponseWriter, r *http.Request) {
+func (a *API) updatePassword(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
if !ok {
HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
@@ -337,7 +336,7 @@ func (a *App) updatePassword(w http.ResponseWriter, r *http.Request) {
}
var account database.Account
- if err := a.DB.Where("user_id = ?", user.ID).First(&account).Error; err != nil {
+ if err := a.App.DB.Where("user_id = ?", user.ID).First(&account).Error; err != nil {
HandleError(w, "getting user", nil, http.StatusInternalServerError)
return
}
@@ -362,7 +361,7 @@ func (a *App) updatePassword(w http.ResponseWriter, r *http.Request) {
return
}
- if err := a.DB.Model(&account).Update("password", string(hashedNewPassword)).Error; err != nil {
+ if err := a.App.DB.Model(&account).Update("password", string(hashedNewPassword)).Error; err != nil {
http.Error(w, errors.Wrap(err, "updating password").Error(), http.StatusInternalServerError)
return
}
diff --git a/pkg/server/handlers/user_test.go b/pkg/server/handlers/user_test.go
index e802b0ef..ea37f8b3 100644
--- a/pkg/server/handlers/user_test.go
+++ b/pkg/server/handlers/user_test.go
@@ -27,6 +27,7 @@ import (
"github.com/dnote/dnote/pkg/assert"
"github.com/dnote/dnote/pkg/clock"
+ "github.com/dnote/dnote/pkg/server/app"
"github.com/dnote/dnote/pkg/server/database"
"github.com/dnote/dnote/pkg/server/presenters"
"github.com/dnote/dnote/pkg/server/testutils"
@@ -40,8 +41,7 @@ func TestUpdatePassword(t *testing.T) {
defer testutils.ClearData()
// Setup
- server := MustNewServer(t, &App{
-
+ server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
defer server.Close()
@@ -69,7 +69,7 @@ func TestUpdatePassword(t *testing.T) {
defer testutils.ClearData()
// Setup
- server := MustNewServer(t, &App{
+ server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
@@ -96,7 +96,7 @@ func TestUpdatePassword(t *testing.T) {
defer testutils.ClearData()
// Setup
- server := MustNewServer(t, &App{
+ server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
@@ -125,7 +125,7 @@ func TestCreateVerificationToken(t *testing.T) {
// Setup
emailBackend := testutils.MockEmailbackendImplementation{}
- server := MustNewServer(t, &App{
+ server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
EmailBackend: &emailBackend,
})
@@ -160,7 +160,7 @@ func TestCreateVerificationToken(t *testing.T) {
defer testutils.ClearData()
// Setup
- server := MustNewServer(t, &App{
+ server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
@@ -194,8 +194,7 @@ func TestVerifyEmail(t *testing.T) {
defer testutils.ClearData()
// Setup
- server := MustNewServer(t, &App{
-
+ server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
defer server.Close()
@@ -236,8 +235,7 @@ func TestVerifyEmail(t *testing.T) {
defer testutils.ClearData()
// Setup
- server := MustNewServer(t, &App{
-
+ server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
defer server.Close()
@@ -281,8 +279,7 @@ func TestVerifyEmail(t *testing.T) {
defer testutils.ClearData()
// Setup
- server := MustNewServer(t, &App{
-
+ server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
defer server.Close()
@@ -324,8 +321,7 @@ func TestVerifyEmail(t *testing.T) {
defer testutils.ClearData()
// Setup
- server := MustNewServer(t, &App{
-
+ server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
defer server.Close()
@@ -370,8 +366,7 @@ func TestUpdateEmail(t *testing.T) {
defer testutils.ClearData()
// Setup
- server := MustNewServer(t, &App{
-
+ server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
defer server.Close()
@@ -405,8 +400,7 @@ func TestUpdateEmailPreference(t *testing.T) {
defer testutils.ClearData()
// Setup
- server := MustNewServer(t, &App{
-
+ server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
defer server.Close()
@@ -432,7 +426,7 @@ func TestUpdateEmailPreference(t *testing.T) {
defer testutils.ClearData()
// Setup
- server := MustNewServer(t, &App{
+ server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
@@ -473,7 +467,7 @@ func TestUpdateEmailPreference(t *testing.T) {
defer testutils.ClearData()
// Setup
- server := MustNewServer(t, &App{
+ server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
@@ -508,7 +502,7 @@ func TestUpdateEmailPreference(t *testing.T) {
defer testutils.ClearData()
// Setup
- server := MustNewServer(t, &App{
+ server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
@@ -545,7 +539,7 @@ func TestUpdateEmailPreference(t *testing.T) {
defer testutils.ClearData()
// Setup
- server := MustNewServer(t, &App{
+ server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
@@ -582,7 +576,7 @@ func TestUpdateEmailPreference(t *testing.T) {
defer testutils.ClearData()
// Setup
- server := MustNewServer(t, &App{
+ server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
@@ -609,7 +603,7 @@ func TestUpdateEmailPreference(t *testing.T) {
defer testutils.ClearData()
// Setup
- server := MustNewServer(t, &App{
+ server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
@@ -643,10 +637,9 @@ func TestUpdateEmailPreference(t *testing.T) {
}
func TestGetEmailPreference(t *testing.T) {
-
defer testutils.ClearData()
// Setup
- server := MustNewServer(t, &App{
+ server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
diff --git a/pkg/server/handlers/v3_auth.go b/pkg/server/handlers/v3_auth.go
index 30cf363f..9104d1bf 100644
--- a/pkg/server/handlers/v3_auth.go
+++ b/pkg/server/handlers/v3_auth.go
@@ -25,8 +25,6 @@ import (
"github.com/dnote/dnote/pkg/server/database"
"github.com/dnote/dnote/pkg/server/log"
- "github.com/dnote/dnote/pkg/server/mailer"
- "github.com/dnote/dnote/pkg/server/operations"
"github.com/jinzhu/gorm"
"github.com/pkg/errors"
"golang.org/x/crypto/bcrypt"
@@ -80,7 +78,7 @@ type signinPayload struct {
Password string `json:"password"`
}
-func (a *App) signin(w http.ResponseWriter, r *http.Request) {
+func (a *API) signin(w http.ResponseWriter, r *http.Request) {
var params signinPayload
err := json.NewDecoder(r.Body).Decode(¶ms)
if err != nil {
@@ -93,7 +91,7 @@ func (a *App) signin(w http.ResponseWriter, r *http.Request) {
}
var account database.Account
- conn := a.DB.Where("email = ?", params.Email).First(&account)
+ conn := a.App.DB.Where("email = ?", params.Email).First(&account)
if conn.RecordNotFound() {
http.Error(w, ErrLoginFailure.Error(), http.StatusUnauthorized)
return
@@ -110,27 +108,27 @@ func (a *App) signin(w http.ResponseWriter, r *http.Request) {
}
var user database.User
- err = a.DB.Where("id = ?", account.UserID).First(&user).Error
+ err = a.App.DB.Where("id = ?", account.UserID).First(&user).Error
if err != nil {
HandleError(w, "finding user", err, http.StatusInternalServerError)
return
}
- err = operations.TouchLastLoginAt(user, a.DB)
+ err = a.App.TouchLastLoginAt(user, a.App.DB)
if err != nil {
http.Error(w, errors.Wrap(err, "touching login timestamp").Error(), http.StatusInternalServerError)
return
}
- respondWithSession(a.DB, w, account.UserID, http.StatusOK)
+ a.respondWithSession(a.App.DB, w, account.UserID, http.StatusOK)
}
-func (a *App) signoutOptions(w http.ResponseWriter, r *http.Request) {
+func (a *API) signoutOptions(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Methods", "POST")
w.Header().Set("Access-Control-Allow-Headers", "Authorization, Version")
}
-func (a *App) signout(w http.ResponseWriter, r *http.Request) {
+func (a *API) signout(w http.ResponseWriter, r *http.Request) {
key, err := getCredential(r)
if err != nil {
HandleError(w, "getting credential", nil, http.StatusInternalServerError)
@@ -142,7 +140,7 @@ func (a *App) signout(w http.ResponseWriter, r *http.Request) {
return
}
- err = operations.DeleteSession(a.DB, key)
+ err = a.App.DeleteSession(key)
if err != nil {
HandleError(w, "deleting session", nil, http.StatusInternalServerError)
return
@@ -177,7 +175,7 @@ func parseRegisterPaylaod(r *http.Request) (registerPayload, error) {
return ret, nil
}
-func (a *App) register(w http.ResponseWriter, r *http.Request) {
+func (a *API) register(w http.ResponseWriter, r *http.Request) {
params, err := parseRegisterPaylaod(r)
if err != nil {
http.Error(w, "invalid payload", http.StatusBadRequest)
@@ -189,7 +187,7 @@ func (a *App) register(w http.ResponseWriter, r *http.Request) {
}
var count int
- if err := a.DB.Model(database.Account{}).Where("email = ?", params.Email).Count(&count).Error; err != nil {
+ if err := a.App.DB.Model(database.Account{}).Where("email = ?", params.Email).Count(&count).Error; err != nil {
HandleError(w, "checking duplicate user", err, http.StatusInternalServerError)
return
}
@@ -198,36 +196,23 @@ func (a *App) register(w http.ResponseWriter, r *http.Request) {
return
}
- user, err := operations.CreateUser(a.DB, params.Email, params.Password)
+ user, err := a.App.CreateUser(params.Email, params.Password)
if err != nil {
HandleError(w, "creating user", err, http.StatusInternalServerError)
return
}
- respondWithSession(a.DB, w, user.ID, http.StatusCreated)
+ a.respondWithSession(a.App.DB, w, user.ID, http.StatusCreated)
- // send welcome email
- body, err := a.EmailTemplates.Execute(mailer.EmailTypeWelcome, mailer.EmailKindText, mailer.WelcomeTmplData{
- AccountEmail: params.Email,
- WebURL: a.WebURL,
- })
- if err != nil {
- log.WithFields(log.Fields{
- "email": params.Email,
- }).ErrorWrap(err, "executing welcome email template")
- return
- }
- if err := a.EmailBackend.Queue("Welcome to Dnote!", "sung@getdnote.com", []string{params.Email}, "text/plain", body); err != nil {
- log.WithFields(log.Fields{
- "email": params.Email,
- }).ErrorWrap(err, "queueing email")
+ if err := a.App.SendWelcomeEmail(params.Email); err != nil {
+ log.ErrorWrap(err, "sending welcome email")
}
}
// respondWithSession makes a HTTP response with the session from the user with the given userID.
// It sets the HTTP-Only cookie for browser clients and also sends a JSON response for non-browser clients.
-func respondWithSession(db *gorm.DB, w http.ResponseWriter, userID int, statusCode int) {
- session, err := operations.CreateSession(db, userID)
+func (a *API) respondWithSession(db *gorm.DB, w http.ResponseWriter, userID int, statusCode int) {
+ session, err := a.App.CreateSession(userID)
if err != nil {
HandleError(w, "creating session", nil, http.StatusBadRequest)
return
diff --git a/pkg/server/handlers/v3_auth_test.go b/pkg/server/handlers/v3_auth_test.go
index f691e9d5..5632cf3a 100644
--- a/pkg/server/handlers/v3_auth_test.go
+++ b/pkg/server/handlers/v3_auth_test.go
@@ -27,6 +27,7 @@ import (
"github.com/dnote/dnote/pkg/assert"
"github.com/dnote/dnote/pkg/clock"
+ "github.com/dnote/dnote/pkg/server/app"
"github.com/dnote/dnote/pkg/server/database"
"github.com/dnote/dnote/pkg/server/testutils"
"github.com/pkg/errors"
@@ -58,20 +59,35 @@ func assertSessionResp(t *testing.T, res *http.Response) {
func TestRegister(t *testing.T) {
testCases := []struct {
- email string
- password string
+ email string
+ password string
+ onPremise bool
+ expectedPro bool
}{
{
- email: "alice@example.com",
- password: "pass1234",
+ email: "alice@example.com",
+ password: "pass1234",
+ onPremise: false,
+ expectedPro: false,
},
{
- email: "bob@example.com",
- password: "Y9EwmjH@Jq6y5a64MSACUoM4w7SAhzvY",
+ email: "bob@example.com",
+ password: "Y9EwmjH@Jq6y5a64MSACUoM4w7SAhzvY",
+ onPremise: false,
+ expectedPro: false,
},
{
- email: "chuck@example.com",
- password: "e*H@kJi^vXbWEcD9T5^Am!Y@7#Po2@PC",
+ email: "chuck@example.com",
+ password: "e*H@kJi^vXbWEcD9T5^Am!Y@7#Po2@PC",
+ onPremise: false,
+ expectedPro: false,
+ },
+ // on premise
+ {
+ email: "dan@example.com",
+ password: "e*H@kJi^vXbWEcD9T5^Am!Y@7#Po2@PC",
+ onPremise: true,
+ expectedPro: true,
},
}
@@ -81,9 +97,10 @@ func TestRegister(t *testing.T) {
// Setup
emailBackend := testutils.MockEmailbackendImplementation{}
- server := MustNewServer(t, &App{
+ server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
EmailBackend: &emailBackend,
+ OnPremise: tc.onPremise,
})
defer server.Close()
@@ -105,7 +122,7 @@ func TestRegister(t *testing.T) {
var user database.User
testutils.MustExec(t, testutils.DB.Where("id = ?", account.UserID).First(&user), "finding user")
- assert.Equal(t, user.Cloud, false, "Cloud mismatch")
+ assert.Equal(t, user.Cloud, tc.expectedPro, "Cloud mismatch")
assert.Equal(t, user.StripeCustomerID, "", "StripeCustomerID mismatch")
assert.Equal(t, user.MaxUSN, 0, "MaxUSN mismatch")
@@ -125,11 +142,10 @@ func TestRegister(t *testing.T) {
func TestRegisterMissingParams(t *testing.T) {
t.Run("missing email", func(t *testing.T) {
-
defer testutils.ClearData()
// Setup
- server := MustNewServer(t, &App{
+ server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
@@ -157,7 +173,7 @@ func TestRegisterMissingParams(t *testing.T) {
defer testutils.ClearData()
// Setup
- server := MustNewServer(t, &App{
+ server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
@@ -185,7 +201,7 @@ func TestRegisterDuplicateEmail(t *testing.T) {
defer testutils.ClearData()
// Setup
- server := MustNewServer(t, &App{
+ server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
@@ -219,11 +235,10 @@ func TestRegisterDuplicateEmail(t *testing.T) {
func TestSignIn(t *testing.T) {
t.Run("success", func(t *testing.T) {
-
defer testutils.ClearData()
// Setup
- server := MustNewServer(t, &App{
+ server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
@@ -250,11 +265,10 @@ func TestSignIn(t *testing.T) {
})
t.Run("wrong password", func(t *testing.T) {
-
defer testutils.ClearData()
// Setup
- server := MustNewServer(t, &App{
+ server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
@@ -286,7 +300,7 @@ func TestSignIn(t *testing.T) {
defer testutils.ClearData()
// Setup
- server := MustNewServer(t, &App{
+ server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
@@ -318,7 +332,7 @@ func TestSignIn(t *testing.T) {
defer testutils.ClearData()
// Setup
- server := MustNewServer(t, &App{
+ server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
@@ -362,7 +376,7 @@ func TestSignout(t *testing.T) {
testutils.MustExec(t, testutils.DB.Save(&session2), "preparing session2")
// Setup
- server := MustNewServer(t, &App{
+ server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
@@ -414,7 +428,7 @@ func TestSignout(t *testing.T) {
testutils.MustExec(t, testutils.DB.Save(&session2), "preparing session2")
// Setup
- server := MustNewServer(t, &App{
+ server := MustNewServer(t, &app.App{
Clock: clock.NewMock(),
})
diff --git a/pkg/server/handlers/v3_books.go b/pkg/server/handlers/v3_books.go
index 2cac6d61..0e6d2c53 100644
--- a/pkg/server/handlers/v3_books.go
+++ b/pkg/server/handlers/v3_books.go
@@ -26,7 +26,6 @@ import (
"github.com/dnote/dnote/pkg/server/database"
"github.com/dnote/dnote/pkg/server/helpers"
- "github.com/dnote/dnote/pkg/server/operations"
"github.com/dnote/dnote/pkg/server/presenters"
"github.com/gorilla/mux"
"github.com/jinzhu/gorm"
@@ -51,7 +50,7 @@ func validateCreateBookPayload(p createBookPayload) error {
}
// CreateBook creates a new book
-func (a *App) CreateBook(w http.ResponseWriter, r *http.Request) {
+func (a *API) CreateBook(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
if !ok {
return
@@ -71,7 +70,7 @@ func (a *App) CreateBook(w http.ResponseWriter, r *http.Request) {
}
var bookCount int
- err = a.DB.Model(database.Book{}).
+ err = a.App.DB.Model(database.Book{}).
Where("user_id = ? AND label = ?", user.ID, params.Name).
Count(&bookCount).Error
if err != nil {
@@ -83,7 +82,7 @@ func (a *App) CreateBook(w http.ResponseWriter, r *http.Request) {
return
}
- book, err := operations.CreateBook(a.DB, user, a.Clock, params.Name)
+ book, err := a.App.CreateBook(user, params.Name)
if err != nil {
HandleError(w, "inserting book", err, http.StatusInternalServerError)
}
@@ -94,7 +93,7 @@ func (a *App) CreateBook(w http.ResponseWriter, r *http.Request) {
}
// BooksOptions is a handler for OPTIONS endpoint for notes
-func (a *App) BooksOptions(w http.ResponseWriter, r *http.Request) {
+func (a *API) BooksOptions(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Methods", "GET, POST")
w.Header().Set("Access-Control-Allow-Headers", "Authorization, Version")
}
@@ -130,7 +129,7 @@ func respondWithBooks(db *gorm.DB, userID int, query url.Values, w http.Response
}
// GetBooks returns books for the user
-func (a *App) GetBooks(w http.ResponseWriter, r *http.Request) {
+func (a *API) GetBooks(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
if !ok {
return
@@ -138,11 +137,11 @@ func (a *App) GetBooks(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
- respondWithBooks(a.DB, user.ID, query, w)
+ respondWithBooks(a.App.DB, user.ID, query, w)
}
// GetBook returns a book for the user
-func (a *App) GetBook(w http.ResponseWriter, r *http.Request) {
+func (a *API) GetBook(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
if !ok {
return
@@ -152,7 +151,7 @@ func (a *App) GetBook(w http.ResponseWriter, r *http.Request) {
bookUUID := vars["bookUUID"]
var book database.Book
- conn := a.DB.Where("uuid = ? AND user_id = ?", bookUUID, user.ID).First(&book)
+ conn := a.App.DB.Where("uuid = ? AND user_id = ?", bookUUID, user.ID).First(&book)
if conn.RecordNotFound() {
w.WriteHeader(http.StatusNotFound)
@@ -177,7 +176,7 @@ type UpdateBookResp struct {
}
// UpdateBook updates a book
-func (a *App) UpdateBook(w http.ResponseWriter, r *http.Request) {
+func (a *API) UpdateBook(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
if !ok {
return
@@ -186,7 +185,7 @@ func (a *App) UpdateBook(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
uuid := vars["bookUUID"]
- tx := a.DB.Begin()
+ tx := a.App.DB.Begin()
var book database.Book
if err := tx.Where("user_id = ? AND uuid = ?", user.ID, uuid).First(&book).Error; err != nil {
@@ -201,7 +200,7 @@ func (a *App) UpdateBook(w http.ResponseWriter, r *http.Request) {
return
}
- book, err = operations.UpdateBook(tx, a.Clock, user, book, params.Name)
+ book, err = a.App.UpdateBook(tx, user, book, params.Name)
if err != nil {
tx.Rollback()
HandleError(w, "updating a book", err, http.StatusInternalServerError)
@@ -222,7 +221,7 @@ type DeleteBookResp struct {
}
// DeleteBook removes a book
-func (a *App) DeleteBook(w http.ResponseWriter, r *http.Request) {
+func (a *API) DeleteBook(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
if !ok {
return
@@ -231,7 +230,7 @@ func (a *App) DeleteBook(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
uuid := vars["bookUUID"]
- tx := a.DB.Begin()
+ tx := a.App.DB.Begin()
var book database.Book
if err := tx.Where("user_id = ? AND uuid = ?", user.ID, uuid).First(&book).Error; err != nil {
@@ -246,12 +245,12 @@ func (a *App) DeleteBook(w http.ResponseWriter, r *http.Request) {
}
for _, note := range notes {
- if _, err := operations.DeleteNote(tx, user, note); err != nil {
+ if _, err := a.App.DeleteNote(tx, user, note); err != nil {
HandleError(w, "deleting a note", err, http.StatusInternalServerError)
return
}
}
- b, err := operations.DeleteBook(tx, user, book)
+ b, err := a.App.DeleteBook(tx, user, book)
if err != nil {
HandleError(w, "deleting book", err, http.StatusInternalServerError)
return
diff --git a/pkg/server/handlers/v3_books_test.go b/pkg/server/handlers/v3_books_test.go
index 375275fc..43c79b0b 100644
--- a/pkg/server/handlers/v3_books_test.go
+++ b/pkg/server/handlers/v3_books_test.go
@@ -26,6 +26,7 @@ import (
"github.com/dnote/dnote/pkg/assert"
"github.com/dnote/dnote/pkg/clock"
+ "github.com/dnote/dnote/pkg/server/app"
"github.com/dnote/dnote/pkg/server/database"
"github.com/dnote/dnote/pkg/server/presenters"
"github.com/dnote/dnote/pkg/server/testutils"
@@ -33,13 +34,13 @@ import (
)
func TestGetBooks(t *testing.T) {
-
+
defer testutils.ClearData()
// Setup
- server := MustNewServer(t, &App{
-
-Clock: clock.NewMock(),
+ server := MustNewServer(t, &app.App{
+
+ Clock: clock.NewMock(),
})
defer server.Close()
@@ -113,13 +114,13 @@ Clock: clock.NewMock(),
}
func TestGetBooksByName(t *testing.T) {
-
+
defer testutils.ClearData()
// Setup
- server := MustNewServer(t, &App{
-
-Clock: clock.NewMock(),
+ server := MustNewServer(t, &app.App{
+
+ Clock: clock.NewMock(),
})
defer server.Close()
@@ -199,13 +200,13 @@ func TestDeleteBook(t *testing.T) {
for _, tc := range testCases {
t.Run(fmt.Sprintf("originally deleted %t", tc.deleted), func(t *testing.T) {
-
+
defer testutils.ClearData()
// Setup
- server := MustNewServer(t, &App{
-
-Clock: clock.NewMock(),
+ server := MustNewServer(t, &app.App{
+
+ Clock: clock.NewMock(),
})
defer server.Close()
@@ -350,13 +351,13 @@ Clock: clock.NewMock(),
}
func TestCreateBook(t *testing.T) {
-
+
defer testutils.ClearData()
// Setup
- server := MustNewServer(t, &App{
-
-Clock: clock.NewMock(),
+ server := MustNewServer(t, &app.App{
+
+ Clock: clock.NewMock(),
})
defer server.Close()
@@ -410,13 +411,13 @@ Clock: clock.NewMock(),
}
func TestCreateBookDuplicate(t *testing.T) {
-
+
defer testutils.ClearData()
// Setup
- server := MustNewServer(t, &App{
-
-Clock: clock.NewMock(),
+ server := MustNewServer(t, &app.App{
+
+ Clock: clock.NewMock(),
})
defer server.Close()
@@ -490,13 +491,13 @@ func TestUpdateBook(t *testing.T) {
for idx, tc := range testCases {
func() {
-
+
defer testutils.ClearData()
// Setup
- server := MustNewServer(t, &App{
-
-Clock: clock.NewMock(),
+ server := MustNewServer(t, &app.App{
+
+ Clock: clock.NewMock(),
})
defer server.Close()
diff --git a/pkg/server/handlers/v3_notes.go b/pkg/server/handlers/v3_notes.go
index 65dc9f17..74cd0bab 100644
--- a/pkg/server/handlers/v3_notes.go
+++ b/pkg/server/handlers/v3_notes.go
@@ -23,9 +23,9 @@ import (
"fmt"
"net/http"
+ "github.com/dnote/dnote/pkg/server/app"
"github.com/dnote/dnote/pkg/server/database"
"github.com/dnote/dnote/pkg/server/helpers"
- "github.com/dnote/dnote/pkg/server/operations"
"github.com/dnote/dnote/pkg/server/presenters"
"github.com/gorilla/mux"
"github.com/pkg/errors"
@@ -47,7 +47,7 @@ func validateUpdateNotePayload(p updateNotePayload) bool {
}
// UpdateNote updates note
-func (a *App) UpdateNote(w http.ResponseWriter, r *http.Request) {
+func (a *API) UpdateNote(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
noteUUID := vars["noteUUID"]
@@ -70,14 +70,14 @@ func (a *App) UpdateNote(w http.ResponseWriter, r *http.Request) {
}
var note database.Note
- if err := a.DB.Where("uuid = ? AND user_id = ?", noteUUID, user.ID).First(¬e).Error; err != nil {
+ if err := a.App.DB.Where("uuid = ? AND user_id = ?", noteUUID, user.ID).First(¬e).Error; err != nil {
HandleError(w, "finding note", err, http.StatusInternalServerError)
return
}
- tx := a.DB.Begin()
+ tx := a.App.DB.Begin()
- note, err = operations.UpdateNote(tx, user, a.Clock, note, &operations.UpdateNoteParams{
+ note, err = a.App.UpdateNote(tx, user, note, &app.UpdateNoteParams{
BookUUID: params.BookUUID,
Content: params.Content,
Public: params.Public,
@@ -114,7 +114,7 @@ type deleteNoteResp struct {
}
// DeleteNote removes note
-func (a *App) DeleteNote(w http.ResponseWriter, r *http.Request) {
+func (a *API) DeleteNote(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
noteUUID := vars["noteUUID"]
@@ -125,14 +125,14 @@ func (a *App) DeleteNote(w http.ResponseWriter, r *http.Request) {
}
var note database.Note
- if err := a.DB.Where("uuid = ? AND user_id = ?", noteUUID, user.ID).Preload("Book").First(¬e).Error; err != nil {
+ if err := a.App.DB.Where("uuid = ? AND user_id = ?", noteUUID, user.ID).Preload("Book").First(¬e).Error; err != nil {
HandleError(w, "finding note", err, http.StatusInternalServerError)
return
}
- tx := a.DB.Begin()
+ tx := a.App.DB.Begin()
- n, err := operations.DeleteNote(tx, user, note)
+ n, err := a.App.DeleteNote(tx, user, note)
if err != nil {
tx.Rollback()
HandleError(w, "deleting note", err, http.StatusInternalServerError)
@@ -169,7 +169,7 @@ type CreateNoteResp struct {
}
// CreateNote creates a note
-func (a *App) CreateNote(w http.ResponseWriter, r *http.Request) {
+func (a *API) CreateNote(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
if !ok {
HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
@@ -190,12 +190,12 @@ func (a *App) CreateNote(w http.ResponseWriter, r *http.Request) {
}
var book database.Book
- if err := a.DB.Where("uuid = ? AND user_id = ?", params.BookUUID, user.ID).First(&book).Error; err != nil {
+ if err := a.App.DB.Where("uuid = ? AND user_id = ?", params.BookUUID, user.ID).First(&book).Error; err != nil {
HandleError(w, "finding book", err, http.StatusInternalServerError)
return
}
- note, err := operations.CreateNote(a.DB, user, a.Clock, params.BookUUID, params.Content, params.AddedOn, params.EditedOn, false)
+ note, err := a.App.CreateNote(user, params.BookUUID, params.Content, params.AddedOn, params.EditedOn, false)
if err != nil {
HandleError(w, "creating note", err, http.StatusInternalServerError)
return
@@ -212,7 +212,7 @@ func (a *App) CreateNote(w http.ResponseWriter, r *http.Request) {
}
// NotesOptions is a handler for OPTIONS endpoint for notes
-func (a *App) NotesOptions(w http.ResponseWriter, r *http.Request) {
+func (a *API) NotesOptions(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Methods", "POST")
w.Header().Set("Access-Control-Allow-Headers", "Authorization, Version")
}
diff --git a/pkg/server/handlers/v3_notes_test.go b/pkg/server/handlers/v3_notes_test.go
index 524018e1..a2dff836 100644
--- a/pkg/server/handlers/v3_notes_test.go
+++ b/pkg/server/handlers/v3_notes_test.go
@@ -25,18 +25,19 @@ import (
"github.com/dnote/dnote/pkg/assert"
"github.com/dnote/dnote/pkg/clock"
+ "github.com/dnote/dnote/pkg/server/app"
"github.com/dnote/dnote/pkg/server/database"
"github.com/dnote/dnote/pkg/server/testutils"
)
func TestCreateNote(t *testing.T) {
-
+
defer testutils.ClearData()
// Setup
- server := MustNewServer(t, &App{
-
-Clock: clock.NewMock(),
+ server := MustNewServer(t, &app.App{
+
+ Clock: clock.NewMock(),
})
defer server.Close()
@@ -235,13 +236,13 @@ func TestUpdateNote(t *testing.T) {
for idx, tc := range testCases {
t.Run(fmt.Sprintf("test case %d", idx), func(t *testing.T) {
-
+
defer testutils.ClearData()
// Setup
- server := MustNewServer(t, &App{
-
-Clock: clock.NewMock(),
+ server := MustNewServer(t, &app.App{
+
+ Clock: clock.NewMock(),
})
defer server.Close()
@@ -331,13 +332,13 @@ func TestDeleteNote(t *testing.T) {
for _, tc := range testCases {
t.Run(fmt.Sprintf("originally deleted %t", tc.deleted), func(t *testing.T) {
-
+
defer testutils.ClearData()
// Setup
- server := MustNewServer(t, &App{
-
-Clock: clock.NewMock(),
+ server := MustNewServer(t, &app.App{
+
+ Clock: clock.NewMock(),
})
defer server.Close()
diff --git a/pkg/server/handlers/v3_sync.go b/pkg/server/handlers/v3_sync.go
index 71468cc4..69d4cad6 100644
--- a/pkg/server/handlers/v3_sync.go
+++ b/pkg/server/handlers/v3_sync.go
@@ -120,13 +120,13 @@ func (e *queryParamError) Error() string {
return fmt.Sprintf("invalid query param %s=%s. %s", e.key, e.value, e.message)
}
-func (a *App) newFragment(userID, userMaxUSN, afterUSN, limit int) (SyncFragment, error) {
+func (a *API) newFragment(userID, userMaxUSN, afterUSN, limit int) (SyncFragment, error) {
var notes []database.Note
- if err := a.DB.Where("user_id = ? AND usn > ? AND usn <= ?", userID, afterUSN, userMaxUSN).Order("usn ASC").Limit(limit).Find(¬es).Error; err != nil {
+ if err := a.App.DB.Where("user_id = ? AND usn > ? AND usn <= ?", userID, afterUSN, userMaxUSN).Order("usn ASC").Limit(limit).Find(¬es).Error; err != nil {
return SyncFragment{}, nil
}
var books []database.Book
- if err := a.DB.Where("user_id = ? AND usn > ? AND usn <= ?", userID, afterUSN, userMaxUSN).Order("usn ASC").Limit(limit).Find(&books).Error; err != nil {
+ if err := a.App.DB.Where("user_id = ? AND usn > ? AND usn <= ?", userID, afterUSN, userMaxUSN).Order("usn ASC").Limit(limit).Find(&books).Error; err != nil {
return SyncFragment{}, nil
}
@@ -191,7 +191,7 @@ func (a *App) newFragment(userID, userMaxUSN, afterUSN, limit int) (SyncFragment
ret := SyncFragment{
FragMaxUSN: fragMaxUSN,
UserMaxUSN: userMaxUSN,
- CurrentTime: a.Clock.Now().Unix(),
+ CurrentTime: a.App.Clock.Now().Unix(),
Notes: fragNotes,
Books: fragBooks,
ExpungedNotes: fragExpungedNotes,
@@ -247,7 +247,7 @@ type GetSyncFragmentResp struct {
}
// GetSyncFragment responds with a sync fragment
-func (a *App) GetSyncFragment(w http.ResponseWriter, r *http.Request) {
+func (a *API) GetSyncFragment(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
if !ok {
HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
@@ -280,7 +280,7 @@ type GetSyncStateResp struct {
}
// GetSyncState responds with a sync fragment
-func (a *App) GetSyncState(w http.ResponseWriter, r *http.Request) {
+func (a *API) GetSyncState(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
if !ok {
HandleError(w, "No authenticated user found", nil, http.StatusInternalServerError)
@@ -291,7 +291,7 @@ func (a *App) GetSyncState(w http.ResponseWriter, r *http.Request) {
FullSyncBefore: fullSyncBefore,
MaxUSN: user.MaxUSN,
// TODO: exposing server time means we probably shouldn't seed random generator with time?
- CurrentTime: a.Clock.Now().Unix(),
+ CurrentTime: a.App.Clock.Now().Unix(),
}
log.WithFields(log.Fields{
diff --git a/pkg/server/job/job_test.go b/pkg/server/job/job_test.go
index 43c049e6..ee5bc61e 100644
--- a/pkg/server/job/job_test.go
+++ b/pkg/server/job/job_test.go
@@ -1,3 +1,21 @@
+/* Copyright (C) 2019 Monomax Software Pty Ltd
+ *
+ * This file is part of Dnote.
+ *
+ * Dnote 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.
+ *
+ * Dnote 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 Dnote. If not, see .
+ */
+
package job
import (
diff --git a/pkg/server/job/repetition/main_test.go b/pkg/server/job/repetition/main_test.go
index 6555ee67..358100a9 100644
--- a/pkg/server/job/repetition/main_test.go
+++ b/pkg/server/job/repetition/main_test.go
@@ -1,3 +1,21 @@
+/* Copyright (C) 2019 Monomax Software Pty Ltd
+ *
+ * This file is part of Dnote.
+ *
+ * Dnote 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.
+ *
+ * Dnote 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 Dnote. If not, see .
+ */
+
package repetition
import (
diff --git a/pkg/server/mailer/backend.go b/pkg/server/mailer/backend.go
index 11fa4ab4..fbc6755a 100644
--- a/pkg/server/mailer/backend.go
+++ b/pkg/server/mailer/backend.go
@@ -1,3 +1,21 @@
+/* Copyright (C) 2019 Monomax Software Pty Ltd
+ *
+ * This file is part of Dnote.
+ *
+ * Dnote 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.
+ *
+ * Dnote 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 Dnote. If not, see .
+ */
+
package mailer
import (
@@ -10,6 +28,9 @@ import (
"gopkg.in/gomail.v2"
)
+// ErrSMTPNotConfigured is an error indicating that SMTP is not configured
+var ErrSMTPNotConfigured = errors.New("SMTP is not configured")
+
// Backend is an interface for sending emails.
type Backend interface {
Queue(subject, from string, to []string, contentType, body string) error
@@ -27,18 +48,35 @@ type dialerParams struct {
Password string
}
+func validateSMTPConfig() bool {
+ port := os.Getenv("SmtpPort")
+ host := os.Getenv("SmtpHost")
+ username := os.Getenv("SmtpUsername")
+ password := os.Getenv("SmtpPassword")
+
+ return port != "" && host != "" && username != "" && password != ""
+}
+
func getSMTPParams() (*dialerParams, error) {
- portStr := os.Getenv("SmtpPort")
- port, err := strconv.Atoi(portStr)
+ portEnv := os.Getenv("SmtpPort")
+ hostEnv := os.Getenv("SmtpHost")
+ usernameEnv := os.Getenv("SmtpUsername")
+ passwordEnv := os.Getenv("SmtpPassword")
+
+ if portEnv != "" && hostEnv != "" && usernameEnv != "" && passwordEnv != "" {
+ return nil, ErrSMTPNotConfigured
+ }
+
+ port, err := strconv.Atoi(portEnv)
if err != nil {
return nil, errors.Wrap(err, "parsing SMTP port")
}
p := &dialerParams{
- Host: os.Getenv("SmtpHost"),
+ Host: hostEnv,
Port: port,
- Username: os.Getenv("SmtpUsername"),
- Password: os.Getenv("SmtpPassword"),
+ Username: usernameEnv,
+ Password: passwordEnv,
}
return p, nil
diff --git a/pkg/server/main.go b/pkg/server/main.go
index d85bba6a..1a7128c6 100644
--- a/pkg/server/main.go
+++ b/pkg/server/main.go
@@ -26,6 +26,7 @@ import (
"os"
"github.com/dnote/dnote/pkg/clock"
+ "github.com/dnote/dnote/pkg/server/app"
"github.com/dnote/dnote/pkg/server/database"
"github.com/dnote/dnote/pkg/server/dbconn"
"github.com/dnote/dnote/pkg/server/handlers"
@@ -55,11 +56,11 @@ func mustFind(box *packr.Box, path string) []byte {
return b
}
-func initContext(db *gorm.DB) web.Context {
+func initContext(a *app.App) web.Context {
staticBox := packr.New("static", "../../web/public/static")
return web.Context{
- DB: db,
+ App: a,
IndexHTML: mustFind(rootBox, "index.html"),
RobotsTxt: mustFind(rootBox, "robots.txt"),
ServiceWorkerJs: mustFind(rootBox, "service-worker.js"),
@@ -67,13 +68,14 @@ func initContext(db *gorm.DB) web.Context {
}
}
-func initServer(app handlers.App) (*http.ServeMux, error) {
- apiRouter, err := handlers.NewRouter(&app)
+func initServer(a app.App) (*http.ServeMux, error) {
+ api := handlers.API{App: &a}
+ apiRouter, err := api.NewRouter()
if err != nil {
return nil, errors.Wrap(err, "initializing router")
}
- webCtx := initContext(app.DB)
+ webCtx := initContext(&a)
webHandlers, err := web.Init(webCtx)
if err != nil {
return nil, errors.Wrap(err, "initializing web handlers")
@@ -110,10 +112,10 @@ func initDB() *gorm.DB {
return db
}
-func initApp() handlers.App {
+func initApp() app.App {
db := initDB()
- return handlers.App{
+ return app.App{
DB: db,
Clock: clock.New(),
StripeAPIBackend: nil,
@@ -123,7 +125,7 @@ func initApp() handlers.App {
}
}
-func runJob(a handlers.App) error {
+func runJob(a app.App) error {
jobRunner, err := job.NewRunner(a.DB, a.Clock, a.EmailTemplates, a.EmailBackend, a.WebURL)
if err != nil {
return errors.Wrap(err, "getting a job runner")
diff --git a/pkg/server/operations/main_test.go b/pkg/server/operations/main_test.go
deleted file mode 100644
index b2e7fab8..00000000
--- a/pkg/server/operations/main_test.go
+++ /dev/null
@@ -1,17 +0,0 @@
-package operations
-
-import (
- "os"
- "testing"
-
- "github.com/dnote/dnote/pkg/server/testutils"
-)
-
-func TestMain(m *testing.M) {
- testutils.InitTestDB()
-
- code := m.Run()
- testutils.ClearData()
-
- os.Exit(code)
-}
diff --git a/pkg/server/tmpl/app.go b/pkg/server/tmpl/app.go
index 770ef6c4..dfd6b64a 100644
--- a/pkg/server/tmpl/app.go
+++ b/pkg/server/tmpl/app.go
@@ -24,7 +24,7 @@ import (
"net/http"
"regexp"
- "github.com/jinzhu/gorm"
+ "github.com/dnote/dnote/pkg/server/app"
"github.com/pkg/errors"
)
@@ -37,14 +37,15 @@ var templateNoteMetaTags = "note_metatags"
// AppShell represents the application in HTML
type AppShell struct {
- T *template.Template
+ App *app.App
+ T *template.Template
}
// ErrNotFound is an error indicating that a resource was not found
var ErrNotFound = errors.New("not found")
// NewAppShell parses the templates for the application
-func NewAppShell(content []byte) (AppShell, error) {
+func NewAppShell(a *app.App, content []byte) (AppShell, error) {
t, err := template.New(templateIndex).Parse(string(content))
if err != nil {
return AppShell{}, errors.Wrap(err, "parsing the index template")
@@ -55,12 +56,12 @@ func NewAppShell(content []byte) (AppShell, error) {
return AppShell{}, errors.Wrap(err, "parsing the note meta tags template")
}
- return AppShell{t}, nil
+ return AppShell{App: a, T: t}, nil
}
// Execute executes the index template
-func (a AppShell) Execute(r *http.Request, db *gorm.DB) ([]byte, error) {
- data, err := a.getData(db, r)
+func (a AppShell) Execute(r *http.Request) ([]byte, error) {
+ data, err := a.getData(r)
if err != nil {
return nil, errors.Wrap(err, "getting data")
}
@@ -73,11 +74,11 @@ func (a AppShell) Execute(r *http.Request, db *gorm.DB) ([]byte, error) {
return buf.Bytes(), nil
}
-func (a AppShell) getData(db *gorm.DB, r *http.Request) (tmplData, error) {
+func (a AppShell) getData(r *http.Request) (tmplData, error) {
path := r.URL.Path
if ok, params := matchPath(path, notesPathRegex); ok {
- p, err := a.newNotePage(db, r, params[0])
+ p, err := a.newNotePage(r, params[0])
if err != nil {
return tmplData{}, errors.Wrap(err, "instantiating note page")
}
diff --git a/pkg/server/tmpl/app_test.go b/pkg/server/tmpl/app_test.go
index d0ea83b0..664c4393 100644
--- a/pkg/server/tmpl/app_test.go
+++ b/pkg/server/tmpl/app_test.go
@@ -24,14 +24,17 @@ import (
"testing"
"github.com/dnote/dnote/pkg/assert"
+ "github.com/dnote/dnote/pkg/server/app"
"github.com/dnote/dnote/pkg/server/database"
"github.com/dnote/dnote/pkg/server/testutils"
"github.com/pkg/errors"
)
func TestAppShellExecute(t *testing.T) {
+ testApp := app.NewTest(nil)
+
t.Run("home", func(t *testing.T) {
- a, err := NewAppShell([]byte("
{{ .Title }}{{ .MetaTags }}"))
+ a, err := NewAppShell(&testApp, []byte("{{ .Title }}{{ .MetaTags }}"))
if err != nil {
t.Fatal(errors.Wrap(err, "preparing app shell"))
}
@@ -41,7 +44,7 @@ func TestAppShellExecute(t *testing.T) {
t.Fatal(errors.Wrap(err, "preparing request"))
}
- b, err := a.Execute(r, testutils.DB)
+ b, err := a.Execute(r)
if err != nil {
t.Fatal(errors.Wrap(err, "executing"))
}
@@ -66,7 +69,7 @@ func TestAppShellExecute(t *testing.T) {
}
testutils.MustExec(t, testutils.DB.Save(&n1), "preparing note")
- a, err := NewAppShell([]byte("{{ .MetaTags }}"))
+ a, err := NewAppShell(&testApp, []byte("{{ .MetaTags }}"))
if err != nil {
t.Fatal(errors.Wrap(err, "preparing app shell"))
}
@@ -77,7 +80,7 @@ func TestAppShellExecute(t *testing.T) {
t.Fatal(errors.Wrap(err, "preparing request"))
}
- b, err := a.Execute(r, testutils.DB)
+ b, err := a.Execute(r)
if err != nil {
t.Fatal(errors.Wrap(err, "executing"))
}
diff --git a/pkg/server/tmpl/data.go b/pkg/server/tmpl/data.go
index fcfce9b0..c40b8bb4 100644
--- a/pkg/server/tmpl/data.go
+++ b/pkg/server/tmpl/data.go
@@ -29,8 +29,6 @@ import (
"github.com/dnote/dnote/pkg/server/database"
"github.com/dnote/dnote/pkg/server/handlers"
- "github.com/dnote/dnote/pkg/server/operations"
- "github.com/jinzhu/gorm"
"github.com/pkg/errors"
)
@@ -52,13 +50,13 @@ type notePage struct {
T *template.Template
}
-func (a AppShell) newNotePage(db *gorm.DB, r *http.Request, noteUUID string) (notePage, error) {
- user, _, err := handlers.AuthWithSession(db, r, nil)
+func (a AppShell) newNotePage(r *http.Request, noteUUID string) (notePage, error) {
+ user, _, err := handlers.AuthWithSession(a.App.DB, r, nil)
if err != nil {
return notePage{}, errors.Wrap(err, "authenticating with session")
}
- note, ok, err := operations.GetNote(db, noteUUID, user)
+ note, ok, err := a.App.GetNote(noteUUID, user)
if !ok {
return notePage{}, ErrNotFound
diff --git a/pkg/server/tmpl/data_test.go b/pkg/server/tmpl/data_test.go
index d0709ec5..c315bc2d 100644
--- a/pkg/server/tmpl/data_test.go
+++ b/pkg/server/tmpl/data_test.go
@@ -24,6 +24,7 @@ import (
"time"
"github.com/dnote/dnote/pkg/assert"
+ "github.com/dnote/dnote/pkg/server/app"
"github.com/dnote/dnote/pkg/server/database"
"github.com/pkg/errors"
)
@@ -38,7 +39,8 @@ func TestDefaultPageGetData(t *testing.T) {
}
func TestNotePageGetData(t *testing.T) {
- a, err := NewAppShell(nil)
+ testApp := app.NewTest(nil)
+ a, err := NewAppShell(&testApp, nil)
if err != nil {
t.Fatal(errors.Wrap(err, "preparing app shell"))
}
diff --git a/pkg/server/tmpl/main_test.go b/pkg/server/tmpl/main_test.go
index 81f2b954..77b7935f 100644
--- a/pkg/server/tmpl/main_test.go
+++ b/pkg/server/tmpl/main_test.go
@@ -1,3 +1,21 @@
+/* Copyright (C) 2019 Monomax Software Pty Ltd
+ *
+ * This file is part of Dnote.
+ *
+ * Dnote 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.
+ *
+ * Dnote 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 Dnote. If not, see .
+ */
+
package tmpl
import (
diff --git a/pkg/server/web/handlers.go b/pkg/server/web/handlers.go
index 415baf02..36316b9a 100644
--- a/pkg/server/web/handlers.go
+++ b/pkg/server/web/handlers.go
@@ -22,15 +22,13 @@ package web
import (
"net/http"
+ "github.com/dnote/dnote/pkg/server/app"
"github.com/dnote/dnote/pkg/server/handlers"
"github.com/dnote/dnote/pkg/server/tmpl"
- "github.com/jinzhu/gorm"
"github.com/pkg/errors"
)
var (
- // ErrEmptyDB is an error for missing database connection in the context
- ErrEmptyDB = errors.New("No database connection was provided")
// ErrEmptyIndexHTML is an error for missing index.html content in the context
ErrEmptyIndexHTML = errors.New("No index.html content was provided")
// ErrEmptyRobotsTxt is an error for missing robots.txt content in the context
@@ -43,7 +41,7 @@ var (
// Context contains contents of web assets
type Context struct {
- DB *gorm.DB
+ App *app.App
IndexHTML []byte
RobotsTxt []byte
ServiceWorkerJs []byte
@@ -59,8 +57,8 @@ type Handlers struct {
}
func validateContext(c Context) error {
- if c.DB == nil {
- return ErrEmptyDB
+ if err := c.App.Validate(); err != nil {
+ return errors.Wrap(err, "validating app")
}
if c.IndexHTML == nil {
return ErrEmptyIndexHTML
@@ -94,7 +92,7 @@ func Init(c Context) (Handlers, error) {
// getRootHandler returns an HTTP handler that serves the app shell
func getRootHandler(c Context) http.HandlerFunc {
- appShell, err := tmpl.NewAppShell(c.IndexHTML)
+ appShell, err := tmpl.NewAppShell(c.App, c.IndexHTML)
if err != nil {
panic(errors.Wrap(err, "initializing app shell"))
}
@@ -103,7 +101,7 @@ func getRootHandler(c Context) http.HandlerFunc {
// index.html must not be cached
w.Header().Set("Cache-Control", "no-cache")
- buf, err := appShell.Execute(r, c.DB)
+ buf, err := appShell.Execute(r)
if err != nil {
if errors.Cause(err) == tmpl.ErrNotFound {
handlers.RespondNotFound(w)
diff --git a/pkg/server/web/handlers_test.go b/pkg/server/web/handlers_test.go
index 508a024f..01e339bd 100644
--- a/pkg/server/web/handlers_test.go
+++ b/pkg/server/web/handlers_test.go
@@ -1,3 +1,21 @@
+/* Copyright (C) 2019 Monomax Software Pty Ltd
+ *
+ * This file is part of Dnote.
+ *
+ * Dnote 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.
+ *
+ * Dnote 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 Dnote. If not, see .
+ */
+
package web
import (
@@ -6,7 +24,7 @@ import (
"testing"
"github.com/dnote/dnote/pkg/assert"
- "github.com/jinzhu/gorm"
+ "github.com/dnote/dnote/pkg/server/app"
"github.com/pkg/errors"
)
@@ -16,13 +34,17 @@ func TestInit(t *testing.T) {
mockServiceWorkerJs := []byte("function() {}")
mockStaticFileSystem := http.Dir(".")
+ testApp := app.NewTest(nil)
+ testAppNoDB := app.NewTest(nil)
+ testAppNoDB.DB = nil
+
testCases := []struct {
ctx Context
expectedErr error
}{
{
ctx: Context{
- DB: &gorm.DB{},
+ App: &testApp,
IndexHTML: mockIndexHTML,
RobotsTxt: mockRobotsTxt,
ServiceWorkerJs: mockServiceWorkerJs,
@@ -32,17 +54,17 @@ func TestInit(t *testing.T) {
},
{
ctx: Context{
- DB: nil,
+ App: &testAppNoDB,
IndexHTML: mockIndexHTML,
RobotsTxt: mockRobotsTxt,
ServiceWorkerJs: mockServiceWorkerJs,
StaticFileSystem: mockStaticFileSystem,
},
- expectedErr: ErrEmptyDB,
+ expectedErr: app.ErrEmptyDB,
},
{
ctx: Context{
- DB: &gorm.DB{},
+ App: &testApp,
IndexHTML: nil,
RobotsTxt: mockRobotsTxt,
ServiceWorkerJs: mockServiceWorkerJs,
@@ -52,7 +74,7 @@ func TestInit(t *testing.T) {
},
{
ctx: Context{
- DB: &gorm.DB{},
+ App: &testApp,
IndexHTML: mockIndexHTML,
RobotsTxt: nil,
ServiceWorkerJs: mockServiceWorkerJs,
@@ -62,7 +84,7 @@ func TestInit(t *testing.T) {
},
{
ctx: Context{
- DB: &gorm.DB{},
+ App: &testApp,
IndexHTML: mockIndexHTML,
RobotsTxt: mockRobotsTxt,
ServiceWorkerJs: nil,
@@ -72,7 +94,7 @@ func TestInit(t *testing.T) {
},
{
ctx: Context{
- DB: &gorm.DB{},
+ App: &testApp,
IndexHTML: mockIndexHTML,
RobotsTxt: mockRobotsTxt,
ServiceWorkerJs: mockServiceWorkerJs,
diff --git a/pkg/server/web/main_test.go b/pkg/server/web/main_test.go
new file mode 100644
index 00000000..0611f9ca
--- /dev/null
+++ b/pkg/server/web/main_test.go
@@ -0,0 +1,35 @@
+/* Copyright (C) 2019 Monomax Software Pty Ltd
+ *
+ * This file is part of Dnote.
+ *
+ * Dnote 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.
+ *
+ * Dnote 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 Dnote. If not, see .
+ */
+
+package web
+
+import (
+ "os"
+ "testing"
+
+ "github.com/dnote/dnote/pkg/server/testutils"
+)
+
+func TestMain(m *testing.M) {
+ testutils.InitTestDB()
+
+ code := m.Run()
+ testutils.ClearData()
+
+ os.Exit(code)
+}
diff --git a/pkg/watcher/main.go b/pkg/watcher/main.go
index 87604fd7..69bca81b 100644
--- a/pkg/watcher/main.go
+++ b/pkg/watcher/main.go
@@ -3,16 +3,16 @@
* This file is part of Dnote.
*
* Dnote is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
+ * it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Dnote 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.
+ * GNU General Public License for more details.
*
- * You should have received a copy of the GNU Affero General Public License
+ * You should have received a copy of the GNU General Public License
* along with Dnote. If not, see .
*/
diff --git a/scripts/license.sh b/scripts/license.sh
index 64be9622..ea9612ad 100755
--- a/scripts/license.sh
+++ b/scripts/license.sh
@@ -56,9 +56,10 @@ agpl="/* Copyright (C) 2019 Monomax Software Pty Ltd
*/"
dir=$(dirname "${BASH_SOURCE[0]}")
-pkgPath="$dir/pkg"
-serverPath="$dir/pkg/server"
-browserPath="$dir/browser"
+basedir="$dir/.."
+pkgPath="$basedir/pkg"
+serverPath="$basedir/pkg/server"
+browserPath="$basedir/browser"
gplFiles=$(find "$pkgPath" "$browserPath" -type f \( -name "*.go" -o -name "*.js" -o -name "*.ts" -o -name "*.tsx" -o -name "*.scss" -o -name "*.css" \) ! -path "**/vendor/*" ! -path "**/node_modules/*" ! -path "$serverPath/*")
@@ -67,8 +68,8 @@ for file in $gplFiles; do
add_notice "$file" "$gpl"
done
-webPath="$dir/web"
-jslibPath="$dir/jslib/src"
+webPath="$basedir/web"
+jslibPath="$basedir/jslib/src"
agplFiles=$(find "$serverPath" "$webPath" "$jslibPath" -type f \( -name "*.go" -o -name "*.js" -o -name "*.ts" -o -name "*.tsx" -o -name "*.scss" -o -name "*.css" \) ! -path "**/vendor/*" ! -path "**/node_modules/*" ! -path "**/dist/*")
for file in $agplFiles; do
diff --git a/web/jest.config.js b/web/jest.config.js
index 99ab01ad..cfbb1ab5 100644
--- a/web/jest.config.js
+++ b/web/jest.config.js
@@ -1,3 +1,21 @@
+/* Copyright (C) 2019 Monomax Software Pty Ltd
+ *
+ * This file is part of Dnote.
+ *
+ * Dnote 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.
+ *
+ * Dnote 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 Dnote. If not, see .
+ */
+
module.exports = {
preset: 'ts-jest',
collectCoverageFrom: ['./src/**/*.ts']
diff --git a/web/src/components/Repetition/Content.tsx b/web/src/components/Repetition/Content.tsx
index af6736e1..461768f0 100644
--- a/web/src/components/Repetition/Content.tsx
+++ b/web/src/components/Repetition/Content.tsx
@@ -1,3 +1,21 @@
+/* Copyright (C) 2019 Monomax Software Pty Ltd
+ *
+ * This file is part of Dnote.
+ *
+ * Dnote 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.
+ *
+ * Dnote 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 Dnote. If not, see .
+ */
+
import React, { Fragment, useState, useEffect } from 'react';
import classnames from 'classnames';
diff --git a/web/src/components/Subscription/Footer.tsx b/web/src/components/Subscription/Footer.tsx
index 8e884b4e..b1bd8a04 100644
--- a/web/src/components/Subscription/Footer.tsx
+++ b/web/src/components/Subscription/Footer.tsx
@@ -1,3 +1,21 @@
+/* Copyright (C) 2019 Monomax Software Pty Ltd
+ *
+ * This file is part of Dnote.
+ *
+ * Dnote 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.
+ *
+ * Dnote 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 Dnote. If not, see .
+ */
+
import React from 'react';
import styles from './Subscription.scss';
diff --git a/web/src/components/Subscription/Plan/ProCTA.tsx b/web/src/components/Subscription/Plan/ProCTA.tsx
index aaa4708c..7d76f12e 100644
--- a/web/src/components/Subscription/Plan/ProCTA.tsx
+++ b/web/src/components/Subscription/Plan/ProCTA.tsx
@@ -1,3 +1,21 @@
+/* Copyright (C) 2019 Monomax Software Pty Ltd
+ *
+ * This file is part of Dnote.
+ *
+ * Dnote 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.
+ *
+ * Dnote 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 Dnote. If not, see .
+ */
+
import React from 'react';
import { Link } from 'react-router-dom';
import classnames from 'classnames';