Просмотр исходного кода

implement DELETE http://localhost:8080/user

Mike 2 лет назад
Родитель
Сommit
f05b0c1b05
7 измененных файлов с 233 добавлено и 1 удалено
  1. 15 0
      README.md
  2. 19 1
      api.yml
  3. 24 0
      server/http/mgmt_api.go
  4. 72 0
      server/http/mgmt_api_test.go
  5. 46 0
      server/http/mock_user_manager_test.go
  6. 22 0
      state/user_store.go
  7. 35 0
      state/user_store_test.go

+ 15 - 0
README.md

@@ -70,6 +70,15 @@ Invoke-WebRequest -Uri http://localhost:8080/user `
   -ContentType "application/json"
 ```
 
+#### Delete Users
+
+```powershell
+Invoke-WebRequest -Uri http://localhost:8080/user `
+  -Body '{"screen_name": "user123"}' `
+  -Method Delete `
+  -ContentType "application/json"
+```
+
 #### Change Password
 
 ```powershell
@@ -101,6 +110,12 @@ curl http://localhost:8080/user
 curl -d'{"screen_name":"myscreenname", "password":"thepassword"}' http://localhost:8080/user
 ```
 
+#### Delete Users
+
+```shell
+curl -X DELETE -d '{"screen_name": "user123"}' http://localhost:8080/user
+```
+
 #### Change Password
 
 ```shell

+ 19 - 1
api.yml

@@ -1,7 +1,7 @@
 openapi: 3.0.3
 info:
   title: User Management API
-  description: API to manage user accounts with screen names and passwords
+  description: API that provides management functionality for Retro AIM Server operators.
   version: 1.0.0
 paths:
   /user:
@@ -44,6 +44,24 @@ paths:
           description: Bad request. Invalid input data.
         '409':
           description: Conflict. A user with the specified screen name already exists.
+    delete:
+      summary: Delete a user
+      description: Delete a user account specified by their screen name.
+      requestBody:
+        required: true
+        content:
+          application/json:
+            schema:
+              type: object
+              properties:
+                screen_name:
+                  type: string
+                  description: The screen name of the user to delete.
+      responses:
+        '204':
+          description: User deleted successfully.
+        '404':
+          description: User not found.
 
   /session:
     get:

+ 24 - 0
server/http/mgmt_api.go

