Skip to content
This repository was archived by the owner on Jan 30, 2026. It is now read-only.

Commit 0382a5b

Browse files
committed
Add testing - internal/catalogapi
Signed-off-by: Jose R. Gonzalez <komish@flutes.dev>
1 parent 180360d commit 0382a5b

8 files changed

Lines changed: 269 additions & 5 deletions

File tree

internal/catalogapi/catalogapi.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ import (
1313
"github.com/opdev/productctl/internal/resource"
1414
)
1515

16+
var (
17+
ErrMissingName = errors.New("listing did not have a name and it is required")
18+
ErrDetachingComponents = errors.New("unable to detach components from product listing")
19+
)
20+
1621
// APIEndpoint represents a full URL to a given Catalog API instance.
1722
type APIEndpoint = string
1823

@@ -40,7 +45,7 @@ func ApplyProduct(
4045
}
4146

4247
if !declaration.Spec.HasName() {
43-
return nil, errors.New("listing did not have a name and it is required")
48+
return nil, ErrMissingName
4449
}
4550

4651
if updateListing {
@@ -54,11 +59,11 @@ func ApplyProduct(
5459
L.Info("declaration enumerated no components. detaching all components from product (if necessary)")
5560
resp, err := genpyxis.SetComponentsForProduct(ctx, client, declaration.Spec.ID, []string{})
5661
if err != nil {
57-
return nil, err
62+
return nil, errors.Join(ErrDetachingComponents, err)
5863
}
5964

6065
if gqlErr := resp.Update_product_listing.GetError(); gqlErr != nil {
61-
return nil, ParseGraphQLResponseError(gqlErr)
66+
return nil, errors.Join(ErrDetachingComponents, ParseGraphQLResponseError(gqlErr))
6267
}
6368

6469
declaration.Spec.LastUpdateDate = resp.Update_product_listing.GetData().GetLast_update_date()
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package catalogapi_test
2+
3+
import (
4+
"testing"
5+
6+
. "github.com/onsi/ginkgo/v2"
7+
. "github.com/onsi/gomega"
8+
)
9+
10+
func TestCatalogapi(t *testing.T) {
11+
RegisterFailHandler(Fail)
12+
RunSpecs(t, "Catalogapi Suite")
13+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package catalogapi_test
2+
3+
import (
4+
. "github.com/onsi/ginkgo/v2"
5+
// . "github.com/onsi/gomega"
6+
// "github.com/opdev/productctl/internal/catalogapi"
7+
)
8+
9+
var _ = PDescribe("catalogapi", func() {
10+
// Unimplemented. At the time of writing. Significant GraphQL client mocking
11+
// needed in order to allow business logic testing.
12+
})

internal/catalogapi/client_test.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package catalogapi_test
2+
3+
import (
4+
"bytes"
5+
"log/slog"
6+
"net/http"
7+
"net/http/httptest"
8+
9+
. "github.com/onsi/ginkgo/v2"
10+
. "github.com/onsi/gomega"
11+
"github.com/opdev/productctl/internal/catalogapi"
12+
)
13+
14+
var _ = Describe("Client", func() {
15+
var (
16+
testLogger *slog.Logger
17+
testServer *httptest.Server
18+
)
19+
20+
BeforeEach(func() {
21+
testLogger = slog.New(slog.DiscardHandler)
22+
testServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) }))
23+
})
24+
AfterEach(func() {
25+
testServer.Close()
26+
})
27+
When("when building an HTTP client", func() {
28+
var (
29+
testToken string
30+
)
31+
32+
BeforeEach(func() {
33+
testToken = "test-token"
34+
})
35+
When("a token is provided", func() {
36+
It("should be included in the client", func() {
37+
client := catalogapi.TokenAuthenticatedHTTPClient(testToken, testLogger)
38+
req, err := http.NewRequest(http.MethodGet, testServer.URL, bytes.NewBuffer([]byte("testRequest")))
39+
Expect(err).ToNot(HaveOccurred())
40+
_, err = client.Do(req)
41+
Expect(err).ToNot(HaveOccurred())
42+
Expect(req.Header.Get("X-API-KEY")).To(Equal(testToken))
43+
})
44+
})
45+
46+
It("should have the appropriate user agent configured", func() {
47+
client := catalogapi.TokenAuthenticatedHTTPClient(testToken, testLogger)
48+
req, err := http.NewRequest(http.MethodGet, testServer.URL, bytes.NewBuffer([]byte("testRequest")))
49+
Expect(err).ToNot(HaveOccurred())
50+
_, err = client.Do(req)
51+
Expect(err).ToNot(HaveOccurred())
52+
Expect(req.Header.Get("User-Agent")).To(Equal(catalogapi.UserAgent))
53+
})
54+
})
55+
})

