package main import ( "context" "encoding/json" "net/http" "testing" "github.com/shellhub-io/shellhub/pkg/api/responses" "github.com/shellhub-io/shellhub/pkg/models" "github.com/shellhub-io/shellhub/tests/environment" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // TestDeviceAcceptNameConflicts covers the hostname an accept would give a device being taken by // another accepted device. A fresh device is refused outright, and so is a device whose MAC would // merge it into a disconnected one, since the merge would still leave two devices answering to // the name. Either accept goes through once the other device lets the name go. func TestDeviceAcceptNameConflicts(t *testing.T) { t.Parallel() ctx := context.Background() compose := newSSHEnvironment(t, ctx, legacyNamespace) t.Run("a fresh device is refused the name an accepted device holds", func(t *testing.T) { holder := enrollDevice(t, compose, "freshtaken", "02:00:00:00:14:01") compose.UpdateDeviceStatus(t, holder, environment.DeviceActionAccept) fresh := enrollDevice(t, compose, "freshtaken", "02:00:00:00:14:02") requireAcceptConflict(t, compose, fresh) assert.Equal(t, "freshtaken", requireDevice(t, compose, holder).Name) renameDevice(t, compose, holder, "freshreleased") compose.UpdateDeviceStatus(t, fresh, environment.DeviceActionAccept) accepted := requireDevice(t, compose, fresh) assert.Equal(t, models.DeviceStatusAccepted, accepted.Status) assert.Equal(t, "freshtaken", accepted.Name) }) t.Run("a MAC merge is refused when another accepted device holds the hostname", func(t *testing.T) { agent, merged := startAcceptedAgent(t, ctx, compose) require.NoError(t, agent.Stop(ctx, nil)) compose.AwaitDeviceOffline(t, merged.UID) holder := enrollDevice(t, compose, "mergetaken", "02:00:00:00:14:03") compose.UpdateDeviceStatus(t, holder, environment.DeviceActionAccept) reinstalled := enrollDevice(t, compose, "mergetaken", merged.Identity.MAC) requireAcceptConflict(t, compose, reinstalled) assert.Equal(t, merged.Name, requireDevice(t, compose, merged.UID).Name) assert.Equal(t, models.DeviceStatusAccepted, requireDevice(t, compose, merged.UID).Status) renameDevice(t, compose, holder, "mergereleased") compose.UpdateDeviceStatus(t, reinstalled, environment.DeviceActionAccept) _, resp, err := compose.GetDevice(t.Context(), merged.UID) require.NoError(t, err) assert.Equal(t, http.StatusNotFound, resp.StatusCode(), "the merge left the disconnected device behind: %s", resp.String()) accepted := requireDevice(t, compose, reinstalled) assert.Equal(t, models.DeviceStatusAccepted, accepted.Status) assert.Equal(t, merged.Name, accepted.Name, "the merge did not carry the disconnected device's name over") }) } func requireAcceptConflict(t *testing.T, compose *environment.DockerCompose, uid string) responses.Error { t.Helper() resp, err := compose.PatchDeviceStatus(t.Context(), uid, environment.DeviceActionAccept) require.NoError(t, err) require.Equal(t, http.StatusConflict, resp.StatusCode(), resp.String()) assert.Equal(t, models.DeviceStatusPending, requireDevice(t, compose, uid).Status) var refusal responses.Error require.NoError(t, json.Unmarshal(resp.Body(), &refusal), resp.String()) return refusal } func renameDevice(t *testing.T, compose *environment.DockerCompose, uid, name string) { t.Helper() resp, err := compose.R(t.Context()).SetBody(map[string]string{"name": name}).Put("/api/devices/" + uid) require.NoError(t, err) require.Equal(t, http.StatusOK, resp.StatusCode(), resp.String()) }