From 5ceeb60c240c01aaec1565d9c3c54000810cbbc7 Mon Sep 17 00:00:00 2001 From: Jonathon Anderson Date: Mon, 28 Jul 2025 09:31:01 -0600 Subject: [PATCH] Refactor wwapi tests for proper isolation Signed-off-by: Jonathon Anderson --- internal/pkg/warewulfd/api/image_test.go | 270 +++++---- internal/pkg/warewulfd/api/node_test.go | 671 +++++++++++++-------- internal/pkg/warewulfd/api/overlay_test.go | 302 +++++----- internal/pkg/warewulfd/api/profile_test.go | 353 ++++++----- 4 files changed, 917 insertions(+), 679 deletions(-) diff --git a/internal/pkg/warewulfd/api/image_test.go b/internal/pkg/warewulfd/api/image_test.go index eef2c7b6..683dcdcb 100644 --- a/internal/pkg/warewulfd/api/image_test.go +++ b/internal/pkg/warewulfd/api/image_test.go @@ -2,143 +2,173 @@ package api import ( "bytes" - "encoding/json" + "fmt" "io" "net" "net/http" "net/http/httptest" - "path" "testing" + "github.com/kinbiko/jsonassert" "github.com/stretchr/testify/assert" "github.com/warewulf/warewulf/internal/pkg/config" "github.com/warewulf/warewulf/internal/pkg/testenv" + "github.com/warewulf/warewulf/internal/pkg/warewulfd" ) -func TestImageAPI(t *testing.T) { - env := testenv.New(t) - defer env.RemoveAll() +var imageTests = map[string]struct { + initFiles []string + request func(serverURL string) (*http.Request, error) + response string + status int + resultFiles []string + resultAbsentFiles []string + authenticate bool +}{ + "test no authentication": { + initFiles: []string{ + "/var/lib/warewulf/chroots/test-image/rootfs/file", + }, + request: func(serverURL string) (*http.Request, error) { + return http.NewRequest(http.MethodGet, serverURL+"/api/images", nil) + }, + response: fmt.Sprintln("Unauthorized"), + status: http.StatusUnauthorized, + authenticate: false, + }, + "test get all images": { + initFiles: []string{ + "/var/lib/warewulf/chroots/test-image/rootfs/file", + }, + request: func(serverURL string) (*http.Request, error) { + return http.NewRequest(http.MethodGet, serverURL+"/api/images", nil) + }, + response: `{"test-image": {"kernels":[], "size":0, "buildtime":0, "writable":true}}`, + authenticate: true, + }, + + "test get single image": { + initFiles: []string{ + "/var/lib/warewulf/chroots/test-image/rootfs/file", + }, + request: func(serverURL string) (*http.Request, error) { + return http.NewRequest(http.MethodGet, serverURL+"/api/images/test-image", nil) + }, + response: `{"kernels":[], "size":0, "buildtime":0, "writable":true}`, + authenticate: true, + }, + + "test build image": { + initFiles: []string{ + "/var/lib/warewulf/chroots/test-image/rootfs/file", + }, + request: func(serverURL string) (*http.Request, error) { + return http.NewRequest(http.MethodPost, serverURL+"/api/images/test-image/build?force=true&default=true", nil) + }, + response: `{"kernels":[], "size":512, "buildtime":"<>", "writable":true}`, + resultFiles: []string{ + "/srv/warewulf/images/test-image.img", + "/srv/warewulf/images/test-image.img.gz", + }, + authenticate: true, + }, + + "test rename image": { + initFiles: []string{ + "/var/lib/warewulf/chroots/test-image/rootfs/file", + }, + request: func(serverURL string) (*http.Request, error) { + return http.NewRequest(http.MethodPatch, serverURL+"/api/images/test-image?build=true", bytes.NewBuffer([]byte(`{"name": "new-image"}`))) + }, + response: `{"kernels":[], "size":512, "buildtime":"<>", "writable":true}`, + authenticate: true, + }, + + "test delete image": { + initFiles: []string{ + "/var/lib/warewulf/chroots/new-image/rootfs/file", + }, + request: func(serverURL string) (*http.Request, error) { + return http.NewRequest(http.MethodDelete, serverURL+"/api/images/new-image", nil) + }, + response: `{"kernels":[], "size":0, "buildtime":"<>", "writable":true}`, + resultAbsentFiles: []string{ + "/var/lib/warewulf/chroots/new-image", + "/srv/warewulf/images/new-image.img", + "/srv/warewulf/images/new-image.img.gz", + }, + authenticate: true, + }, +} + +func TestImageAPI(t *testing.T) { authData := ` users: - name: admin password hash: $2b$05$5QVWDpiWE7L4SDL9CYdi3O/l6HnbNOLoXgY2sa1bQQ7aSBKdSqvsC ` - auth := config.NewAuthentication() - err := auth.ParseFromRaw([]byte(authData)) - assert.NoError(t, err) - allowedNets := []net.IPNet{ - { - IP: net.IPv4(127, 0, 0, 0), - Mask: net.CIDRMask(8, 32), - }, + for name, tt := range imageTests { + t.Run(name, func(t *testing.T) { + warewulfd.SetNoDaemon() + env := testenv.New(t) + defer env.RemoveAll() + + // Create test files + for _, fileName := range tt.initFiles { + env.CreateFile(fileName) + } + + auth := config.NewAuthentication() + err := auth.ParseFromRaw([]byte(authData)) + assert.NoError(t, err) + + allowedNets := []net.IPNet{ + { + IP: net.IPv4(127, 0, 0, 0), + Mask: net.CIDRMask(8, 32), + }, + } + srv := httptest.NewServer(Handler(auth, allowedNets)) + defer srv.Close() + + req, err := tt.request(srv.URL) + assert.NoError(t, err) + + if tt.authenticate { + req.SetBasicAuth("admin", "admin") + } + + resp, err := http.DefaultTransport.RoundTrip(req) + assert.NoError(t, err) + + expectedStatus := tt.status + if expectedStatus == 0 { + expectedStatus = http.StatusOK + } + assert.Equal(t, expectedStatus, resp.StatusCode) + + body, err := io.ReadAll(resp.Body) + assert.NoError(t, err) + assert.NoError(t, resp.Body.Close()) + + if expectedStatus == http.StatusUnauthorized { + // For plain text responses like Unauthorized + assert.Equal(t, tt.response, string(body)) + } else { + // For JSON responses + ja := jsonassert.New(t) + ja.Assertf(string(body), tt.response) //nolint:govet + } + + for _, fileName := range tt.resultFiles { + assert.FileExists(t, env.GetPath(fileName)) + } + + for _, fileName := range tt.resultAbsentFiles { + assert.NoFileExists(t, env.GetPath(fileName)) + } + }) } - srv := httptest.NewServer(Handler(auth, allowedNets)) - defer srv.Close() - env.WriteFile(path.Join(testenv.WWChrootdir, "test-image/rootfs/file"), `test`) - - t.Run("test no authentication", func(t *testing.T) { - req, err := http.NewRequest(http.MethodGet, srv.URL+"/api/images", nil) - assert.NoError(t, err) - - resp, err := http.DefaultTransport.RoundTrip(req) - assert.NoError(t, err) - - body, err := io.ReadAll(resp.Body) - assert.Equal(t, resp.StatusCode, http.StatusUnauthorized) - assert.NoError(t, resp.Body.Close()) - assert.NoError(t, err) - assert.Equal(t, "Unauthorized\n", string(body)) - }) - - t.Run("test get all images", func(t *testing.T) { - req, err := http.NewRequest(http.MethodGet, srv.URL+"/api/images", nil) - assert.NoError(t, err) - req.SetBasicAuth("admin", "admin") - - resp, err := http.DefaultTransport.RoundTrip(req) - assert.NoError(t, err) - - body, err := io.ReadAll(resp.Body) - assert.NoError(t, resp.Body.Close()) - assert.NoError(t, err) - assert.JSONEq(t, `{"test-image": {"kernels":[], "size":0, "buildtime":0, "writable":true}}`, string(body)) - }) - - t.Run("test get single image", func(t *testing.T) { - req, err := http.NewRequest(http.MethodGet, srv.URL+"/api/images/test-image", nil) - assert.NoError(t, err) - req.SetBasicAuth("admin", "admin") - - resp, err := http.DefaultTransport.RoundTrip(req) - assert.NoError(t, err) - - body, err := io.ReadAll(resp.Body) - assert.NoError(t, resp.Body.Close()) - assert.NoError(t, err) - assert.JSONEq(t, `{"kernels":[] ,"size":0, "buildtime":0, "writable":true}`, string(body)) - }) - - t.Run("test build image", func(t *testing.T) { - req, err := http.NewRequest(http.MethodPost, srv.URL+"/api/images/test-image/build?force=true&default=true", nil) - assert.NoError(t, err) - req.SetBasicAuth("admin", "admin") - - resp, err := http.DefaultTransport.RoundTrip(req) - assert.NoError(t, err) - - body, err := io.ReadAll(resp.Body) - assert.NoError(t, resp.Body.Close()) - assert.NoError(t, err) - - var bodyData map[string]interface{} - assert.NoError(t, json.Unmarshal([]byte(body), &bodyData)) - assert.True(t, bodyData["buildtime"].(float64) > 0.0) - - bodyData["buildtime"] = 0.0 - assert.Equal(t, map[string]interface{}{"kernels": []interface{}{}, "size": 512.0, "buildtime": 0.0, "writable": true}, bodyData) - }) - - t.Run("test rename image", func(t *testing.T) { - req, err := http.NewRequest(http.MethodPatch, srv.URL+"/api/images/test-image?build=true", bytes.NewBuffer([]byte(`{"name": "new-image"}`))) - assert.NoError(t, err) - req.SetBasicAuth("admin", "admin") - - resp, err := http.DefaultTransport.RoundTrip(req) - assert.NoError(t, err) - - body, err := io.ReadAll(resp.Body) - assert.NoError(t, resp.Body.Close()) - assert.NoError(t, err) - - var bodyData map[string]interface{} - assert.NoError(t, json.Unmarshal([]byte(body), &bodyData)) - assert.True(t, bodyData["buildtime"].(float64) > 0.0) - - bodyData["buildtime"] = 0.0 - assert.Equal(t, map[string]interface{}{"kernels": []interface{}{}, "size": 512.0, "buildtime": 0.0, "writable": true}, bodyData) - }) - - t.Run("test delete image", func(t *testing.T) { - req, err := http.NewRequest(http.MethodDelete, srv.URL+"/api/images/new-image", nil) - assert.NoError(t, err) - req.SetBasicAuth("admin", "admin") - - // send request - resp, err := http.DefaultTransport.RoundTrip(req) - assert.NoError(t, err) - - // validate the resp - body, err := io.ReadAll(resp.Body) - assert.Equal(t, resp.StatusCode, http.StatusOK) - assert.NoError(t, err) - - var bodyData map[string]interface{} - assert.NoError(t, json.Unmarshal([]byte(body), &bodyData)) - assert.True(t, bodyData["buildtime"].(float64) > 0.0) - - bodyData["buildtime"] = 0.0 - assert.Equal(t, map[string]interface{}{"kernels": []interface{}{}, "size": 512.0, "buildtime": 0.0, "writable": true}, bodyData) - }) } diff --git a/internal/pkg/warewulfd/api/node_test.go b/internal/pkg/warewulfd/api/node_test.go index 05732708..5eb26a5e 100644 --- a/internal/pkg/warewulfd/api/node_test.go +++ b/internal/pkg/warewulfd/api/node_test.go @@ -2,13 +2,11 @@ package api import ( "bytes" - "encoding/json" "io" "net" "net/http" "net/http/httptest" "testing" - "time" "github.com/kinbiko/jsonassert" "github.com/stretchr/testify/assert" @@ -16,26 +14,23 @@ import ( "github.com/warewulf/warewulf/internal/pkg/warewulfd" ) -func TestNodeAPI(t *testing.T) { - warewulfd.SetNoDaemon() - env := testenv.New(t) - env.MkdirAll("/var/lib/warewulf/overlays/so1") - env.MkdirAll("/var/lib/warewulf/overlays/ro1") - defer env.RemoveAll() - - allowedNets := []net.IPNet{ - { - IP: net.IPv4(127, 0, 0, 0), - Mask: net.CIDRMask(8, 32), +var nodeTests = map[string]struct { + initConf string + initFiles []string + request func(serverURL string) (*http.Request, error) + response string + status int + resultConf string + resultFiles []string +}{ + "add a node": { + initConf: "", + initFiles: []string{ + "/var/lib/warewulf/overlays/so1/rootfs/file", + "/var/lib/warewulf/overlays/ro1/rootfs/file", }, - } - srv := httptest.NewServer(Handler(nil, allowedNets)) - defer srv.Close() - - t.Run("add a node", func(t *testing.T) { - // prepareration - - testNode := `{ + request: func(serverURL string) (*http.Request, error) { + testNode := `{ "node":{ "system overlay": ["so1"], "runtime overlay": ["ro1"], @@ -45,221 +40,251 @@ func TestNodeAPI(t *testing.T) { } } }` - req, err := http.NewRequest(http.MethodPut, srv.URL+"/api/nodes/test", bytes.NewBuffer([]byte(testNode))) - assert.NoError(t, err) + return http.NewRequest(http.MethodPut, serverURL+"/api/nodes/n1", bytes.NewBuffer([]byte(testNode))) + }, + response: `{"system overlay": ["so1"], "runtime overlay": ["ro1"], "kernel": {"version": "v1.0.0", "args": ["kernel-args"]}}`, + resultConf: ` +nodeprofiles: {} +nodes: + n1: + system overlay: + - so1 + runtime overlay: + - ro1 + kernel: + version: "v1.0.0" + args: + - "kernel-args" +`, + }, - resp, err := http.DefaultTransport.RoundTrip(req) - assert.NoError(t, err) + "read all nodes": { + initConf: ` +nodeprofiles: {} +nodes: + n1: + system overlay: + - so1 + runtime overlay: + - ro1 + kernel: + version: "v1.0.0" + args: + - "kernel-args" +`, + request: func(serverURL string) (*http.Request, error) { + return http.NewRequest(http.MethodGet, serverURL+"/api/nodes", nil) + }, + response: `{"n1": {"system overlay": ["so1"], "runtime overlay": ["ro1"], "kernel": {"version": "v1.0.0", "args": ["kernel-args"]}}}`, + }, - body, err := io.ReadAll(resp.Body) - assert.NoError(t, err) - assert.NoError(t, resp.Body.Close()) - - assert.JSONEq(t, `{"system overlay": ["so1"], "runtime overlay": ["ro1"], "kernel": {"version": "v1.0.0", "args": ["kernel-args"]}}`, string(body)) - }) - - t.Run("read all nodes", func(t *testing.T) { - req, err := http.NewRequest(http.MethodGet, srv.URL+"/api/nodes", nil) - assert.NoError(t, err) - - // send request - resp, err := http.DefaultTransport.RoundTrip(req) - assert.NoError(t, err) - - // validate the resp - body, err := io.ReadAll(resp.Body) - assert.NoError(t, err) - assert.NoError(t, resp.Body.Close()) - - assert.JSONEq(t, `{"node1": {}, "test": {"system overlay": ["so1"], "runtime overlay": ["ro1"], "kernel": {"version": "v1.0.0", "args": ["kernel-args"]}}}`, string(body)) - }) - - t.Run("test idempotency (put same node again)", func(t *testing.T) { - // prepareration - - testNode := `{ + "test idempotency (replacing existing node)": { + initConf: ` +nodeprofiles: {} +nodes: + n1: + system overlay: + - so1 + runtime overlay: + - ro1 + kernel: + version: "v1.0.0" + args: + - "kernel-args" +`, + initFiles: []string{ + "/var/lib/warewulf/overlays/so2/rootfs/file", + "/var/lib/warewulf/overlays/ro2/rootfs/file", + }, + request: func(serverURL string) (*http.Request, error) { + testNode := `{ "node":{ - "system overlay": ["so1"], - "runtime overlay": ["ro1"], + "system overlay": ["so2"], + "runtime overlay": ["ro2"], "kernel": { "version": "v1.0.0", "args": ["kernel-args"] } } }` - req, err := http.NewRequest(http.MethodPut, srv.URL+"/api/nodes/test", bytes.NewBuffer([]byte(testNode))) - assert.NoError(t, err) + return http.NewRequest(http.MethodPut, serverURL+"/api/nodes/n1", bytes.NewBuffer([]byte(testNode))) + }, + response: `{"system overlay": ["so2"], "runtime overlay": ["ro2"], "kernel": {"version": "v1.0.0", "args": ["kernel-args"]}}`, + resultConf: ` +nodeprofiles: {} +nodes: + n1: + system overlay: + - so2 + runtime overlay: + - ro2 + kernel: + version: "v1.0.0" + args: + - "kernel-args" +`, + }, - resp, err := http.DefaultTransport.RoundTrip(req) - assert.NoError(t, err) - - body, err := io.ReadAll(resp.Body) - assert.NoError(t, err) - assert.NoError(t, resp.Body.Close()) - - assert.JSONEq(t, `{"system overlay": ["so1"], "runtime overlay": ["ro1"], "kernel": {"version": "v1.0.0", "args": ["kernel-args"]}}`, string(body)) - }) - - t.Run("fail if node already exists (given appropriate header)", func(t *testing.T) { - // prepareration - - testNode := `{ + "test preventing replacing a node": { + initConf: ` +nodeprofiles: {} +nodes: + n1: + system overlay: + - so1 + runtime overlay: + - ro1 + kernel: + version: "v1.0.0" + args: + - "kernel-args" +`, + initFiles: []string{ + "/var/lib/warewulf/overlays/so2/rootfs/file", + "/var/lib/warewulf/overlays/ro2/rootfs/file", + }, + request: func(serverURL string) (*http.Request, error) { + testNode := `{ "node":{ - "system overlay": ["so1"], - "runtime overlay": ["ro1"], + "system overlay": ["so2"], + "runtime overlay": ["ro2"], "kernel": { "version": "v1.0.0", "args": ["kernel-args"] } } }` - req, err := http.NewRequest(http.MethodPut, srv.URL+"/api/nodes/test", bytes.NewBuffer([]byte(testNode))) - assert.NoError(t, err) - req.Header.Set("If-None-Match", "*") + req, err := http.NewRequest(http.MethodPut, serverURL+"/api/nodes/n1", bytes.NewBuffer([]byte(testNode))) + req.Header.Set("If-None-Match", "*") + return req, err + }, + response: `{"error": "invalid argument: node 'n1' already exists", "status": "INVALID_ARGUMENT"}`, + status: http.StatusBadRequest, + resultConf: ` +nodeprofiles: {} +nodes: + n1: + system overlay: + - so1 + runtime overlay: + - ro1 + kernel: + version: "v1.0.0" + args: + - "kernel-args" +`, + }, - resp, err := http.DefaultTransport.RoundTrip(req) - assert.NoError(t, err) + "get one specific node": { + initConf: ` +nodeprofiles: {} +nodes: + n1: + system overlay: + - so1 + runtime overlay: + - ro1 + kernel: + version: "v1.0.0" + args: + - "kernel-args" +`, + request: func(serverURL string) (*http.Request, error) { + return http.NewRequest(http.MethodGet, serverURL+"/api/nodes/n1", nil) + }, + response: `{"system overlay": ["so1"], "runtime overlay": ["ro1"], "kernel": {"version": "v1.0.0", "args": ["kernel-args"]}}`, + }, - body, err := io.ReadAll(resp.Body) - assert.NoError(t, err) - assert.NoError(t, resp.Body.Close()) + "get one specific node with a profile": { + initConf: ` +nodeprofiles: + default: + system overlay: + - so1 + runtime overlay: + - ro1 +nodes: + n1: + profiles: + - default + kernel: + version: "v1.0.0" + args: + - "kernel-args" +`, + request: func(serverURL string) (*http.Request, error) { + return http.NewRequest(http.MethodGet, serverURL+"/api/nodes/n1", nil) + }, + response: `{"profiles": ["default"], "system overlay": ["so1"], "runtime overlay": ["ro1"], "kernel": {"version": "v1.0.0", "args": ["kernel-args"]}}`, + }, - assert.Equal(t, http.StatusBadRequest, resp.StatusCode) - assert.Contains(t, string(body), "node 'test' already exists") - }) + "get one specific raw node with a profile": { + initConf: ` +nodeprofiles: + default: + system overlay: + - so1 + runtime overlay: + - ro1 +nodes: + n1: + profiles: + - default + kernel: + version: "v1.0.0" + args: + - "kernel-args" +`, + request: func(serverURL string) (*http.Request, error) { + return http.NewRequest(http.MethodGet, serverURL+"/api/nodes/n1/raw", nil) + }, + response: `{"profiles": ["default"], "kernel": {"version": "v1.0.0", "args": ["kernel-args"]}}`, + }, - t.Run("get one specific node", func(t *testing.T) { - req, err := http.NewRequest(http.MethodGet, srv.URL+"/api/nodes/test", nil) - assert.NoError(t, err) + "get unbuilt overlay info for the node": { + initConf: ` +nodeprofiles: {} +nodes: + n1: + system overlay: + - so1 + runtime overlay: + - ro1 + kernel: + version: "v1.0.0" + args: + - "kernel-args" +`, + request: func(serverURL string) (*http.Request, error) { + return http.NewRequest(http.MethodGet, serverURL+"/api/nodes/n1/overlays", nil) + }, + response: `{"system overlay": { "overlays": ["so1"] }, "runtime overlay": { "overlays": ["ro1"] }}`, + }, - // send request - resp, err := http.DefaultTransport.RoundTrip(req) - assert.NoError(t, err) - - // validate the resp - body, err := io.ReadAll(resp.Body) - assert.NoError(t, err) - assert.NoError(t, resp.Body.Close()) - - assert.JSONEq(t, `{"system overlay": ["so1"], "runtime overlay": ["ro1"], "kernel": {"version": "v1.0.0", "args": ["kernel-args"]}}`, string(body)) - }) - - t.Run("get unbuilt overlay info for the node", func(t *testing.T) { - req, err := http.NewRequest(http.MethodGet, srv.URL+"/api/nodes/test/overlays", nil) - assert.NoError(t, err) - - // send request - resp, err := http.DefaultTransport.RoundTrip(req) - assert.NoError(t, err) - - // validate the response - body, err := io.ReadAll(resp.Body) - assert.NoError(t, err) - assert.NoError(t, resp.Body.Close()) - - ja := jsonassert.New(t) - ja.Assertf(string(body), `{ - "system overlay": { - "overlays": ["so1"] - }, - "runtime overlay": { - "overlays": ["ro1"] - } - }`) - }) - - t.Run("update the node", func(t *testing.T) { - updateNode := `{ - "node":{ - "kernel": { - "version": "v1.0.1-newversion" - } - } -}` - req, err := http.NewRequest(http.MethodPatch, srv.URL+"/api/nodes/test", bytes.NewBuffer([]byte(updateNode))) - assert.NoError(t, err) - - resp, err := http.DefaultTransport.RoundTrip(req) - assert.NoError(t, err) - - body, err := io.ReadAll(resp.Body) - assert.NoError(t, err) - assert.NoError(t, resp.Body.Close()) - - assert.JSONEq(t, `{"system overlay": ["so1"], "runtime overlay": ["ro1"], "kernel": {"version": "v1.0.1-newversion", "args": ["kernel-args"]}}`, string(body)) - }) - - t.Run("get one specific node (again)", func(t *testing.T) { - req, err := http.NewRequest(http.MethodGet, srv.URL+"/api/nodes/test", nil) - assert.NoError(t, err) - - resp, err := http.DefaultTransport.RoundTrip(req) - assert.NoError(t, err) - - body, err := io.ReadAll(resp.Body) - assert.NoError(t, err) - assert.NoError(t, resp.Body.Close()) - - assert.JSONEq(t, `{"system overlay": ["so1"], "runtime overlay": ["ro1"], "kernel": {"version": "v1.0.1-newversion", "args": ["kernel-args"]}}`, string(body)) - }) - - t.Run("get one specific (raw) node", func(t *testing.T) { - req, err := http.NewRequest(http.MethodGet, srv.URL+"/api/nodes/test/raw", nil) - assert.NoError(t, err) - - resp, err := http.DefaultTransport.RoundTrip(req) - assert.NoError(t, err) - - body, err := io.ReadAll(resp.Body) - assert.NoError(t, err) - assert.NoError(t, resp.Body.Close()) - - assert.JSONEq(t, `{"system overlay": ["so1"], "runtime overlay": ["ro1"], "kernel": {"version": "v1.0.1-newversion", "args": ["kernel-args"]}}`, string(body)) - }) - - t.Run("test build all nodes overlays", func(t *testing.T) { - req, err := http.NewRequest(http.MethodPost, srv.URL+"/api/nodes/overlays/build", nil) - assert.NoError(t, err) - - resp, err := http.DefaultTransport.RoundTrip(req) - assert.NoError(t, err) - - body, err := io.ReadAll(resp.Body) - assert.NoError(t, err) - assert.NoError(t, resp.Body.Close()) - - assert.JSONEq(t, `["node1", "test"]`, string(body)) - }) - - t.Run("test build one node's overlays", func(t *testing.T) { - req, err := http.NewRequest(http.MethodPost, srv.URL+"/api/nodes/test/overlays/build", nil) - assert.NoError(t, err) - - resp, err := http.DefaultTransport.RoundTrip(req) - assert.NoError(t, err) - - body, err := io.ReadAll(resp.Body) - assert.NoError(t, err) - assert.NoError(t, resp.Body.Close()) - - assert.JSONEq(t, `"test"`, string(body)) - }) - - t.Run("get built overlay info for the node", func(t *testing.T) { - req, err := http.NewRequest(http.MethodGet, srv.URL+"/api/nodes/test/overlays", nil) - assert.NoError(t, err) - - // send request - resp, err := http.DefaultTransport.RoundTrip(req) - assert.NoError(t, err) - - // validate the response - body, err := io.ReadAll(resp.Body) - assert.NoError(t, err) - assert.NoError(t, resp.Body.Close()) - - ja := jsonassert.New(t) - ja.Assertf(string(body), `{ + "get built overlay info for the node": { + initConf: ` +nodeprofiles: {} +nodes: + n1: + system overlay: + - so1 + runtime overlay: + - ro1 + kernel: + version: "v1.0.0" + args: + - "kernel-args" +`, + initFiles: []string{ + "/srv/warewulf/overlays/n1/__SYSTEM__.img", + "/srv/warewulf/overlays/n1/__SYSTEM__.img.gz", + "/srv/warewulf/overlays/n1/__RUNTIME__.img", + "/srv/warewulf/overlays/n1/__RUNTIME__.img.gz", + }, + request: func(serverURL string) (*http.Request, error) { + return http.NewRequest(http.MethodGet, serverURL+"/api/nodes/n1/overlays", nil) + }, + response: `{ "system overlay": { "overlays": ["so1"], "mtime": "<>" @@ -268,57 +293,187 @@ func TestNodeAPI(t *testing.T) { "overlays": ["ro1"], "mtime": "<>" } - }`) + }`, + }, - data := map[string]any{} - assert.NoError(t, json.Unmarshal(body, &data)) - { - _, err := time.Parse(time.RFC3339, data["system overlay"].(map[string]any)["mtime"].(string)) - assert.NoError(t, err) - } - { - _, err := time.Parse(time.RFC3339, data["runtime overlay"].(map[string]any)["mtime"].(string)) - assert.NoError(t, err) - } - }) - - t.Run("replace a node", func(t *testing.T) { - // prepareration - - testNode := `{ + "update a node": { + initConf: ` +nodeprofiles: {} +nodes: + n1: + system overlay: + - so1 + runtime overlay: + - ro1 + kernel: + version: "v1.0.0" + args: + - "kernel-args" +`, + request: func(serverURL string) (*http.Request, error) { + updateNode := `{ "node":{ - "system overlay": ["so1"], - "runtime overlay": ["ro1"], "kernel": { - "version": "v1.0.0", - "args": ["kernel-args"] + "version": "v1.0.1-newversion" } } }` - req, err := http.NewRequest(http.MethodPut, srv.URL+"/api/nodes/test", bytes.NewBuffer([]byte(testNode))) - assert.NoError(t, err) + return http.NewRequest(http.MethodPatch, serverURL+"/api/nodes/n1", bytes.NewBuffer([]byte(updateNode))) + }, + response: `{"system overlay": ["so1"], "runtime overlay": ["ro1"], "kernel": {"version": "v1.0.1-newversion", "args": ["kernel-args"]}}`, + resultConf: ` +nodeprofiles: {} +nodes: + n1: + system overlay: + - so1 + runtime overlay: + - ro1 + kernel: + version: "v1.0.1-newversion" + args: + - "kernel-args" +`, + }, - resp, err := http.DefaultTransport.RoundTrip(req) - assert.NoError(t, err) + "test build all nodes overlays": { + initConf: ` +nodeprofiles: + default: + system overlay: + - so1 + runtime overlay: + - ro1 +nodes: + n1: + profiles: + - default + n2: + profiles: + - default +`, + initFiles: []string{ + "/var/lib/warewulf/overlays/so1/rootfs/file", + "/var/lib/warewulf/overlays/ro1/rootfs/file", + }, + request: func(serverURL string) (*http.Request, error) { + return http.NewRequest(http.MethodPost, serverURL+"/api/nodes/overlays/build", nil) + }, + response: `["n1", "n2"]`, + resultFiles: []string{ + "/srv/warewulf/overlays/n1/__SYSTEM__.img", + "/srv/warewulf/overlays/n1/__SYSTEM__.img.gz", + "/srv/warewulf/overlays/n1/__RUNTIME__.img", + "/srv/warewulf/overlays/n1/__RUNTIME__.img.gz", + "/srv/warewulf/overlays/n2/__SYSTEM__.img", + "/srv/warewulf/overlays/n2/__SYSTEM__.img.gz", + "/srv/warewulf/overlays/n2/__RUNTIME__.img", + "/srv/warewulf/overlays/n2/__RUNTIME__.img.gz", + }, + }, - body, err := io.ReadAll(resp.Body) - assert.NoError(t, err) - assert.NoError(t, resp.Body.Close()) + "test build one node's overlays": { + initConf: ` +nodeprofiles: {} +nodes: + n1: + system overlay: + - so1 + runtime overlay: + - ro1 + kernel: + version: "v1.0.0" + args: + - "kernel-args" +`, + initFiles: []string{ + "/var/lib/warewulf/overlays/so1/rootfs/file", + "/var/lib/warewulf/overlays/ro1/rootfs/file", + }, + request: func(serverURL string) (*http.Request, error) { + return http.NewRequest(http.MethodPost, serverURL+"/api/nodes/n1/overlays/build", nil) + }, + response: `"n1"`, + resultFiles: []string{ + "/srv/warewulf/overlays/n1/__SYSTEM__.img", + "/srv/warewulf/overlays/n1/__SYSTEM__.img.gz", + "/srv/warewulf/overlays/n1/__RUNTIME__.img", + "/srv/warewulf/overlays/n1/__RUNTIME__.img.gz", + }, + }, - assert.JSONEq(t, `{"system overlay": ["so1"], "runtime overlay": ["ro1"], "kernel": {"version": "v1.0.0", "args": ["kernel-args"]}}`, string(body)) - }) - - t.Run("test delete nodes", func(t *testing.T) { - req, err := http.NewRequest(http.MethodDelete, srv.URL+"/api/nodes/test", nil) - assert.NoError(t, err) - - resp, err := http.DefaultTransport.RoundTrip(req) - assert.NoError(t, err) - - body, err := io.ReadAll(resp.Body) - assert.NoError(t, err) - assert.NoError(t, resp.Body.Close()) - - assert.JSONEq(t, `{"system overlay": ["so1"], "runtime overlay": ["ro1"], "kernel": {"version": "v1.0.0", "args": ["kernel-args"]}}`, string(body)) - }) + "test delete nodes": { + initConf: ` +nodeprofiles: {} +nodes: + n1: + system overlay: + - so1 + runtime overlay: + - ro1 + kernel: + version: "v1.0.0" + args: + - "kernel-args" +`, + request: func(serverURL string) (*http.Request, error) { + return http.NewRequest(http.MethodDelete, serverURL+"/api/nodes/n1", nil) + }, + response: `{"system overlay": ["so1"], "runtime overlay": ["ro1"], "kernel": {"version": "v1.0.0", "args": ["kernel-args"]}}`, + resultConf: ` +nodeprofiles: {} +nodes: {} +`, + }, +} + +func TestNodeAPI(t *testing.T) { + for name, tt := range nodeTests { + t.Run(name, func(t *testing.T) { + warewulfd.SetNoDaemon() + env := testenv.New(t) + defer env.RemoveAll() + + env.WriteFile("/etc/warewulf/nodes.conf", tt.initConf) + for _, fileName := range tt.initFiles { + env.CreateFile(fileName) + } + + allowedNets := []net.IPNet{ + { + IP: net.IPv4(127, 0, 0, 0), + Mask: net.CIDRMask(8, 32), + }, + } + srv := httptest.NewServer(Handler(nil, allowedNets)) + defer srv.Close() + + req, err := tt.request(srv.URL) + assert.NoError(t, err) + + resp, err := http.DefaultTransport.RoundTrip(req) + assert.NoError(t, err) + + expectedStatus := tt.status + if expectedStatus == 0 { + expectedStatus = http.StatusOK + } + assert.Equal(t, expectedStatus, resp.StatusCode) + + body, err := io.ReadAll(resp.Body) + assert.NoError(t, err) + assert.NoError(t, resp.Body.Close()) + + ja := jsonassert.New(t) + ja.Assertf(string(body), tt.response) //nolint:govet + + if tt.resultConf != "" { + assert.YAMLEq(t, tt.resultConf, env.ReadFile("/etc/warewulf/nodes.conf")) + } + + for _, fileName := range tt.resultFiles { + assert.FileExists(t, env.GetPath(fileName)) + } + }) + } } diff --git a/internal/pkg/warewulfd/api/overlay_test.go b/internal/pkg/warewulfd/api/overlay_test.go index 8fb03e07..91e1c0da 100644 --- a/internal/pkg/warewulfd/api/overlay_test.go +++ b/internal/pkg/warewulfd/api/overlay_test.go @@ -6,6 +6,7 @@ import ( "net" "net/http" "net/http/httptest" + "strings" "testing" "github.com/kinbiko/jsonassert" @@ -14,175 +15,162 @@ import ( "github.com/warewulf/warewulf/internal/pkg/warewulfd" ) -func TestOverlayAPI(t *testing.T) { - warewulfd.SetNoDaemon() - env := testenv.New(t) - defer env.RemoveAll() - env.WriteFile("usr/share/warewulf/overlays/testoverlay/email.ww", ` -{{ if .Tags.email }}eMail: {{ .Tags.email }}{{else}} noMail{{- end }} -`) +const sampleTemplate = `{{ if .Tags.email }}eMail: {{ .Tags.email }}{{else}} noMail{{- end }} +` - allowedNets := []net.IPNet{ - { - IP: net.IPv4(127, 0, 0, 0), - Mask: net.CIDRMask(8, 32), +var overlayTests = map[string]struct { + initFiles map[string]string + request func(serverURL string) (*http.Request, error) + response string + status int + resultFiles []string + validateFiles map[string]string // file path -> expected content +}{ + "get all overlays": { + initFiles: map[string]string{ + "/usr/share/warewulf/overlays/testoverlay/email.ww": sampleTemplate, }, - } - srv := httptest.NewServer(Handler(nil, allowedNets)) - defer srv.Close() + request: func(serverURL string) (*http.Request, error) { + return http.NewRequest(http.MethodGet, serverURL+"/api/overlays", nil) + }, + response: `{"testoverlay":{"files":["/email.ww"], "site":false}}`, + }, - t.Run("get all overlays", func(t *testing.T) { - req, err := http.NewRequest(http.MethodGet, srv.URL+"/api/overlays", nil) - assert.NoError(t, err) + "get one specific overlay": { + initFiles: map[string]string{ + "/usr/share/warewulf/overlays/testoverlay/email.ww": sampleTemplate, + }, + request: func(serverURL string) (*http.Request, error) { + return http.NewRequest(http.MethodGet, serverURL+"/api/overlays/testoverlay", nil) + }, + response: `{"files":["/email.ww"], "site":false}`, + }, - // send request - resp, err := http.DefaultTransport.RoundTrip(req) - assert.NoError(t, err) - - // validate the resp - body, err := io.ReadAll(resp.Body) - assert.NoError(t, err) - assert.NoError(t, resp.Body.Close()) - - assert.JSONEq(t, `{"testoverlay":{"files":["/email.ww"], "site":false}}`, string(body)) - }) - - t.Run("get one specific overlay", func(t *testing.T) { - req, err := http.NewRequest(http.MethodGet, srv.URL+"/api/overlays/testoverlay", nil) - assert.NoError(t, err) - - // send request - resp, err := http.DefaultTransport.RoundTrip(req) - assert.NoError(t, err) - - // validate the resp - body, err := io.ReadAll(resp.Body) - assert.NoError(t, err) - assert.NoError(t, resp.Body.Close()) - - assert.JSONEq(t, `{"files":["/email.ww"], "site":false}`, string(body)) - }) - - t.Run("get overlay file", func(t *testing.T) { - req, err := http.NewRequest(http.MethodGet, srv.URL+"/api/overlays/testoverlay/file?path=email.ww", nil) - assert.NoError(t, err) - - // send request - resp, err := http.DefaultTransport.RoundTrip(req) - assert.NoError(t, err) - - // validate the resp - body, err := io.ReadAll(resp.Body) - assert.NoError(t, err) - assert.NoError(t, resp.Body.Close()) - - // gid and uid values may vary depending on where this test is run. (local box, github, etc) - // Assert the keys exist, but ignore the values. - ja := jsonassert.New(t) - ja.Assert(string(body), ` - { + "get overlay file": { + initFiles: map[string]string{ + "/usr/share/warewulf/overlays/testoverlay/email.ww": sampleTemplate, + }, + request: func(serverURL string) (*http.Request, error) { + return http.NewRequest(http.MethodGet, serverURL+"/api/overlays/testoverlay/file?path=email.ww", nil) + }, + response: `{ "overlay": "testoverlay", "path": "email.ww", - "contents": "\n{{ if .Tags.email }}eMail: {{ .Tags.email }}{{else}} noMail{{- end }}\n", + "contents": "{{ if .Tags.email }}eMail: {{ .Tags.email }}{{else}} noMail{{- end }}\n", "perms": "<>", "uid": "<>", "gid": "<>" - }`) - }) + }`, + }, - t.Run("update overlay file", func(t *testing.T) { - req, err := http.NewRequest(http.MethodPut, srv.URL+"/api/overlays/testoverlay/file?path=email.ww", bytes.NewReader([]byte("{\"content\":\"hello world\"}"))) - assert.NoError(t, err) + "update overlay file": { + initFiles: map[string]string{ + "/usr/share/warewulf/overlays/testoverlay/email.ww": sampleTemplate, + }, + request: func(serverURL string) (*http.Request, error) { + return http.NewRequest(http.MethodPut, serverURL+"/api/overlays/testoverlay/file?path=email.ww", bytes.NewReader([]byte("{\"content\":\"hello world\"}"))) + }, + response: `{"files":["/email.ww"], "site":true}`, + validateFiles: map[string]string{ + "/var/lib/warewulf/overlays/testoverlay/email.ww": "hello world", + }, + }, - // set request - resp, err := http.DefaultTransport.RoundTrip(req) - assert.NoError(t, err) + "create an overlay": { + request: func(serverURL string) (*http.Request, error) { + return http.NewRequest(http.MethodPut, serverURL+"/api/overlays/test", nil) + }, + response: `{"files":null, "site":true}`, + resultFiles: []string{ + "/var/lib/warewulf/overlays/test", + }, + }, - // validate the resp - body, err := io.ReadAll(resp.Body) - assert.NoError(t, err) - assert.NoError(t, resp.Body.Close()) + "get all overlays after creation": { + initFiles: map[string]string{ + "/usr/share/warewulf/overlays/testoverlay/email.ww": sampleTemplate, + "/var/lib/warewulf/overlays/test/rootfs/": "", + "/var/lib/warewulf/overlays/testoverlay/email.ww": sampleTemplate, + }, + request: func(serverURL string) (*http.Request, error) { + return http.NewRequest(http.MethodGet, serverURL+"/api/overlays", nil) + }, + response: `{"test":{"files":null, "site":true},"testoverlay":{"files":["/email.ww"], "site":true}}`, + }, - assert.JSONEq(t, `{"files":["/email.ww"], "site":true}`, string(body)) + "delete overlay file": { + initFiles: map[string]string{ + "/var/lib/warewulf/overlays/testoverlay/email.ww": sampleTemplate, + }, + request: func(serverURL string) (*http.Request, error) { + return http.NewRequest(http.MethodDelete, serverURL+"/api/overlays/testoverlay/file?path=email.ww&force=true", nil) + }, + response: `{"files":null, "site":true}`, + }, - // get again - req, err = http.NewRequest(http.MethodGet, srv.URL+"/api/overlays/testoverlay/file?path=email.ww", nil) - assert.NoError(t, err) - - // send request - resp, err = http.DefaultTransport.RoundTrip(req) - assert.NoError(t, err) - - // validate the resp - body, err = io.ReadAll(resp.Body) - assert.NoError(t, err) - assert.NoError(t, resp.Body.Close()) - - ja := jsonassert.New(t) - ja.Assert(string(body), ` - { - "overlay": "testoverlay", - "path": "email.ww", - "contents": "hello world", - "perms": "<>", - "uid": "<>", - "gid": "<>" - }`) - }) - - t.Run("create an overlay", func(t *testing.T) { - req, err := http.NewRequest(http.MethodPut, srv.URL+"/api/overlays/test", nil) - assert.NoError(t, err) - - resp, err := http.DefaultTransport.RoundTrip(req) - assert.NoError(t, err) - - body, err := io.ReadAll(resp.Body) - assert.NoError(t, err) - assert.NoError(t, resp.Body.Close()) - - assert.JSONEq(t, `{"files":null, "site":true}`, string(body)) - }) - - t.Run("get all overlays", func(t *testing.T) { - req, err := http.NewRequest(http.MethodGet, srv.URL+"/api/overlays", nil) - assert.NoError(t, err) - - resp, err := http.DefaultTransport.RoundTrip(req) - assert.NoError(t, err) - - body, err := io.ReadAll(resp.Body) - assert.NoError(t, err) - assert.NoError(t, resp.Body.Close()) - - assert.JSONEq(t, `{"test":{"files":null, "site":true},"testoverlay":{"files":["/email.ww"], "site":true}}`, string(body)) - }) - - t.Run("test delete overlay file", func(t *testing.T) { - req, err := http.NewRequest(http.MethodDelete, srv.URL+"/api/overlays/testoverlay/file?path=email.ww&force=true", nil) - assert.NoError(t, err) - - resp, err := http.DefaultTransport.RoundTrip(req) - assert.NoError(t, err) - - body, err := io.ReadAll(resp.Body) - assert.NoError(t, err) - assert.NoError(t, resp.Body.Close()) - - assert.JSONEq(t, `{"files":null, "site":true}`, string(body)) - }) - - t.Run("test delete overlays", func(t *testing.T) { - req, err := http.NewRequest(http.MethodDelete, srv.URL+"/api/overlays/test?force=true", nil) - assert.NoError(t, err) - - resp, err := http.DefaultTransport.RoundTrip(req) - assert.NoError(t, err) - - body, err := io.ReadAll(resp.Body) - assert.NoError(t, err) - assert.NoError(t, resp.Body.Close()) - - assert.JSONEq(t, `{"files":[], "site":true}`, string(body)) - }) + "delete overlay": { + initFiles: map[string]string{ + "/var/lib/warewulf/overlays/test/": "", + }, + request: func(serverURL string) (*http.Request, error) { + return http.NewRequest(http.MethodDelete, serverURL+"/api/overlays/test?force=true", nil) + }, + response: `{"files":[], "site":true}`, + }, +} + +func TestOverlayAPI(t *testing.T) { + for name, tt := range overlayTests { + t.Run(name, func(t *testing.T) { + warewulfd.SetNoDaemon() + env := testenv.New(t) + defer env.RemoveAll() + + // Create test files + for fileName, fileContent := range tt.initFiles { + if strings.HasSuffix(fileName, "/") { + env.MkdirAll(fileName) + } else { + env.WriteFile(fileName, fileContent) + } + } + + allowedNets := []net.IPNet{ + { + IP: net.IPv4(127, 0, 0, 0), + Mask: net.CIDRMask(8, 32), + }, + } + srv := httptest.NewServer(Handler(nil, allowedNets)) + defer srv.Close() + + req, err := tt.request(srv.URL) + assert.NoError(t, err) + + resp, err := http.DefaultTransport.RoundTrip(req) + assert.NoError(t, err) + + expectedStatus := tt.status + if expectedStatus == 0 { + expectedStatus = http.StatusOK + } + assert.Equal(t, expectedStatus, resp.StatusCode) + + body, err := io.ReadAll(resp.Body) + assert.NoError(t, err) + assert.NoError(t, resp.Body.Close()) + + ja := jsonassert.New(t) + ja.Assertf(string(body), tt.response) //nolint:govet // tt.response is used as a format string with special tokens + + for _, fileName := range tt.resultFiles { + assert.DirExists(t, env.GetPath(fileName)) + } + + for filePath, expectedContent := range tt.validateFiles { + actualContent := env.ReadFile(filePath) + assert.Equal(t, expectedContent, actualContent) + } + }) + } } diff --git a/internal/pkg/warewulfd/api/profile_test.go b/internal/pkg/warewulfd/api/profile_test.go index d0882f53..b07b2d91 100644 --- a/internal/pkg/warewulfd/api/profile_test.go +++ b/internal/pkg/warewulfd/api/profile_test.go @@ -8,160 +8,225 @@ import ( "net/http/httptest" "testing" + "github.com/kinbiko/jsonassert" "github.com/stretchr/testify/assert" "github.com/warewulf/warewulf/internal/pkg/testenv" "github.com/warewulf/warewulf/internal/pkg/warewulfd" ) -func TestProfileAPI(t *testing.T) { - warewulfd.SetNoDaemon() - env := testenv.New(t) - defer env.RemoveAll() - - allowedNets := []net.IPNet{ - { - IP: net.IPv4(127, 0, 0, 0), - Mask: net.CIDRMask(8, 32), +var profileTests = map[string]struct { + initConf string + initFiles []string + request func(serverURL string) (*http.Request, error) + response string + status int + resultConf string +}{ + "get all profiles": { + initConf: ` +nodeprofiles: + default: {} +nodes: {} +`, + request: func(serverURL string) (*http.Request, error) { + return http.NewRequest(http.MethodGet, serverURL+"/api/profiles", nil) }, - } - srv := httptest.NewServer(Handler(nil, allowedNets)) - defer srv.Close() + response: `{"default": {}}`, + }, - t.Run("get all profiles", func(t *testing.T) { - req, err := http.NewRequest(http.MethodGet, srv.URL+"/api/profiles", nil) - assert.NoError(t, err) + "add a new profile": { + initConf: ` +nodeprofiles: + default: {} +nodes: {} +`, + request: func(serverURL string) (*http.Request, error) { + testProfile := `{"profile": {"kernel": {"version": "v1.0.0", "args": ["kernel-args"]}}}` + return http.NewRequest(http.MethodPut, serverURL+"/api/profiles/p1", bytes.NewBuffer([]byte(testProfile))) + }, + response: `{"kernel": {"version": "v1.0.0", "args": ["kernel-args"]}}`, + resultConf: ` +nodeprofiles: + default: {} + p1: + kernel: + version: "v1.0.0" + args: + - "kernel-args" +nodes: {} +`, + }, - resp, err := http.DefaultTransport.RoundTrip(req) - assert.NoError(t, err) + "test idempotency (replacing existing profile)": { + initConf: ` +nodeprofiles: + default: {} + p1: + kernel: + version: "v1.0.0" + args: + - "kernel-args" +nodes: {} +`, + request: func(serverURL string) (*http.Request, error) { + testProfile := `{"profile": {"kernel": {"version": "v1.0.0", "args": ["kernel-args"]}}}` + return http.NewRequest(http.MethodPut, serverURL+"/api/profiles/p1", bytes.NewBuffer([]byte(testProfile))) + }, + response: `{"kernel": {"version": "v1.0.0", "args": ["kernel-args"]}}`, + resultConf: ` +nodeprofiles: + default: {} + p1: + kernel: + version: "v1.0.0" + args: + - "kernel-args" +nodes: {} +`, + }, - body, err := io.ReadAll(resp.Body) - assert.NoError(t, resp.Body.Close()) - assert.NoError(t, err) - assert.JSONEq(t, `{"default": {}}`, string(body)) - }) + "test preventing replacing a profile": { + initConf: ` +nodeprofiles: + default: {} + p1: + kernel: + version: "v1.0.0" + args: + - "kernel-args" +nodes: {} +`, + request: func(serverURL string) (*http.Request, error) { + testProfile := `{"profile": {"kernel": {"version": "v1.0.0", "args": ["kernel-args"]}}}` + req, err := http.NewRequest(http.MethodPut, serverURL+"/api/profiles/p1", bytes.NewBuffer([]byte(testProfile))) + req.Header.Set("If-None-Match", "*") + return req, err + }, + response: `{"error": "invalid argument: profile 'p1' already exists", "status": "INVALID_ARGUMENT"}`, + status: http.StatusBadRequest, + resultConf: ` +nodeprofiles: + default: {} + p1: + kernel: + version: "v1.0.0" + args: + - "kernel-args" +nodes: {} +`, + }, - t.Run("add a new profile", func(t *testing.T) { - testProfile := `{"profile": {"kernel": {"version": "v1.0.0", "args": ["kernel-args"]}}}` - req, err := http.NewRequest(http.MethodPut, srv.URL+"/api/profiles/test", bytes.NewBuffer([]byte(testProfile))) - assert.NoError(t, err) + "get one specific profile": { + initConf: ` +nodeprofiles: + default: {} + p1: + kernel: + version: "v1.0.0" + args: + - "kernel-args" +nodes: {} +`, + request: func(serverURL string) (*http.Request, error) { + return http.NewRequest(http.MethodGet, serverURL+"/api/profiles/p1", nil) + }, + response: `{"kernel": {"version": "v1.0.0", "args": ["kernel-args"]}}`, + }, - resp, err := http.DefaultTransport.RoundTrip(req) - assert.NoError(t, err) + "update a profile": { + initConf: ` +nodeprofiles: + default: {} + p1: + kernel: + version: "v1.0.0" + args: + - "kernel-args" +nodes: {} +`, + request: func(serverURL string) (*http.Request, error) { + updateProfile := `{"profile": {"kernel": {"version": "v1.0.1-newversion"}}}` + return http.NewRequest(http.MethodPatch, serverURL+"/api/profiles/p1", bytes.NewBuffer([]byte(updateProfile))) + }, + response: `{"kernel": {"version": "v1.0.1-newversion", "args": ["kernel-args"]}}`, + resultConf: ` +nodeprofiles: + default: {} + p1: + kernel: + version: "v1.0.1-newversion" + args: + - "kernel-args" +nodes: {} +`, + }, - body, err := io.ReadAll(resp.Body) - assert.NoError(t, resp.Body.Close()) - assert.NoError(t, err) - assert.JSONEq(t, `{"kernel": {"version": "v1.0.0", "args": ["kernel-args"]}}`, string(body)) - }) - - t.Run("test idempotency", func(t *testing.T) { - testProfile := `{"profile": {"kernel": {"version": "v1.0.0", "args": ["kernel-args"]}}}` - req, err := http.NewRequest(http.MethodPut, srv.URL+"/api/profiles/test", bytes.NewBuffer([]byte(testProfile))) - assert.NoError(t, err) - - resp, err := http.DefaultTransport.RoundTrip(req) - assert.NoError(t, err) - - body, err := io.ReadAll(resp.Body) - assert.NoError(t, resp.Body.Close()) - assert.NoError(t, err) - assert.JSONEq(t, `{"kernel": {"version": "v1.0.0", "args": ["kernel-args"]}}`, string(body)) - }) - - t.Run("fail if profile already exists (given appropriate header)", func(t *testing.T) { - testProfile := `{"profile": {"kernel": {"version": "v1.0.0", "args": ["kernel-args"]}}}` - req, err := http.NewRequest(http.MethodPut, srv.URL+"/api/profiles/test", bytes.NewBuffer([]byte(testProfile))) - assert.NoError(t, err) - req.Header.Set("If-None-Match", "*") - - resp, err := http.DefaultTransport.RoundTrip(req) - assert.NoError(t, err) - - body, err := io.ReadAll(resp.Body) - assert.NoError(t, resp.Body.Close()) - assert.NoError(t, err) - - assert.Equal(t, http.StatusBadRequest, resp.StatusCode) - assert.Contains(t, string(body), "profile 'test' already exists") - }) - - t.Run("re-read all profiles", func(t *testing.T) { - req, err := http.NewRequest(http.MethodGet, srv.URL+"/api/profiles", nil) - assert.NoError(t, err) - - resp, err := http.DefaultTransport.RoundTrip(req) - assert.NoError(t, err) - - body, err := io.ReadAll(resp.Body) - assert.NoError(t, resp.Body.Close()) - assert.NoError(t, err) - assert.JSONEq(t, `{"default": {}, "test": {"kernel": {"version": "v1.0.0", "args": ["kernel-args"]}}}`, string(body)) - }) - - t.Run("get one specific profile (that was just added)", func(t *testing.T) { - req, err := http.NewRequest(http.MethodGet, srv.URL+"/api/profiles/test", nil) - assert.NoError(t, err) - - resp, err := http.DefaultTransport.RoundTrip(req) - assert.NoError(t, err) - - body, err := io.ReadAll(resp.Body) - assert.NoError(t, resp.Body.Close()) - assert.NoError(t, err) - assert.JSONEq(t, `{"kernel": {"version": "v1.0.0", "args": ["kernel-args"]}}`, string(body)) - }) - - t.Run("update the profile", func(t *testing.T) { - updateProfile := `{"profile": {"kernel": {"version": "v1.0.1-newversion"}}}` - req, err := http.NewRequest(http.MethodPatch, srv.URL+"/api/profiles/test", bytes.NewBuffer([]byte(updateProfile))) - assert.NoError(t, err) - - resp, err := http.DefaultTransport.RoundTrip(req) - assert.NoError(t, err) - - body, err := io.ReadAll(resp.Body) - assert.NoError(t, resp.Body.Close()) - assert.NoError(t, err) - assert.JSONEq(t, `{"kernel": {"version": "v1.0.1-newversion", "args": ["kernel-args"]}}`, string(body)) - }) - - t.Run("get one specific profile (that was just updated)", func(t *testing.T) { - req, err := http.NewRequest(http.MethodGet, srv.URL+"/api/profiles/test", nil) - assert.NoError(t, err) - - resp, err := http.DefaultTransport.RoundTrip(req) - assert.NoError(t, err) - - body, err := io.ReadAll(resp.Body) - assert.NoError(t, resp.Body.Close()) - assert.NoError(t, err) - assert.JSONEq(t, `{"kernel": {"version": "v1.0.1-newversion", "args": ["kernel-args"]}}`, string(body)) - }) - - t.Run("test overwrite", func(t *testing.T) { - testProfile := `{"profile": {"kernel": {"version": "v1.0.0", "args": ["kernel-args"]}}}` - req, err := http.NewRequest(http.MethodPut, srv.URL+"/api/profiles/test", bytes.NewBuffer([]byte(testProfile))) - assert.NoError(t, err) - - resp, err := http.DefaultTransport.RoundTrip(req) - assert.NoError(t, err) - - body, err := io.ReadAll(resp.Body) - assert.NoError(t, resp.Body.Close()) - assert.NoError(t, err) - assert.JSONEq(t, `{"kernel": {"version": "v1.0.0", "args": ["kernel-args"]}}`, string(body)) - }) - - t.Run("test delete a profile", func(t *testing.T) { - req, err := http.NewRequest(http.MethodDelete, srv.URL+"/api/profiles/test", nil) - assert.NoError(t, err) - - resp, err := http.DefaultTransport.RoundTrip(req) - assert.NoError(t, err) - - body, err := io.ReadAll(resp.Body) - assert.NoError(t, resp.Body.Close()) - assert.NoError(t, err) - assert.JSONEq(t, `{"kernel": {"version": "v1.0.0", "args": ["kernel-args"]}}`, string(body)) - }) + "test delete a profile": { + initConf: ` +nodeprofiles: + default: {} + p1: + kernel: + version: "v1.0.0" + args: + - "kernel-args" +nodes: {} +`, + request: func(serverURL string) (*http.Request, error) { + return http.NewRequest(http.MethodDelete, serverURL+"/api/profiles/p1", nil) + }, + response: `{"kernel": {"version": "v1.0.0", "args": ["kernel-args"]}}`, + resultConf: ` +nodeprofiles: + default: {} +nodes: {} +`, + }, +} + +func TestProfileAPI(t *testing.T) { + for name, tt := range profileTests { + t.Run(name, func(t *testing.T) { + warewulfd.SetNoDaemon() + env := testenv.New(t) + defer env.RemoveAll() + + env.WriteFile("/etc/warewulf/nodes.conf", tt.initConf) + for _, fileName := range tt.initFiles { + env.CreateFile(fileName) + } + + allowedNets := []net.IPNet{ + { + IP: net.IPv4(127, 0, 0, 0), + Mask: net.CIDRMask(8, 32), + }, + } + srv := httptest.NewServer(Handler(nil, allowedNets)) + defer srv.Close() + + req, err := tt.request(srv.URL) + assert.NoError(t, err) + + resp, err := http.DefaultTransport.RoundTrip(req) + assert.NoError(t, err) + + expectedStatus := tt.status + if expectedStatus == 0 { + expectedStatus = http.StatusOK + } + assert.Equal(t, expectedStatus, resp.StatusCode) + + body, err := io.ReadAll(resp.Body) + assert.NoError(t, err) + assert.NoError(t, resp.Body.Close()) + + ja := jsonassert.New(t) + ja.Assertf(string(body), tt.response) //nolint:govet + + if tt.resultConf != "" { + assert.YAMLEq(t, tt.resultConf, env.ReadFile("/etc/warewulf/nodes.conf")) + } + }) + } }