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")
}