Skip to content

Commit

Permalink
retire accounts via rpc on deletion
Browse files Browse the repository at this point in the history
  • Loading branch information
m90 committed Aug 3, 2019
1 parent ce7bc36 commit e991003
Show file tree
Hide file tree
Showing 8 changed files with 156 additions and 5 deletions.
2 changes: 1 addition & 1 deletion accounts/accounts/models.py
Expand Up @@ -11,7 +11,7 @@ class Account(db.Model):
__tablename__ = "accounts"
account_id = db.Column(db.String(36), primary_key=True, default=generate_key)
name = db.Column(db.Text, nullable=False)
users = db.relationship("AccountUserAssociation", back_populates="account")
users = db.relationship("AccountUserAssociation", back_populates="account", cascade="delete")

def __repr__(self):
return self.name
Expand Down
24 changes: 22 additions & 2 deletions accounts/accounts/views.py
Expand Up @@ -20,7 +20,7 @@ def __str__(self):
)


def create_remote_account(account_id):
def _call_remote_server(account_id, method):
# expires in 30 seconds as this will mean the HTTP request would have
# timed out anyways
expiry = datetime.utcnow() + timedelta(seconds=30)
Expand All @@ -30,7 +30,16 @@ def create_remote_account(account_id):
algorithm="RS256",
).decode("utf-8")

r = requests.post(
do_request = None
if method == "POST":
do_request = requests.post
elif method == "DELETE":
do_request = requests.delete

if not do_request:
raise Exception("Received unsupported method {}, cannot continue.".format(method))

r = do_request(
"{}/accounts".format(app.config["SERVER_HOST"]),
json={"accountId": account_id},
headers={"X-RPC-Authentication": encoded},
Expand All @@ -43,6 +52,14 @@ def create_remote_account(account_id):
raise remote_err


def create_remote_account(account_id):
return _call_remote_server(account_id, "POST")


def retire_remote_account(account_id):
return _call_remote_server(account_id, "DELETE")


class AccountForm(Form):
name = StringField(
"Account Name",
Expand All @@ -65,6 +82,9 @@ def after_model_change(self, form, model, is_created):
db.session.commit()
raise server_error

def after_model_delete(self, model):
retire_remote_account(model.account_id)


class UserView(ModelView):
inline_models = [(AccountUserAssociation, dict(form_columns=["id", "account"]))]
Expand Down
1 change: 1 addition & 0 deletions server/persistence/persistence.go
Expand Up @@ -6,6 +6,7 @@ type Database interface {
Query(Query) (map[string][]EventResult, error)
GetAccount(accountID string, events bool, eventsSince string) (AccountResult, error)
CreateAccount(accountID string) error
RetireAccount(accountID string) error
GetDeletedEvents(ids []string, userID string) ([]string, error)
AssociateUserSecret(accountID, userID, encryptedUserSecret string) error
Purge(userID string) error
Expand Down
11 changes: 11 additions & 0 deletions server/persistence/relational/accounts.go
Expand Up @@ -171,3 +171,14 @@ func (r *relationalDatabase) CreateAccount(accountID string) error {
Retired: false,
}).Error
}

func (r *relationalDatabase) RetireAccount(accountID string) error {
var account Account
if err := r.db.First(&account, "account_id = ? AND retired = ?", accountID, false).Error; err != nil {
if gorm.IsRecordNotFoundError(err) {
return persistence.ErrUnknownAccount(fmt.Sprintf("unknown account with id %s or it is already retired", accountID))
}
return err
}
return r.db.Model(&Account{AccountID: accountID}).Update("retired", true).Error
}
57 changes: 57 additions & 0 deletions server/persistence/relational/accounts_test.go
Expand Up @@ -48,6 +48,63 @@ func (m *mockEncrypter) Encrypt([]byte) ([]byte, error) {
return m.result, m.err
}

func TestRelationalDatabase_RetireAccount(t *testing.T) {
tests := []struct {
name string
setup func(*gorm.DB) error
assertion func(*gorm.DB) error
expectError bool
}{
{
"unknown account",
func(db *gorm.DB) error {
return nil
},
func(db *gorm.DB) error {
return nil
},
true,
},
{
"ok",
func(db *gorm.DB) error {
return db.Create(&Account{AccountID: "account-id"}).Error
},
func(db *gorm.DB) error {
var match Account
db.First(&match, "account_id = ?", "account-id")
if match.Retired != true {
return errors.New("expected account to be retired")
}
return nil
},
false,
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
db, closeDB := createTestDatabase()
defer closeDB()

if err := test.setup(db); err != nil {
t.Fatalf("Unexpected error setting up test %v", err)
}

relational := relationalDatabase{db: db}

err := relational.RetireAccount("account-id")
if (err != nil) != test.expectError {
t.Errorf("Unexpected error value %v", err)
}

if err := test.assertion(db); err != nil {
t.Errorf("Unexpected assertion error %v", err)
}
})
}
}

func TestRelationalDatabase_CreateAccount(t *testing.T) {
tests := []struct {
name string
Expand Down
13 changes: 13 additions & 0 deletions server/router/accounts.go
Expand Up @@ -52,3 +52,16 @@ func (rt *router) postAccount(w http.ResponseWriter, r *http.Request) {
}
w.WriteHeader(http.StatusNoContent)
}

func (rt *router) deleteAccount(w http.ResponseWriter, r *http.Request) {
var payload accountPayload
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
httputil.RespondWithJSONError(w, fmt.Errorf("router: error parsing request payload: %v", err), http.StatusBadRequest)
return
}
if err := rt.db.RetireAccount(payload.AccountID); err != nil {
httputil.RespondWithJSONError(w, fmt.Errorf("router: error retiring account: %v", err), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
}
52 changes: 50 additions & 2 deletions server/router/accounts_test.go
Expand Up @@ -118,13 +118,13 @@ func TestRouter_PostAccount(t *testing.T) {
{
"bad database",
&mockCreateAccountDatabase{err: errors.New("did not work")},
`{"accountId":"some-account-id","name":"Woz"}`,
`{"accountId":"some-account-id"}`,
http.StatusInternalServerError,
},
{
"ok",
&mockCreateAccountDatabase{},
`{"accountId":"some-account-id","name":"Woz"}`,
`{"accountId":"some-account-id"}`,
http.StatusNoContent,
},
}
Expand All @@ -140,3 +140,51 @@ func TestRouter_PostAccount(t *testing.T) {
})
}
}

