mirror of
https://github.com/juanfont/headscale.git
synced 2026-11-04 04:34:07 +09:00
State.RenameUser went through UpdateUser and skipped the checks db.RenameUser applies, so the API could rename an OIDC user, whose name the identity provider owns, or rename any user to an invalid name. Apply both checks in the update function and map the username validation errors to 400.
154 lines
4.5 KiB
Go
154 lines
4.5 KiB
Go
package hscontrol
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"testing"
|
|
|
|
"github.com/juanfont/headscale/hscontrol/types"
|
|
"github.com/juanfont/headscale/hscontrol/util"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func seedUsers(names ...string) func(t *testing.T, app *Headscale) {
|
|
return func(t *testing.T, app *Headscale) {
|
|
t.Helper()
|
|
|
|
for _, n := range names {
|
|
app.state.CreateUserForTest(n)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAPIV1CreateUser(t *testing.T) {
|
|
t.Run("huma response shape", func(t *testing.T) {
|
|
h := newAPIV1Harness(t)
|
|
|
|
res := h.callHuma(http.MethodPost, "/api/v1/user", []byte(`{"name":"test"}`))
|
|
require.Equal(t, http.StatusOK, res.status)
|
|
|
|
var got struct {
|
|
User map[string]any `json:"user"`
|
|
}
|
|
require.NoError(t, json.Unmarshal(res.body, &got))
|
|
|
|
assert.Equal(t, "1", got.User["id"])
|
|
assert.Equal(t, "test", got.User["name"])
|
|
assert.Contains(t, got.User, "createdAt")
|
|
// Zero-value fields are emitted as empty strings (EmitUnpopulated parity).
|
|
assert.Empty(t, got.User["email"])
|
|
assert.Empty(t, got.User["displayName"])
|
|
})
|
|
|
|
t.Run("parity", func(t *testing.T) {
|
|
assertParityIsolated(t, nil, http.MethodPost, "/api/v1/user",
|
|
[]byte(`{"name":"test","displayName":"Test","email":"t@example.com"}`))
|
|
})
|
|
|
|
t.Run("duplicate name parity", func(t *testing.T) {
|
|
res := assertParityIsolated(t, seedUsers("dup"), http.MethodPost, "/api/v1/user",
|
|
[]byte(`{"name":"dup"}`))
|
|
assertStatus(t, res, http.StatusConflict)
|
|
})
|
|
}
|
|
|
|
func TestAPIV1RenameUser(t *testing.T) {
|
|
t.Run("parity", func(t *testing.T) {
|
|
assertParityIsolated(t, seedUsers("alice"), http.MethodPost,
|
|
"/api/v1/user/1/rename/bob", nil)
|
|
})
|
|
|
|
t.Run("nonexistent parity", func(t *testing.T) {
|
|
h := newAPIV1Harness(t)
|
|
res := h.assertParity(t, http.MethodPost, "/api/v1/user/999/rename/bob", nil)
|
|
assertStatus(t, res, http.StatusNotFound)
|
|
})
|
|
|
|
t.Run("invalid id parity", func(t *testing.T) {
|
|
h := newAPIV1Harness(t)
|
|
res := h.assertParity(t, http.MethodPost, "/api/v1/user/abc/rename/bob", nil)
|
|
assertStatus(t, res, http.StatusBadRequest)
|
|
})
|
|
|
|
t.Run("oidc user refused", func(t *testing.T) {
|
|
h := newAPIV1Harness(t)
|
|
user := h.app.state.CreateUserForTest("alice")
|
|
require.NoError(t, h.app.state.DB().DB.Model(user).
|
|
Update("provider", util.RegisterMethodOIDC).Error)
|
|
|
|
res := h.callHuma(http.MethodPost, "/api/v1/user/1/rename/bob", nil)
|
|
assertStatus(t, res, http.StatusBadRequest)
|
|
|
|
got, err := h.app.state.GetUserByID(types.UserID(user.ID))
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "alice", got.Name)
|
|
})
|
|
|
|
for _, name := range []string{"b", "1bob", "bob!", "bob@@example.com"} {
|
|
t.Run("invalid name "+name+" refused", func(t *testing.T) {
|
|
h := newAPIV1Harness(t)
|
|
seedUsers("alice")(t, h.app)
|
|
|
|
res := h.callHuma(http.MethodPost, "/api/v1/user/1/rename/"+name, nil)
|
|
assertStatus(t, res, http.StatusBadRequest)
|
|
|
|
got, err := h.app.state.GetUserByID(1)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "alice", got.Name)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAPIV1DeleteUser(t *testing.T) {
|
|
t.Run("parity", func(t *testing.T) {
|
|
assertParityIsolated(t, seedUsers("alice"), http.MethodDelete, "/api/v1/user/1", nil)
|
|
})
|
|
|
|
t.Run("nonexistent parity", func(t *testing.T) {
|
|
h := newAPIV1Harness(t)
|
|
res := h.assertParity(t, http.MethodDelete, "/api/v1/user/999", nil)
|
|
assertStatus(t, res, http.StatusNotFound)
|
|
})
|
|
}
|
|
|
|
func TestAPIV1ListUsers(t *testing.T) {
|
|
t.Run("empty returns empty array", func(t *testing.T) {
|
|
h := newAPIV1Harness(t)
|
|
|
|
res := h.callHuma(http.MethodGet, "/api/v1/user", nil)
|
|
require.Equal(t, http.StatusOK, res.status)
|
|
assert.JSONEq(t, `{"users":[]}`, string(res.body))
|
|
})
|
|
|
|
t.Run("empty parity", func(t *testing.T) {
|
|
h := newAPIV1Harness(t)
|
|
h.assertParity(t, http.MethodGet, "/api/v1/user", nil)
|
|
})
|
|
|
|
t.Run("all parity", func(t *testing.T) {
|
|
h := newAPIV1Harness(t)
|
|
seedUsers("alice", "bob", "carol")(t, h.app)
|
|
h.assertParity(t, http.MethodGet, "/api/v1/user", nil)
|
|
})
|
|
|
|
t.Run("filter by name parity", func(t *testing.T) {
|
|
h := newAPIV1Harness(t)
|
|
seedUsers("alice", "bob")(t, h.app)
|
|
h.assertParity(t, http.MethodGet, "/api/v1/user?name=alice", nil)
|
|
})
|
|
|
|
t.Run("filter by id parity", func(t *testing.T) {
|
|
h := newAPIV1Harness(t)
|
|
seedUsers("alice", "bob")(t, h.app)
|
|
h.assertParity(t, http.MethodGet, "/api/v1/user?id=2", nil)
|
|
})
|
|
|
|
t.Run("invalid id parity", func(t *testing.T) {
|
|
h := newAPIV1Harness(t)
|
|
seedUsers("alice")(t, h.app)
|
|
res := h.assertParity(t, http.MethodGet, "/api/v1/user?id=abc", nil)
|
|
assertStatus(t, res, http.StatusBadRequest)
|
|
})
|
|
}
|