Refactor wwapi tests for proper isolation

Signed-off-by: Jonathon Anderson <janderson@ciq.com>
This commit is contained in:
Jonathon Anderson
2025-07-28 09:31:01 -06:00
parent e149a332e6
commit 5ceeb60c24
4 changed files with 917 additions and 679 deletions

View File

@@ -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":"<<PRESENCE>>", "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":"<<PRESENCE>>", "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":"<<PRESENCE>>", "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)
})
}

View File

@@ -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": "<<PRESENCE>>"
@@ -268,57 +293,187 @@ func TestNodeAPI(t *testing.T) {
"overlays": ["ro1"],
"mtime": "<<PRESENCE>>"
}
}`)
}`,
},
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))
}
})
}
}

View File

@@ -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": "<<PRESENCE>>",
"uid": "<<PRESENCE>>",
"gid": "<<PRESENCE>>"
}`)
})
}`,
},
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": "<<PRESENCE>>",
"uid": "<<PRESENCE>>",
"gid": "<<PRESENCE>>"
}`)
})
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)
}
})
}
}

View File

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