diff --git a/Makefile b/Makefile index 5add6a1b..e88dab67 100644 --- a/Makefile +++ b/Makefile @@ -35,7 +35,7 @@ clean: ## Clean up the dev server .PHONY: test test: ## Run tests - @go test ./... -covermode atomic -coverprofile=cover.out -v + @go test ./... -covermode atomic -coverprofile=cover.out -v -failfast .PHONY: coverage coverage: ## Show html coverage diff --git a/cmd/archivistactl/cmd/e2e_test.go b/cmd/archivistactl/cmd/e2e_test.go index d54320cd..79b00c80 100644 --- a/cmd/archivistactl/cmd/e2e_test.go +++ b/cmd/archivistactl/cmd/e2e_test.go @@ -53,7 +53,6 @@ func (e2e *E2EStoreSuite) TearDownTest() { // Run the E2E tests func (e2e *E2EStoreSuite) Test_E2E() { - // Define tests for supported dbs testDBCases := []string{"mysql", "pgsql"} @@ -208,5 +207,4 @@ func (e2e *E2EStoreSuite) Test_E2E() { } } } - } diff --git a/cmd/archivistactl/cmd/retrieve.go b/cmd/archivistactl/cmd/retrieve.go index 3c52ec7b..b5770907 100644 --- a/cmd/archivistactl/cmd/retrieve.go +++ b/cmd/archivistactl/cmd/retrieve.go @@ -49,7 +49,6 @@ var ( defer file.Close() out = file } - return api.DownloadWithWriter(cmd.Context(), archivistaUrl, args[0], out) }, } @@ -60,7 +59,12 @@ var ( SilenceUsage: true, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - results, err := api.GraphQlQuery[retrieveSubjectResults](cmd.Context(), archivistaUrl, retrieveSubjectsQuery, retrieveSubjectVars{Gitoid: args[0]}) + results, err := api.GraphQlQuery[api.RetrieveSubjectResults]( + cmd.Context(), + archivistaUrl, + api.RetrieveSubjectsQuery, + api.RetrieveSubjectVars{Gitoid: args[0]}, + ) if err != nil { return err } @@ -78,7 +82,7 @@ func init() { envelopeCmd.Flags().StringVarP(&outFile, "out", "o", "", "File to write the envelope out to. Defaults to stdout") } -func printSubjects(results retrieveSubjectResults) { +func printSubjects(results api.RetrieveSubjectResults) { for _, edge := range results.Subjects.Edges { digestStrings := make([]string, 0, len(edge.Node.SubjectDigests)) for _, digest := range edge.Node.SubjectDigests { @@ -88,43 +92,3 @@ func printSubjects(results retrieveSubjectResults) { rootCmd.Printf("Name: %s\nDigests: %s\n", edge.Node.Name, strings.Join(digestStrings, ", ")) } } - -type retrieveSubjectVars struct { - Gitoid string `json:"gitoid"` -} - -type retrieveSubjectResults struct { - Subjects struct { - Edges []struct { - Node struct { - Name string `json:"name"` - SubjectDigests []struct { - Algorithm string `json:"algorithm"` - Value string `json:"value"` - } `json:"subjectDigests"` - } `json:"node"` - } `json:"edges"` - } `json:"subjects"` -} - -const retrieveSubjectsQuery = `query($gitoid: String!) { - subjects( - where: { - hasStatementWith:{ - hasDsseWith:{ - gitoidSha256: $gitoid - } - } - } - ) { - edges { - node{ - name - subjectDigests{ - algorithm - value - } - } - } - } -}` diff --git a/cmd/archivistactl/cmd/search.go b/cmd/archivistactl/cmd/search.go index e41306fd..698ef7e0 100644 --- a/cmd/archivistactl/cmd/search.go +++ b/cmd/archivistactl/cmd/search.go @@ -22,42 +22,46 @@ import ( "github.com/spf13/cobra" ) -var ( - searchCmd = &cobra.Command{ - Use: "search", - Short: "Searches the archivista instance for an attestation matching a query", - SilenceUsage: true, - Long: `Searches the archivista instance for an envelope with a specified subject digest. +var searchCmd = &cobra.Command{ + Use: "search", + Short: "Searches the archivista instance for an attestation matching a query", + SilenceUsage: true, + Long: `Searches the archivista instance for an envelope with a specified subject digest. Optionally a collection name can be provided to further constrain results. Digests are expected to be in the form algorithm:digest, for instance: sha256:456c0c9a7c05e2a7f84c139bbacedbe3e8e88f9c`, - Args: func(cmd *cobra.Command, args []string) error { - if len(args) != 1 { - return errors.New("expected exactly 1 argument") - } + Args: func(cmd *cobra.Command, args []string) error { + if len(args) != 1 { + return errors.New("expected exactly 1 argument") + } - if _, _, err := validateDigestString(args[0]); err != nil { - return err - } + if _, _, err := validateDigestString(args[0]); err != nil { + return err + } - return nil - }, - RunE: func(cmd *cobra.Command, args []string) error { - algo, digest, err := validateDigestString(args[0]) - if err != nil { - return err - } + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + algo, digest, err := validateDigestString(args[0]) + if err != nil { + return err + } - results, err := api.GraphQlQuery[searchResults](cmd.Context(), archivistaUrl, searchQuery, searchVars{Algorithm: algo, Digest: digest}) - if err != nil { - return err - } + results, err := api.GraphQlQuery[api.SearchResults]( + cmd.Context(), + archivistaUrl, + api.SearchQuery, + api.SearchVars{Algorithm: algo, Digest: digest}, + ) + if err != nil { + return err + } - printResults(results) - return nil - }, - } -) + // TODO(nick): e2e test are failing here because of output, this is kind of terrible + printResults(results) + return nil + }, +} func init() { rootCmd.AddCommand(searchCmd) @@ -72,7 +76,7 @@ func validateDigestString(ds string) (algo, digest string, err error) { return algo, digest, nil } -func printResults(results searchResults) { +func printResults(results api.SearchResults) { for _, edge := range results.Dsses.Edges { rootCmd.Printf("Gitoid: %s\n", edge.Node.GitoidSha256) rootCmd.Printf("Collection name: %s\n", edge.Node.Statement.AttestationCollection.Name) @@ -84,55 +88,3 @@ func printResults(results searchResults) { rootCmd.Printf("Attestations: %s\n\n", strings.Join(types, ", ")) } } - -type searchVars struct { - Algorithm string `json:"algo"` - Digest string `json:"digest"` -} - -type searchResults struct { - Dsses struct { - Edges []struct { - Node struct { - GitoidSha256 string `json:"gitoidSha256"` - Statement struct { - AttestationCollection struct { - Name string `json:"name"` - Attestations []struct { - Type string `json:"type"` - } `json:"attestations"` - } `json:"attestationCollections"` - } `json:"statement"` - } `json:"node"` - } `json:"edges"` - } `json:"dsses"` -} - -const searchQuery = `query($algo: String!, $digest: String!) { - dsses( - where: { - hasStatementWith: { - hasSubjectsWith: { - hasSubjectDigestsWith: { - value: $digest, - algorithm: $algo - } - } - } - } - ) { - edges { - node { - gitoidSha256 - statement { - attestationCollections { - name - attestations { - type - } - } - } - } - } - } -}` diff --git a/cmd/archivistactl/cmd/store.go b/cmd/archivistactl/cmd/store.go index 87b3e59d..ef7e76a2 100644 --- a/cmd/archivistactl/cmd/store.go +++ b/cmd/archivistactl/cmd/store.go @@ -54,7 +54,7 @@ func storeAttestationByPath(ctx context.Context, baseUrl, path string) (string, } defer file.Close() - resp, err := api.UploadWithReader(ctx, baseUrl, file) + resp, err := api.StoreWithReader(ctx, baseUrl, file) if err != nil { return "", err } diff --git a/pkg/api/download.go b/pkg/api/download.go index 06c67883..21487acf 100644 --- a/pkg/api/download.go +++ b/pkg/api/download.go @@ -26,9 +26,40 @@ import ( "github.com/in-toto/go-witness/dsse" ) -func Download(ctx context.Context, baseUrl string, gitoid string) (dsse.Envelope, error) { +func DownloadReadCloser(ctx context.Context, baseURL string, gitoid string) (io.ReadCloser, error) { + return DownloadReadCloserWithHTTPClient(ctx, &http.Client{}, baseURL, gitoid) +} + +func DownloadReadCloserWithHTTPClient(ctx context.Context, client *http.Client, baseURL string, gitoid string) (io.ReadCloser, error) { + downloadURL, err := url.JoinPath(baseURL, "download", gitoid) + if err != nil { + return nil, err + } + req, err := http.NewRequestWithContext(ctx, http.MethodGet, downloadURL, nil) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json") + resp, err := client.Do(req) + if err != nil { + return nil, err + } + if resp.StatusCode != http.StatusOK { + // NOTE: attempt to read body on error and + // only close if an error occurs + defer resp.Body.Close() + errMsg, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + return nil, errors.New(string(errMsg)) + } + return resp.Body, nil +} + +func Download(ctx context.Context, baseURL string, gitoid string) (dsse.Envelope, error) { buf := &bytes.Buffer{} - if err := DownloadWithWriter(ctx, baseUrl, gitoid, buf); err != nil { + if err := DownloadWithWriter(ctx, baseURL, gitoid, buf); err != nil { return dsse.Envelope{}, err } @@ -41,13 +72,17 @@ func Download(ctx context.Context, baseUrl string, gitoid string) (dsse.Envelope return env, nil } -func DownloadWithWriter(ctx context.Context, baseUrl, gitoid string, dst io.Writer) error { - downloadUrl, err := url.JoinPath(baseUrl, "download", gitoid) +func DownloadWithWriter(ctx context.Context, baseURL string, gitoid string, dst io.Writer) error { + return DownloadWithWriterWithHTTPClient(ctx, &http.Client{}, baseURL, gitoid, dst) +} + +func DownloadWithWriterWithHTTPClient(ctx context.Context, client *http.Client, baseURL string, gitoid string, dst io.Writer) error { + downloadUrl, err := url.JoinPath(baseURL, "download", gitoid) if err != nil { return err } - req, err := http.NewRequestWithContext(ctx, "GET", downloadUrl, nil) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, downloadUrl, nil) if err != nil { return err } diff --git a/pkg/api/graphql.go b/pkg/api/graphql.go index 73dbfb4e..246b8e2b 100644 --- a/pkg/api/graphql.go +++ b/pkg/api/graphql.go @@ -25,19 +25,56 @@ import ( "net/url" ) -type graphQLError struct { - Message string `json:"message"` -} - -type graphQLResponse[T any] struct { - Data T `json:"data,omitempty"` - Errors []graphQLError `json:"errors,omitempty"` -} +const RetrieveSubjectsQuery = `query($gitoid: String!) { + subjects( + where: { + hasStatementWith:{ + hasDsseWith:{ + gitoidSha256: $gitoid + } + } + } + ) { + edges { + node{ + name + subjectDigests{ + algorithm + value + } + } + } + } +}` -type graphQLRequestBody[TVars any] struct { - Query string `json:"query"` - Variables TVars `json:"variables,omitempty"` -} +const SearchQuery = `query($algo: String!, $digest: String!) { + dsses( + where: { + hasStatementWith: { + hasSubjectsWith: { + hasSubjectDigestsWith: { + value: $digest, + algorithm: $algo + } + } + } + } + ) { + edges { + node { + gitoidSha256 + statement { + attestationCollections { + name + attestations { + type + } + } + } + } + } + } +}` func GraphQlQuery[TRes any, TVars any](ctx context.Context, baseUrl, query string, vars TVars) (TRes, error) { var response TRes @@ -46,7 +83,7 @@ func GraphQlQuery[TRes any, TVars any](ctx context.Context, baseUrl, query strin return response, err } - requestBody := graphQLRequestBody[TVars]{ + requestBody := GraphQLRequestBodyGeneric[TVars]{ Query: query, Variables: vars, } @@ -56,7 +93,7 @@ func GraphQlQuery[TRes any, TVars any](ctx context.Context, baseUrl, query strin return response, err } - req, err := http.NewRequestWithContext(ctx, "POST", queryUrl, bytes.NewReader(reqBody)) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, queryUrl, bytes.NewReader(reqBody)) if err != nil { return response, err } @@ -79,7 +116,7 @@ func GraphQlQuery[TRes any, TVars any](ctx context.Context, baseUrl, query strin } dec := json.NewDecoder(res.Body) - gqlRes := graphQLResponse[TRes]{} + gqlRes := GraphQLResponseGeneric[TRes]{} if err := dec.Decode(&gqlRes); err != nil { return response, err } diff --git a/pkg/api/graphql_test.go b/pkg/api/graphql_test.go index 30948db2..e936a426 100644 --- a/pkg/api/graphql_test.go +++ b/pkg/api/graphql_test.go @@ -24,7 +24,7 @@ import ( "github.com/stretchr/testify/suite" ) -// Test Suite: UT APIStore +// Test Suite: UT APIGraphQL type UTAPIGraphQLSuite struct { suite.Suite } @@ -34,7 +34,6 @@ func TestAPIGraphQLSuite(t *testing.T) { } func (ut *UTAPIGraphQLSuite) Test_Store() { - testServer := httptest.NewServer( http.HandlerFunc( func(w http.ResponseWriter, r *http.Request) { @@ -62,7 +61,6 @@ func (ut *UTAPIGraphQLSuite) Test_Store() { } func (ut *UTAPIGraphQLSuite) Test_Store_NoServer() { - ctx := context.TODO() type testSubjectVar struct { @@ -72,13 +70,17 @@ func (ut *UTAPIGraphQLSuite) Test_Store_NoServer() { type testSubjectResult struct { Data string `json:"data"` } - result, err := api.GraphQlQuery[testSubjectResult](ctx, "http://invalid-archivista", `query`, testSubjectVar{Gitoid: "test_Gitoid"}) + result, err := api.GraphQlQuery[testSubjectResult]( + ctx, + "http://invalid-archivista", + `query`, + testSubjectVar{Gitoid: "test_Gitoid"}, + ) ut.Error(err) ut.Equal(testSubjectResult{Data: ""}, result) } func (ut *UTAPIGraphQLSuite) Test_Store_BadStatusCode_NoMsg() { - testServer := httptest.NewServer( http.HandlerFunc( func(w http.ResponseWriter, r *http.Request) { @@ -102,7 +104,6 @@ func (ut *UTAPIGraphQLSuite) Test_Store_BadStatusCode_NoMsg() { } func (ut *UTAPIGraphQLSuite) Test_Store_InvalidData() { - testServer := httptest.NewServer( http.HandlerFunc( func(w http.ResponseWriter, r *http.Request) { @@ -130,7 +131,6 @@ func (ut *UTAPIGraphQLSuite) Test_Store_InvalidData() { } func (ut *UTAPIGraphQLSuite) Test_Store_QLReponseWithErrors() { - testServer := httptest.NewServer( http.HandlerFunc( func(w http.ResponseWriter, r *http.Request) { diff --git a/pkg/api/structs.go b/pkg/api/structs.go new file mode 100644 index 00000000..9cdfff01 --- /dev/null +++ b/pkg/api/structs.go @@ -0,0 +1,76 @@ +package api + +type GraphQLError struct { + Message string `json:"message"` +} + +type GraphQLResponseGeneric[T any] struct { + Data T `json:"data,omitempty"` + Errors []GraphQLError `json:"errors,omitempty"` +} + +type GraphQLRequestBodyGeneric[TVars any] struct { + Query string `json:"query"` + Variables TVars `json:"variables,omitempty"` +} + +type RetrieveSubjectVars struct { + Gitoid string `json:"gitoid"` +} + +type SearchVars struct { + Algorithm string `json:"algo"` + Digest string `json:"digest"` +} + +type RetrieveSubjectResults struct { + Subjects Subjects `json:"subjects"` +} + +type Subjects struct { + Edges []SubjectEdge `json:"edges"` +} + +type SubjectEdge struct { + Node SubjectNode `json:"node"` +} + +type SubjectNode struct { + Name string `json:"name"` + SubjectDigests []SubjectDigest `json:"subjectDigests"` +} + +type SubjectDigest struct { + Algorithm string `json:"algorithm"` + Value string `json:"value"` +} + +type SearchResults struct { + Dsses DSSES `json:"dsses"` +} + +type DSSES struct { + Edges []SearchEdge `json:"edges"` +} + +type SearchEdge struct { + Node SearchNode `json:"node"` +} + +type SearchNode struct { + GitoidSha256 string `json:"gitoidSha256"` + Statement Statement `json:"statement"` +} + +type Statement struct { + AttestationCollection AttestationCollection `json:"attestationCollections"` +} + +type AttestationCollection struct { + Name string `json:"name"` + Attestations []Attestation `json:"attestations"` +} + +type Attestation struct { + Type string `json:"type"` +} diff --git a/pkg/api/upload.go b/pkg/api/upload.go index 94504412..3d42e772 100644 --- a/pkg/api/upload.go +++ b/pkg/api/upload.go @@ -1,4 +1,4 @@ -// Copyright 2023 The Witness Contributors +// Copyright 2023 The Archivista Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -33,28 +33,22 @@ type UploadResponse struct { // Deprecated: Use UploadResponse instead. It will be removed in version >= v0.6.0 type StoreResponse = UploadResponse -// Deprecated: Use Upload instead. It will be removed in version >= v0.6.0 -func Store(ctx context.Context, baseUrl string, envelope dsse.Envelope) (StoreResponse, error) { - return Upload(ctx, baseUrl, envelope) -} - -func Upload(ctx context.Context, baseUrl string, envelope dsse.Envelope) (StoreResponse, error) { +func Store(ctx context.Context, baseURL string, envelope dsse.Envelope) (StoreResponse, error) { buf := &bytes.Buffer{} enc := json.NewEncoder(buf) if err := enc.Encode(envelope); err != nil { return StoreResponse{}, err } - return UploadWithReader(ctx, baseUrl, buf) + return StoreWithReader(ctx, baseURL, buf) } -// Deprecated: Use UploadWithReader instead. It will be removed in version >= v0.6.0 -func StoreWithReader(ctx context.Context, baseUrl string, r io.Reader) (StoreResponse, error) { - return UploadWithReader(ctx, baseUrl, r) +func StoreWithReader(ctx context.Context, baseURL string, r io.Reader) (StoreResponse, error) { + return StoreWithReaderWithHTTPClient(ctx, &http.Client{}, baseURL, r) } -func UploadWithReader(ctx context.Context, baseUrl string, r io.Reader) (StoreResponse, error) { - uploadPath, err := url.JoinPath(baseUrl, "upload") +func StoreWithReaderWithHTTPClient(ctx context.Context, client *http.Client, baseURL string, r io.Reader) (StoreResponse, error) { + uploadPath, err := url.JoinPath(baseURL, "upload") if err != nil { return UploadResponse{}, err } diff --git a/pkg/http-client/client.go b/pkg/http-client/client.go new file mode 100644 index 00000000..61f18bef --- /dev/null +++ b/pkg/http-client/client.go @@ -0,0 +1,181 @@ +// Copyright 2024 The Archivista Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package httpclient + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + + "github.com/in-toto/archivista/pkg/api" + "github.com/in-toto/go-witness/dsse" +) + +type ArchivistaClient struct { + BaseURL string + GraphQLURL string + *http.Client +} + +func CreateArchivistaClient(httpClient *http.Client, baseURL string) (*ArchivistaClient, error) { + client := ArchivistaClient{ + BaseURL: baseURL, + Client: http.DefaultClient, + } + if httpClient != nil { + client.Client = httpClient + } + var err error + client.GraphQLURL, err = url.JoinPath(client.BaseURL, "query") + if err != nil { + return nil, err + } + return &client, nil +} + +func (ac *ArchivistaClient) DownloadDSSE(ctx context.Context, gitoid string) (dsse.Envelope, error) { + reader, err := api.DownloadReadCloserWithHTTPClient(ctx, ac.Client, ac.BaseURL, gitoid) + if err != nil { + return dsse.Envelope{}, err + } + env := dsse.Envelope{} + if err := json.NewDecoder(reader).Decode(&env); err != nil { + return dsse.Envelope{}, err + } + return env, nil +} + +func (ac *ArchivistaClient) DownloadReadCloser(ctx context.Context, gitoid string) (io.ReadCloser, error) { + return api.DownloadReadCloserWithHTTPClient(ctx, ac.Client, ac.BaseURL, gitoid) +} + +func (ac *ArchivistaClient) DownloadWithWriter(ctx context.Context, gitoid string, dst io.Writer) error { + return api.DownloadWithWriterWithHTTPClient(ctx, ac.Client, ac.BaseURL, gitoid, dst) +} + +func (ac *ArchivistaClient) Store(ctx context.Context, envelope dsse.Envelope) (api.StoreResponse, error) { + return api.Store(ctx, ac.BaseURL, envelope) +} + +func (ac *ArchivistaClient) StoreWithReader(ctx context.Context, r io.Reader) (api.StoreResponse, error) { + return api.StoreWithReader(ctx, ac.BaseURL, r) +} + +type GraphQLRequestBodyInterface struct { + Query string `json:"query"` + Variables interface{} `json:"variables,omitempty"` +} + +type GraphQLResponseInterface struct { + Data interface{} + Errors []api.GraphQLError `json:"errors,omitempty"` +} + +func (ac *ArchivistaClient) GraphQLRetrieveSubjectResults( + ctx context.Context, + gitoid string, +) (api.RetrieveSubjectResults, error) { + return api.GraphQlQuery[api.RetrieveSubjectResults]( + ctx, + ac.BaseURL, + api.RetrieveSubjectsQuery, + api.RetrieveSubjectVars{Gitoid: gitoid}, + ) +} + +func (ac *ArchivistaClient) GraphQLRetrieveSearchResults( + ctx context.Context, + algo string, + digest string, +) (api.SearchResults, error) { + return api.GraphQlQuery[api.SearchResults]( + ctx, + ac.BaseURL, + api.SearchQuery, + api.SearchVars{Algorithm: algo, Digest: digest}, + ) +} + +func (ac *ArchivistaClient) GraphQLQueryIface( + ctx context.Context, + query string, + variables interface{}, +) (*GraphQLResponseInterface, error) { + reader, err := ac.GraphQLQueryReadCloser(ctx, query, variables) + if err != nil { + return nil, err + } + defer reader.Close() + gqlRes := GraphQLResponseInterface{} + dec := json.NewDecoder(reader) + if err := dec.Decode(&gqlRes); err != nil { + return nil, err + } + if len(gqlRes.Errors) > 0 { + return nil, fmt.Errorf("graph ql query failed: %v", gqlRes.Errors) + } + return &gqlRes, nil +} + +func (ac *ArchivistaClient) GraphQLQueryToDst(ctx context.Context, query string, variables interface{}, dst interface{}) error { + reader, err := ac.GraphQLQueryReadCloser(ctx, query, variables) + if err != nil { + return err + } + defer reader.Close() + dec := json.NewDecoder(reader) + if err := dec.Decode(&dst); err != nil { + return err + } + return nil +} + +func (ac *ArchivistaClient) GraphQLQueryReadCloser( + ctx context.Context, + query string, + variables interface{}, +) (io.ReadCloser, error) { + requestBodyMap := GraphQLRequestBodyInterface{ + Query: query, + Variables: variables, + } + requestBodyJSON, err := json.Marshal(requestBodyMap) + if err != nil { + return nil, err + } + req, err := http.NewRequestWithContext(ctx, http.MethodPost, ac.GraphQLURL, bytes.NewReader(requestBodyJSON)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json") + res, err := ac.Do(req) + if err != nil { + return nil, err + } + if res.StatusCode != http.StatusOK { + defer res.Body.Close() + errMsg, err := io.ReadAll(res.Body) + if err != nil { + return nil, err + } + return nil, errors.New(string(errMsg)) + } + return res.Body, nil +} diff --git a/pkg/http-client/client_test.go b/pkg/http-client/client_test.go new file mode 100644 index 00000000..e2de972d --- /dev/null +++ b/pkg/http-client/client_test.go @@ -0,0 +1,320 @@ +// Copyright 2024 The Archivista Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package httpclient_test + +import ( + "bytes" + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "os" + "testing" + + "github.com/in-toto/archivista/pkg/api" + httpclient "github.com/in-toto/archivista/pkg/http-client" + "github.com/in-toto/go-witness/dsse" + "github.com/stretchr/testify/suite" +) + +// Test Suite: UT HTTPClientDownloadSuite +type UTHTTPClientDownloadSuite struct { + suite.Suite +} + +func TestHTTPClientAPIDownloadSuite(t *testing.T) { + suite.Run(t, new(UTHTTPClientDownloadSuite)) +} + +func (ut *UTHTTPClientDownloadSuite) Test_DownloadDSSE() { + testEnvelope, err := os.ReadFile("../../test/package.attestation.json") + if err != nil { + ut.FailNow(err.Error()) + } + expectedEnvelop := dsse.Envelope{} + err = json.Unmarshal(testEnvelope, &expectedEnvelop) + if err != nil { + ut.FailNow(err.Error()) + } + testServer := httptest.NewServer( + http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _, err = w.Write(testEnvelope) + if err != nil { + ut.FailNow(err.Error()) + } + }, + ), + ) + defer testServer.Close() + ctx := context.TODO() + client, err := httpclient.CreateArchivistaClient(http.DefaultClient, testServer.URL) + if err != nil { + ut.FailNow(err.Error()) + } + resp, err := client.DownloadDSSE(ctx, "gitoid_test") + if err != nil { + ut.FailNow(err.Error()) + } + ut.Equal(expectedEnvelop, resp) +} + +func (ut *UTHTTPClientDownloadSuite) Test_DownloadReadCloser() { + testEnvelope, err := os.ReadFile("../../test/package.attestation.json") + if err != nil { + ut.FailNow(err.Error()) + } + expectedEnvelop := dsse.Envelope{} + err = json.Unmarshal(testEnvelope, &expectedEnvelop) + if err != nil { + ut.FailNow(err.Error()) + } + testServer := httptest.NewServer( + http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _, err = w.Write(testEnvelope) + if err != nil { + ut.FailNow(err.Error()) + } + }, + ), + ) + defer testServer.Close() + ctx := context.TODO() + client, err := httpclient.CreateArchivistaClient(http.DefaultClient, testServer.URL) + if err != nil { + ut.FailNow(err.Error()) + } + readCloser, err := client.DownloadReadCloser(ctx, "gitoid_test") + if err != nil { + ut.FailNow(err.Error()) + } + env := dsse.Envelope{} + if err := json.NewDecoder(readCloser).Decode(&env); err != nil { + ut.FailNow(err.Error()) + } + ut.Equal(expectedEnvelop, env) +} + +func (ut *UTHTTPClientDownloadSuite) Test_DownloadWithWriter() { + testEnvelope, err := os.ReadFile("../../test/package.attestation.json") + if err != nil { + ut.FailNow(err.Error()) + } + expectedEnvelop := dsse.Envelope{} + err = json.Unmarshal(testEnvelope, &expectedEnvelop) + if err != nil { + ut.FailNow(err.Error()) + } + testServer := httptest.NewServer( + http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _, err = w.Write(testEnvelope) + if err != nil { + ut.FailNow(err.Error()) + } + }, + ), + ) + defer testServer.Close() + ctx := context.TODO() + client, err := httpclient.CreateArchivistaClient(http.DefaultClient, testServer.URL) + if err != nil { + ut.FailNow(err.Error()) + } + buf := bytes.NewBuffer(nil) + if err := client.DownloadWithWriter(ctx, "gitoid_test", buf); err != nil { + ut.FailNow(err.Error()) + } + env := dsse.Envelope{} + if err := json.NewDecoder(buf).Decode(&env); err != nil { + ut.FailNow(err.Error()) + } + ut.Equal(expectedEnvelop, env) +} + +// Test Suite: UT HTTPClientStore +type UTHTTPClientStoreSuite struct { + suite.Suite +} + +func TestAPIStoreSuite(t *testing.T) { + suite.Run(t, new(UTHTTPClientStoreSuite)) +} + +func (ut *UTHTTPClientStoreSuite) Test_Store() { + testServer := httptest.NewServer( + http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _, err := w.Write([]byte(`{"gitoid":"test"}`)) + if err != nil { + ut.FailNow(err.Error()) + } + }, + ), + ) + defer testServer.Close() + ctx := context.TODO() + attFile, err := os.ReadFile("../../test/package.attestation.json") + if err != nil { + ut.FailNow(err.Error()) + } + attEnvelop := dsse.Envelope{} + err = json.Unmarshal(attFile, &attEnvelop) + if err != nil { + ut.FailNow(err.Error()) + } + client, err := httpclient.CreateArchivistaClient(http.DefaultClient, testServer.URL) + if err != nil { + ut.FailNow(err.Error()) + } + resp, err := client.Store(ctx, attEnvelop) + if err != nil { + ut.FailNow(err.Error()) + } + ut.Equal(resp, api.StoreResponse{Gitoid: "test"}) +} + +func (ut *UTHTTPClientStoreSuite) Test_StoreWithReader() { + testServer := httptest.NewServer( + http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _, err := w.Write([]byte(`{"gitoid":"test"}`)) + if err != nil { + ut.FailNow(err.Error()) + } + }, + ), + ) + defer testServer.Close() + attIo, err := os.Open("../../test/package.attestation.json") + if err != nil { + ut.FailNow(err.Error()) + } + ctx := context.TODO() + client, err := httpclient.CreateArchivistaClient(http.DefaultClient, testServer.URL) + if err != nil { + ut.FailNow(err.Error()) + } + resp, err := client.StoreWithReader(ctx, attIo) + if err != nil { + ut.FailNow(err.Error()) + } + ut.Equal(resp, api.StoreResponse{Gitoid: "test"}) +} + +// Test Suite: UT HTTPClientStore +type UTHTTPClientGraphQLSuite struct { + suite.Suite +} + +func TestAPIGraphQLSuite(t *testing.T) { + suite.Run(t, new(UTHTTPClientGraphQLSuite)) +} + +func (ut *UTHTTPClientGraphQLSuite) Test_GraphQLRetrieveSubjectResults() { + expected := api.GraphQLResponseGeneric[api.RetrieveSubjectResults]{ + Data: api.RetrieveSubjectResults{ + Subjects: api.Subjects{ + Edges: []api.SubjectEdge{ + { + Node: api.SubjectNode{ + Name: "test_Gitoid", + SubjectDigests: []api.SubjectDigest{ + { + Algorithm: "test_Gitoid", + Value: "test_Gitoid", + }, + }, + }, + }, + }, + }, + }, + Errors: []api.GraphQLError{}, + } + testServer := httptest.NewServer( + http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + if err := json.NewEncoder(w).Encode(expected); err != nil { + ut.FailNow(err.Error()) + } + }, + ), + ) + defer testServer.Close() + ctx := context.TODO() + client, err := httpclient.CreateArchivistaClient(http.DefaultClient, testServer.URL) + if err != nil { + ut.FailNow(err.Error()) + } + actual, err := client.GraphQLRetrieveSubjectResults(ctx, "test_Gitoid") + ut.NoError(err) + ut.Equal(expected.Data, actual) +} + +func (ut *UTHTTPClientGraphQLSuite) Test_GraphQLSearchResults() { + expected := api.GraphQLResponseGeneric[api.SearchResults]{ + Data: api.SearchResults{ + Dsses: api.DSSES{ + Edges: []api.SearchEdge{ + { + Node: api.SearchNode{ + GitoidSha256: "test_Gitoid", + Statement: api.Statement{ + AttestationCollection: api.AttestationCollection{ + Name: "test_Gitoid", + Attestations: []api.Attestation{ + { + Type: "test", + }, + }, + }, + }, + }, + }, + }, + }, + }, + Errors: []api.GraphQLError{}, + } + testServer := httptest.NewServer( + http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + if err := json.NewEncoder(w).Encode(expected); err != nil { + ut.FailNow(err.Error()) + } + }, + ), + ) + defer testServer.Close() + ctx := context.TODO() + client, err := httpclient.CreateArchivistaClient(http.DefaultClient, testServer.URL) + if err != nil { + ut.FailNow(err.Error()) + } + actual, err := client.GraphQLRetrieveSearchResults(ctx, "test_Gitoid", "test_Gitoid") + ut.NoError(err) + ut.Equal(expected.Data, actual) +}