a73x

internal/server/api/principal_test.go

Ref:   Size: 4.7 KiB   History

package api

import (
	"bytes"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/a73x/eitri/internal/server/store"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestMayActAs(t *testing.T) {
	for _, tc := range []struct {
		name   string
		p      Principal
		tenant string
		want   bool
	}{
		{"same tenant", Principal{Tenant: "t1"}, "t1", true},
		{"different tenant", Principal{Tenant: "t1"}, "t2", false},
		{"zero principal fails closed", Principal{}, "t1", false},
		// The empty-tenant/empty-target degenerate: a zero principal must not
		// accidentally match a (never-valid) empty resource tenant via "" == "".
		// Guarded by mayActAs's explicit empty check.
		{"zero principal vs empty tenant", Principal{}, "", false},
	} {
		t.Run(tc.name, func(t *testing.T) {
			assert.Equal(t, tc.want, mayActAs(tc.p, tc.tenant))
		})
	}
}

func TestPrincipalFromContextFailsClosed(t *testing.T) {
	// A request that never went through userAuth carries no principal: the zero
	// value has no tenant, so every downstream mayActAs check fails closed.
	r := httptest.NewRequest("GET", "/", nil)
	p := principalFromContext(r)
	assert.Empty(t, p.Tenant)
}

func TestFilterByTenant(t *testing.T) {
	vms := []store.VM{{ID: "a", Tenant: "t1"}, {ID: "b", Tenant: "t2"}}
	hosts := []store.Host{{ID: "h1", Tenant: "t1"}, {ID: "h2", Tenant: "t2"}}

	member := Principal{Tenant: "t1"}
	gotVMs := filterVMs(member, vms)
	require.Len(t, gotVMs, 1)
	assert.Equal(t, "a", gotVMs[0].ID)
	gotHosts := filterHosts(member, hosts)
	require.Len(t, gotHosts, 1)
	assert.Equal(t, "h1", gotHosts[0].ID)

	// A different tenant sees only its own; there is no fleet-wide view.
	other := Principal{Tenant: "t2"}
	require.Len(t, filterVMs(other, vms), 1)
	assert.Equal(t, "b", filterVMs(other, vms)[0].ID)
	require.Len(t, filterHosts(other, hosts), 1)
	assert.Equal(t, "h2", filterHosts(other, hosts)[0].ID)
}

// foreignRequest builds a request carrying a Principal scoped to a tenant
// other than "default" (where testServer/enroll resources land), for calling
// handlers DIRECTLY — bypassing the mux, since userAuth would otherwise
// overwrite the principal with the credential's own tenant.
func foreignRequest(t *testing.T, method, path string, body any) *http.Request {
	t.Helper()
	var buf bytes.Buffer
	if body != nil {
		require.NoError(t, json.NewEncoder(&buf).Encode(body))
	}
	r := httptest.NewRequest(method, path, &buf)
	return r.WithContext(withPrincipal(r.Context(), Principal{Tenant: "other"}))
}

// TestHandleListVMsFiltersForeignTenant proves the wiring in snapshotVMs: a
// principal from a different tenant sees an empty list even though a VM
// exists (in "default", where enroll/create land).
func TestHandleListVMsFiltersForeignTenant(t *testing.T) {
	ts, _, _, _, a := newServer(t)
	out := enroll(t, ts)
	resp := do(t, "POST", ts.URL+"/api/v1/vms", testPAT,
		map[string]any{"host_id": out["host_id"], "name": "vm-a"})
	require.Equal(t, 201, resp.StatusCode)

	r := foreignRequest(t, "GET", "/api/v1/vms", nil)
	w := httptest.NewRecorder()
	a.handleListVMs(w, r)

	require.Equal(t, 200, w.Code)
	var vms []map[string]any
	require.NoError(t, json.NewDecoder(w.Body).Decode(&vms))
	assert.Empty(t, vms, "a foreign-tenant principal must not see another tenant's VMs")
}

// TestHandleDeleteVMForeignTenantIsNotFoundAndNoop proves mutateVM's ownership
// gate runs BEFORE mutate: a foreign-tenant delete gets the same not-found
// response as a missing VM, and the VM is still live afterward.
func TestHandleDeleteVMForeignTenantIsNotFoundAndNoop(t *testing.T) {
	ts, st, _, _, a := newServer(t)
	out := enroll(t, ts)
	created := map[string]string{}
	resp := do(t, "POST", ts.URL+"/api/v1/vms", testPAT,
		map[string]any{"host_id": out["host_id"], "name": "vm-a"})
	require.Equal(t, 201, resp.StatusCode)
	require.NoError(t, json.NewDecoder(resp.Body).Decode(&created))
	id := created["id"]

	r := foreignRequest(t, "DELETE", "/api/v1/vms/"+id, nil)
	r.SetPathValue("id", id)
	w := httptest.NewRecorder()
	a.handleDeleteVM(w, r)

	assert.Equal(t, http.StatusNotFound, w.Code)

	vm, err := st.GetVM(id)
	require.NoError(t, err)
	assert.Nil(t, vm.DeletedAt, "the ownership gate must reject before mutate runs")
}

// TestHandleCreateVMForeignTenantHostIsUnknown proves the create pre-check: a
// foreign-tenant principal placing a VM on a "default"-tenant host gets the
// same 400 as an absent host_id.
func TestHandleCreateVMForeignTenantHostIsUnknown(t *testing.T) {
	ts, _, _, _, a := newServer(t)
	out := enroll(t, ts)

	r := foreignRequest(t, "POST", "/api/v1/vms", map[string]any{"host_id": out["host_id"]})
	w := httptest.NewRecorder()
	a.handleCreateVM(w, r)

	assert.Equal(t, http.StatusBadRequest, w.Code)
	assert.Contains(t, w.Body.String(), "unknown host_id")
}