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 ( import (
"bytes" "bytes"
"encoding/json" "fmt"
"io" "io"
"net" "net"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"path"
"testing" "testing"
"github.com/kinbiko/jsonassert"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/warewulf/warewulf/internal/pkg/config" "github.com/warewulf/warewulf/internal/pkg/config"
"github.com/warewulf/warewulf/internal/pkg/testenv" "github.com/warewulf/warewulf/internal/pkg/testenv"
"github.com/warewulf/warewulf/internal/pkg/warewulfd"
) )
func TestImageAPI(t *testing.T) { var imageTests = map[string]struct {
env := testenv.New(t) initFiles []string
defer env.RemoveAll() 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 := ` authData := `
users: users:
- name: admin - name: admin
password hash: $2b$05$5QVWDpiWE7L4SDL9CYdi3O/l6HnbNOLoXgY2sa1bQQ7aSBKdSqvsC password hash: $2b$05$5QVWDpiWE7L4SDL9CYdi3O/l6HnbNOLoXgY2sa1bQQ7aSBKdSqvsC
` `
auth := config.NewAuthentication()
err := auth.ParseFromRaw([]byte(authData))
assert.NoError(t, err)
allowedNets := []net.IPNet{ for name, tt := range imageTests {
{ t.Run(name, func(t *testing.T) {
IP: net.IPv4(127, 0, 0, 0), warewulfd.SetNoDaemon()
Mask: net.CIDRMask(8, 32), 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 ( import (
"bytes" "bytes"
"encoding/json"
"io" "io"
"net" "net"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"testing" "testing"
"time"
"github.com/kinbiko/jsonassert" "github.com/kinbiko/jsonassert"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@@ -16,26 +14,23 @@ import (
"github.com/warewulf/warewulf/internal/pkg/warewulfd" "github.com/warewulf/warewulf/internal/pkg/warewulfd"
) )
func TestNodeAPI(t *testing.T) { var nodeTests = map[string]struct {
warewulfd.SetNoDaemon() initConf string
env := testenv.New(t) initFiles []string
env.MkdirAll("/var/lib/warewulf/overlays/so1") request func(serverURL string) (*http.Request, error)
env.MkdirAll("/var/lib/warewulf/overlays/ro1") response string
defer env.RemoveAll() status int
resultConf string
allowedNets := []net.IPNet{ resultFiles []string
{ }{
IP: net.IPv4(127, 0, 0, 0), "add a node": {
Mask: net.CIDRMask(8, 32), initConf: "",
initFiles: []string{
"/var/lib/warewulf/overlays/so1/rootfs/file",
"/var/lib/warewulf/overlays/ro1/rootfs/file",
}, },
} request: func(serverURL string) (*http.Request, error) {
srv := httptest.NewServer(Handler(nil, allowedNets)) testNode := `{
defer srv.Close()
t.Run("add a node", func(t *testing.T) {
// prepareration
testNode := `{
"node":{ "node":{
"system overlay": ["so1"], "system overlay": ["so1"],
"runtime overlay": ["ro1"], "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))) return http.NewRequest(http.MethodPut, serverURL+"/api/nodes/n1", bytes.NewBuffer([]byte(testNode)))
assert.NoError(t, err) },
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) "read all nodes": {
assert.NoError(t, err) 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) "test idempotency (replacing existing node)": {
assert.NoError(t, err) initConf: `
assert.NoError(t, resp.Body.Close()) nodeprofiles: {}
nodes:
assert.JSONEq(t, `{"system overlay": ["so1"], "runtime overlay": ["ro1"], "kernel": {"version": "v1.0.0", "args": ["kernel-args"]}}`, string(body)) n1:
}) system overlay:
- so1
t.Run("read all nodes", func(t *testing.T) { runtime overlay:
req, err := http.NewRequest(http.MethodGet, srv.URL+"/api/nodes", nil) - ro1
assert.NoError(t, err) kernel:
version: "v1.0.0"
// send request args:
resp, err := http.DefaultTransport.RoundTrip(req) - "kernel-args"
assert.NoError(t, err) `,
initFiles: []string{
// validate the resp "/var/lib/warewulf/overlays/so2/rootfs/file",
body, err := io.ReadAll(resp.Body) "/var/lib/warewulf/overlays/ro2/rootfs/file",
assert.NoError(t, err) },
assert.NoError(t, resp.Body.Close()) request: func(serverURL string) (*http.Request, error) {
testNode := `{
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 := `{
"node":{ "node":{
"system overlay": ["so1"], "system overlay": ["so2"],
"runtime overlay": ["ro1"], "runtime overlay": ["ro2"],
"kernel": { "kernel": {
"version": "v1.0.0", "version": "v1.0.0",
"args": ["kernel-args"] "args": ["kernel-args"]
} }
} }
}` }`
req, err := http.NewRequest(http.MethodPut, srv.URL+"/api/nodes/test", bytes.NewBuffer([]byte(testNode))) return http.NewRequest(http.MethodPut, serverURL+"/api/nodes/n1", bytes.NewBuffer([]byte(testNode)))
assert.NoError(t, err) },
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) "test preventing replacing a node": {
assert.NoError(t, err) initConf: `
nodeprofiles: {}
body, err := io.ReadAll(resp.Body) nodes:
assert.NoError(t, err) n1:
assert.NoError(t, resp.Body.Close()) system overlay:
- so1
assert.JSONEq(t, `{"system overlay": ["so1"], "runtime overlay": ["ro1"], "kernel": {"version": "v1.0.0", "args": ["kernel-args"]}}`, string(body)) runtime overlay:
}) - ro1
kernel:
t.Run("fail if node already exists (given appropriate header)", func(t *testing.T) { version: "v1.0.0"
// prepareration args:
- "kernel-args"
testNode := `{ `,
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":{ "node":{
"system overlay": ["so1"], "system overlay": ["so2"],
"runtime overlay": ["ro1"], "runtime overlay": ["ro2"],
"kernel": { "kernel": {
"version": "v1.0.0", "version": "v1.0.0",
"args": ["kernel-args"] "args": ["kernel-args"]
} }
} }
}` }`
req, err := http.NewRequest(http.MethodPut, srv.URL+"/api/nodes/test", bytes.NewBuffer([]byte(testNode))) req, err := http.NewRequest(http.MethodPut, serverURL+"/api/nodes/n1", bytes.NewBuffer([]byte(testNode)))
assert.NoError(t, err) req.Header.Set("If-None-Match", "*")
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) "get one specific node": {
assert.NoError(t, err) 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) "get one specific node with a profile": {
assert.NoError(t, err) initConf: `
assert.NoError(t, resp.Body.Close()) 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) "get one specific raw node with a profile": {
assert.Contains(t, string(body), "node 'test' already exists") 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) { "get unbuilt overlay info for the node": {
req, err := http.NewRequest(http.MethodGet, srv.URL+"/api/nodes/test", nil) initConf: `
assert.NoError(t, err) 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 "get built overlay info for the node": {
resp, err := http.DefaultTransport.RoundTrip(req) initConf: `
assert.NoError(t, err) nodeprofiles: {}
nodes:
// validate the resp n1:
body, err := io.ReadAll(resp.Body) system overlay:
assert.NoError(t, err) - so1
assert.NoError(t, resp.Body.Close()) runtime overlay:
- ro1
assert.JSONEq(t, `{"system overlay": ["so1"], "runtime overlay": ["ro1"], "kernel": {"version": "v1.0.0", "args": ["kernel-args"]}}`, string(body)) kernel:
}) version: "v1.0.0"
args:
t.Run("get unbuilt overlay info for the node", func(t *testing.T) { - "kernel-args"
req, err := http.NewRequest(http.MethodGet, srv.URL+"/api/nodes/test/overlays", nil) `,
assert.NoError(t, err) initFiles: []string{
"/srv/warewulf/overlays/n1/__SYSTEM__.img",
// send request "/srv/warewulf/overlays/n1/__SYSTEM__.img.gz",
resp, err := http.DefaultTransport.RoundTrip(req) "/srv/warewulf/overlays/n1/__RUNTIME__.img",
assert.NoError(t, err) "/srv/warewulf/overlays/n1/__RUNTIME__.img.gz",
},
// validate the response request: func(serverURL string) (*http.Request, error) {
body, err := io.ReadAll(resp.Body) return http.NewRequest(http.MethodGet, serverURL+"/api/nodes/n1/overlays", nil)
assert.NoError(t, err) },
assert.NoError(t, resp.Body.Close()) response: `{
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), `{
"system overlay": { "system overlay": {
"overlays": ["so1"], "overlays": ["so1"],
"mtime": "<<PRESENCE>>" "mtime": "<<PRESENCE>>"
@@ -268,57 +293,187 @@ func TestNodeAPI(t *testing.T) {
"overlays": ["ro1"], "overlays": ["ro1"],
"mtime": "<<PRESENCE>>" "mtime": "<<PRESENCE>>"
} }
}`) }`,
},
data := map[string]any{} "update a node": {
assert.NoError(t, json.Unmarshal(body, &data)) initConf: `
{ nodeprofiles: {}
_, err := time.Parse(time.RFC3339, data["system overlay"].(map[string]any)["mtime"].(string)) nodes:
assert.NoError(t, err) n1:
} system overlay:
{ - so1
_, err := time.Parse(time.RFC3339, data["runtime overlay"].(map[string]any)["mtime"].(string)) runtime overlay:
assert.NoError(t, err) - ro1
} kernel:
}) version: "v1.0.0"
args:
t.Run("replace a node", func(t *testing.T) { - "kernel-args"
// prepareration `,
request: func(serverURL string) (*http.Request, error) {
testNode := `{ updateNode := `{
"node":{ "node":{
"system overlay": ["so1"],
"runtime overlay": ["ro1"],
"kernel": { "kernel": {
"version": "v1.0.0", "version": "v1.0.1-newversion"
"args": ["kernel-args"]
} }
} }
}` }`
req, err := http.NewRequest(http.MethodPut, srv.URL+"/api/nodes/test", bytes.NewBuffer([]byte(testNode))) return http.NewRequest(http.MethodPatch, serverURL+"/api/nodes/n1", bytes.NewBuffer([]byte(updateNode)))
assert.NoError(t, err) },
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) "test build all nodes overlays": {
assert.NoError(t, err) 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) "test build one node's overlays": {
assert.NoError(t, err) initConf: `
assert.NoError(t, resp.Body.Close()) 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)) "test delete nodes": {
}) initConf: `
nodeprofiles: {}
t.Run("test delete nodes", func(t *testing.T) { nodes:
req, err := http.NewRequest(http.MethodDelete, srv.URL+"/api/nodes/test", nil) n1:
assert.NoError(t, err) system overlay:
- so1
resp, err := http.DefaultTransport.RoundTrip(req) runtime overlay:
assert.NoError(t, err) - ro1
kernel:
body, err := io.ReadAll(resp.Body) version: "v1.0.0"
assert.NoError(t, err) args:
assert.NoError(t, resp.Body.Close()) - "kernel-args"
`,
assert.JSONEq(t, `{"system overlay": ["so1"], "runtime overlay": ["ro1"], "kernel": {"version": "v1.0.0", "args": ["kernel-args"]}}`, string(body)) 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"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"strings"
"testing" "testing"
"github.com/kinbiko/jsonassert" "github.com/kinbiko/jsonassert"
@@ -14,175 +15,162 @@ import (
"github.com/warewulf/warewulf/internal/pkg/warewulfd" "github.com/warewulf/warewulf/internal/pkg/warewulfd"
) )
func TestOverlayAPI(t *testing.T) { const sampleTemplate = `{{ if .Tags.email }}eMail: {{ .Tags.email }}{{else}} noMail{{- end }}
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 }}
`)
allowedNets := []net.IPNet{ var overlayTests = map[string]struct {
{ initFiles map[string]string
IP: net.IPv4(127, 0, 0, 0), request func(serverURL string) (*http.Request, error)
Mask: net.CIDRMask(8, 32), 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,
}, },
} request: func(serverURL string) (*http.Request, error) {
srv := httptest.NewServer(Handler(nil, allowedNets)) return http.NewRequest(http.MethodGet, serverURL+"/api/overlays", nil)
defer srv.Close() },
response: `{"testoverlay":{"files":["/email.ww"], "site":false}}`,
},
t.Run("get all overlays", func(t *testing.T) { "get one specific overlay": {
req, err := http.NewRequest(http.MethodGet, srv.URL+"/api/overlays", nil) initFiles: map[string]string{
assert.NoError(t, err) "/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 "get overlay file": {
resp, err := http.DefaultTransport.RoundTrip(req) initFiles: map[string]string{
assert.NoError(t, err) "/usr/share/warewulf/overlays/testoverlay/email.ww": sampleTemplate,
},
// validate the resp request: func(serverURL string) (*http.Request, error) {
body, err := io.ReadAll(resp.Body) return http.NewRequest(http.MethodGet, serverURL+"/api/overlays/testoverlay/file?path=email.ww", nil)
assert.NoError(t, err) },
assert.NoError(t, resp.Body.Close()) response: `{
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), `
{
"overlay": "testoverlay", "overlay": "testoverlay",
"path": "email.ww", "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>>", "perms": "<<PRESENCE>>",
"uid": "<<PRESENCE>>", "uid": "<<PRESENCE>>",
"gid": "<<PRESENCE>>" "gid": "<<PRESENCE>>"
}`) }`,
}) },
t.Run("update overlay file", func(t *testing.T) { "update overlay file": {
req, err := http.NewRequest(http.MethodPut, srv.URL+"/api/overlays/testoverlay/file?path=email.ww", bytes.NewReader([]byte("{\"content\":\"hello world\"}"))) initFiles: map[string]string{
assert.NoError(t, err) "/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 "create an overlay": {
resp, err := http.DefaultTransport.RoundTrip(req) request: func(serverURL string) (*http.Request, error) {
assert.NoError(t, err) 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 "get all overlays after creation": {
body, err := io.ReadAll(resp.Body) initFiles: map[string]string{
assert.NoError(t, err) "/usr/share/warewulf/overlays/testoverlay/email.ww": sampleTemplate,
assert.NoError(t, resp.Body.Close()) "/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 "delete overlay": {
req, err = http.NewRequest(http.MethodGet, srv.URL+"/api/overlays/testoverlay/file?path=email.ww", nil) initFiles: map[string]string{
assert.NoError(t, err) "/var/lib/warewulf/overlays/test/": "",
},
// send request request: func(serverURL string) (*http.Request, error) {
resp, err = http.DefaultTransport.RoundTrip(req) return http.NewRequest(http.MethodDelete, serverURL+"/api/overlays/test?force=true", nil)
assert.NoError(t, err) },
response: `{"files":[], "site":true}`,
// validate the resp },
body, err = io.ReadAll(resp.Body) }
assert.NoError(t, err)
assert.NoError(t, resp.Body.Close()) func TestOverlayAPI(t *testing.T) {
for name, tt := range overlayTests {
ja := jsonassert.New(t) t.Run(name, func(t *testing.T) {
ja.Assert(string(body), ` warewulfd.SetNoDaemon()
{ env := testenv.New(t)
"overlay": "testoverlay", defer env.RemoveAll()
"path": "email.ww",
"contents": "hello world", // Create test files
"perms": "<<PRESENCE>>", for fileName, fileContent := range tt.initFiles {
"uid": "<<PRESENCE>>", if strings.HasSuffix(fileName, "/") {
"gid": "<<PRESENCE>>" env.MkdirAll(fileName)
}`) } else {
}) env.WriteFile(fileName, fileContent)
}
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) allowedNets := []net.IPNet{
{
resp, err := http.DefaultTransport.RoundTrip(req) IP: net.IPv4(127, 0, 0, 0),
assert.NoError(t, err) Mask: net.CIDRMask(8, 32),
},
body, err := io.ReadAll(resp.Body) }
assert.NoError(t, err) srv := httptest.NewServer(Handler(nil, allowedNets))
assert.NoError(t, resp.Body.Close()) defer srv.Close()
assert.JSONEq(t, `{"files":null, "site":true}`, string(body)) req, err := tt.request(srv.URL)
}) assert.NoError(t, err)
t.Run("get all overlays", func(t *testing.T) { resp, err := http.DefaultTransport.RoundTrip(req)
req, err := http.NewRequest(http.MethodGet, srv.URL+"/api/overlays", nil) assert.NoError(t, err)
assert.NoError(t, err)
expectedStatus := tt.status
resp, err := http.DefaultTransport.RoundTrip(req) if expectedStatus == 0 {
assert.NoError(t, err) expectedStatus = http.StatusOK
}
body, err := io.ReadAll(resp.Body) assert.Equal(t, expectedStatus, resp.StatusCode)
assert.NoError(t, err)
assert.NoError(t, resp.Body.Close()) body, err := io.ReadAll(resp.Body)
assert.NoError(t, err)
assert.JSONEq(t, `{"test":{"files":null, "site":true},"testoverlay":{"files":["/email.ww"], "site":true}}`, string(body)) assert.NoError(t, resp.Body.Close())
})
ja := jsonassert.New(t)
t.Run("test delete overlay file", func(t *testing.T) { ja.Assertf(string(body), tt.response) //nolint:govet // tt.response is used as a format string with special tokens
req, err := http.NewRequest(http.MethodDelete, srv.URL+"/api/overlays/testoverlay/file?path=email.ww&force=true", nil)
assert.NoError(t, err) for _, fileName := range tt.resultFiles {
assert.DirExists(t, env.GetPath(fileName))
resp, err := http.DefaultTransport.RoundTrip(req) }
assert.NoError(t, err)
for filePath, expectedContent := range tt.validateFiles {
body, err := io.ReadAll(resp.Body) actualContent := env.ReadFile(filePath)
assert.NoError(t, err) assert.Equal(t, expectedContent, actualContent)
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))
})
} }

View File

@@ -8,160 +8,225 @@ import (
"net/http/httptest" "net/http/httptest"
"testing" "testing"
"github.com/kinbiko/jsonassert"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/warewulf/warewulf/internal/pkg/testenv" "github.com/warewulf/warewulf/internal/pkg/testenv"
"github.com/warewulf/warewulf/internal/pkg/warewulfd" "github.com/warewulf/warewulf/internal/pkg/warewulfd"
) )
func TestProfileAPI(t *testing.T) { var profileTests = map[string]struct {
warewulfd.SetNoDaemon() initConf string
env := testenv.New(t) initFiles []string
defer env.RemoveAll() request func(serverURL string) (*http.Request, error)
response string
allowedNets := []net.IPNet{ status int
{ resultConf string
IP: net.IPv4(127, 0, 0, 0), }{
Mask: net.CIDRMask(8, 32), "get all profiles": {
initConf: `
nodeprofiles:
default: {}
nodes: {}
`,
request: func(serverURL string) (*http.Request, error) {
return http.NewRequest(http.MethodGet, serverURL+"/api/profiles", nil)
}, },
} response: `{"default": {}}`,
srv := httptest.NewServer(Handler(nil, allowedNets)) },
defer srv.Close()
t.Run("get all profiles", func(t *testing.T) { "add a new profile": {
req, err := http.NewRequest(http.MethodGet, srv.URL+"/api/profiles", nil) initConf: `
assert.NoError(t, err) 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) "test idempotency (replacing existing profile)": {
assert.NoError(t, err) 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) "test preventing replacing a profile": {
assert.NoError(t, resp.Body.Close()) initConf: `
assert.NoError(t, err) nodeprofiles:
assert.JSONEq(t, `{"default": {}}`, string(body)) 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) { "get one specific profile": {
testProfile := `{"profile": {"kernel": {"version": "v1.0.0", "args": ["kernel-args"]}}}` initConf: `
req, err := http.NewRequest(http.MethodPut, srv.URL+"/api/profiles/test", bytes.NewBuffer([]byte(testProfile))) nodeprofiles:
assert.NoError(t, err) 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) "update a profile": {
assert.NoError(t, err) 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) "test delete a profile": {
assert.NoError(t, resp.Body.Close()) initConf: `
assert.NoError(t, err) nodeprofiles:
assert.JSONEq(t, `{"kernel": {"version": "v1.0.0", "args": ["kernel-args"]}}`, string(body)) default: {}
}) p1:
kernel:
t.Run("test idempotency", func(t *testing.T) { version: "v1.0.0"
testProfile := `{"profile": {"kernel": {"version": "v1.0.0", "args": ["kernel-args"]}}}` args:
req, err := http.NewRequest(http.MethodPut, srv.URL+"/api/profiles/test", bytes.NewBuffer([]byte(testProfile))) - "kernel-args"
assert.NoError(t, err) nodes: {}
`,
resp, err := http.DefaultTransport.RoundTrip(req) request: func(serverURL string) (*http.Request, error) {
assert.NoError(t, err) return http.NewRequest(http.MethodDelete, serverURL+"/api/profiles/p1", nil)
},
body, err := io.ReadAll(resp.Body) response: `{"kernel": {"version": "v1.0.0", "args": ["kernel-args"]}}`,
assert.NoError(t, resp.Body.Close()) resultConf: `
assert.NoError(t, err) nodeprofiles:
assert.JSONEq(t, `{"kernel": {"version": "v1.0.0", "args": ["kernel-args"]}}`, string(body)) default: {}
}) nodes: {}
`,
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) func TestProfileAPI(t *testing.T) {
req.Header.Set("If-None-Match", "*") for name, tt := range profileTests {
t.Run(name, func(t *testing.T) {
resp, err := http.DefaultTransport.RoundTrip(req) warewulfd.SetNoDaemon()
assert.NoError(t, err) env := testenv.New(t)
defer env.RemoveAll()
body, err := io.ReadAll(resp.Body)
assert.NoError(t, resp.Body.Close()) env.WriteFile("/etc/warewulf/nodes.conf", tt.initConf)
assert.NoError(t, err) for _, fileName := range tt.initFiles {
env.CreateFile(fileName)
assert.Equal(t, http.StatusBadRequest, resp.StatusCode) }
assert.Contains(t, string(body), "profile 'test' already exists")
}) allowedNets := []net.IPNet{
{
t.Run("re-read all profiles", func(t *testing.T) { IP: net.IPv4(127, 0, 0, 0),
req, err := http.NewRequest(http.MethodGet, srv.URL+"/api/profiles", nil) Mask: net.CIDRMask(8, 32),
assert.NoError(t, err) },
}
resp, err := http.DefaultTransport.RoundTrip(req) srv := httptest.NewServer(Handler(nil, allowedNets))
assert.NoError(t, err) defer srv.Close()
body, err := io.ReadAll(resp.Body) req, err := tt.request(srv.URL)
assert.NoError(t, resp.Body.Close()) assert.NoError(t, err)
assert.NoError(t, err)
assert.JSONEq(t, `{"default": {}, "test": {"kernel": {"version": "v1.0.0", "args": ["kernel-args"]}}}`, string(body)) resp, err := http.DefaultTransport.RoundTrip(req)
}) assert.NoError(t, err)
t.Run("get one specific profile (that was just added)", func(t *testing.T) { expectedStatus := tt.status
req, err := http.NewRequest(http.MethodGet, srv.URL+"/api/profiles/test", nil) if expectedStatus == 0 {
assert.NoError(t, err) expectedStatus = http.StatusOK
}
resp, err := http.DefaultTransport.RoundTrip(req) assert.Equal(t, expectedStatus, resp.StatusCode)
assert.NoError(t, err)
body, err := io.ReadAll(resp.Body)
body, err := io.ReadAll(resp.Body) assert.NoError(t, err)
assert.NoError(t, resp.Body.Close()) assert.NoError(t, resp.Body.Close())
assert.NoError(t, err)
assert.JSONEq(t, `{"kernel": {"version": "v1.0.0", "args": ["kernel-args"]}}`, string(body)) ja := jsonassert.New(t)
}) ja.Assertf(string(body), tt.response) //nolint:govet
t.Run("update the profile", func(t *testing.T) { if tt.resultConf != "" {
updateProfile := `{"profile": {"kernel": {"version": "v1.0.1-newversion"}}}` assert.YAMLEq(t, tt.resultConf, env.ReadFile("/etc/warewulf/nodes.conf"))
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))
})
} }