diff --git a/.github/workflows/verify-licence.yml b/.github/workflows/verify-licence.yml index bf4c97c3..5ec94888 100644 --- a/.github/workflows/verify-licence.yml +++ b/.github/workflows/verify-licence.yml @@ -40,4 +40,4 @@ jobs: - name: Check license headers run: | set -e - addlicense --check -l apache -c 'The Archivista Contributors' --ignore "ent/migrate/migrations/**" -v ./ + addlicense --check -l apache -c 'The Archivista Contributors' --ignore "ent/migrate/migrations/**" --ignore "docs/**" -v ./ diff --git a/Makefile b/Makefile index 11b105ee..5add6a1b 100644 --- a/Makefile +++ b/Makefile @@ -49,6 +49,10 @@ lint: ## Run linter @go vet ./... +.PHONY: docs +docs: ## Generate swagger docs + @go install github.com/swaggo/swag/cmd/swag@v1.16.2 + @swag init -o docs -d internal/server -g server.go -pd .PHONY: db-migrations db-migrations: ## Run the migrations for the database diff --git a/cmd/archivista/main.go b/cmd/archivista/main.go index a2c46fea..7ab1c7a1 100644 --- a/cmd/archivista/main.go +++ b/cmd/archivista/main.go @@ -1,4 +1,4 @@ -// Copyright 2022 The Archivista Contributors +// Copyright 2022-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. @@ -30,13 +30,8 @@ import ( "syscall" "time" - "entgo.io/contrib/entgql" - "github.com/99designs/gqlgen/graphql/handler" - "github.com/99designs/gqlgen/graphql/playground" nested "github.com/antonfisher/nested-logrus-formatter" "github.com/gorilla/handlers" - "github.com/gorilla/mux" - "github.com/in-toto/archivista" "github.com/in-toto/archivista/internal/config" "github.com/in-toto/archivista/internal/metadatastorage/sqlstore" "github.com/in-toto/archivista/internal/objectstorage/blobstore" @@ -97,7 +92,7 @@ func main() { logrus.Fatalf("could not create ent client: %+v", err) } - mysqlStore, mysqlStoreCh, err := sqlstore.New(ctx, entClient) + sqlStore, sqlStoreCh, err := sqlstore.New(ctx, entClient) if err != nil { logrus.Fatalf("error initializing mysql client: %+v", err) } @@ -108,22 +103,10 @@ func main() { logrus.Infof("executing phase 3: create and register http service (time since start: %s)", time.Since(startTime)) // ******************************************************************************** now = time.Now() - server := server.New(mysqlStore, fileStore) - router := mux.NewRouter() - router.HandleFunc("/download/{gitoid}", server.GetHandler) - router.HandleFunc("/upload", server.StoreHandler) - - if cfg.EnableGraphql { - client := mysqlStore.GetClient() - srv := handler.NewDefaultServer(archivista.NewSchema(client)) - srv.Use(entgql.Transactioner{TxOpener: client}) - router.Handle("/query", srv) - if cfg.GraphqlWebClientEnable { - router.Handle("/", - playground.Handler("Archivista", "/query"), - ) - } - } + + // initialize the server + sqlClient := sqlStore.GetClient() + server := server.New(sqlStore, fileStore, cfg, sqlClient) listenAddress := cfg.ListenOn listenAddress = strings.ToLower(strings.TrimSpace(listenAddress)) @@ -146,7 +129,7 @@ func main() { handlers.AllowedOrigins(cfg.CORSAllowOrigins), handlers.AllowedMethods([]string{"GET", "POST", "OPTIONS"}), handlers.AllowedHeaders([]string{"Accept", "Content-Type", "Content-Length", "Accept-Encoding", "X-CSRF-Token", "Authorization"}), - )(router)); err != nil { + )(server.Router())); err != nil { logrus.Fatalf("unable to start http server: %+v", err) } }() @@ -156,7 +139,7 @@ func main() { <-ctx.Done() <-fileStoreCh - <-mysqlStoreCh + <-sqlStoreCh logrus.Infof("exiting, uptime: %v", time.Since(startTime)) } diff --git a/cmd/archivistactl/cmd/store.go b/cmd/archivistactl/cmd/store.go index ef7e76a2..87b3e59d 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.StoreWithReader(ctx, baseUrl, file) + resp, err := api.UploadWithReader(ctx, baseUrl, file) if err != nil { return "", err } diff --git a/docs/docs.go b/docs/docs.go new file mode 100644 index 00000000..f33069cd --- /dev/null +++ b/docs/docs.go @@ -0,0 +1,230 @@ +// Package docs Code generated by swaggo/swag. DO NOT EDIT +package docs + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "contact": { + "name": "Archivista Contributors", + "url": "https://github.com/in-toto/archivista/issues/new" + }, + "license": { + "url": "https://opensource.org/licenses/Apache-2" + }, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/donwload/{gitoid}": { + "post": { + "description": "download an attestation", + "produces": [ + "application/json" + ], + "summary": "Download", + "deprecated": true, + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dsse.Envelope" + } + } + } + } + }, + "/upload": { + "post": { + "description": "stores an attestation", + "produces": [ + "application/json" + ], + "summary": "Store", + "deprecated": true, + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.StoreResponse" + } + } + } + } + }, + "/v1/donwload/{gitoid}": { + "post": { + "description": "download an attestation", + "produces": [ + "application/json" + ], + "tags": [ + "attestation" + ], + "summary": "Download", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dsse.Envelope" + } + } + } + } + }, + "/v1/query": { + "post": { + "description": "GraphQL query", + "produces": [ + "application/json" + ], + "tags": [ + "graphql" + ], + "summary": "Query GraphQL", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/archivista.Resolver" + } + } + } + } + }, + "/v1/upload": { + "post": { + "description": "stores an attestation", + "produces": [ + "application/json" + ], + "tags": [ + "attestation" + ], + "summary": "Store", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.StoreResponse" + } + } + } + } + } + }, + "definitions": { + "api.StoreResponse": { + "type": "object", + "properties": { + "gitoid": { + "type": "string" + } + } + }, + "archivista.Resolver": { + "type": "object" + }, + "dsse.Envelope": { + "type": "object", + "properties": { + "payload": { + "type": "array", + "items": { + "type": "integer" + } + }, + "payloadType": { + "type": "string" + }, + "signatures": { + "type": "array", + "items": { + "$ref": "#/definitions/dsse.Signature" + } + } + } + }, + "dsse.Signature": { + "type": "object", + "properties": { + "certificate": { + "type": "array", + "items": { + "type": "integer" + } + }, + "intermediates": { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "integer" + } + } + }, + "keyid": { + "type": "string" + }, + "sig": { + "type": "array", + "items": { + "type": "integer" + } + }, + "timestamps": { + "type": "array", + "items": { + "$ref": "#/definitions/dsse.SignatureTimestamp" + } + } + } + }, + "dsse.SignatureTimestamp": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "integer" + } + }, + "type": { + "$ref": "#/definitions/dsse.SignatureTimestampType" + } + } + }, + "dsse.SignatureTimestampType": { + "type": "string", + "enum": [ + "tsp" + ], + "x-enum-varnames": [ + "TimestampRFC3161" + ] + } + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "v1", + Host: "", + BasePath: "", + Schemes: []string{}, + Title: "Archivista API", + Description: "Archivista API", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, + LeftDelim: "{{", + RightDelim: "}}", +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/docs/swagger.json b/docs/swagger.json new file mode 100644 index 00000000..78a95e77 --- /dev/null +++ b/docs/swagger.json @@ -0,0 +1,204 @@ +{ + "swagger": "2.0", + "info": { + "description": "Archivista API", + "title": "Archivista API", + "contact": { + "name": "Archivista Contributors", + "url": "https://github.com/in-toto/archivista/issues/new" + }, + "license": { + "url": "https://opensource.org/licenses/Apache-2" + }, + "version": "v1" + }, + "paths": { + "/donwload/{gitoid}": { + "post": { + "description": "download an attestation", + "produces": [ + "application/json" + ], + "summary": "Download", + "deprecated": true, + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dsse.Envelope" + } + } + } + } + }, + "/upload": { + "post": { + "description": "stores an attestation", + "produces": [ + "application/json" + ], + "summary": "Store", + "deprecated": true, + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.StoreResponse" + } + } + } + } + }, + "/v1/donwload/{gitoid}": { + "post": { + "description": "download an attestation", + "produces": [ + "application/json" + ], + "tags": [ + "attestation" + ], + "summary": "Download", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dsse.Envelope" + } + } + } + } + }, + "/v1/query": { + "post": { + "description": "GraphQL query", + "produces": [ + "application/json" + ], + "tags": [ + "graphql" + ], + "summary": "Query GraphQL", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/archivista.Resolver" + } + } + } + } + }, + "/v1/upload": { + "post": { + "description": "stores an attestation", + "produces": [ + "application/json" + ], + "tags": [ + "attestation" + ], + "summary": "Store", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.StoreResponse" + } + } + } + } + } + }, + "definitions": { + "api.StoreResponse": { + "type": "object", + "properties": { + "gitoid": { + "type": "string" + } + } + }, + "archivista.Resolver": { + "type": "object" + }, + "dsse.Envelope": { + "type": "object", + "properties": { + "payload": { + "type": "array", + "items": { + "type": "integer" + } + }, + "payloadType": { + "type": "string" + }, + "signatures": { + "type": "array", + "items": { + "$ref": "#/definitions/dsse.Signature" + } + } + } + }, + "dsse.Signature": { + "type": "object", + "properties": { + "certificate": { + "type": "array", + "items": { + "type": "integer" + } + }, + "intermediates": { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "integer" + } + } + }, + "keyid": { + "type": "string" + }, + "sig": { + "type": "array", + "items": { + "type": "integer" + } + }, + "timestamps": { + "type": "array", + "items": { + "$ref": "#/definitions/dsse.SignatureTimestamp" + } + } + } + }, + "dsse.SignatureTimestamp": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "integer" + } + }, + "type": { + "$ref": "#/definitions/dsse.SignatureTimestampType" + } + } + }, + "dsse.SignatureTimestampType": { + "type": "string", + "enum": [ + "tsp" + ], + "x-enum-varnames": [ + "TimestampRFC3161" + ] + } + } +} \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml new file mode 100644 index 00000000..8d100082 --- /dev/null +++ b/docs/swagger.yaml @@ -0,0 +1,133 @@ +definitions: + api.StoreResponse: + properties: + gitoid: + type: string + type: object + archivista.Resolver: + type: object + dsse.Envelope: + properties: + payload: + items: + type: integer + type: array + payloadType: + type: string + signatures: + items: + $ref: '#/definitions/dsse.Signature' + type: array + type: object + dsse.Signature: + properties: + certificate: + items: + type: integer + type: array + intermediates: + items: + items: + type: integer + type: array + type: array + keyid: + type: string + sig: + items: + type: integer + type: array + timestamps: + items: + $ref: '#/definitions/dsse.SignatureTimestamp' + type: array + type: object + dsse.SignatureTimestamp: + properties: + data: + items: + type: integer + type: array + type: + $ref: '#/definitions/dsse.SignatureTimestampType' + type: object + dsse.SignatureTimestampType: + enum: + - tsp + type: string + x-enum-varnames: + - TimestampRFC3161 +info: + contact: + name: Archivista Contributors + url: https://github.com/in-toto/archivista/issues/new + description: Archivista API + license: + url: https://opensource.org/licenses/Apache-2 + title: Archivista API + version: v1 +paths: + /donwload/{gitoid}: + post: + deprecated: true + description: download an attestation + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dsse.Envelope' + summary: Download + /upload: + post: + deprecated: true + description: stores an attestation + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/api.StoreResponse' + summary: Store + /v1/donwload/{gitoid}: + post: + description: download an attestation + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dsse.Envelope' + summary: Download + tags: + - attestation + /v1/query: + post: + description: GraphQL query + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/archivista.Resolver' + summary: Query GraphQL + tags: + - graphql + /v1/upload: + post: + description: stores an attestation + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/api.StoreResponse' + summary: Store + tags: + - attestation +swagger: "2.0" diff --git a/go.mod b/go.mod index 00ce15bf..ecfabae4 100644 --- a/go.mod +++ b/go.mod @@ -21,12 +21,15 @@ require ( github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.8.0 github.com/stretchr/testify v1.8.4 + github.com/swaggo/http-swagger/v2 v2.0.2 + github.com/swaggo/swag v1.16.2 github.com/vektah/gqlparser/v2 v2.5.11 golang.org/x/sync v0.6.0 ) require ( ariga.io/atlas v0.14.1-0.20230918065911-83ad451a4935 // indirect + github.com/KyleBanks/depth v1.2.1 // indirect github.com/agext/levenshtein v1.2.1 // indirect github.com/agnivade/levenshtein v1.1.1 // indirect github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect @@ -37,6 +40,10 @@ require ( github.com/go-logr/logr v1.2.4 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/inflect v0.19.0 // indirect + github.com/go-openapi/jsonpointer v0.19.5 // indirect + github.com/go-openapi/jsonreference v0.20.0 // indirect + github.com/go-openapi/spec v0.20.6 // indirect + github.com/go-openapi/swag v0.19.15 // indirect github.com/go-test/deep v1.1.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/google/go-cmp v0.6.0 // indirect @@ -46,11 +53,13 @@ require ( github.com/hashicorp/golang-lru/v2 v2.0.3 // indirect github.com/hashicorp/hcl/v2 v2.13.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.17.4 // indirect github.com/klauspost/cpuid/v2 v2.2.6 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/kylelemons/godebug v1.1.0 // indirect + github.com/mailru/easyjson v0.7.6 // indirect github.com/minio/md5-simd v1.1.2 // indirect github.com/minio/sha256-simd v1.0.1 // indirect github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect @@ -62,6 +71,8 @@ require ( github.com/rs/xid v1.5.0 // indirect github.com/sosodev/duration v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/stretchr/objx v0.5.0 // indirect + github.com/swaggo/files/v2 v2.0.0 // indirect github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/zclconf/go-cty v1.12.1 // indirect @@ -78,5 +89,6 @@ require ( golang.org/x/tools v0.13.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 6820bc2f..b613800a 100644 --- a/go.sum +++ b/go.sum @@ -11,6 +11,8 @@ github.com/99designs/gqlgen v0.17.43 h1:I4SYg6ahjowErAQcHFVKy5EcWuwJ3+Xw9z2fLpuF github.com/99designs/gqlgen v0.17.43/go.mod h1:lO0Zjy8MkZgBdv4T1U91x09r0e0WFOdhVUutlQs1Rsc= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM= github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8= github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= @@ -56,6 +58,16 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-openapi/inflect v0.19.0 h1:9jCH9scKIbHeV9m12SmPilScz6krDxKRasNNSNPXu/4= github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA= +github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= +github.com/go-openapi/spec v0.20.6 h1:ich1RQ3WDbfoeTqTAb+5EIxNmpKVJZWBNah9RAT0jIQ= +github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg= @@ -105,6 +117,8 @@ github.com/in-toto/go-witness v0.2.1 h1:eAxMBWUPbz3oPU3lsfEYi/Kdj6weej2umm59bOXP github.com/in-toto/go-witness v0.2.1/go.mod h1:xURJVj4QRD3xnzOJps7gT0pMCFPpAHcPqDC3EyuLuUE= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= @@ -114,6 +128,7 @@ github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6K github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc= github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -125,6 +140,10 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= @@ -141,6 +160,7 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -172,6 +192,12 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw= +github.com/swaggo/files/v2 v2.0.0/go.mod h1:24kk2Y9NYEJ5lHuCra6iVwkMjIekMCaFq/0JQj66kyM= +github.com/swaggo/http-swagger/v2 v2.0.2 h1:FKCdLsl+sFCx60KFsyM0rDarwiUSZ8DqbfSyIKC9OBg= +github.com/swaggo/http-swagger/v2 v2.0.2/go.mod h1:r7/GBkAWIfK6E/OLnE8fXnviHiDeAHmgIyooa4xm3AQ= +github.com/swaggo/swag v1.16.2 h1:28Pp+8DkQoV+HLzLx8RGJZXNGKbFqnuvSbAAtoxiY04= +github.com/swaggo/swag v1.16.2/go.mod h1:6YzXnDcpr0767iOejs318CwYkCQqyGer6BizOg03f+E= github.com/vektah/gqlparser/v2 v2.5.11 h1:JJxLtXIoN7+3x6MBdtIP59TP1RANnY7pXOaDnADQSf8= github.com/vektah/gqlparser/v2 v2.5.11/go.mod h1:1rCcfwB2ekJofmluGWXMSEnPMZgbxzwj6FaZ/4OT8Cc= github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= @@ -254,12 +280,17 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/internal/server/server.go b/internal/server/server.go index ea213782..c207eff4 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -1,4 +1,4 @@ -// Copyright 2022 The Archivista Contributors +// Copyright 2022-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. @@ -24,15 +24,24 @@ import ( "net/http" "strings" + "entgo.io/contrib/entgql" + "github.com/99designs/gqlgen/graphql/handler" + "github.com/99designs/gqlgen/graphql/playground" "github.com/edwarnicke/gitoid" "github.com/gorilla/mux" + "github.com/in-toto/archivista" + _ "github.com/in-toto/archivista/docs" + "github.com/in-toto/archivista/ent" + "github.com/in-toto/archivista/internal/config" "github.com/in-toto/archivista/pkg/api" "github.com/sirupsen/logrus" + httpSwagger "github.com/swaggo/http-swagger/v2" ) type Server struct { metadataStore Storer objectStore StorerGetter + router *mux.Router } type Storer interface { @@ -48,47 +57,93 @@ type StorerGetter interface { Getter } -func New(metadataStore Storer, objectStore StorerGetter) *Server { - return &Server{metadataStore, objectStore} +func New(metadataStore Storer, objectStore StorerGetter, cfg *config.Config, sqlClient *ent.Client) Server { + r := mux.NewRouter() + s := &Server{metadataStore, objectStore, nil} + + // TODO: remove from future version (v0.6.0) endpoint with version + r.HandleFunc("/download/{gitoid}", s.DownloadHandler) + r.HandleFunc("/upload", s.UploadHandler) + if cfg.EnableGraphql { + r.Handle("/query", s.Query(sqlClient)) + r.Handle("/v1/query", s.Query(sqlClient)) + } + + r.HandleFunc("/v1/download/{gitoid}", s.DownloadHandler) + r.HandleFunc("/v1/upload", s.UploadHandler) + if cfg.GraphqlWebClientEnable { + r.Handle("/", + playground.Handler("Archivista", "/v1/query"), + ) + } + r.PathPrefix("/swagger/").Handler(httpSwagger.WrapHandler) + s.router = r + + return *s + +} + +// @title Archivista API +// @description Archivista API +// @version v1 +// @contact.name Archivista Contributors +// @contact.url https://github.com/in-toto/archivista/issues/new +// @license Apache 2 +// @license.url https://opensource.org/licenses/Apache-2 +// InitRoutes initializes the HTTP API routes for the server +func (s *Server) Router() *mux.Router { + return s.router } -func (s *Server) Store(ctx context.Context, r io.Reader) (api.StoreResponse, error) { +// @Summary Upload +// @Description stores an attestation +// @Produce json +// @Success 200 {object} api.StoreResponse +// @Tags attestation +// @Router /v1/upload [post] +func (s *Server) Upload(ctx context.Context, r io.Reader) (api.UploadResponse, error) { payload, err := io.ReadAll(r) if err != nil { - return api.StoreResponse{}, err + return api.UploadResponse{}, err } gid, err := gitoid.New(bytes.NewReader(payload), gitoid.WithContentLength(int64(len(payload))), gitoid.WithSha256()) if err != nil { logrus.Errorf("failed to generate gitoid: %v", err) - return api.StoreResponse{}, err + return api.UploadResponse{}, err } if s.objectStore != nil { if err := s.objectStore.Store(ctx, gid.String(), payload); err != nil { logrus.Errorf("received error from object store: %+v", err) - return api.StoreResponse{}, err + return api.UploadResponse{}, err } } if err := s.metadataStore.Store(ctx, gid.String(), payload); err != nil { logrus.Errorf("received error from metadata store: %+v", err) - return api.StoreResponse{}, err + return api.UploadResponse{}, err } - return api.StoreResponse{Gitoid: gid.String()}, nil + return api.UploadResponse{Gitoid: gid.String()}, nil } -func (s *Server) StoreHandler(w http.ResponseWriter, r *http.Request) { +// @Summary Upload +// @Description stores an attestation +// @Produce json +// @Success 200 {object} api.StoreResponse +// @Router /upload [post] +// @Deprecated +func (s *Server) UploadHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, fmt.Sprintf("%s is an unsupported method", r.Method), http.StatusBadRequest) return } defer r.Body.Close() - resp, err := s.Store(r.Context(), r.Body) + resp, err := s.Upload(r.Context(), r.Body) if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) + http.Error(w, err.Error(), http.StatusInternalServerError) return } @@ -102,7 +157,13 @@ func (s *Server) StoreHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") } -func (s *Server) Get(ctx context.Context, gitoid string) (io.ReadCloser, error) { +// @Summary Download +// @Description download an attestation +// @Produce json +// @Success 200 {object} dsse.Envelope +// @Tags attestation +// @Router /v1/download/{gitoid} [post] +func (s *Server) Download(ctx context.Context, gitoid string) (io.ReadCloser, error) { if len(strings.TrimSpace(gitoid)) == 0 { return nil, errors.New("gitoid parameter is required") } @@ -119,25 +180,48 @@ func (s *Server) Get(ctx context.Context, gitoid string) (io.ReadCloser, error) return objReader, err } -func (s *Server) GetHandler(w http.ResponseWriter, r *http.Request) { +// @Summary Download +// @Description download an attestation +// @Produce json +// @Success 200 {object} dsse.Envelope +// @Deprecated +// @Router /download/{gitoid} [post] +func (s *Server) DownloadHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, fmt.Sprintf("%s is an unsupported method", r.Method), http.StatusBadRequest) return } vars := mux.Vars(r) - attestationReader, err := s.Get(r.Context(), vars["gitoid"]) + if vars == nil { + http.Error(w, "gitoid parameter is required", http.StatusBadRequest) + return + } + + attestationReader, err := s.Download(r.Context(), vars["gitoid"]) if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) + http.Error(w, err.Error(), http.StatusInternalServerError) return } defer attestationReader.Close() if _, err := io.Copy(w, attestationReader); err != nil { logrus.Errorf("failed to copy attestation to response: %+v", err) - w.WriteHeader(http.StatusBadRequest) + w.WriteHeader(http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") } + +// @Summary Query GraphQL +// @Description GraphQL query +// @Produce json +// @Success 200 {object} archivista.Resolver +// @Tags graphql +// @Router /v1/query [post] +func (s *Server) Query(sqlclient *ent.Client) *handler.Server { + srv := handler.NewDefaultServer(archivista.NewSchema(sqlclient)) + srv.Use(entgql.Transactioner{TxOpener: sqlclient}) + return srv +} diff --git a/internal/server/server_test.go b/internal/server/server_test.go new file mode 100644 index 00000000..d573b197 --- /dev/null +++ b/internal/server/server_test.go @@ -0,0 +1,352 @@ +// 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 server + +import ( + "bytes" + "context" + "errors" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/gorilla/mux" + "github.com/in-toto/archivista/internal/config" + "github.com/in-toto/archivista/pkg/api" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" +) + +type StorerMock struct { + mock.Mock + Storer +} + +type StorerGetterMock struct { + mock.Mock + StorerGetter +} + +type UTServerSuite struct { + suite.Suite + mockedStorer *StorerMock + mockedStorerGetter *StorerGetterMock + testServer Server +} + +func TestUTServerSuite(t *testing.T) { + suite.Run(t, new(UTServerSuite)) +} + +func (ut *UTServerSuite) SetupTest() { + ut.mockedStorer = new(StorerMock) + ut.mockedStorerGetter = new(StorerGetterMock) + ut.testServer = Server{ut.mockedStorer, ut.mockedStorerGetter, nil} +} + +func (ut *UTServerSuite) Test_New() { + cfg := new(config.Config) + cfg.EnableGraphql = true + cfg.GraphqlWebClientEnable = true + ut.testServer = New(ut.mockedStorer, ut.mockedStorerGetter, cfg, nil) + ut.NotNil(ut.testServer) + router := ut.testServer.Router() + ut.NotNil(router) + + allPaths := []string{} + err := router.Walk(func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error { + pathTemplate, err := route.GetPathTemplate() + if err != nil { + ut.FailNow(err.Error()) + } + allPaths = append(allPaths, pathTemplate) + return nil + }) + if err != nil { + ut.FailNow(err.Error()) + } + ut.Contains(allPaths, "/download/{gitoid}") + ut.Contains(allPaths, "/upload") + ut.Contains(allPaths, "/query") + ut.Contains(allPaths, "/v1/download/{gitoid}") + ut.Contains(allPaths, "/v1/upload") + ut.Contains(allPaths, "/v1/query") + ut.Contains(allPaths, "/") + ut.Contains(allPaths, "/swagger/") +} + +func (ut *UTServerSuite) Test_New_EnableGraphQL_False() { + cfg := new(config.Config) + cfg.EnableGraphql = false + cfg.GraphqlWebClientEnable = true + ut.testServer = New(ut.mockedStorer, ut.mockedStorerGetter, cfg, nil) + ut.NotNil(ut.testServer) + router := ut.testServer.Router() + ut.NotNil(router) + + allPaths := []string{} + err := router.Walk(func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error { + pathTemplate, err := route.GetPathTemplate() + if err != nil { + ut.FailNow(err.Error()) + } + allPaths = append(allPaths, pathTemplate) + return nil + }) + + if err != nil { + ut.FailNow(err.Error()) + } + ut.Contains(allPaths, "/download/{gitoid}") + ut.Contains(allPaths, "/upload") + ut.NotContains(allPaths, "/query") + ut.Contains(allPaths, "/v1/download/{gitoid}") + ut.Contains(allPaths, "/v1/upload") + ut.NotContains(allPaths, "/v1/query") + ut.Contains(allPaths, "/") + ut.Contains(allPaths, "/swagger/") +} + +func (ut *UTServerSuite) Test_New_GraphqlWebClientEnable_False() { + cfg := new(config.Config) + cfg.EnableGraphql = true + cfg.GraphqlWebClientEnable = false + ut.testServer = New(ut.mockedStorer, ut.mockedStorerGetter, cfg, nil) + ut.NotNil(ut.testServer) + router := ut.testServer.Router() + ut.NotNil(router) + + allPaths := []string{} + err := router.Walk(func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error { + pathTemplate, err := route.GetPathTemplate() + if err != nil { + ut.FailNow(err.Error()) + } + allPaths = append(allPaths, pathTemplate) + return nil + }) + + if err != nil { + ut.FailNow(err.Error()) + } + ut.Contains(allPaths, "/download/{gitoid}") + ut.Contains(allPaths, "/upload") + ut.Contains(allPaths, "/query") + ut.Contains(allPaths, "/v1/download/{gitoid}") + ut.Contains(allPaths, "/v1/upload") + ut.Contains(allPaths, "/v1/query") + ut.NotContains(allPaths, "/") + ut.Contains(allPaths, "/swagger/") +} + +// Mock StorerGetter.Store() +func (m *StorerGetterMock) Store(context.Context, string, []byte) error { + args := m.Called() + return args.Error(0) +} + +// Mock StorerGetter.Get() +func (m *StorerGetterMock) Get(context.Context, string) (io.ReadCloser, error) { + args := m.Called() + stringReader := strings.NewReader("testData") + stringReadCloser := io.NopCloser(stringReader) + return stringReadCloser, args.Error(0) +} + +// Mock StorerMock.Store() +func (m *StorerMock) Store(context.Context, string, []byte) error { + args := m.Called() + return args.Error(0) +} + +func (ut *UTServerSuite) Test_Upload() { + ctx := context.TODO() + r := strings.NewReader("fakeTestData") + + ut.mockedStorerGetter.On("Store").Return(nil) // mock Get() to return nil + ut.mockedStorer.On("Store").Return(nil) // mock Store() to return nil + + resp, err := ut.testServer.Upload(ctx, r) + ut.NoError(err) + ut.NotEqual("", resp.Gitoid) +} + +func (ut *UTServerSuite) Test_Upload_NoObjectStorage() { + ctx := context.TODO() + r := strings.NewReader("fakeTestData") + + ut.testServer.objectStore = nil + ut.mockedStorer.On("Store").Return(nil) // mock Store() to return nil + + resp, err := ut.testServer.Upload(ctx, r) + ut.NoError(err) + ut.NotEqual("", resp.Gitoid) +} + +func (ut *UTServerSuite) Test_Upload_FailedObjectStorage() { + ctx := context.TODO() + r := strings.NewReader("fakeTestData") + + ut.mockedStorerGetter.On("Store").Return(errors.New("Bad S3")) // mock Get() to return err + ut.mockedStorer.On("Store").Return(nil) // mock Store() to return nil + + resp, err := ut.testServer.Upload(ctx, r) + ut.ErrorContains(err, "Bad S3") + ut.Equal(api.UploadResponse{}, resp) +} + +func (ut *UTServerSuite) Test_Upload_FailedMetadatStprage() { + ctx := context.TODO() + r := strings.NewReader("fakeTestData") + + ut.mockedStorerGetter.On("Store").Return(nil) // mock Get() to return nil + ut.mockedStorer.On("Store").Return(errors.New("Bad SQL")) // mock Store() to return err + + resp, err := ut.testServer.Upload(ctx, r) + ut.ErrorContains(err, "Bad SQL") + ut.Equal(api.UploadResponse{}, resp) +} + +func (ut *UTServerSuite) Test_UploadHandler() { + + w := httptest.NewRecorder() + requestBody := []byte("fakePayload") + request := httptest.NewRequest(http.MethodPost, "/v1/upload", bytes.NewBuffer(requestBody)) + + ut.mockedStorerGetter.On("Store").Return(nil) // mock Get() to return nil + ut.mockedStorer.On("Store").Return(nil) // mock Store() to return nil + + ut.testServer.UploadHandler(w, request) + ut.Equal(http.StatusOK, w.Code) +} + +func (ut *UTServerSuite) Test_UploadHandler_WrongMethod() { + + w := httptest.NewRecorder() + requestBody := []byte("fakePayload") + request := httptest.NewRequest(http.MethodGet, "/upload", bytes.NewBuffer(requestBody)) + + ut.mockedStorerGetter.On("Store").Return(nil) // mock Get() to return nil + ut.mockedStorer.On("Store").Return(nil) // mock Store() to return nil + + ut.testServer.UploadHandler(w, request) + ut.Equal(http.StatusBadRequest, w.Code) + ut.Contains(w.Body.String(), "is an unsupported method") +} + +func (ut *UTServerSuite) Test_UploadHandler_FailureUpload() { + + w := httptest.NewRecorder() + requestBody := []byte("fakePayload") + request := httptest.NewRequest(http.MethodPost, "/upload", bytes.NewBuffer(requestBody)) + + ut.mockedStorerGetter.On("Store").Return(errors.New("BAD S3")) // mock Get() to return nil + ut.mockedStorer.On("Store").Return(nil) // mock Store() to return nil + + ut.testServer.UploadHandler(w, request) + ut.Equal(http.StatusInternalServerError, w.Code) + ut.Contains(w.Body.String(), "BAD S3") +} + +func (ut *UTServerSuite) Test_Download() { + ctx := context.TODO() + ut.mockedStorerGetter.On("Get").Return(nil) // mock Get() to return nil + + resp, err := ut.testServer.Download(ctx, "fakeGitoid") + ut.NoError(err) + data, _ := io.ReadAll(resp) + ut.Equal("testData", string(data)) +} + +func (ut *UTServerSuite) Test_Download_EmptyGitoid() { + ctx := context.TODO() + ut.mockedStorerGetter.On("Get").Return(nil) // mock Get() to return nil + + _, err := ut.testServer.Download(ctx, "") + ut.ErrorContains(err, "gitoid parameter is required") +} + +func (ut *UTServerSuite) Test_Download_EmptyGitoidTrimmed() { + ctx := context.TODO() + ut.mockedStorerGetter.On("Get").Return(nil) // mock Get() to return nil + + _, err := ut.testServer.Download(ctx, " ") + ut.ErrorContains(err, "gitoid parameter is required") +} + +func (ut *UTServerSuite) Test_Download_NoObjectStorage() { + ctx := context.TODO() + ut.testServer.objectStore = nil + + _, err := ut.testServer.Download(ctx, "fakeGitoid") + ut.ErrorContains(err, "object store unavailable") +} + +func (ut *UTServerSuite) Test_Download_ObjectStorageError() { + ctx := context.TODO() + ut.mockedStorerGetter.On("Get").Return(errors.New("BAD S3")) // mock Get() to return nil + + _, err := ut.testServer.Download(ctx, "fakeGitoid") + ut.ErrorContains(err, "BAD S3") +} + +func (ut *UTServerSuite) Test_DownloadHandler() { + w := httptest.NewRecorder() + request := httptest.NewRequest(http.MethodGet, "/v1/download", nil) + request = mux.SetURLVars(request, map[string]string{"gitoid": "fakeGitoid"}) + + ut.mockedStorerGetter.On("Get").Return(nil) // mock Get() to return nil + + ut.testServer.DownloadHandler(w, request) + ut.Equal(http.StatusOK, w.Code) + ut.Equal("testData", w.Body.String()) +} + +func (ut *UTServerSuite) Test_DownloadHandler_BadMethod() { + w := httptest.NewRecorder() + request := httptest.NewRequest(http.MethodPost, "/v1/download", nil) + request = mux.SetURLVars(request, map[string]string{"gitoid": "fakeGitoid"}) + + ut.mockedStorerGetter.On("Get").Return(nil) // mock Get() to return nil + + ut.testServer.DownloadHandler(w, request) + ut.Equal(http.StatusBadRequest, w.Code) + ut.Contains(w.Body.String(), "POST is an unsupported method") +} + +func (ut *UTServerSuite) Test_DownloadHandler_MissingGitOID() { + w := httptest.NewRecorder() + request := httptest.NewRequest(http.MethodGet, "/v1/download", nil) + + ut.mockedStorerGetter.On("Get").Return(nil) // mock Get() to return nil + + ut.testServer.DownloadHandler(w, request) + ut.Equal(http.StatusBadRequest, w.Code) + ut.Contains(w.Body.String(), "gitoid parameter is required") +} + +func (ut *UTServerSuite) Test_DownloadHandler_ObjectStorageFailed() { + w := httptest.NewRecorder() + request := httptest.NewRequest(http.MethodGet, "/v1/download", nil) + request = mux.SetURLVars(request, map[string]string{"gitoid": "fakeGitoid"}) + + ut.mockedStorerGetter.On("Get").Return(errors.New("BAD S3")) // mock Get() to return nil + + ut.testServer.DownloadHandler(w, request) + ut.Equal(http.StatusInternalServerError, w.Code) + ut.Contains(w.Body.String(), "BAD S3") +} diff --git a/pkg/api/store.go b/pkg/api/upload.go similarity index 61% rename from pkg/api/store.go rename to pkg/api/upload.go index 937b8819..94504412 100644 --- a/pkg/api/store.go +++ b/pkg/api/upload.go @@ -26,52 +26,65 @@ import ( "github.com/in-toto/go-witness/dsse" ) -type StoreResponse struct { +type UploadResponse struct { Gitoid string `json:"gitoid"` } +// 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) { buf := &bytes.Buffer{} enc := json.NewEncoder(buf) if err := enc.Encode(envelope); err != nil { return StoreResponse{}, err } - return StoreWithReader(ctx, baseUrl, buf) + return UploadWithReader(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 UploadWithReader(ctx context.Context, baseUrl string, r io.Reader) (StoreResponse, error) { uploadPath, err := url.JoinPath(baseUrl, "upload") if err != nil { - return StoreResponse{}, err + return UploadResponse{}, err } req, err := http.NewRequestWithContext(ctx, "POST", uploadPath, r) if err != nil { - return StoreResponse{}, err + return UploadResponse{}, err } req.Header.Set("Content-Type", "application/json") hc := &http.Client{} resp, err := hc.Do(req) if err != nil { - return StoreResponse{}, err + return UploadResponse{}, err } defer resp.Body.Close() bodyBytes, err := io.ReadAll(resp.Body) if err != nil { - return StoreResponse{}, err + return UploadResponse{}, err } if resp.StatusCode != http.StatusOK { - return StoreResponse{}, errors.New(string(bodyBytes)) + return UploadResponse{}, errors.New(string(bodyBytes)) } - storeResp := StoreResponse{} - if err := json.Unmarshal(bodyBytes, &storeResp); err != nil { - return StoreResponse{}, err + uploadResp := UploadResponse{} + if err := json.Unmarshal(bodyBytes, &uploadResp); err != nil { + return UploadResponse{}, err } - return storeResp, nil + return uploadResp, nil } diff --git a/pkg/api/store_test.go b/pkg/api/upload_test.go similarity index 100% rename from pkg/api/store_test.go rename to pkg/api/upload_test.go