internal/catalogapi/gqlerr_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package catalogapi_test
2+
3+
import (
4+
"strconv"
5+
6+
. "github.com/onsi/ginkgo/v2"
7+
. "github.com/onsi/gomega"
8+
9+
"github.com/opdev/productctl/internal/catalogapi"
10+
"github.com/opdev/productctl/internal/genpyxis"
11+
)
12+
13+
var _ = Describe("Gqlerr", func() {
14+
var (
15+
gqlErr catalogapi.GraphQLResponseError
16+
)
17+
18+
When("parsing GraphQL errors", func() {
19+
BeforeEach(func() {
20+
gqlErr = &genpyxis.MutateProductListingCommonResponseError{
21+
Status: 404,
22+
Detail: "some error detail",
23+
}
24+
})
25+
It("should return an error message containing the Status and Detail values", func() {
26+
parsed := catalogapi.ParseGraphQLResponseError(gqlErr)
27+
statusStr := strconv.Itoa(gqlErr.GetStatus())
28+
Expect(parsed.Error()).To(ContainSubstring(gqlErr.GetDetail()))
29+
Expect(parsed.Error()).To(ContainSubstring(statusStr))
30+
})
31+
})
32+
})

internal/catalogapi/paging.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,15 @@ package catalogapi
22

33
import (
44
"context"
5+
"errors"
56

67
"github.com/opdev/productctl/internal/logger"
78
)
89