type mockRetireAccountDatabase struct {
persistence.Database
err error
}

func (m *mockRetireAccountDatabase) RetireAccount(string) error {
return m.err
}

func TestRouter_RetireAccount(t *testing.T) {
tests := []struct {
name string
database persistence.Database
payload string
expectedStatusCode int
}{
{
"bad payload",
&mockRetireAccountDatabase{},
"this-is-not-json",
http.StatusBadRequest,
},
{
"bad database",
&mockRetireAccountDatabase{err: errors.New("did not work")},
`{"accountId":"some-account-id"}`,
http.StatusInternalServerError,
},
{
"ok",
&mockRetireAccountDatabase{},
`{"accountId":"some-account-id"}`,
http.StatusNoContent,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
rt := router{db: test.database}
w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodDelete, "/", strings.NewReader(test.payload))
rt.deleteAccount(w, r)
if w.Code != test.expectedStatusCode {
t.Errorf("Unexpected status code %v", w.Code)
}
})
}
}
1 change: 1 addition & 0 deletions server/router/router.go
Expand Up @@ -173,6 +173,7 @@ func New(opts ...Config) http.Handler {
accounts := m.PathPrefix("/accounts").Subrouter()
accounts.Handle("", getAuth(http.HandlerFunc(rt.getAccount))).Methods(http.MethodGet)
accounts.Handle("", postAuth(http.HandlerFunc(rt.postAccount))).Methods(http.MethodPost)
accounts.Handle("", postAuth(http.HandlerFunc(rt.deleteAccount))).Methods(http.MethodDelete)

deleted := m.PathPrefix("/deleted").Subrouter()
deletedEventsForUser := userCookie(http.HandlerFunc(rt.getDeletedEvents))
Expand Down

0 comments on commit e991003

Please sign in to comment.