@@ -34,6 +34,7 @@ type onlineUsers struct {
 
 type UserManager interface {
 	AllUsers() ([]state.User, error)
+	DeleteUser(screenName string) error
 	InsertUser(u state.User) error
 	SetUserPassword(u state.User) error
 	User(screenName string) (*state.User, error)
@@ -77,6 +78,8 @@ func userHandler(
 	logger *slog.Logger,
 ) {
 	switch r.Method {
+	case http.MethodDelete:
+		deleteUserHandler(w, r, userManager, logger)
 	case http.MethodGet:
 		getUserHandler(w, r, userManager, logger)
 	case http.MethodPost:
@@ -86,6 +89,27 @@ func userHandler(
 	}
 }
 
+func deleteUserHandler(w http.ResponseWriter, r *http.Request, manager UserManager, logger *slog.Logger) {
+	user := state.User{}
+	if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
+		http.Error(w, "malformed input", http.StatusBadRequest)
+		return
+	}
+	err := manager.DeleteUser(user.ScreenName)
+	switch {
+	case errors.Is(err, state.ErrNoUser):
+		http.Error(w, "user does not exist", http.StatusNotFound)
+		return
+	case err != nil:
+		logger.Error("error deleting user DELETE /user", "err", err.Error())
+		http.Error(w, "internal server error", http.StatusInternalServerError)
+		return
+	}
+
+	w.WriteHeader(http.StatusNoContent)
+	fmt.Fprintln(w, "User account successfully deleted.")
+}
+
 func userPasswordHandler(
 	w http.ResponseWriter,
 	r *http.Request,

+ 72 - 0
server/http/mgmt_api_test.go

@@ -228,6 +228,78 @@ func TestUserHandler_POST(t *testing.T) {
 	}
 }
 
+func TestUserHandler_DELETE(t *testing.T) {
+	tt := []struct {
+		name           string
+		body           string
+		user           state.User
+		userHandlerErr error
+		want           string
+		statusCode     int
+	}{
+		{
+			name: "with valid user",
+			body: `{"screen_name":"userA"}`,
+			user: state.User{
+				ScreenName: "userA",
+			},
+			want:       `User account successfully deleted.`,
+			statusCode: http.StatusNoContent,
+		},
+		{
+			name: "with non-existent user",
+			body: `{"screen_name":"userA"}`,
+			user: state.User{
+				ScreenName: "userA",
+			},
+			userHandlerErr: state.ErrNoUser,
+			want:           `user does not exist`,
+			statusCode:     http.StatusNotFound,
+		},
+		{
+			name:       "with malformed body",
+			body:       `{"screen_name":"userA"`,
+			user:       state.User{},
+			want:       `malformed input`,
+			statusCode: http.StatusBadRequest,
+		},
+		{
+			name: "user handler error",
+			body: `{"screen_name":"userA"}`,
+			user: state.User{
+				ScreenName: "userA",
+			},
+			userHandlerErr: io.EOF,
+			want:           `internal server error`,
+			statusCode:     http.StatusInternalServerError,
+		},
+	}
+
+	for _, tc := range tt {
+		t.Run(tc.name, func(t *testing.T) {
+			request := httptest.NewRequest(http.MethodDelete, "/user", strings.NewReader(tc.body))
+			responseRecorder := httptest.NewRecorder()
+
+			userManager := newMockUserManager(t)
+			if tc.user.ScreenName != "" {
+				userManager.EXPECT().
+					DeleteUser(tc.user.ScreenName).
+					Return(tc.userHandlerErr)
+			}
+
+			userHandler(responseRecorder, request, userManager, nil, slog.Default())
+
+			if responseRecorder.Code != tc.statusCode {
+				t.Errorf("want status '%d', got '%d'", tc.statusCode, responseRecorder.Code)
+			}
+
+			if strings.TrimSpace(responseRecorder.Body.String()) != tc.want {
+				t.Errorf("want '%s', got '%s'", tc.want, responseRecorder.Body)
+			}
+		})
+	}
+}
+
 func TestUserPasswordHandler_PUT(t *testing.T) {
 	tt := []struct {
 		name           string

+ 46 - 0
server/http/mock_user_manager_test.go

@@ -77,6 +77,52 @@ func (_c *mockUserManager_AllUsers_Call) RunAndReturn(run func() ([]state.User,
 	return _c
 }
 
+// DeleteUser provides a mock function with given fields: screenName
+func (_m *mockUserManager) DeleteUser(screenName string) error {
+	ret := _m.Called(screenName)
+
+	if len(ret) == 0 {
+		panic("no return value specified for DeleteUser")
+	}
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(string) error); ok {
+		r0 = rf(screenName)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// mockUserManager_DeleteUser_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteUser'
+type mockUserManager_DeleteUser_Call struct {
+	*mock.Call
+}
+
+// DeleteUser is a helper method to define mock.On call
+//   - screenName string
+func (_e *mockUserManager_Expecter) DeleteUser(screenName interface{}) *mockUserManager_DeleteUser_Call {
+	return &mockUserManager_DeleteUser_Call{Call: _e.mock.On("DeleteUser", screenName)}
+}
+
+func (_c *mockUserManager_DeleteUser_Call) Run(run func(screenName string)) *mockUserManager_DeleteUser_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(string))
+	})
+	return _c
+}
+
+func (_c *mockUserManager_DeleteUser_Call) Return(_a0 error) *mockUserManager_DeleteUser_Call {
+	_c.Call.Return(_a0)
+	return _c
+}
+
+func (_c *mockUserManager_DeleteUser_Call) RunAndReturn(run func(string) error) *mockUserManager_DeleteUser_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
 // InsertUser provides a mock function with given fields: u
 func (_m *mockUserManager) InsertUser(u state.User) error {
 	ret := _m.Called(u)

+ 22 - 0
state/user_store.go

@@ -192,6 +192,28 @@ func (f SQLiteUserStore) InsertUser(u User) error {
 	return nil
 }
 
+// DeleteUser deletes a user from the store. Return ErrNoUser if the user did
+// not exist prior to deletion.
+func (f SQLiteUserStore) DeleteUser(screenName string) error {
+	q := `
+		DELETE FROM user WHERE screenName = ?
+	`
+	result, err := f.db.Exec(q, screenName)
+	if err != nil {
+		return err
+	}
+
+	rowsAffected, err := result.RowsAffected()
+	if err != nil {
+		return err
+	}
+	if rowsAffected == 0 {
+		return ErrNoUser
+	}
+
+	return nil
+}
+
 // SetUserPassword sets the user's password hashes and auth key.
 func (f SQLiteUserStore) SetUserPassword(u User) error {
 	tx, err := f.db.Begin()

+ 35 - 0
state/user_store_test.go

@@ -419,6 +419,41 @@ func TestSQLiteUserStore_Users(t *testing.T) {
 	assert.Equal(t, want, have)
 }
 
+func TestSQLiteUserStore_DeleteUser_DeleteExistentUser(t *testing.T) {
+	defer func() {
+		assert.NoError(t, os.Remove(testFile))
+	}()
+
+	f, err := NewSQLiteUserStore(testFile)
+	assert.NoError(t, err)
+
+	err = f.InsertUser(User{ScreenName: "userA"})
+	assert.NoError(t, err)
+	err = f.InsertUser(User{ScreenName: "userB"})
+	assert.NoError(t, err)
+
+	err = f.DeleteUser("userA")
+	assert.NoError(t, err)
+
+	have, err := f.AllUsers()
+	assert.NoError(t, err)
+
+	want := []User{{ScreenName: "userB"}}
+	assert.Equal(t, want, have)
+}
+
+func TestSQLiteUserStore_DeleteUser_DeleteNonExistentUser(t *testing.T) {
+	defer func() {
+		assert.NoError(t, os.Remove(testFile))
+	}()
+
+	f, err := NewSQLiteUserStore(testFile)
+	assert.NoError(t, err)
+
+	err = f.DeleteUser("userA")
+	assert.ErrorIs(t, ErrNoUser, err)
+}
+
 func TestSQLiteUserStore_Buddies(t *testing.T) {
 	defer func() {
 		assert.NoError(t, os.Remove(testFile))