10+
var (
11+
ErrQueryPageFailed = errors.New("failed to query page")
12+
)
13+
914
// QueryAll returns all items of type T in a paginated response from
1015
// startingPage with the set pageSize.
1116
func QueryAll[T any](
@@ -23,7 +28,7 @@ func QueryAll[T any](
2328
L := L.With("page", page)
2429
returned, total, err := queryPageFn(page, pageSize)
2530
if err != nil {
26-
return nil, err
31+
return nil, errors.Join(ErrQueryPageFailed, err)
2732
}
2833

2934
allItems = append(allItems, returned...)

internal/catalogapi/paging_test.go

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
package catalogapi_test
2+
3+
import (
4+
"context"
5+
"errors"
6+
7+
. "github.com/onsi/ginkgo/v2"
8+
. "github.com/onsi/gomega"
9+
"github.com/opdev/productctl/internal/catalogapi"
10+
)
11+
12+
func mockData(size uint) []int {
13+
d := make([]int, size)
14+
for i := range d {
15+
// value is 1-based, to align with the size input.
16+
d[i] = i + 1
17+
}
18+
19+
return d
20+
}
21+
22+
func mockSuccessfulPaginatedQueryWithInput(inputData []int) func(page, pageSize int) ([]int, int, error) {
23+
// maximum number of "queries" allowed.
24+
max := 8
25+
counter := 0
26+
return func(page, pageSize int) ([]int, int, error) {
27+
if counter > max {
28+
return nil, -1, errors.New("TESTING CONSTRAINT REACHED: Max query count reached in mock function - Adjust unit tests to use less queries")
29+
}
30+
defer func() { counter++ }()
31+
start := (page - 1) * pageSize
32+
// account for start and end points that are outside of inputData bounds.
33+
if start > len(inputData) {
34+
return []int{}, 0, nil
35+
}
36+
end := start + pageSize
37+
if end > len(inputData) {
38+
end = len(inputData)
39+
}
40+
return inputData[start:end], len(inputData), nil
41+
42+
}
43+
}
44+
45+
var _ = Describe("Paging", func() {
46+
var (
47+
ctx context.Context
48+
)
49+
50+
BeforeEach(func() {
51+
ctx = context.TODO()
52+
})
53+
54+
When("querying all records", func() {
55+
var (
56+
inputData []int
57+
startingPage int
58+
pageSize int
59+
)
60+
61+
BeforeEach(func() {
62+
inputData = mockData(5)
63+
startingPage = 1
64+
pageSize = catalogapi.DefaultPageSize
65+
})
66+
67+
When("the query function returns an error", func() {
68+
It("should return library errors and the provided error", func() {
69+
returnedErr := errors.New("query error")
70+
_, err := catalogapi.QueryAll(
71+
ctx,
72+
startingPage,
73+
pageSize,
74+
func(page, pageSize int) (returnedItems []int, totalItems int, queryError error) {
75+
return nil, 0, returnedErr
76+
},
77+
)
78+
Expect(err).To(MatchError(returnedErr))
79+
Expect(err).To(MatchError(catalogapi.ErrQueryPageFailed))
80+
})
81+
})
82+
83+
When("you start at page 0", func() {
84+
BeforeEach(func() {
85+
startingPage = 1
86+
})
87+
88+
It("should query all records", func() {
89+
records, err := catalogapi.QueryAll(
90+
ctx,
91+
startingPage,
92+
pageSize,
93+
mockSuccessfulPaginatedQueryWithInput(inputData),
94+
)
95+
Expect(err).ToNot(HaveOccurred())
96+
Expect(records).To(BeEquivalentTo(inputData))
97+
})
98+
})
99+
100+
When("you start at a specific page", func() {
101+
102+
BeforeEach(func() {
103+
inputData = mockData(3)
104+
startingPage = 2
105+
// pageSize out of scope, but adjusted to make this assertion easier.
106+
pageSize = 1
107+
})
108+
109+
It("should only contain the records from that point forward", func() {
110+
records, err := catalogapi.QueryAll(
111+
ctx,
112+
startingPage,
113+
pageSize,
114+
mockSuccessfulPaginatedQueryWithInput(inputData),
115+
)
116+
Expect(err).ToNot(HaveOccurred())
117+
Expect(records).To(BeEquivalentTo(inputData[(startingPage-1)*pageSize:]))
118+
})
119+
})
120+
When("you define a page size", func() {
121+
BeforeEach(func() {
122+
inputData = mockData(5)
123+
startingPage = 1
124+
pageSize = 1
125+
})
126+
It("should use the expected number of queries to query all records", func() {
127+
totalQueryCount := 0
128+
_, err := catalogapi.QueryAll(
129+
ctx,
130+
startingPage,
131+
pageSize,
132+
func(page, pageSize int) (returnedItems []int, totalItems int, queryError error) {
133+
totalQueryCount++
134+
return mockSuccessfulPaginatedQueryWithInput(inputData)(page, pageSize)
135+
},
136+
)
137+
Expect(err).ToNot(HaveOccurred())
138+
Expect(totalQueryCount).To(BeEquivalentTo(len(inputData)))
139+
140+
})
141+
})
142+
})
143+
})

internal/transport/tokenauth.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ type APITokenAuthenticated struct {
2323
func (t *APITokenAuthenticated) RoundTrip(req *http.Request) (*http.Response, error) {
2424
t.logger().Debug("adding api key and user agent headers to request")
2525
req.Header.Set("X-API-KEY", t.Token)
26-
req.Header.Set("User-Agent", t.UserAgent)
2726
return t.Wrapped.RoundTrip(req)
2827
}
2928

0 commit comments

Comments
 (0)