diff --git a/internal/slurm/slurmdb_assocs.go b/internal/slurm/slurmdb_assocs.go new file mode 100644 index 0000000..8c98a93 --- /dev/null +++ b/internal/slurm/slurmdb_assocs.go @@ -0,0 +1,208 @@ +package slurm + +import ( + "context" + "net/url" +) + +// GetAssocsOptions specifies optional query parameters for association endpoints. +// Shared by GetAssociations, GetAssociation, DeleteAssociations, and DeleteAssociation. +type GetAssocsOptions struct { + Account *string `url:"account,omitempty"` + Cluster *string `url:"cluster,omitempty"` + DefaultQos *string `url:"default_qos,omitempty"` + Format *string `url:"format,omitempty"` + ID *string `url:"id,omitempty"` + OnlyDefaults *string `url:"only_defaults,omitempty"` + ParentAccount *string `url:"parent_account,omitempty"` + Partition *string `url:"partition,omitempty"` + Qos *string `url:"qos,omitempty"` + UsageEnd *string `url:"usage_end,omitempty"` + UsageStart *string `url:"usage_start,omitempty"` + User *string `url:"user,omitempty"` + WithUsage *string `url:"with_usage,omitempty"` + WithDeleted *string `url:"with_deleted,omitempty"` + WithRawQos *string `url:"with_raw_qos,omitempty"` + WithSubAccts *string `url:"with_sub_accts,omitempty"` + WithoutParentInfo *string `url:"without_parent_info,omitempty"` + WithoutParentLimits *string `url:"without_parent_limits,omitempty"` +} + +// setAssocsQueryParams applies GetAssocsOptions fields as query parameters on the request URL. +func setAssocsQueryParams(reqURL *url.URL, opts *GetAssocsOptions) { + if opts == nil { + return + } + q := reqURL.Query() + if opts.Account != nil { + q.Set("account", *opts.Account) + } + if opts.Cluster != nil { + q.Set("cluster", *opts.Cluster) + } + if opts.DefaultQos != nil { + q.Set("default_qos", *opts.DefaultQos) + } + if opts.Format != nil { + q.Set("format", *opts.Format) + } + if opts.ID != nil { + q.Set("id", *opts.ID) + } + if opts.OnlyDefaults != nil { + q.Set("only_defaults", *opts.OnlyDefaults) + } + if opts.ParentAccount != nil { + q.Set("parent_account", *opts.ParentAccount) + } + if opts.Partition != nil { + q.Set("partition", *opts.Partition) + } + if opts.Qos != nil { + q.Set("qos", *opts.Qos) + } + if opts.UsageEnd != nil { + q.Set("usage_end", *opts.UsageEnd) + } + if opts.UsageStart != nil { + q.Set("usage_start", *opts.UsageStart) + } + if opts.User != nil { + q.Set("user", *opts.User) + } + if opts.WithUsage != nil { + q.Set("with_usage", *opts.WithUsage) + } + if opts.WithDeleted != nil { + q.Set("with_deleted", *opts.WithDeleted) + } + if opts.WithRawQos != nil { + q.Set("with_raw_qos", *opts.WithRawQos) + } + if opts.WithSubAccts != nil { + q.Set("with_sub_accts", *opts.WithSubAccts) + } + if opts.WithoutParentInfo != nil { + q.Set("without_parent_info", *opts.WithoutParentInfo) + } + if opts.WithoutParentLimits != nil { + q.Set("without_parent_limits", *opts.WithoutParentLimits) + } + reqURL.RawQuery = q.Encode() +} + +// GetAssociations lists all associations matching the given options. +func (s *SlurmdbAssocsService) GetAssociations(ctx context.Context, opts *GetAssocsOptions) (*OpenapiAssocsResp, *Response, error) { + path := "slurmdb/v0.0.40/associations" + req, err := s.client.NewRequest("GET", path, nil) + if err != nil { + return nil, nil, err + } + + if opts != nil { + u, parseErr := url.Parse(req.URL.String()) + if parseErr != nil { + return nil, nil, parseErr + } + setAssocsQueryParams(u, opts) + req.URL = u + } + + var result OpenapiAssocsResp + resp, err := s.client.Do(ctx, req, &result) + if err != nil { + return nil, resp, err + } + return &result, resp, nil +} + +// GetAssociation gets a single association matching the given options. +func (s *SlurmdbAssocsService) GetAssociation(ctx context.Context, opts *GetAssocsOptions) (*OpenapiAssocsResp, *Response, error) { + path := "slurmdb/v0.0.40/association" + req, err := s.client.NewRequest("GET", path, nil) + if err != nil { + return nil, nil, err + } + + if opts != nil { + u, parseErr := url.Parse(req.URL.String()) + if parseErr != nil { + return nil, nil, parseErr + } + setAssocsQueryParams(u, opts) + req.URL = u + } + + var result OpenapiAssocsResp + resp, err := s.client.Do(ctx, req, &result) + if err != nil { + return nil, resp, err + } + return &result, resp, nil +} + +// PostAssociations creates or updates associations. +func (s *SlurmdbAssocsService) PostAssociations(ctx context.Context, body *OpenapiAssocsResp) (*OpenapiResp, *Response, error) { + path := "slurmdb/v0.0.40/associations" + req, err := s.client.NewRequest("POST", path, body) + if err != nil { + return nil, nil, err + } + + var result OpenapiResp + resp, err := s.client.Do(ctx, req, &result) + if err != nil { + return nil, resp, err + } + return &result, resp, nil +} + +// DeleteAssociations deletes associations matching the given options. +func (s *SlurmdbAssocsService) DeleteAssociations(ctx context.Context, opts *GetAssocsOptions) (*OpenapiAssocsRemovedResp, *Response, error) { + path := "slurmdb/v0.0.40/associations" + req, err := s.client.NewRequest("DELETE", path, nil) + if err != nil { + return nil, nil, err + } + + if opts != nil { + u, parseErr := url.Parse(req.URL.String()) + if parseErr != nil { + return nil, nil, parseErr + } + setAssocsQueryParams(u, opts) + req.URL = u + } + + var result OpenapiAssocsRemovedResp + resp, err := s.client.Do(ctx, req, &result) + if err != nil { + return nil, resp, err + } + return &result, resp, nil +} + +// DeleteAssociation deletes a single association matching the given options. +func (s *SlurmdbAssocsService) DeleteAssociation(ctx context.Context, opts *GetAssocsOptions) (*OpenapiAssocsRemovedResp, *Response, error) { + path := "slurmdb/v0.0.40/association" + req, err := s.client.NewRequest("DELETE", path, nil) + if err != nil { + return nil, nil, err + } + + if opts != nil { + u, parseErr := url.Parse(req.URL.String()) + if parseErr != nil { + return nil, nil, parseErr + } + setAssocsQueryParams(u, opts) + req.URL = u + } + + var result OpenapiAssocsRemovedResp + resp, err := s.client.Do(ctx, req, &result) + if err != nil { + return nil, resp, err + } + return &result, resp, nil +} diff --git a/internal/slurm/slurmdb_assocs_test.go b/internal/slurm/slurmdb_assocs_test.go new file mode 100644 index 0000000..c26eb55 --- /dev/null +++ b/internal/slurm/slurmdb_assocs_test.go @@ -0,0 +1,352 @@ +package slurm + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +func TestSlurmdbAssocsService_GetAssociations(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("/slurmdb/v0.0.40/associations", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `{"associations": []}`) + }) + server := httptest.NewServer(mux) + defer server.Close() + + client, _ := NewClient(server.URL, nil) + resp, _, err := client.SlurmdbAssocs.GetAssociations(context.Background(), nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if resp == nil { + t.Fatal("expected non-nil response") + } +} + +func TestSlurmdbAssocsService_GetAssociations_WithOptions(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("/slurmdb/v0.0.40/associations", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + q := r.URL.Query() + if q.Get("account") != "testacct" { + t.Errorf("expected account=testacct, got %s", q.Get("account")) + } + if q.Get("cluster") != "testcluster" { + t.Errorf("expected cluster=testcluster, got %s", q.Get("cluster")) + } + if q.Get("user") != "testuser" { + t.Errorf("expected user=testuser, got %s", q.Get("user")) + } + if q.Get("with_deleted") != "true" { + t.Errorf("expected with_deleted=true, got %s", q.Get("with_deleted")) + } + if q.Get("with_usage") != "true" { + t.Errorf("expected with_usage=true, got %s", q.Get("with_usage")) + } + fmt.Fprint(w, `{"associations": []}`) + }) + server := httptest.NewServer(mux) + defer server.Close() + + client, _ := NewClient(server.URL, nil) + opts := &GetAssocsOptions{ + Account: Ptr("testacct"), + Cluster: Ptr("testcluster"), + User: Ptr("testuser"), + WithDeleted: Ptr("true"), + WithUsage: Ptr("true"), + } + resp, _, err := client.SlurmdbAssocs.GetAssociations(context.Background(), opts) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if resp == nil { + t.Fatal("expected non-nil response") + } +} + +func TestSlurmdbAssocsService_GetAssociation(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("/slurmdb/v0.0.40/association", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `{"associations": [{"account": "testacct"}]}`) + }) + server := httptest.NewServer(mux) + defer server.Close() + + client, _ := NewClient(server.URL, nil) + opts := &GetAssocsOptions{ + Account: Ptr("testacct"), + } + resp, _, err := client.SlurmdbAssocs.GetAssociation(context.Background(), opts) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if resp == nil { + t.Fatal("expected non-nil response") + } +} + +func TestSlurmdbAssocsService_GetAssociation_AllQueryParams(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("/slurmdb/v0.0.40/association", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + q := r.URL.Query() + expectedParams := []string{ + "account", "cluster", "default_qos", "format", "id", + "only_defaults", "parent_account", "partition", "qos", + "usage_end", "usage_start", "user", "with_usage", + "with_deleted", "with_raw_qos", "with_sub_accts", + "without_parent_info", "without_parent_limits", + } + for _, p := range expectedParams { + if q.Get(p) == "" { + t.Errorf("expected query param %s to be set", p) + } + } + fmt.Fprint(w, `{"associations": []}`) + }) + server := httptest.NewServer(mux) + defer server.Close() + + client, _ := NewClient(server.URL, nil) + opts := &GetAssocsOptions{ + Account: Ptr("acct"), + Cluster: Ptr("clust"), + DefaultQos: Ptr("dqos"), + Format: Ptr("fmt"), + ID: Ptr("1"), + OnlyDefaults: Ptr("1"), + ParentAccount: Ptr("parent"), + Partition: Ptr("part"), + Qos: Ptr("qos"), + UsageEnd: Ptr("9999"), + UsageStart: Ptr("0000"), + User: Ptr("usr"), + WithUsage: Ptr("1"), + WithDeleted: Ptr("1"), + WithRawQos: Ptr("1"), + WithSubAccts: Ptr("1"), + WithoutParentInfo: Ptr("1"), + WithoutParentLimits: Ptr("1"), + } + resp, _, err := client.SlurmdbAssocs.GetAssociation(context.Background(), opts) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if resp == nil { + t.Fatal("expected non-nil response") + } +} + +func TestSlurmdbAssocsService_PostAssociations(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("/slurmdb/v0.0.40/associations", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + if ct := r.Header.Get("Content-Type"); ct != "application/json" { + t.Errorf("expected Content-Type application/json, got %s", ct) + } + fmt.Fprint(w, `{"meta": {"plugin": {"type": "openapi/v0.0.40"}}}`) + }) + server := httptest.NewServer(mux) + defer server.Close() + + client, _ := NewClient(server.URL, nil) + body := &OpenapiAssocsResp{ + Associations: AssocList{}, + } + resp, _, err := client.SlurmdbAssocs.PostAssociations(context.Background(), body) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if resp == nil { + t.Fatal("expected non-nil response") + } +} + +func TestSlurmdbAssocsService_DeleteAssociations(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("/slurmdb/v0.0.40/associations", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + q := r.URL.Query() + if q.Get("account") != "delacct" { + t.Errorf("expected account=delacct, got %s", q.Get("account")) + } + if q.Get("cluster") != "delcluster" { + t.Errorf("expected cluster=delcluster, got %s", q.Get("cluster")) + } + fmt.Fprint(w, `{"removed_associations": ["acct1"]}`) + }) + server := httptest.NewServer(mux) + defer server.Close() + + client, _ := NewClient(server.URL, nil) + opts := &GetAssocsOptions{ + Account: Ptr("delacct"), + Cluster: Ptr("delcluster"), + } + resp, _, err := client.SlurmdbAssocs.DeleteAssociations(context.Background(), opts) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if resp == nil { + t.Fatal("expected non-nil response") + } +} + +func TestSlurmdbAssocsService_DeleteAssociation(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("/slurmdb/v0.0.40/association", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + q := r.URL.Query() + if q.Get("user") != "deluser" { + t.Errorf("expected user=deluser, got %s", q.Get("user")) + } + if q.Get("partition") != "delpart" { + t.Errorf("expected partition=delpart, got %s", q.Get("partition")) + } + fmt.Fprint(w, `{"removed_associations": ["assoc1"]}`) + }) + server := httptest.NewServer(mux) + defer server.Close() + + client, _ := NewClient(server.URL, nil) + opts := &GetAssocsOptions{ + User: Ptr("deluser"), + Partition: Ptr("delpart"), + } + resp, _, err := client.SlurmdbAssocs.DeleteAssociation(context.Background(), opts) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if resp == nil { + t.Fatal("expected non-nil response") + } +} + +func TestSlurmdbAssocsService_DeleteAssociation_NoOptions(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("/slurmdb/v0.0.40/association", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + q := r.URL.Query() + if len(q) != 0 { + t.Errorf("expected no query params, got %v", q) + } + fmt.Fprint(w, `{"removed_associations": []}`) + }) + server := httptest.NewServer(mux) + defer server.Close() + + client, _ := NewClient(server.URL, nil) + resp, _, err := client.SlurmdbAssocs.DeleteAssociation(context.Background(), nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if resp == nil { + t.Fatal("expected non-nil response") + } +} + +func TestSlurmdbAssocsService_GetAssociations_Error(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("/slurmdb/v0.0.40/associations", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprint(w, `{"errors": [{"error": "internal error"}]}`) + }) + server := httptest.NewServer(mux) + defer server.Close() + + client, _ := NewClient(server.URL, nil) + _, _, err := client.SlurmdbAssocs.GetAssociations(context.Background(), nil) + if err == nil { + t.Fatal("expected error for 500 response") + } + if !strings.Contains(err.Error(), "500") { + t.Errorf("expected error to contain 500, got %v", err) + } +} + +func TestSlurmdbAssocsService_DeleteAssociations_Error(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("/slurmdb/v0.0.40/associations", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + fmt.Fprint(w, `{"errors": [{"error": "not found"}]}`) + }) + server := httptest.NewServer(mux) + defer server.Close() + + client, _ := NewClient(server.URL, nil) + _, _, err := client.SlurmdbAssocs.DeleteAssociations(context.Background(), nil) + if err == nil { + t.Fatal("expected error for 404 response") + } +} + +func TestSlurmdbAssocsService_PathCorrectness(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("/slurmdb/v0.0.40/associations", func(w http.ResponseWriter, r *http.Request) { + if r.Method == "GET" { + fmt.Fprint(w, `{"associations": [], "path": "plural"}`) + } else if r.Method == "DELETE" { + fmt.Fprint(w, `{"removed_associations": [], "path": "plural"}`) + } else { + fmt.Fprint(w, `{"meta": {}}`) + } + }) + mux.HandleFunc("/slurmdb/v0.0.40/association", func(w http.ResponseWriter, r *http.Request) { + if r.Method == "GET" { + fmt.Fprint(w, `{"associations": [], "path": "singular"}`) + } else if r.Method == "DELETE" { + fmt.Fprint(w, `{"removed_associations": [], "path": "singular"}`) + } + }) + server := httptest.NewServer(mux) + defer server.Close() + + client, _ := NewClient(server.URL, nil) + + resp, _, err := client.SlurmdbAssocs.GetAssociations(context.Background(), nil) + if err != nil { + t.Fatalf("GetAssociations: unexpected error: %v", err) + } + if resp == nil { + t.Fatal("GetAssociations: expected non-nil response") + } + + resp, _, err = client.SlurmdbAssocs.GetAssociation(context.Background(), nil) + if err != nil { + t.Fatalf("GetAssociation: unexpected error: %v", err) + } + if resp == nil { + t.Fatal("GetAssociation: expected non-nil response") + } + + delResp, _, err := client.SlurmdbAssocs.DeleteAssociations(context.Background(), nil) + if err != nil { + t.Fatalf("DeleteAssociations: unexpected error: %v", err) + } + if delResp == nil { + t.Fatal("DeleteAssociations: expected non-nil response") + } + + delResp, _, err = client.SlurmdbAssocs.DeleteAssociation(context.Background(), nil) + if err != nil { + t.Fatalf("DeleteAssociation: unexpected error: %v", err) + } + if delResp == nil { + t.Fatal("DeleteAssociation: expected non-nil response") + } + + postResp, _, err := client.SlurmdbAssocs.PostAssociations(context.Background(), &OpenapiAssocsResp{}) + if err != nil { + t.Fatalf("PostAssociations: unexpected error: %v", err) + } + if postResp == nil { + t.Fatal("PostAssociations: expected non-nil response") + } +}