Compare commits
9 commits
921a5ca96e
...
649ca66bf9
| Author | SHA1 | Date | |
|---|---|---|---|
|
649ca66bf9 |
|||
|
ba2f8080be |
|||
|
712692d414 |
|||
|
9950e0e24a |
|||
|
b420217f2b |
|||
|
396f2842e5 |
|||
|
1791196c87 |
|||
|
71aaaacf7b |
|||
|
6b104c4784 |
19 changed files with 1145 additions and 42 deletions
27
internal/api/agent/helpers_test.go
Normal file
27
internal/api/agent/helpers_test.go
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
package agentapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
configuration "git.g3e.fr/syonad/two/internal/config/agent"
|
||||||
|
dispatcher "git.g3e.fr/syonad/two/internal/dispatcher/agent"
|
||||||
|
"git.g3e.fr/syonad/two/pkg/db/kv"
|
||||||
|
"git.g3e.fr/syonad/two/pkg/worker"
|
||||||
|
"github.com/dgraph-io/badger/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
// newTestServer builds a Server backed by an in-memory Badger DB.
|
||||||
|
// The worker queue is buffered but has no running goroutines: Dispatch enqueues
|
||||||
|
// without blocking and Execute never runs, so DB state reflects only Prepare writes.
|
||||||
|
func newTestServer(t *testing.T) (*Server, *badger.DB) {
|
||||||
|
t.Helper()
|
||||||
|
db := kv.InitDB(kv.Config{Path: t.TempDir()}, false)
|
||||||
|
t.Cleanup(func() { db.Close() })
|
||||||
|
q := worker.New(100)
|
||||||
|
cfg := &configuration.Config{DefaultInterface: "br-test"}
|
||||||
|
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
|
||||||
|
d := dispatcher.New(q, db, cfg, logger)
|
||||||
|
return New(d, db, logger), db
|
||||||
|
}
|
||||||
|
|
@ -69,7 +69,12 @@ func (s *Server) deleteSubnet(w http.ResponseWriter, _ *http.Request, name strin
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
s.dispatcher.Dispatch(cmd)
|
s.dispatcher.Dispatch(cmd)
|
||||||
state, _ := kv.GetFromDB(s.db, "subnet/"+name+"/state")
|
state, err := kv.GetFromDB(s.db, "subnet/"+name+"/state")
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
json.NewEncoder(w).Encode(ErrorResponse{Error: "failed to read subnet state"})
|
||||||
|
return
|
||||||
|
}
|
||||||
w.WriteHeader(http.StatusAccepted)
|
w.WriteHeader(http.StatusAccepted)
|
||||||
json.NewEncoder(w).Encode(Subnet{Name: name, State: state})
|
json.NewEncoder(w).Encode(Subnet{Name: name, State: state})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
234
internal/api/agent/subnet_test.go
Normal file
234
internal/api/agent/subnet_test.go
Normal file
|
|
@ -0,0 +1,234 @@
|
||||||
|
package agentapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.g3e.fr/syonad/two/pkg/db/kv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// --- SubnetsHandler ---
|
||||||
|
|
||||||
|
func TestListSubnets_Empty(t *testing.T) {
|
||||||
|
s, _ := newTestServer(t)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
s.SubnetsHandler(w, httptest.NewRequest(http.MethodGet, "/subnets", nil))
|
||||||
|
if w.Code != http.StatusOK {
|
||||||
|
t.Fatalf("attendu 200, obtenu %d", w.Code)
|
||||||
|
}
|
||||||
|
var result []Subnet
|
||||||
|
json.NewDecoder(w.Body).Decode(&result)
|
||||||
|
if len(result) != 0 {
|
||||||
|
t.Errorf("attendu liste vide, obtenu %v", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListSubnets_WithData(t *testing.T) {
|
||||||
|
s, db := newTestServer(t)
|
||||||
|
kv.AddInDB(db, "subnet/sn-1/state", "created")
|
||||||
|
kv.AddInDB(db, "subnet/sn-1/vpc", "vpc-1")
|
||||||
|
kv.AddInDB(db, "subnet/sn-2/state", "creating")
|
||||||
|
kv.AddInDB(db, "subnet/sn-2/vpc", "vpc-1")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
s.SubnetsHandler(w, httptest.NewRequest(http.MethodGet, "/subnets", nil))
|
||||||
|
if w.Code != http.StatusOK {
|
||||||
|
t.Fatalf("attendu 200, obtenu %d", w.Code)
|
||||||
|
}
|
||||||
|
var result []Subnet
|
||||||
|
json.NewDecoder(w.Body).Decode(&result)
|
||||||
|
if len(result) != 2 {
|
||||||
|
t.Errorf("attendu 2 subnets, obtenu %d", len(result))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListSubnets_InvalidMethod(t *testing.T) {
|
||||||
|
s, _ := newTestServer(t)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
s.SubnetsHandler(w, httptest.NewRequest(http.MethodPut, "/subnets", nil))
|
||||||
|
if w.Code != http.StatusMethodNotAllowed {
|
||||||
|
t.Errorf("attendu 405, obtenu %d", w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostSubnet_Created(t *testing.T) {
|
||||||
|
s, db := newTestServer(t)
|
||||||
|
kv.AddInDB(db, "vpc/vpc-1/state", "created")
|
||||||
|
req := SubnetCreateRequest{
|
||||||
|
Name: "sn-new",
|
||||||
|
VPC: "vpc-1",
|
||||||
|
IfaceType: "vms",
|
||||||
|
GatewayIP: "10.0.0.1",
|
||||||
|
CIDR: "10.0.0.0/24",
|
||||||
|
}
|
||||||
|
body, _ := json.Marshal(req)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
s.SubnetsHandler(w, httptest.NewRequest(http.MethodPost, "/subnets", bytes.NewReader(body)))
|
||||||
|
if w.Code != http.StatusAccepted {
|
||||||
|
t.Fatalf("attendu 202, obtenu %d: %s", w.Code, w.Body.String())
|
||||||
|
}
|
||||||
|
var result Subnet
|
||||||
|
json.NewDecoder(w.Body).Decode(&result)
|
||||||
|
if result.Name != "sn-new" {
|
||||||
|
t.Errorf("name attendu sn-new, obtenu %q", result.Name)
|
||||||
|
}
|
||||||
|
if result.State != "creating" {
|
||||||
|
t.Errorf("state attendu creating, obtenu %q", result.State)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostSubnet_MissingFields(t *testing.T) {
|
||||||
|
s, _ := newTestServer(t)
|
||||||
|
body, _ := json.Marshal(SubnetCreateRequest{Name: "sn-1"}) // vpc, iface_type, gateway_ip, cidr manquants
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
s.SubnetsHandler(w, httptest.NewRequest(http.MethodPost, "/subnets", bytes.NewReader(body)))
|
||||||
|
if w.Code != http.StatusBadRequest {
|
||||||
|
t.Errorf("attendu 400, obtenu %d", w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostSubnet_VPCNotFound(t *testing.T) {
|
||||||
|
s, _ := newTestServer(t)
|
||||||
|
req := SubnetCreateRequest{
|
||||||
|
Name: "sn-1",
|
||||||
|
VPC: "vpc-inexistant",
|
||||||
|
IfaceType: "vms",
|
||||||
|
GatewayIP: "10.0.0.1",
|
||||||
|
CIDR: "10.0.0.0/24",
|
||||||
|
}
|
||||||
|
body, _ := json.Marshal(req)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
s.SubnetsHandler(w, httptest.NewRequest(http.MethodPost, "/subnets", bytes.NewReader(body)))
|
||||||
|
if w.Code != http.StatusConflict {
|
||||||
|
t.Errorf("attendu 409, obtenu %d", w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostSubnet_Duplicate(t *testing.T) {
|
||||||
|
s, db := newTestServer(t)
|
||||||
|
kv.AddInDB(db, "vpc/vpc-1/state", "created")
|
||||||
|
kv.AddInDB(db, "subnet/sn-exist/state", "created")
|
||||||
|
req := SubnetCreateRequest{
|
||||||
|
Name: "sn-exist",
|
||||||
|
VPC: "vpc-1",
|
||||||
|
IfaceType: "vms",
|
||||||
|
GatewayIP: "10.0.0.1",
|
||||||
|
CIDR: "10.0.0.0/24",
|
||||||
|
}
|
||||||
|
body, _ := json.Marshal(req)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
s.SubnetsHandler(w, httptest.NewRequest(http.MethodPost, "/subnets", bytes.NewReader(body)))
|
||||||
|
if w.Code != http.StatusConflict {
|
||||||
|
t.Errorf("attendu 409, obtenu %d", w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostSubnet_VPCDeleting(t *testing.T) {
|
||||||
|
s, db := newTestServer(t)
|
||||||
|
kv.AddInDB(db, "vpc/vpc-dying/state", "deleting")
|
||||||
|
req := SubnetCreateRequest{
|
||||||
|
Name: "sn-1",
|
||||||
|
VPC: "vpc-dying",
|
||||||
|
IfaceType: "vms",
|
||||||
|
GatewayIP: "10.0.0.1",
|
||||||
|
CIDR: "10.0.0.0/24",
|
||||||
|
}
|
||||||
|
body, _ := json.Marshal(req)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
s.SubnetsHandler(w, httptest.NewRequest(http.MethodPost, "/subnets", bytes.NewReader(body)))
|
||||||
|
if w.Code != http.StatusConflict {
|
||||||
|
t.Errorf("attendu 409, obtenu %d", w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostSubnet_InvalidBody(t *testing.T) {
|
||||||
|
s, _ := newTestServer(t)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
s.SubnetsHandler(w, httptest.NewRequest(http.MethodPost, "/subnets", bytes.NewReader([]byte("not json"))))
|
||||||
|
if w.Code != http.StatusBadRequest {
|
||||||
|
t.Errorf("attendu 400, obtenu %d", w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- SubnetByNameHandler ---
|
||||||
|
|
||||||
|
func TestGetSubnet_Found(t *testing.T) {
|
||||||
|
s, db := newTestServer(t)
|
||||||
|
kv.AddInDB(db, "subnet/sn-1/state", "created")
|
||||||
|
kv.AddInDB(db, "subnet/sn-1/vpc", "vpc-1")
|
||||||
|
kv.AddInDB(db, "subnet/sn-1/cidr", "10.0.0.0/24")
|
||||||
|
kv.AddInDB(db, "subnet/sn-1/gateway_ip", "10.0.0.1")
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/subnets/sn-1", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
s.SubnetByNameHandler(w, req)
|
||||||
|
if w.Code != http.StatusOK {
|
||||||
|
t.Fatalf("attendu 200, obtenu %d", w.Code)
|
||||||
|
}
|
||||||
|
var result Subnet
|
||||||
|
json.NewDecoder(w.Body).Decode(&result)
|
||||||
|
if result.Name != "sn-1" || result.State != "created" {
|
||||||
|
t.Errorf("résultat inattendu : %+v", result)
|
||||||
|
}
|
||||||
|
if result.VPC != "vpc-1" {
|
||||||
|
t.Errorf("vpc attendu vpc-1, obtenu %q", result.VPC)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetSubnet_NotFound(t *testing.T) {
|
||||||
|
s, _ := newTestServer(t)
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/subnets/inexistant", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
s.SubnetByNameHandler(w, req)
|
||||||
|
if w.Code != http.StatusNotFound {
|
||||||
|
t.Errorf("attendu 404, obtenu %d", w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetSubnet_EmptyName(t *testing.T) {
|
||||||
|
s, _ := newTestServer(t)
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/subnets/", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
s.SubnetByNameHandler(w, req)
|
||||||
|
if w.Code != http.StatusNotFound {
|
||||||
|
t.Errorf("attendu 404, obtenu %d", w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteSubnet_Success(t *testing.T) {
|
||||||
|
s, db := newTestServer(t)
|
||||||
|
kv.AddInDB(db, "subnet/sn-del/state", "created")
|
||||||
|
req := httptest.NewRequest(http.MethodDelete, "/subnets/sn-del", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
s.SubnetByNameHandler(w, req)
|
||||||
|
if w.Code != http.StatusAccepted {
|
||||||
|
t.Fatalf("attendu 202, obtenu %d: %s", w.Code, w.Body.String())
|
||||||
|
}
|
||||||
|
var result Subnet
|
||||||
|
json.NewDecoder(w.Body).Decode(&result)
|
||||||
|
if result.State != "deleting" {
|
||||||
|
t.Errorf("state attendu deleting, obtenu %q", result.State)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteSubnet_NotFound(t *testing.T) {
|
||||||
|
s, _ := newTestServer(t)
|
||||||
|
req := httptest.NewRequest(http.MethodDelete, "/subnets/inexistant", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
s.SubnetByNameHandler(w, req)
|
||||||
|
if w.Code != http.StatusNotFound {
|
||||||
|
t.Errorf("attendu 404, obtenu %d", w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubnetByName_InvalidMethod(t *testing.T) {
|
||||||
|
s, db := newTestServer(t)
|
||||||
|
kv.AddInDB(db, "subnet/sn-1/state", "created")
|
||||||
|
req := httptest.NewRequest(http.MethodPut, "/subnets/sn-1", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
s.SubnetByNameHandler(w, req)
|
||||||
|
if w.Code != http.StatusMethodNotAllowed {
|
||||||
|
t.Errorf("attendu 405, obtenu %d", w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -88,7 +88,12 @@ func (s *Server) postSubnet(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
s.dispatcher.Dispatch(cmd)
|
s.dispatcher.Dispatch(cmd)
|
||||||
entries, _ := kv.ListByPrefix(s.db, "subnet/"+req.Name+"/")
|
entries, err := kv.ListByPrefix(s.db, "subnet/"+req.Name+"/")
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
json.NewEncoder(w).Encode(ErrorResponse{Error: "failed to read subnet state"})
|
||||||
|
return
|
||||||
|
}
|
||||||
sub := Subnet{Name: req.Name}
|
sub := Subnet{Name: req.Name}
|
||||||
for key, value := range entries {
|
for key, value := range entries {
|
||||||
parts := strings.Split(key, "/")
|
parts := strings.Split(key, "/")
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,12 @@ func (s *Server) deleteVpc(w http.ResponseWriter, _ *http.Request, name string)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
s.dispatcher.Dispatch(cmd)
|
s.dispatcher.Dispatch(cmd)
|
||||||
state, _ := kv.GetFromDB(s.db, "vpc/"+name+"/state")
|
state, err := kv.GetFromDB(s.db, "vpc/"+name+"/state")
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
json.NewEncoder(w).Encode(ErrorResponse{Error: "failed to read vpc state"})
|
||||||
|
return
|
||||||
|
}
|
||||||
w.WriteHeader(http.StatusAccepted)
|
w.WriteHeader(http.StatusAccepted)
|
||||||
json.NewEncoder(w).Encode(VPC{Name: name, State: state})
|
json.NewEncoder(w).Encode(VPC{Name: name, State: state})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
188
internal/api/agent/vpc_test.go
Normal file
188
internal/api/agent/vpc_test.go
Normal file
|
|
@ -0,0 +1,188 @@
|
||||||
|
package agentapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.g3e.fr/syonad/two/pkg/db/kv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// --- VpcsHandler ---
|
||||||
|
|
||||||
|
func TestListVpcs_Empty(t *testing.T) {
|
||||||
|
s, _ := newTestServer(t)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
s.VpcsHandler(w, httptest.NewRequest(http.MethodGet, "/vpcs", nil))
|
||||||
|
if w.Code != http.StatusOK {
|
||||||
|
t.Fatalf("attendu 200, obtenu %d", w.Code)
|
||||||
|
}
|
||||||
|
var result []VPC
|
||||||
|
json.NewDecoder(w.Body).Decode(&result)
|
||||||
|
if len(result) != 0 {
|
||||||
|
t.Errorf("attendu liste vide, obtenu %v", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListVpcs_WithData(t *testing.T) {
|
||||||
|
s, db := newTestServer(t)
|
||||||
|
kv.AddInDB(db, "vpc/v1/state", "created")
|
||||||
|
kv.AddInDB(db, "vpc/v2/state", "creating")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
s.VpcsHandler(w, httptest.NewRequest(http.MethodGet, "/vpcs", nil))
|
||||||
|
if w.Code != http.StatusOK {
|
||||||
|
t.Fatalf("attendu 200, obtenu %d", w.Code)
|
||||||
|
}
|
||||||
|
var result []VPC
|
||||||
|
json.NewDecoder(w.Body).Decode(&result)
|
||||||
|
if len(result) != 2 {
|
||||||
|
t.Errorf("attendu 2 VPCs, obtenu %d", len(result))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListVpcs_InvalidMethod(t *testing.T) {
|
||||||
|
s, _ := newTestServer(t)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
s.VpcsHandler(w, httptest.NewRequest(http.MethodPut, "/vpcs", nil))
|
||||||
|
if w.Code != http.StatusMethodNotAllowed {
|
||||||
|
t.Errorf("attendu 405, obtenu %d", w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostVpc_Created(t *testing.T) {
|
||||||
|
s, _ := newTestServer(t)
|
||||||
|
body, _ := json.Marshal(VPCCreateRequest{Name: "vpc-new"})
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
s.VpcsHandler(w, httptest.NewRequest(http.MethodPost, "/vpcs", bytes.NewReader(body)))
|
||||||
|
if w.Code != http.StatusAccepted {
|
||||||
|
t.Fatalf("attendu 202, obtenu %d: %s", w.Code, w.Body.String())
|
||||||
|
}
|
||||||
|
var result VPC
|
||||||
|
json.NewDecoder(w.Body).Decode(&result)
|
||||||
|
if result.Name != "vpc-new" {
|
||||||
|
t.Errorf("name attendu vpc-new, obtenu %q", result.Name)
|
||||||
|
}
|
||||||
|
if result.State != "creating" {
|
||||||
|
t.Errorf("state attendu creating, obtenu %q", result.State)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostVpc_MissingName(t *testing.T) {
|
||||||
|
s, _ := newTestServer(t)
|
||||||
|
body, _ := json.Marshal(VPCCreateRequest{})
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
s.VpcsHandler(w, httptest.NewRequest(http.MethodPost, "/vpcs", bytes.NewReader(body)))
|
||||||
|
if w.Code != http.StatusBadRequest {
|
||||||
|
t.Errorf("attendu 400, obtenu %d", w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostVpc_Duplicate(t *testing.T) {
|
||||||
|
s, db := newTestServer(t)
|
||||||
|
kv.AddInDB(db, "vpc/vpc-exist/state", "created")
|
||||||
|
body, _ := json.Marshal(VPCCreateRequest{Name: "vpc-exist"})
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
s.VpcsHandler(w, httptest.NewRequest(http.MethodPost, "/vpcs", bytes.NewReader(body)))
|
||||||
|
if w.Code != http.StatusConflict {
|
||||||
|
t.Errorf("attendu 409, obtenu %d", w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostVpc_InvalidBody(t *testing.T) {
|
||||||
|
s, _ := newTestServer(t)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
s.VpcsHandler(w, httptest.NewRequest(http.MethodPost, "/vpcs", bytes.NewReader([]byte("not json"))))
|
||||||
|
if w.Code != http.StatusBadRequest {
|
||||||
|
t.Errorf("attendu 400, obtenu %d", w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- VpcByNameHandler ---
|
||||||
|
|
||||||
|
func TestGetVpc_Found(t *testing.T) {
|
||||||
|
s, db := newTestServer(t)
|
||||||
|
kv.AddInDB(db, "vpc/vpc-1/state", "created")
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/vpcs/vpc-1", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
s.VpcByNameHandler(w, req)
|
||||||
|
if w.Code != http.StatusOK {
|
||||||
|
t.Fatalf("attendu 200, obtenu %d", w.Code)
|
||||||
|
}
|
||||||
|
var result VPC
|
||||||
|
json.NewDecoder(w.Body).Decode(&result)
|
||||||
|
if result.Name != "vpc-1" || result.State != "created" {
|
||||||
|
t.Errorf("résultat inattendu : %+v", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetVpc_NotFound(t *testing.T) {
|
||||||
|
s, _ := newTestServer(t)
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/vpcs/inexistant", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
s.VpcByNameHandler(w, req)
|
||||||
|
if w.Code != http.StatusNotFound {
|
||||||
|
t.Errorf("attendu 404, obtenu %d", w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetVpc_EmptyName(t *testing.T) {
|
||||||
|
s, _ := newTestServer(t)
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/vpcs/", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
s.VpcByNameHandler(w, req)
|
||||||
|
if w.Code != http.StatusNotFound {
|
||||||
|
t.Errorf("attendu 404, obtenu %d", w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteVpc_Success(t *testing.T) {
|
||||||
|
s, db := newTestServer(t)
|
||||||
|
kv.AddInDB(db, "vpc/vpc-del/state", "created")
|
||||||
|
req := httptest.NewRequest(http.MethodDelete, "/vpcs/vpc-del", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
s.VpcByNameHandler(w, req)
|
||||||
|
if w.Code != http.StatusAccepted {
|
||||||
|
t.Fatalf("attendu 202, obtenu %d: %s", w.Code, w.Body.String())
|
||||||
|
}
|
||||||
|
var result VPC
|
||||||
|
json.NewDecoder(w.Body).Decode(&result)
|
||||||
|
if result.State != "deleting" {
|
||||||
|
t.Errorf("state attendu deleting, obtenu %q", result.State)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteVpc_NotFound(t *testing.T) {
|
||||||
|
s, _ := newTestServer(t)
|
||||||
|
req := httptest.NewRequest(http.MethodDelete, "/vpcs/inexistant", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
s.VpcByNameHandler(w, req)
|
||||||
|
if w.Code != http.StatusNotFound {
|
||||||
|
t.Errorf("attendu 404, obtenu %d", w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteVpc_BlockedByActiveSubnet(t *testing.T) {
|
||||||
|
s, db := newTestServer(t)
|
||||||
|
kv.AddInDB(db, "vpc/vpc-busy/state", "created")
|
||||||
|
kv.AddInDB(db, "subnet/sn-1/state", "created")
|
||||||
|
kv.AddInDB(db, "subnet/sn-1/vpc", "vpc-busy")
|
||||||
|
req := httptest.NewRequest(http.MethodDelete, "/vpcs/vpc-busy", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
s.VpcByNameHandler(w, req)
|
||||||
|
if w.Code != http.StatusNotFound {
|
||||||
|
t.Errorf("attendu 404 (Prepare échoue), obtenu %d: %s", w.Code, w.Body.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVpcByName_InvalidMethod(t *testing.T) {
|
||||||
|
s, db := newTestServer(t)
|
||||||
|
kv.AddInDB(db, "vpc/vpc-1/state", "created")
|
||||||
|
req := httptest.NewRequest(http.MethodPut, "/vpcs/vpc-1", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
s.VpcByNameHandler(w, req)
|
||||||
|
if w.Code != http.StatusMethodNotAllowed {
|
||||||
|
t.Errorf("attendu 405, obtenu %d", w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -69,7 +69,12 @@ func (s *Server) postVpc(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
s.dispatcher.Dispatch(cmd)
|
s.dispatcher.Dispatch(cmd)
|
||||||
state, _ := kv.GetFromDB(s.db, "vpc/"+req.Name+"/state")
|
state, err := kv.GetFromDB(s.db, "vpc/"+req.Name+"/state")
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
json.NewEncoder(w).Encode(ErrorResponse{Error: "failed to read vpc state"})
|
||||||
|
return
|
||||||
|
}
|
||||||
w.WriteHeader(http.StatusAccepted)
|
w.WriteHeader(http.StatusAccepted)
|
||||||
json.NewEncoder(w).Encode(VPC{Name: req.Name, State: state})
|
json.NewEncoder(w).Encode(VPC{Name: req.Name, State: state})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
63
internal/dispatcher/agent/dispatcher_test.go
Normal file
63
internal/dispatcher/agent/dispatcher_test.go
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
package dispatcher
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
configuration "git.g3e.fr/syonad/two/internal/config/agent"
|
||||||
|
"github.com/dgraph-io/badger/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDispatcher_Prepare_Success(t *testing.T) {
|
||||||
|
d, _ := newTestDispatcher(t)
|
||||||
|
cmd := mockCmd{
|
||||||
|
prepareFn: func(*badger.DB, *configuration.Config) error { return nil },
|
||||||
|
executeFn: func(*badger.DB, *configuration.Config) error { return nil },
|
||||||
|
}
|
||||||
|
if err := d.Prepare(cmd); err != nil {
|
||||||
|
t.Errorf("Prepare devrait retourner nil, obtenu : %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDispatcher_Prepare_PropagatesError(t *testing.T) {
|
||||||
|
d, _ := newTestDispatcher(t)
|
||||||
|
want := errors.New("prepare failed")
|
||||||
|
cmd := mockCmd{
|
||||||
|
prepareFn: func(*badger.DB, *configuration.Config) error { return want },
|
||||||
|
executeFn: func(*badger.DB, *configuration.Config) error { return nil },
|
||||||
|
}
|
||||||
|
if err := d.Prepare(cmd); !errors.Is(err, want) {
|
||||||
|
t.Errorf("attendu %v, obtenu %v", want, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDispatcher_Dispatch_ExecutesCommand(t *testing.T) {
|
||||||
|
d, _ := newTestDispatcher(t)
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(1)
|
||||||
|
cmd := mockCmd{
|
||||||
|
prepareFn: func(*badger.DB, *configuration.Config) error { return nil },
|
||||||
|
executeFn: func(*badger.DB, *configuration.Config) error {
|
||||||
|
wg.Done()
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
d.Dispatch(cmd)
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDispatcher_Dispatch_ExecuteErrorLogged(t *testing.T) {
|
||||||
|
d, _ := newTestDispatcher(t)
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(1)
|
||||||
|
cmd := mockCmd{
|
||||||
|
prepareFn: func(*badger.DB, *configuration.Config) error { return nil },
|
||||||
|
executeFn: func(*badger.DB, *configuration.Config) error {
|
||||||
|
defer wg.Done()
|
||||||
|
return errors.New("execute failed")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
d.Dispatch(cmd)
|
||||||
|
wg.Wait() // Execute s'est terminé — l'erreur est loggée, pas propagée
|
||||||
|
}
|
||||||
38
internal/dispatcher/agent/helpers_test.go
Normal file
38
internal/dispatcher/agent/helpers_test.go
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
package dispatcher
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
configuration "git.g3e.fr/syonad/two/internal/config/agent"
|
||||||
|
"git.g3e.fr/syonad/two/pkg/db/kv"
|
||||||
|
"git.g3e.fr/syonad/two/pkg/worker"
|
||||||
|
"github.com/dgraph-io/badger/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newTestDispatcher(t *testing.T) (*Dispatcher, *badger.DB) {
|
||||||
|
t.Helper()
|
||||||
|
db := kv.InitDB(kv.Config{Path: t.TempDir()}, false)
|
||||||
|
t.Cleanup(func() { db.Close() })
|
||||||
|
q := worker.New(100)
|
||||||
|
q.Start(2)
|
||||||
|
cfg := &configuration.Config{DefaultInterface: "br-default"}
|
||||||
|
cfg.Interfaces = map[string]string{"vms": "br-vms"}
|
||||||
|
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
|
||||||
|
return New(q, db, cfg, logger), db
|
||||||
|
}
|
||||||
|
|
||||||
|
// mockCmd implémente Command sans aucune dépendance système.
|
||||||
|
type mockCmd struct {
|
||||||
|
prepareFn func(*badger.DB, *configuration.Config) error
|
||||||
|
executeFn func(*badger.DB, *configuration.Config) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m mockCmd) Prepare(db *badger.DB, cfg *configuration.Config) error {
|
||||||
|
return m.prepareFn(db, cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m mockCmd) Execute(db *badger.DB, cfg *configuration.Config) error {
|
||||||
|
return m.executeFn(db, cfg)
|
||||||
|
}
|
||||||
135
internal/dispatcher/agent/subnet_commands_test.go
Normal file
135
internal/dispatcher/agent/subnet_commands_test.go
Normal file
|
|
@ -0,0 +1,135 @@
|
||||||
|
package dispatcher
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
configuration "git.g3e.fr/syonad/two/internal/config/agent"
|
||||||
|
"git.g3e.fr/syonad/two/pkg/db/kv"
|
||||||
|
)
|
||||||
|
|
||||||
|
func testCfg() *configuration.Config {
|
||||||
|
cfg := &configuration.Config{DefaultInterface: "br-default"}
|
||||||
|
cfg.Interfaces = map[string]string{"vms": "br-vms"}
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- CreateSubnetCommand.Prepare ---
|
||||||
|
|
||||||
|
func TestCreateSubnetCommand_Prepare_Success(t *testing.T) {
|
||||||
|
_, db := newTestDispatcher(t)
|
||||||
|
kv.AddInDB(db, "vpc/vpc-1/state", "created")
|
||||||
|
cmd := CreateSubnetCommand{
|
||||||
|
Name: "sn-1", VPC: "vpc-1", VxlanID: 100,
|
||||||
|
IfaceType: "vms", GatewayIP: "10.0.0.1", CIDR: "10.0.0.0/24",
|
||||||
|
}
|
||||||
|
if err := cmd.Prepare(db, testCfg()); err != nil {
|
||||||
|
t.Fatalf("Prepare a échoué : %v", err)
|
||||||
|
}
|
||||||
|
state, _ := kv.GetFromDB(db, "subnet/sn-1/state")
|
||||||
|
if state != "creating" {
|
||||||
|
t.Errorf("state attendu creating, obtenu %q", state)
|
||||||
|
}
|
||||||
|
vpc, _ := kv.GetFromDB(db, "subnet/sn-1/vpc")
|
||||||
|
if vpc != "vpc-1" {
|
||||||
|
t.Errorf("vpc attendu vpc-1, obtenu %q", vpc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateSubnetCommand_Prepare_UsesIfaceTypeMapping(t *testing.T) {
|
||||||
|
_, db := newTestDispatcher(t)
|
||||||
|
kv.AddInDB(db, "vpc/vpc-1/state", "created")
|
||||||
|
cmd := CreateSubnetCommand{
|
||||||
|
Name: "sn-1", VPC: "vpc-1", VxlanID: 100,
|
||||||
|
IfaceType: "vms", GatewayIP: "10.0.0.1", CIDR: "10.0.0.0/24",
|
||||||
|
}
|
||||||
|
cmd.Prepare(db, testCfg())
|
||||||
|
iface, _ := kv.GetFromDB(db, "subnet/sn-1/local_iface")
|
||||||
|
if iface != "br-vms" {
|
||||||
|
t.Errorf("local_iface attendu br-vms, obtenu %q", iface)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateSubnetCommand_Prepare_UsesDefaultIfaceWhenTypeUnknown(t *testing.T) {
|
||||||
|
_, db := newTestDispatcher(t)
|
||||||
|
kv.AddInDB(db, "vpc/vpc-1/state", "created")
|
||||||
|
cmd := CreateSubnetCommand{
|
||||||
|
Name: "sn-1", VPC: "vpc-1", VxlanID: 100,
|
||||||
|
IfaceType: "inconnu", GatewayIP: "10.0.0.1", CIDR: "10.0.0.0/24",
|
||||||
|
}
|
||||||
|
cmd.Prepare(db, testCfg())
|
||||||
|
iface, _ := kv.GetFromDB(db, "subnet/sn-1/local_iface")
|
||||||
|
if iface != "br-default" {
|
||||||
|
t.Errorf("local_iface attendu br-default, obtenu %q", iface)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateSubnetCommand_Prepare_Duplicate(t *testing.T) {
|
||||||
|
_, db := newTestDispatcher(t)
|
||||||
|
kv.AddInDB(db, "vpc/vpc-1/state", "created")
|
||||||
|
kv.AddInDB(db, "subnet/sn-exist/state", "created")
|
||||||
|
cmd := CreateSubnetCommand{
|
||||||
|
Name: "sn-exist", VPC: "vpc-1", VxlanID: 100,
|
||||||
|
IfaceType: "vms", GatewayIP: "10.0.0.1", CIDR: "10.0.0.0/24",
|
||||||
|
}
|
||||||
|
if err := cmd.Prepare(db, testCfg()); err == nil {
|
||||||
|
t.Error("Prepare devrait échouer sur un subnet déjà existant")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateSubnetCommand_Prepare_VPCNotFound(t *testing.T) {
|
||||||
|
_, db := newTestDispatcher(t)
|
||||||
|
cmd := CreateSubnetCommand{
|
||||||
|
Name: "sn-1", VPC: "vpc-inexistant", VxlanID: 100,
|
||||||
|
IfaceType: "vms", GatewayIP: "10.0.0.1", CIDR: "10.0.0.0/24",
|
||||||
|
}
|
||||||
|
if err := cmd.Prepare(db, testCfg()); err == nil {
|
||||||
|
t.Error("Prepare devrait échouer si le VPC n'existe pas")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateSubnetCommand_Prepare_VPCDeleting(t *testing.T) {
|
||||||
|
_, db := newTestDispatcher(t)
|
||||||
|
kv.AddInDB(db, "vpc/vpc-dying/state", "deleting")
|
||||||
|
cmd := CreateSubnetCommand{
|
||||||
|
Name: "sn-1", VPC: "vpc-dying", VxlanID: 100,
|
||||||
|
IfaceType: "vms", GatewayIP: "10.0.0.1", CIDR: "10.0.0.0/24",
|
||||||
|
}
|
||||||
|
if err := cmd.Prepare(db, testCfg()); err == nil {
|
||||||
|
t.Error("Prepare devrait échouer si le VPC est en cours de suppression")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateSubnetCommand_Prepare_VPCDeleted(t *testing.T) {
|
||||||
|
_, db := newTestDispatcher(t)
|
||||||
|
kv.AddInDB(db, "vpc/vpc-gone/state", "deleted")
|
||||||
|
cmd := CreateSubnetCommand{
|
||||||
|
Name: "sn-1", VPC: "vpc-gone", VxlanID: 100,
|
||||||
|
IfaceType: "vms", GatewayIP: "10.0.0.1", CIDR: "10.0.0.0/24",
|
||||||
|
}
|
||||||
|
if err := cmd.Prepare(db, testCfg()); err == nil {
|
||||||
|
t.Error("Prepare devrait échouer si le VPC est supprimé")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- DeleteSubnetCommand.Prepare ---
|
||||||
|
|
||||||
|
func TestDeleteSubnetCommand_Prepare_Success(t *testing.T) {
|
||||||
|
_, db := newTestDispatcher(t)
|
||||||
|
kv.AddInDB(db, "subnet/sn-del/state", "created")
|
||||||
|
cmd := DeleteSubnetCommand{Name: "sn-del"}
|
||||||
|
if err := cmd.Prepare(db, nil); err != nil {
|
||||||
|
t.Fatalf("Prepare a échoué : %v", err)
|
||||||
|
}
|
||||||
|
state, _ := kv.GetFromDB(db, "subnet/sn-del/state")
|
||||||
|
if state != "deleting" {
|
||||||
|
t.Errorf("state attendu deleting, obtenu %q", state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteSubnetCommand_Prepare_NotFound(t *testing.T) {
|
||||||
|
_, db := newTestDispatcher(t)
|
||||||
|
cmd := DeleteSubnetCommand{Name: "sn-inexistant"}
|
||||||
|
if err := cmd.Prepare(db, nil); err == nil {
|
||||||
|
t.Error("Prepare devrait échouer si le subnet n'existe pas")
|
||||||
|
}
|
||||||
|
}
|
||||||
89
internal/dispatcher/agent/vpc_commands_test.go
Normal file
89
internal/dispatcher/agent/vpc_commands_test.go
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
package dispatcher
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.g3e.fr/syonad/two/pkg/db/kv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// --- CreateVPCCommand.Prepare ---
|
||||||
|
|
||||||
|
func TestCreateVPCCommand_Prepare_NewVPC(t *testing.T) {
|
||||||
|
_, db := newTestDispatcher(t)
|
||||||
|
cmd := CreateVPCCommand{Name: "vpc-1"}
|
||||||
|
if err := cmd.Prepare(db, nil); err != nil {
|
||||||
|
t.Fatalf("Prepare a échoué : %v", err)
|
||||||
|
}
|
||||||
|
state, err := kv.GetFromDB(db, "vpc/vpc-1/state")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("état non écrit en DB : %v", err)
|
||||||
|
}
|
||||||
|
if state != "creating" {
|
||||||
|
t.Errorf("state attendu creating, obtenu %q", state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateVPCCommand_Prepare_Duplicate(t *testing.T) {
|
||||||
|
_, db := newTestDispatcher(t)
|
||||||
|
kv.AddInDB(db, "vpc/vpc-exist/state", "created")
|
||||||
|
cmd := CreateVPCCommand{Name: "vpc-exist"}
|
||||||
|
if err := cmd.Prepare(db, nil); err == nil {
|
||||||
|
t.Error("Prepare devrait échouer sur un VPC déjà existant")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- DeleteVPCCommand.Prepare ---
|
||||||
|
|
||||||
|
func TestDeleteVPCCommand_Prepare_Success(t *testing.T) {
|
||||||
|
_, db := newTestDispatcher(t)
|
||||||
|
kv.AddInDB(db, "vpc/vpc-del/state", "created")
|
||||||
|
cmd := DeleteVPCCommand{Name: "vpc-del"}
|
||||||
|
if err := cmd.Prepare(db, nil); err != nil {
|
||||||
|
t.Fatalf("Prepare a échoué : %v", err)
|
||||||
|
}
|
||||||
|
state, _ := kv.GetFromDB(db, "vpc/vpc-del/state")
|
||||||
|
if state != "deleting" {
|
||||||
|
t.Errorf("state attendu deleting, obtenu %q", state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteVPCCommand_Prepare_NotFound(t *testing.T) {
|
||||||
|
_, db := newTestDispatcher(t)
|
||||||
|
cmd := DeleteVPCCommand{Name: "vpc-inexistant"}
|
||||||
|
if err := cmd.Prepare(db, nil); err == nil {
|
||||||
|
t.Error("Prepare devrait échouer si le VPC n'existe pas")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteVPCCommand_Prepare_BlockedByActiveSubnet(t *testing.T) {
|
||||||
|
_, db := newTestDispatcher(t)
|
||||||
|
kv.AddInDB(db, "vpc/vpc-busy/state", "created")
|
||||||
|
kv.AddInDB(db, "subnet/sn-1/state", "created")
|
||||||
|
kv.AddInDB(db, "subnet/sn-1/vpc", "vpc-busy")
|
||||||
|
cmd := DeleteVPCCommand{Name: "vpc-busy"}
|
||||||
|
if err := cmd.Prepare(db, nil); err == nil {
|
||||||
|
t.Error("Prepare devrait échouer si un subnet actif existe")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteVPCCommand_Prepare_AllowedWhenSubnetDeleted(t *testing.T) {
|
||||||
|
_, db := newTestDispatcher(t)
|
||||||
|
kv.AddInDB(db, "vpc/vpc-ok/state", "created")
|
||||||
|
kv.AddInDB(db, "subnet/sn-1/state", "deleted")
|
||||||
|
kv.AddInDB(db, "subnet/sn-1/vpc", "vpc-ok")
|
||||||
|
cmd := DeleteVPCCommand{Name: "vpc-ok"}
|
||||||
|
if err := cmd.Prepare(db, nil); err != nil {
|
||||||
|
t.Fatalf("Prepare devrait réussir si le subnet est deleted : %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteVPCCommand_Prepare_AllowedWhenSubnetDeleting(t *testing.T) {
|
||||||
|
_, db := newTestDispatcher(t)
|
||||||
|
kv.AddInDB(db, "vpc/vpc-ok/state", "created")
|
||||||
|
kv.AddInDB(db, "subnet/sn-1/state", "deleting")
|
||||||
|
kv.AddInDB(db, "subnet/sn-1/vpc", "vpc-ok")
|
||||||
|
cmd := DeleteVPCCommand{Name: "vpc-ok"}
|
||||||
|
if err := cmd.Prepare(db, nil); err != nil {
|
||||||
|
t.Fatalf("Prepare devrait réussir si le subnet est deleting : %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
58
internal/ebtables/ebtables.go
Normal file
58
internal/ebtables/ebtables.go
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
package ebtables
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
)
|
||||||
|
|
||||||
|
func addRule(args ...string) error {
|
||||||
|
return exec.Command("ebtables", append([]string{"-A"}, args...)...).Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteRule(args ...string) error {
|
||||||
|
return exec.Command("ebtables", append([]string{"-D"}, args...)...).Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func DropARPToGateway(bridge, gatewayIP string) error {
|
||||||
|
if err := addRule("FORWARD",
|
||||||
|
"--out-interface", bridge,
|
||||||
|
"-p", "arp",
|
||||||
|
"--arp-op", "Request",
|
||||||
|
"--arp-ip-dst", gatewayIP,
|
||||||
|
"-j", "DROP"); err != nil {
|
||||||
|
return fmt.Errorf("ebtables arp rule: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DropDHCP(bridge string) error {
|
||||||
|
if err := addRule("FORWARD",
|
||||||
|
"--out-interface", bridge,
|
||||||
|
"-p", "IPv4",
|
||||||
|
"--ip-protocol", "udp",
|
||||||
|
"--ip-source-port", "67:68",
|
||||||
|
"--ip-destination-port", "67:68",
|
||||||
|
"-j", "DROP"); err != nil {
|
||||||
|
return fmt.Errorf("ebtables dhcp rule: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteARPToGateway(bridge, gatewayIP string) error {
|
||||||
|
return deleteRule("FORWARD",
|
||||||
|
"--out-interface", bridge,
|
||||||
|
"-p", "arp",
|
||||||
|
"--arp-op", "Request",
|
||||||
|
"--arp-ip-dst", gatewayIP,
|
||||||
|
"-j", "DROP")
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteDHCP(bridge string) error {
|
||||||
|
return deleteRule("FORWARD",
|
||||||
|
"--out-interface", bridge,
|
||||||
|
"-p", "IPv4",
|
||||||
|
"--ip-protocol", "udp",
|
||||||
|
"--ip-source-port", "67:68",
|
||||||
|
"--ip-destination-port", "67:68",
|
||||||
|
"-j", "DROP")
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
package metadata
|
package metadata
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
|
@ -181,6 +183,103 @@ func TestUnLoadNoCloudInDB_RemovesAllKeys(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- getIP ---
|
||||||
|
|
||||||
|
func TestGetIP_ValidHostPort(t *testing.T) {
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
req.RemoteAddr = "10.0.0.1:4567"
|
||||||
|
if ip := getIP(req); ip != "10.0.0.1" {
|
||||||
|
t.Errorf("attendu 10.0.0.1, obtenu %q", ip)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetIP_IPv6(t *testing.T) {
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
req.RemoteAddr = "[::1]:8080"
|
||||||
|
if ip := getIP(req); ip != "::1" {
|
||||||
|
t.Errorf("attendu ::1, obtenu %q", ip)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetIP_NoPort(t *testing.T) {
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
req.RemoteAddr = "10.0.0.1"
|
||||||
|
if ip := getIP(req); ip != "10.0.0.1" {
|
||||||
|
t.Errorf("attendu RemoteAddr brut, obtenu %q", ip)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- rootHandler ---
|
||||||
|
|
||||||
|
func TestRootHandler_UserData(t *testing.T) {
|
||||||
|
data = NoCloudData{UserData: "userdata-content"}
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/user-data", nil)
|
||||||
|
rootHandler(w, req)
|
||||||
|
if w.Code != http.StatusOK {
|
||||||
|
t.Errorf("attendu 200, obtenu %d", w.Code)
|
||||||
|
}
|
||||||
|
if body := w.Body.String(); body != "userdata-content" {
|
||||||
|
t.Errorf("body inattendu : %q", body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRootHandler_MetaData(t *testing.T) {
|
||||||
|
data = NoCloudData{MetaData: "metadata-content"}
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
rootHandler(w, httptest.NewRequest(http.MethodGet, "/meta-data", nil))
|
||||||
|
if w.Code != http.StatusOK {
|
||||||
|
t.Errorf("attendu 200, obtenu %d", w.Code)
|
||||||
|
}
|
||||||
|
if body := w.Body.String(); body != "metadata-content" {
|
||||||
|
t.Errorf("body inattendu : %q", body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRootHandler_NetworkConfig(t *testing.T) {
|
||||||
|
data = NoCloudData{NetworkConfig: "network-content"}
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
rootHandler(w, httptest.NewRequest(http.MethodGet, "/network-config", nil))
|
||||||
|
if w.Code != http.StatusOK {
|
||||||
|
t.Errorf("attendu 200, obtenu %d", w.Code)
|
||||||
|
}
|
||||||
|
if body := w.Body.String(); body != "network-content" {
|
||||||
|
t.Errorf("body inattendu : %q", body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRootHandler_VendorData(t *testing.T) {
|
||||||
|
data = NoCloudData{VendorData: "vendor-content"}
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
rootHandler(w, httptest.NewRequest(http.MethodGet, "/vendor-data", nil))
|
||||||
|
if w.Code != http.StatusOK {
|
||||||
|
t.Errorf("attendu 200, obtenu %d", w.Code)
|
||||||
|
}
|
||||||
|
if body := w.Body.String(); body != "vendor-content" {
|
||||||
|
t.Errorf("body inattendu : %q", body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRootHandler_UnknownPath(t *testing.T) {
|
||||||
|
data = NoCloudData{}
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
rootHandler(w, httptest.NewRequest(http.MethodGet, "/unknown", nil))
|
||||||
|
if w.Code != http.StatusNotFound {
|
||||||
|
t.Errorf("attendu 404, obtenu %d", w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRootHandler_ContentType(t *testing.T) {
|
||||||
|
data = NoCloudData{MetaData: "x"}
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
rootHandler(w, httptest.NewRequest(http.MethodGet, "/meta-data", nil))
|
||||||
|
if ct := w.Header().Get("Content-Type"); ct != "text/yaml" {
|
||||||
|
t.Errorf("Content-Type attendu text/yaml, obtenu %q", ct)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- UnLoadNoCloudInDB_DoesNotAffectOtherVMs ---
|
||||||
|
|
||||||
func TestUnLoadNoCloudInDB_DoesNotAffectOtherVMs(t *testing.T) {
|
func TestUnLoadNoCloudInDB_DoesNotAffectOtherVMs(t *testing.T) {
|
||||||
db := kv.InitDB(kv.Config{Path: t.TempDir()}, false)
|
db := kv.InitDB(kv.Config{Path: t.TempDir()}, false)
|
||||||
t.Cleanup(func() { db.Close() })
|
t.Cleanup(func() { db.Close() })
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import (
|
||||||
"github.com/vishvananda/netlink"
|
"github.com/vishvananda/netlink"
|
||||||
)
|
)
|
||||||
|
|
||||||
func CreateVxlan(name string, vxlanID int, localIface string) error {
|
func CreateVxlan(name string, vxlanID int, localIface string, mtu int) error {
|
||||||
link, err := netlink.LinkByName(localIface)
|
link, err := netlink.LinkByName(localIface)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -12,6 +12,7 @@ func CreateVxlan(name string, vxlanID int, localIface string) error {
|
||||||
vxlan := &netlink.Vxlan{
|
vxlan := &netlink.Vxlan{
|
||||||
LinkAttrs: netlink.LinkAttrs{
|
LinkAttrs: netlink.LinkAttrs{
|
||||||
Name: name,
|
Name: name,
|
||||||
|
MTU: mtu,
|
||||||
},
|
},
|
||||||
VxlanId: vxlanID,
|
VxlanId: vxlanID,
|
||||||
Port: 4789,
|
Port: 4789,
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,11 @@ package subnet
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"os/exec"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.g3e.fr/syonad/two/internal/dhcp"
|
"git.g3e.fr/syonad/two/internal/dhcp"
|
||||||
|
"git.g3e.fr/syonad/two/internal/ebtables"
|
||||||
"git.g3e.fr/syonad/two/internal/netif"
|
"git.g3e.fr/syonad/two/internal/netif"
|
||||||
"git.g3e.fr/syonad/two/internal/netns"
|
"git.g3e.fr/syonad/two/internal/netns"
|
||||||
"git.g3e.fr/syonad/two/pkg/db/kv"
|
"git.g3e.fr/syonad/two/pkg/db/kv"
|
||||||
|
|
@ -86,7 +86,7 @@ func CreateSubnet(db *badger.DB, subnetName string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// vxlan
|
// vxlan
|
||||||
if err := netif.CreateVxlan(vxlanIface, vxlanID, localIface); err != nil {
|
if err := netif.CreateVxlan(vxlanIface, vxlanID, localIface, 1500); err != nil {
|
||||||
return fmt.Errorf("create vxlan: %w", err)
|
return fmt.Errorf("create vxlan: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -136,25 +136,11 @@ func CreateSubnet(db *badger.DB, subnetName string) error {
|
||||||
return fmt.Errorf("add route in netns: %w", err)
|
return fmt.Errorf("add route in netns: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ebtables : drop ARP Request vers la gateway sur ce bridge
|
if err := ebtables.DropARPToGateway(bridge, gatewayIP.String()); err != nil {
|
||||||
if err := exec.Command("ebtables", "-A", "FORWARD",
|
return err
|
||||||
"--out-interface", bridge,
|
|
||||||
"-p", "arp",
|
|
||||||
"--arp-op", "Request",
|
|
||||||
"--arp-ip-dst", gatewayIP.String(),
|
|
||||||
"-j", "DROP").Run(); err != nil {
|
|
||||||
return fmt.Errorf("ebtables arp rule: %w", err)
|
|
||||||
}
|
}
|
||||||
|
if err := ebtables.DropDHCP(bridge); err != nil {
|
||||||
// ebtables : drop trafic DHCP sur ce bridge
|
return err
|
||||||
if err := exec.Command("ebtables", "-A", "FORWARD",
|
|
||||||
"--out-interface", bridge,
|
|
||||||
"-p", "IPv4",
|
|
||||||
"--ip-protocol", "udp",
|
|
||||||
"--ip-source-port", "67:68",
|
|
||||||
"--ip-destination-port", "67:68",
|
|
||||||
"-j", "DROP").Run(); err != nil {
|
|
||||||
return fmt.Errorf("ebtables dhcp rule: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// génération de la config dnsmasq et démarrage du service
|
// génération de la config dnsmasq et démarrage du service
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,9 @@ package subnet
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"git.g3e.fr/syonad/two/internal/ebtables"
|
||||||
"git.g3e.fr/syonad/two/internal/netif"
|
"git.g3e.fr/syonad/two/internal/netif"
|
||||||
"git.g3e.fr/syonad/two/internal/netns"
|
"git.g3e.fr/syonad/two/internal/netns"
|
||||||
"git.g3e.fr/syonad/two/pkg/db/kv"
|
"git.g3e.fr/syonad/two/pkg/db/kv"
|
||||||
|
|
@ -33,6 +33,11 @@ func DeleteSubnet(db *badger.DB, subnetName string) error {
|
||||||
return fmt.Errorf("get vxlan_id: %w", err)
|
return fmt.Errorf("get vxlan_id: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gatewayIP, err := kv.GetFromDB(db, "subnet/"+subnetName+"/gateway_ip")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get gateway_ip: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
subnetID := strings.SplitN(subnetName, "-", 2)[1]
|
subnetID := strings.SplitN(subnetName, "-", 2)[1]
|
||||||
bridge := "br-" + subnetID
|
bridge := "br-" + subnetID
|
||||||
vxlanIface := "vxlan-" + vxlanIDStr
|
vxlanIface := "vxlan-" + vxlanIDStr
|
||||||
|
|
@ -55,19 +60,12 @@ func DeleteSubnet(db *badger.DB, subnetName string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// suppression des règles ebtables
|
// suppression des règles ebtables
|
||||||
exec.Command("ebtables", "-D", "FORWARD",
|
if err := ebtables.DeleteARPToGateway(bridge, gatewayIP); err != nil {
|
||||||
"--out-interface", bridge,
|
return fmt.Errorf("delete ebtables arp rule: %w", err)
|
||||||
"-p", "arp",
|
}
|
||||||
"--arp-op", "Request",
|
if err := ebtables.DeleteDHCP(bridge); err != nil {
|
||||||
"-j", "DROP").Run()
|
return fmt.Errorf("delete ebtables dhcp rule: %w", err)
|
||||||
|
}
|
||||||
exec.Command("ebtables", "-D", "FORWARD",
|
|
||||||
"--out-interface", bridge,
|
|
||||||
"-p", "IPv4",
|
|
||||||
"--ip-protocol", "udp",
|
|
||||||
"--ip-source-port", "67:68",
|
|
||||||
"--ip-destination-port", "67:68",
|
|
||||||
"-j", "DROP").Run()
|
|
||||||
|
|
||||||
// suppression du bridge dans le netns VPC
|
// suppression du bridge dans le netns VPC
|
||||||
if err := netns.Call(vpcName, func() error {
|
if err := netns.Call(vpcName, func() error {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
package kv
|
package kv
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
|
||||||
|
|
||||||
"github.com/dgraph-io/badger/v4"
|
"github.com/dgraph-io/badger/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -35,7 +33,7 @@ func DeleteInDB(db *badger.DB, key string) error {
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return deleteKey(db, key)
|
return deleteKey(db, key)
|
||||||
|
|
|
||||||
|
|
@ -151,3 +151,69 @@ func TestDeleteInDB_MissingKey(t *testing.T) {
|
||||||
t.Logf("DeleteInDB clé inexistante retourne : %v (non bloquant)", err)
|
t.Logf("DeleteInDB clé inexistante retourne : %v (non bloquant)", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- ListByPrefix ---
|
||||||
|
|
||||||
|
func TestListByPrefix_MatchingKeys(t *testing.T) {
|
||||||
|
db := newTestDB(t)
|
||||||
|
AddInDB(db, "subnet/sn1/state", "created")
|
||||||
|
AddInDB(db, "subnet/sn1/vpc", "vpc-1")
|
||||||
|
AddInDB(db, "subnet/sn1/cidr", "10.0.0.0/24")
|
||||||
|
|
||||||
|
entries, err := ListByPrefix(db, "subnet/sn1/")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ListByPrefix a échoué : %v", err)
|
||||||
|
}
|
||||||
|
if len(entries) != 3 {
|
||||||
|
t.Fatalf("attendu 3 entrées, obtenu %d", len(entries))
|
||||||
|
}
|
||||||
|
if entries["subnet/sn1/state"] != "created" {
|
||||||
|
t.Errorf("valeur inattendue pour state : %q", entries["subnet/sn1/state"])
|
||||||
|
}
|
||||||
|
if entries["subnet/sn1/vpc"] != "vpc-1" {
|
||||||
|
t.Errorf("valeur inattendue pour vpc : %q", entries["subnet/sn1/vpc"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListByPrefix_NoMatch(t *testing.T) {
|
||||||
|
db := newTestDB(t)
|
||||||
|
AddInDB(db, "vpc/v1/state", "created")
|
||||||
|
|
||||||
|
entries, err := ListByPrefix(db, "subnet/")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ListByPrefix a échoué : %v", err)
|
||||||
|
}
|
||||||
|
if len(entries) != 0 {
|
||||||
|
t.Errorf("attendu 0 entrées, obtenu %d", len(entries))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListByPrefix_IsolatesPrefix(t *testing.T) {
|
||||||
|
db := newTestDB(t)
|
||||||
|
AddInDB(db, "subnet/sn1/state", "created")
|
||||||
|
AddInDB(db, "subnet/sn2/state", "creating")
|
||||||
|
AddInDB(db, "vpc/v1/state", "created")
|
||||||
|
|
||||||
|
entries, err := ListByPrefix(db, "subnet/sn1/")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ListByPrefix a échoué : %v", err)
|
||||||
|
}
|
||||||
|
if len(entries) != 1 {
|
||||||
|
t.Errorf("attendu 1 entrée, obtenu %d : %v", len(entries), entries)
|
||||||
|
}
|
||||||
|
if _, ok := entries["subnet/sn1/state"]; !ok {
|
||||||
|
t.Error("subnet/sn1/state devrait être présent")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListByPrefix_EmptyDB(t *testing.T) {
|
||||||
|
db := newTestDB(t)
|
||||||
|
|
||||||
|
entries, err := ListByPrefix(db, "subnet/")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ListByPrefix a échoué : %v", err)
|
||||||
|
}
|
||||||
|
if len(entries) != 0 {
|
||||||
|
t.Errorf("attendu 0 entrées sur DB vide, obtenu %d", len(entries))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
103
pkg/worker/queue_test.go
Normal file
103
pkg/worker/queue_test.go
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
package worker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNew_ReturnsQueue(t *testing.T) {
|
||||||
|
q := New(10)
|
||||||
|
if q == nil {
|
||||||
|
t.Fatal("New devrait retourner une queue non-nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQueue_SingleTaskExecuted(t *testing.T) {
|
||||||
|
q := New(1)
|
||||||
|
q.Start(1)
|
||||||
|
|
||||||
|
var done atomic.Bool
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(1)
|
||||||
|
q.Submit(func() {
|
||||||
|
done.Store(true)
|
||||||
|
wg.Done()
|
||||||
|
})
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
if !done.Load() {
|
||||||
|
t.Error("la tâche n'a pas été exécutée")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQueue_AllTasksExecuted(t *testing.T) {
|
||||||
|
const n = 50
|
||||||
|
q := New(n)
|
||||||
|
q.Start(1)
|
||||||
|
|
||||||
|
var count atomic.Int32
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(n)
|
||||||
|
for range n {
|
||||||
|
q.Submit(func() {
|
||||||
|
count.Add(1)
|
||||||
|
wg.Done()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
if count.Load() != n {
|
||||||
|
t.Errorf("attendu %d exécutions, obtenu %d", n, count.Load())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQueue_MultipleWorkers(t *testing.T) {
|
||||||
|
const n = 100
|
||||||
|
q := New(n)
|
||||||
|
q.Start(4)
|
||||||
|
|
||||||
|
var count atomic.Int32
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(n)
|
||||||
|
for range n {
|
||||||
|
q.Submit(func() {
|
||||||
|
count.Add(1)
|
||||||
|
wg.Done()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
if count.Load() != n {
|
||||||
|
t.Errorf("attendu %d exécutions, obtenu %d", n, count.Load())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQueue_SubmitBlocksWhenFull(t *testing.T) {
|
||||||
|
q := New(1)
|
||||||
|
// Remplit le buffer sans worker
|
||||||
|
q.Submit(func() {})
|
||||||
|
|
||||||
|
submitted := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
q.Submit(func() {}) // doit bloquer jusqu'à ce qu'un worker consomme
|
||||||
|
close(submitted)
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-submitted:
|
||||||
|
t.Error("Submit aurait dû bloquer sur une queue pleine")
|
||||||
|
case <-time.After(50 * time.Millisecond):
|
||||||
|
// comportement attendu : goroutine bloquée
|
||||||
|
}
|
||||||
|
|
||||||
|
// Démarre un worker pour débloquer
|
||||||
|
q.Start(1)
|
||||||
|
select {
|
||||||
|
case <-submitted:
|
||||||
|
// Submit a pu avancer
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
t.Error("Submit aurait dû se débloquer après démarrage d'un worker")
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue