diff --git a/.github/workflows/helm.yml b/.github/workflows/helm.yml index 25e4fba1..1074c4ca 100644 --- a/.github/workflows/helm.yml +++ b/.github/workflows/helm.yml @@ -24,17 +24,39 @@ jobs: - chart_name: crosslink-broker chart_version: "0.1.0-main.${{ github.run_number }}" chart_path: ./broker/chart + md_path: ./broker/descriptors/ModuleDescriptor-template.json - chart_name: crosslink-illmock chart_version: "0.1.0-main.${{ github.run_number }}" chart_path: ./illmock/chart steps: - uses: actions/checkout@v4 + + - name: Calculate chart and app version with short SHA + run: | + SHORT_SHA=$(echo $GITHUB_SHA | cut -c1-7) + CHART_VERSION="${{ matrix.chart_base_version }}+sha.$SHORT_SHA" + APP_VERSION="sha-$SHORT_SHA" + echo "CHART_VERSION=$CHART_VERSION" >> $GITHUB_ENV + echo "APP_VERSION=$APP_VERSION" >> $GITHUB_ENV + + - name: Process MD for okapi-hooks + if: ${{ matrix.md_path }} + run: | + sed -i "s/@version@/$CHART_VERSION/" ${{ matrix.md_path }} + sed 's/^/ /' ${{ matrix.md_path }} > indented.json + sed -i -e '/@descriptor@/{ + r indented.json + d + }' ${{ matrix.chart_path }}/values.yaml - name: helm lint run: | helm lint ${{ matrix.chart_path }} - name: helm login run: | echo ${{ secrets.GITHUB_TOKEN }} | helm registry login ghcr.io -u $ --password-stdin + - name: helm dependency + run: | + helm dependency build ${{ matrix.chart_path }} - name: helm package run: | helm package ${{ matrix.chart_path }} --version "${{ matrix.chart_version }}+sha.$(echo $GITHUB_SHA | cut -c1-7)" --app-version "sha-$(echo $GITHUB_SHA | cut -c1-7)" diff --git a/broker/README.md b/broker/README.md index 57c6bc4f..b5f56163 100644 --- a/broker/README.md +++ b/broker/README.md @@ -15,6 +15,8 @@ The broker's API uses hyperlinks to connect JSON resources. If you're using Chrome or another browser to explore the API, consider using an extension like [JSON Formatter](https://chromewebstore.google.com/detail/json-formatter/bcjindcccaagfpapjjmafapmmgkkhgoa) which allows to easily navigate hyperlinked JSON. +Note that API is also available with base path `/broker` if env +`TENANT_TO_SYMBOL` is defined. # Configuration @@ -39,6 +41,9 @@ Configuration is provided via environment variables: | DIRECTORY_API_URL | Comma separated list of URLs when DIRECTORY_ADAPTER is `api` | `http://localhost:8081/directory/entries` | | BROKER_MODE | Should broker forward supplier/requester symbols: `opaque` or `transparent` | `opaque` | | LOCAL_SUPPLY | Should we check if requester can supply item: `true` or `false` | `false` | +| TENANT_TO_SYMBOL | Limits results to include only transactions with `requesterSymbol`matching | `` | +| | TENANT_TO_SYMBOL with {tenant} being replaced by X-Okapi-Tenant value | | + # Build diff --git a/broker/api/api-handler.go b/broker/api/api-handler.go index b5250759..092d8f12 100644 --- a/broker/api/api-handler.go +++ b/broker/api/api-handler.go @@ -5,12 +5,13 @@ import ( "encoding/json" "errors" "fmt" - "github.com/indexdata/go-utils/utils" "net/http" "reflect" "strings" "time" + "github.com/indexdata/go-utils/utils" + "github.com/google/uuid" icql "github.com/indexdata/cql-go/cql" extctx "github.com/indexdata/crosslink/broker/common" @@ -27,15 +28,32 @@ var PEERS_PATH = "/peers" var ILL_TRANSACTION_QUERY = "ill_transaction_id=" type ApiHandler struct { - eventRepo events.EventRepo - illRepo ill_db.IllRepo + eventRepo events.EventRepo + illRepo ill_db.IllRepo + tenantToSymbol string // non-empty if in /broker mode } -func NewApiHandler(eventRepo events.EventRepo, illRepo ill_db.IllRepo) ApiHandler { +func NewApiHandler(eventRepo events.EventRepo, illRepo ill_db.IllRepo, tenentToSymbol string) ApiHandler { return ApiHandler{ - eventRepo: eventRepo, - illRepo: illRepo, + eventRepo: eventRepo, + illRepo: illRepo, + tenantToSymbol: tenentToSymbol, + } +} + +func (a *ApiHandler) TenantFilter(trans *ill_db.IllTransaction, tenant *string, requesterSymbol *string) bool { + if tenant == nil && requesterSymbol != nil { + return trans.RequesterSymbol.String == *requesterSymbol + } + if a.tenantToSymbol == "" { + return true + } + // this is the /broker mode + if tenant == nil { + return false } + full := strings.ReplaceAll(a.tenantToSymbol, "{tenant}", strings.ToUpper(*tenant)) + return trans.RequesterSymbol.String == full } func (a *ApiHandler) GetEvents(w http.ResponseWriter, r *http.Request, params oapi.GetEventsParams) { @@ -46,18 +64,35 @@ func (a *ApiHandler) GetEvents(w http.ResponseWriter, r *http.Request, params oa ctx := extctx.CreateExtCtxWithArgs(context.Background(), &extctx.LoggerArgs{ Other: logParams, }) - resp := []oapi.Event{} var eventList []events.Event var err error - if params.IllTransactionId != nil { - eventList, err = a.eventRepo.GetIllTransactionEvents(ctx, *params.IllTransactionId) - } else { + if params.RequesterReqId != nil { + var tran ill_db.IllTransaction + tran, err = a.illRepo.GetIllTransactionByRequesterRequestId(ctx, pgtype.Text{ + String: *params.RequesterReqId, + Valid: true, + }) + if err == nil && a.TenantFilter(&tran, params.XOkapiTenant, params.RequesterSymbol) { + eventList, err = a.eventRepo.GetIllTransactionEvents(ctx, tran.ID) + } + } else if params.IllTransactionId != nil { + var tran ill_db.IllTransaction + tran, err = a.illRepo.GetIllTransactionById(ctx, *params.IllTransactionId) + if err == nil && a.TenantFilter(&tran, params.XOkapiTenant, params.RequesterSymbol) { + eventList, err = a.eventRepo.GetIllTransactionEvents(ctx, tran.ID) + } + } else if a.tenantToSymbol == "" { eventList, err = a.eventRepo.ListEvents(ctx) } - if err != nil { + if err != nil && !errors.Is(err, pgx.ErrNoRows) { addInternalError(ctx, w, err) return } + if len(eventList) == 0 && a.tenantToSymbol != "" { + addForbiddenError(ctx, w) + return + } + resp := []oapi.Event{} for _, event := range eventList { resp = append(resp, toApiEvent(event)) } @@ -78,33 +113,43 @@ func (a *ApiHandler) GetIllTransactions(w http.ResponseWriter, r *http.Request, addInternalError(ctx, w, err) return } - resp = append(resp, toApiIllTransaction(r, tran)) - } else { + if a.TenantFilter(&tran, params.XOkapiTenant, params.RequesterSymbol) { + resp = append(resp, toApiIllTransaction(r, tran)) + } + } else if a.tenantToSymbol == "" { trans, err := a.illRepo.ListIllTransactions(ctx) if err != nil { addInternalError(ctx, w, err) return } for _, t := range trans { - resp = append(resp, toApiIllTransaction(r, t)) + if a.TenantFilter(&t, params.XOkapiTenant, params.RequesterSymbol) { + resp = append(resp, toApiIllTransaction(r, t)) + } } } + if len(resp) == 0 && a.tenantToSymbol != "" { + addForbiddenError(ctx, w) + return + } writeJsonResponse(w, resp) } -func (a *ApiHandler) GetIllTransactionsId(w http.ResponseWriter, r *http.Request, id string) { +func (a *ApiHandler) GetIllTransactionsId(w http.ResponseWriter, r *http.Request, id string, params oapi.GetIllTransactionsIdParams) { ctx := extctx.CreateExtCtxWithArgs(context.Background(), &extctx.LoggerArgs{ Other: map[string]string{"method": "GetIllTransactionsId", "id": id}, }) trans, err := a.illRepo.GetIllTransactionById(ctx, id) - if err != nil { - if errors.Is(err, pgx.ErrNoRows) { - addNotFoundError(w) - return - } else { - addInternalError(ctx, w, err) + if err != nil && !errors.Is(err, pgx.ErrNoRows) { + addInternalError(ctx, w, err) + return + } + if err != nil || !a.TenantFilter(&trans, params.XOkapiTenant, params.RequesterSymbol) { + if a.tenantToSymbol != "" { + addForbiddenError(ctx, w) return } + addNotFoundError(w) } writeJsonResponse(w, toApiIllTransaction(r, trans)) } @@ -275,7 +320,7 @@ func (a *ApiHandler) PostPeers(w http.ResponseWriter, r *http.Request) { return } for _, s := range newPeer.Symbols { - if s == "" || !strings.Contains(s, ":") { + if !strings.Contains(s, ":") { addBadRequestError(ctx, w, fmt.Errorf("symbol should be in \"ISIL:SYMBOL\" format but got %v", s)) return } @@ -450,20 +495,37 @@ func (a *ApiHandler) GetLocatedSuppliers(w http.ResponseWriter, r *http.Request, ctx := extctx.CreateExtCtxWithArgs(context.Background(), &extctx.LoggerArgs{ Other: logParams, }) - resp := []oapi.LocatedSupplier{} var supList []ill_db.LocatedSupplier var err error - if params.IllTransactionId != nil { - supList, err = a.illRepo.GetLocatedSupplierByIllTransition(ctx, *params.IllTransactionId) - } else { + if params.RequesterReqId != nil { + var tran ill_db.IllTransaction + tran, err = a.illRepo.GetIllTransactionByRequesterRequestId(ctx, pgtype.Text{ + String: *params.RequesterReqId, + Valid: true, + }) + if err == nil && a.TenantFilter(&tran, params.XOkapiTenant, params.RequesterSymbol) { + supList, err = a.illRepo.GetLocatedSupplierByIllTransition(ctx, tran.ID) + } + } else if params.IllTransactionId != nil { + var tran ill_db.IllTransaction + tran, err = a.illRepo.GetIllTransactionById(ctx, *params.IllTransactionId) + if err == nil && a.TenantFilter(&tran, params.XOkapiTenant, params.RequesterSymbol) { + supList, err = a.illRepo.GetLocatedSupplierByIllTransition(ctx, *params.IllTransactionId) + } + } else if a.tenantToSymbol == "" { supList, err = a.illRepo.ListLocatedSuppliers(ctx) } - if err != nil { + if err != nil && !errors.Is(err, pgx.ErrNoRows) { addInternalError(ctx, w, err) return } - for _, event := range supList { - resp = append(resp, toApiLocatedSupplier(r, event)) + if len(supList) == 0 && a.tenantToSymbol != "" { + addForbiddenError(ctx, w) + return + } + resp := []oapi.LocatedSupplier{} + for _, supplier := range supList { + resp = append(resp, toApiLocatedSupplier(r, supplier)) } writeJsonResponse(w, resp) } @@ -500,6 +562,16 @@ func addInternalError(ctx extctx.ExtendedContext, w http.ResponseWriter, err err _ = json.NewEncoder(w).Encode(resp) } +func addForbiddenError(ctx extctx.ExtendedContext, w http.ResponseWriter) { + resp := ErrorMessage{ + Error: "forbidden", + } + ctx.Logger().Error("error serving api request", "error", "forbidden") + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusForbidden) + _ = json.NewEncoder(w).Encode(resp) +} + func addBadRequestError(ctx extctx.ExtendedContext, w http.ResponseWriter, err error) { resp := ErrorMessage{ Error: err.Error(), @@ -690,6 +762,9 @@ func toLink(r *http.Request, path string, id string, query string) string { if strings.Contains(urlHost, "localhost") { urlScheme = "http" } + if strings.Contains(r.RequestURI, "/broker/") { + path = "/broker" + path + } if id != "" { path = path + "/" + id } diff --git a/broker/api/api-handler_test.go b/broker/api/api-handler_test.go index b060b18c..6f9dfd63 100644 --- a/broker/api/api-handler_test.go +++ b/broker/api/api-handler_test.go @@ -1,10 +1,11 @@ package api import ( - "github.com/indexdata/cql-go/cql" - "github.com/stretchr/testify/assert" "strings" "testing" + + "github.com/indexdata/cql-go/cql" + "github.com/stretchr/testify/assert" ) func TestMatchQueries(t *testing.T) { diff --git a/broker/app/app.go b/broker/app/app.go index 3dc226f2..7e4cf793 100644 --- a/broker/app/app.go +++ b/broker/app/app.go @@ -52,6 +52,7 @@ var MAX_MESSAGE_SIZE, _ = utils.GetEnvAny("MAX_MESSAGE_SIZE", int(100*1024), fun }) var BROKER_MODE = utils.GetEnv("BROKER_MODE", "opaque") var LOCAL_SUPPLY = utils.Must(utils.GetEnvBool("LOCAL_SUPPLY", false)) +var TENANT_TO_SYMBOL = os.Getenv("TENANT_TO_SYMBOL") var appCtx = extctx.CreateExtCtxWithLogArgsAndHandler(context.Background(), nil, configLog()) @@ -128,8 +129,12 @@ func StartServer(context Context) error { http.ServeFile(w, r, "handler/open-api.yaml") }) - apiHandler := api.NewApiHandler(context.EventRepo, context.IllRepo) + apiHandler := api.NewApiHandler(context.EventRepo, context.IllRepo, "") oapi.HandlerFromMux(&apiHandler, mux) + if TENANT_TO_SYMBOL != "" { + apiHandler := api.NewApiHandler(context.EventRepo, context.IllRepo, TENANT_TO_SYMBOL) + oapi.HandlerFromMuxWithBaseURL(&apiHandler, mux, "/broker") + } appCtx.Logger().Info("Server started on http://localhost:" + strconv.Itoa(HTTP_PORT)) return http.ListenAndServe(":"+strconv.Itoa(HTTP_PORT), mux) diff --git a/broker/chart/Chart.yaml b/broker/chart/Chart.yaml index a55ddb73..08a0c523 100644 --- a/broker/chart/Chart.yaml +++ b/broker/chart/Chart.yaml @@ -8,3 +8,8 @@ version: 0.1.0 # appVersion is used as the Docker image tag by the chart appVersion: "main" + +dependencies: + - name: okapi-hooks + repository: oci://ghcr.io/indexdata/charts + version: ">0.1.0-0" diff --git a/broker/chart/values.yaml b/broker/chart/values.yaml index 454c12bd..d4fad53c 100644 --- a/broker/chart/values.yaml +++ b/broker/chart/values.yaml @@ -13,6 +13,11 @@ image: # Overrides the image tag whose default is the chart appVersion. tag: "" +okapi-hooks: + moduleUrl: http://crosslink-broker:80 + moduleDescriptor: | + @descriptor@ + containerPort: 8080 # envvars passed to the container env: {} diff --git a/broker/descriptors/ModuleDescriptor-template.json b/broker/descriptors/ModuleDescriptor-template.json new file mode 100644 index 00000000..bcde3946 --- /dev/null +++ b/broker/descriptors/ModuleDescriptor-template.json @@ -0,0 +1,99 @@ +{ + "id": "mod-broker-@version@", + "name": "crosslink broker", + "provides": [ + { + "id": "crosslink-broker", + "version": "1.0", + "handlers": [ + { + "methods": [ + "GET" + ], + "pathPattern": "/broker/ill_transactions/{id}", + "permissionsRequired": [ + "broker.ill_transactions.item.get" + ] + }, + { + "methods": [ + "GET" + ], + "pathPattern": "/broker/ill_transactions", + "permissionsRequired": [ + "broker.ill_transactions.get" + ] + }, + { + "methods": [ + "GET" + ], + "pathPattern": "/broker/located_suppliers", + "permissionsRequired": [ + "broker.located_suppliers.get" + ] + }, + { + "methods": [ + "GET" + ], + "pathPattern": "/broker/events", + "permissionsRequired": [ + "broker.events.get" + ] + }, + { + "methods": [ + "GET" + ], + "pathPattern": "/broker/peers/{id}", + "permissionsRequired": [ + "broker.peers.item.get" + ] + }, + { + "methods": [ + "GET" + ], + "pathPattern": "/broker/peers", + "permissionsRequired": [ + "broker.peers.get" + ] + } + ] + } + ], + "requires": [], + "permissionSets": [ + { + "description": "Read ILL transaction", + "displayName": "Broker - read ILL transaction", + "permissionName": "broker.ill_transactions.item.get" + }, + { + "description": "Read ILL transactions", + "displayName": "Broker - read ILL transactions", + "permissionName": "broker.ill_transactions.get" + }, + { + "description": "Read located suppliers", + "displayName": "Broker - read located suppliers", + "permissionName": "broker.located_suppliers.get" + }, + { + "description": "Read events", + "displayName": "Broker - read events", + "permissionName": "broker.events.get" + }, + { + "description": "Read peer", + "displayName": "Broker - read peer", + "permissionName": "broker.peers.item.get" + }, + { + "description": "Read peers", + "displayName": "Broker - read peers", + "permissionName": "broker.peers.get" + } + ] +} diff --git a/broker/oapi/open-api.yaml b/broker/oapi/open-api.yaml index 273ed42a..83f828cb 100644 --- a/broker/oapi/open-api.yaml +++ b/broker/oapi/open-api.yaml @@ -5,6 +5,33 @@ info: description: API for retrieving broker data components: + parameters: + Tenant: + name: X-Okapi-Tenant + in: header + description: Okapi Tenant + required: false + schema: + type: string + pattern: '^[_a-z][_a-z0-9]*$' + RequesterRequestId: + name: requester_req_id + in: query + description: Filter Ill transactions by requester request id + schema: + type: string + IllTransactionId: + name: ill_transaction_id + in: query + schema: + type: string + description: Filter by ILL transaction ID + RequesterSymbol: + name: requester_symbol + in: query + schema: + type: string + description: Filter by requester symbol schemas: Event: type: object @@ -222,11 +249,10 @@ paths: get: summary: Retrieve events parameters: - - in: query - name: ill_transaction_id - schema: - type: string - description: Filter events by ILL transaction ID + - $ref: '#/components/parameters/Tenant' + - $ref: '#/components/parameters/RequesterRequestId' + - $ref: '#/components/parameters/IllTransactionId' + - $ref: '#/components/parameters/RequesterSymbol' responses: '200': description: Successful retrieval of events @@ -246,6 +272,16 @@ paths: error: type: string description: Error message + '403': # Example error response + description: Forbidden. Invalid tenant. + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message '500': # Example error response description: Internal Server Error content: @@ -260,6 +296,8 @@ paths: get: summary: Get an ILL transaction by ID parameters: + - $ref: '#/components/parameters/Tenant' + - $ref: '#/components/parameters/RequesterSymbol' - in: path name: id schema: @@ -283,6 +321,16 @@ paths: error: type: string description: Error message + '403': # Example error response + description: Forbidden. Invalid tenant. + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message '500': # Example error response description: Internal Server Error content: @@ -313,11 +361,9 @@ paths: get: summary: Get all ILL transactions parameters: - - in: query - name: requester_req_id - schema: - type: string - description: Filter Ill transactions by requester request id + - $ref: '#/components/parameters/Tenant' + - $ref: '#/components/parameters/RequesterRequestId' + - $ref: '#/components/parameters/RequesterSymbol' responses: '200': description: Successful retrieval of the ILL transaction @@ -337,6 +383,16 @@ paths: error: type: string description: Error message + '403': # Example error response + description: Forbidden. Invalid tenant. + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message '500': # Example error response description: Internal Server Error content: @@ -446,11 +502,10 @@ paths: get: summary: Retrieve located suppliers parameters: - - in: query - name: ill_transaction_id - schema: - type: string - description: Filter located suppliers by ILL transaction ID + - $ref: '#/components/parameters/Tenant' + - $ref: '#/components/parameters/RequesterRequestId' + - $ref: '#/components/parameters/IllTransactionId' + - $ref: '#/components/parameters/RequesterSymbol' responses: '200': description: Successful retrieval of located suppliers @@ -470,6 +525,16 @@ paths: error: type: string description: Error message + '403': # Example error response + description: Forbidden. Invalid tenant. + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message '500': description: Internal Server Error content: diff --git a/broker/test/api/api-handler_test.go b/broker/test/api/api-handler_test.go index 33225d3a..e16f45de 100644 --- a/broker/test/api/api-handler_test.go +++ b/broker/test/api/api-handler_test.go @@ -4,9 +4,6 @@ import ( "bytes" "context" "encoding/json" - extctx "github.com/indexdata/crosslink/broker/common" - "github.com/indexdata/crosslink/iso18626" - "github.com/jackc/pgx/v5/pgtype" "io" "net/http" "net/http/httptest" @@ -16,6 +13,11 @@ import ( "testing" "time" + extctx "github.com/indexdata/crosslink/broker/common" + "github.com/indexdata/crosslink/iso18626" + "github.com/jackc/pgx/v5/pgtype" + "github.com/stretchr/testify/assert" + "github.com/google/uuid" "github.com/indexdata/crosslink/broker/api" "github.com/indexdata/crosslink/broker/app" @@ -34,9 +36,10 @@ var illRepo ill_db.IllRepo var eventRepo events.EventRepo var mockIllRepoError = new(test.MockIllRepositoryError) var mockEventRepoError = new(test.MockEventRepositoryError) -var handlerMock = api.NewApiHandler(mockEventRepoError, mockIllRepoError) +var handlerMock = api.NewApiHandler(mockEventRepoError, mockIllRepoError, "") func TestMain(m *testing.M) { + app.TENANT_TO_SYMBOL = "ISIL:DK-{tenant}" ctx := context.Background() pgContainer, err := postgres.Run(ctx, "postgres", @@ -144,20 +147,12 @@ func TestGetIllTransactionsId(t *testing.T) { if resp.ID != illId { t.Errorf("did not find the same ILL transaction") } + assert.Equal(t, getLocalhostWithPort()+"/events?ill_transaction_id="+url.PathEscape(illId), resp.EventsLink) + assert.Equal(t, getLocalhostWithPort()+"/located_suppliers?ill_transaction_id="+url.PathEscape(illId), resp.LocatedSuppliersLink) + // Delete peer - req, err := http.NewRequest("DELETE", getLocalhostWithPort()+"/ill_transactions/"+illId, nil) - if err != nil { - t.Errorf("Error creating delete transaction request: %s", err) - } - req.Header.Set("Content-Type", "application/json") - client := &http.Client{} - delResp, err := client.Do(req) - if err != nil { - t.Errorf("Error deleting peer request: %s", err) - } - if delResp.StatusCode != http.StatusNoContent { - t.Errorf("Expected response 204 got %d", delResp.StatusCode) - } + httpRequest(t, "DELETE", "/ill_transactions/"+illId, nil, "", http.StatusNoContent) + httpRequest(t, "DELETE", "/ill_transactions/"+illId, nil, "", http.StatusNotFound) } func TestGetLocatedSuppliers(t *testing.T) { @@ -199,6 +194,100 @@ func TestGetLocatedSuppliers(t *testing.T) { } } +func TestBrokerCRUD(t *testing.T) { + // app.TENANT_TO_SYMBOL = "ISIL:DK-{tenant}" + illId := uuid.New().String() + reqReqId := uuid.New().String() + _, err := illRepo.SaveIllTransaction(extctx.CreateExtCtxWithArgs(context.Background(), nil), ill_db.SaveIllTransactionParams{ + ID: illId, + RequesterSymbol: pgtype.Text{ + String: "ISIL:DK-DIKU", + Valid: true, + }, + RequesterRequestID: pgtype.Text{ + String: reqReqId, + Valid: true, + }, + Timestamp: test.GetNow(), + }) + assert.NoError(t, err) + + body := httpGet(t, "/broker/ill_transactions/"+illId, "diku", http.StatusOK) + var tran oapi.IllTransaction + err = json.Unmarshal(body, &tran) + assert.NoError(t, err) + assert.Equal(t, illId, tran.ID) + assert.Equal(t, getLocalhostWithPort()+"/broker/events?ill_transaction_id="+url.PathEscape(illId), tran.EventsLink) + assert.Equal(t, getLocalhostWithPort()+"/broker/located_suppliers?ill_transaction_id="+url.PathEscape(illId), tran.LocatedSuppliersLink) + + httpGet(t, "/broker/ill_transactions/"+illId+"?requester_symbol="+url.QueryEscape("ISIL:DK-DIKU"), "ruc", http.StatusForbidden) + + httpGet(t, "/broker/ill_transactions/"+illId, "ruc", http.StatusForbidden) + + httpGet(t, "/broker/ill_transactions/"+illId, "", http.StatusForbidden) + + body = httpGet(t, "/broker/ill_transactions/"+illId+"?requester_symbol="+url.QueryEscape("ISIL:DK-DIKU"), "", http.StatusOK) + err = json.Unmarshal(body, &tran) + assert.NoError(t, err) + assert.Equal(t, illId, tran.ID) + + httpGet(t, "/broker/ill_transactions", "diku", http.StatusForbidden) + + httpGet(t, "/broker/ill_transactions", "ruc", http.StatusForbidden) + + body = httpGet(t, "/broker/ill_transactions?requester_req_id="+url.QueryEscape(reqReqId), "diku", http.StatusOK) + var trans []oapi.IllTransaction + err = json.Unmarshal(body, &trans) + assert.NoError(t, err) + assert.Len(t, trans, 1) + assert.Equal(t, illId, trans[0].ID) + + peer := test.CreatePeer(t, illRepo, "ISIL:LOC_OTHER", "") + locSup := test.CreateLocatedSupplier(t, illRepo, illId, peer.ID, "ISIL:LOC_OTHER", string(iso18626.TypeStatusLoaned)) + + body = httpGet(t, "/broker/located_suppliers?requester_req_id="+url.QueryEscape(reqReqId), "diku", http.StatusOK) + var supps []oapi.LocatedSupplier + err = json.Unmarshal(body, &supps) + assert.NoError(t, err) + assert.Len(t, supps, 1) + assert.Equal(t, locSup.ID, supps[0].ID) + + body = httpGet(t, "/broker/located_suppliers?ill_transaction_id="+url.QueryEscape(illId), "diku", http.StatusOK) + err = json.Unmarshal(body, &supps) + assert.NoError(t, err) + assert.Len(t, supps, 1) + assert.Equal(t, locSup.ID, supps[0].ID) + + httpGet(t, "/broker/located_suppliers?requester_req_id="+url.QueryEscape(reqReqId), "ruc", http.StatusForbidden) + + httpGet(t, "/broker/located_suppliers?requester_req_id="+url.QueryEscape(uuid.NewString()), "diku", http.StatusForbidden) + + eventId := test.GetEventId(t, eventRepo, illId, events.EventTypeNotice, events.EventStatusSuccess, events.EventNameMessageRequester) + + body = httpGet(t, "/broker/events?requester_req_id="+url.QueryEscape(reqReqId), "diku", http.StatusOK) + var events []oapi.Event + err = json.Unmarshal(body, &events) + assert.NoError(t, err) + assert.Len(t, events, 1) + assert.Equal(t, eventId, events[0].ID) + + body = httpGet(t, "/broker/events?requester_req_id="+url.QueryEscape(reqReqId)+"&requester_symbol="+url.QueryEscape("ISIL:DK-DIKU"), "", http.StatusOK) + err = json.Unmarshal(body, &events) + assert.NoError(t, err) + assert.Len(t, events, 1) + assert.Equal(t, eventId, events[0].ID) + + body = httpGet(t, "/broker/events?ill_transaction_id="+url.QueryEscape(illId), "diku", http.StatusOK) + err = json.Unmarshal(body, &events) + assert.NoError(t, err) + assert.Len(t, events, 1) + assert.Equal(t, eventId, events[0].ID) + + httpGet(t, "/broker/events?requester_req_id="+url.QueryEscape(reqReqId), "ruc", http.StatusForbidden) + + httpGet(t, "/broker/events?requester_req_id="+url.QueryEscape(uuid.NewString()), "diku", http.StatusForbidden) +} + func TestPeersCRUD(t *testing.T) { // Create peer toCreate := oapi.Peer{ @@ -212,23 +301,7 @@ func TestPeersCRUD(t *testing.T) { if err != nil { t.Errorf("Error marshaling JSON: %s", err) } - req, err := http.NewRequest("POST", getLocalhostWithPort()+"/peers", bytes.NewBuffer(jsonBytes)) - if err != nil { - t.Errorf("Error creating post peer request: %s", err) - } - req.Header.Set("Content-Type", "application/json") - client := &http.Client{} - resp, err := client.Do(req) - if err != nil { - t.Errorf("Error posting peer request: %s", err) - } - body, err := io.ReadAll(resp.Body) - if err != nil { - t.Errorf("Error reading response body: %s", err) - } - if resp.StatusCode != http.StatusCreated { - t.Errorf("Expected response 201 got %d", resp.StatusCode) - } + body := httpRequest(t, "POST", "/peers", jsonBytes, "", http.StatusCreated) var respPeer oapi.Peer err = json.Unmarshal(body, &respPeer) if err != nil { @@ -238,35 +311,16 @@ func TestPeersCRUD(t *testing.T) { t.Errorf("expected same peer %s got %s", toCreate.ID, respPeer.ID) } // Cannot post same again - resp, err = client.Do(req) - if err != nil { - t.Errorf("Error posting peer request: %s", err) - } - if resp.StatusCode != http.StatusBadRequest { - t.Errorf("Expected response 400 got %d", resp.StatusCode) - } + httpRequest(t, "POST", "/peers", jsonBytes, "", http.StatusBadRequest) + // Update peer toCreate.Name = "Updated" jsonBytes, err = json.Marshal(toCreate) if err != nil { t.Errorf("Error marshaling JSON: %s", err) } - req, err = http.NewRequest("PUT", getLocalhostWithPort()+"/peers/"+toCreate.ID, bytes.NewBuffer(jsonBytes)) - if err != nil { - t.Errorf("Error creating put peer request: %s", err) - } - req.Header.Set("Content-Type", "application/json") - resp, err = client.Do(req) - if err != nil { - t.Errorf("Error putting peer request: %s", err) - } - body, err = io.ReadAll(resp.Body) - if err != nil { - t.Errorf("Error reading response body: %s", err) - } - if resp.StatusCode != http.StatusOK { - t.Errorf("Expected response 200 got %d", resp.StatusCode) - } + body = httpRequest(t, "PUT", "/peers/"+toCreate.ID, jsonBytes, "", http.StatusOK) + err = json.Unmarshal(body, &respPeer) if err != nil { t.Errorf("Failed to unmarshal json: %s", err) @@ -298,18 +352,9 @@ func TestPeersCRUD(t *testing.T) { t.Errorf("expected same peer %s got %s", toCreate.ID, respPeers[0].ID) } // Delete peer - req, err = http.NewRequest("DELETE", getLocalhostWithPort()+"/peers/"+toCreate.ID, nil) - if err != nil { - t.Errorf("Error creating delete peer request: %s", err) - } - req.Header.Set("Content-Type", "application/json") - resp, err = client.Do(req) - if err != nil { - t.Errorf("Error deleting peer request: %s", err) - } - if resp.StatusCode != http.StatusNoContent { - t.Errorf("Expected response 204 got %d", resp.StatusCode) - } + httpRequest(t, "DELETE", "/peers/"+toCreate.ID, nil, "", http.StatusNoContent) + httpRequest(t, "DELETE", "/peers/"+toCreate.ID, nil, "", http.StatusNotFound) + // Check no peers left respPeers = getPeers(t) for _, p := range respPeers { @@ -348,30 +393,7 @@ func TestNotFound(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if tt.method == "GET" { - resp, err := http.Get(getLocalhostWithPort() + tt.endpoint) - if err != nil { - t.Errorf("Error making GET request: %s", err) - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusNotFound { - t.Errorf("Expected response 404 got %d", resp.StatusCode) - } - } else { - req, err := http.NewRequest(tt.method, getLocalhostWithPort()+tt.endpoint, nil) - if err != nil { - t.Errorf("Error creating post peer request: %s", err) - } - req.Header.Set("Content-Type", "application/json") - client := &http.Client{} - resp, err := client.Do(req) - if err != nil { - t.Errorf("Error doing peer request: %s", err) - } - if resp.StatusCode != http.StatusNotFound { - t.Errorf("Expected response 404 got %d", resp.StatusCode) - } - } + httpRequest(t, tt.method, tt.endpoint, nil, "", http.StatusNotFound) }) } } @@ -396,7 +418,7 @@ func TestGetIllTransactionsDbError(t *testing.T) { func TestGetIllTransactionsIdDbError(t *testing.T) { req, _ := http.NewRequest("GET", "/", nil) rr := httptest.NewRecorder() - handlerMock.GetIllTransactionsId(rr, req, "id") + handlerMock.GetIllTransactionsId(rr, req, "id", oapi.GetIllTransactionsIdParams{}) if status := rr.Code; status != http.StatusInternalServerError { t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusInternalServerError) @@ -499,22 +521,32 @@ func getPeerById(t *testing.T, symbol string) oapi.Peer { } func getResponseBody(t *testing.T, endpoint string) []byte { - resp, err := http.Get(getLocalhostWithPort() + endpoint) - if err != nil { - t.Errorf("Error making GET request: %s", err) - } - defer resp.Body.Close() + return httpGet(t, endpoint, "", http.StatusOK) +} - body, err := io.ReadAll(resp.Body) - if err != nil { - t.Errorf("Error reading response body: %s", err) - } - if resp.StatusCode != http.StatusOK { - t.Errorf("Expected response 200 got %d", resp.StatusCode) - } +func httpRequest(t *testing.T, method string, uriPath string, reqbytes []byte, tenant string, expectStatus int) []byte { + client := http.DefaultClient + hreq, err := http.NewRequest(method, getLocalhostWithPort()+uriPath, bytes.NewBuffer(reqbytes)) + assert.NoError(t, err) + if tenant != "" { + hreq.Header.Set("X-Okapi-Tenant", tenant) + } + if method == "POST" || method == "PUT" { + hreq.Header.Set("Content-Type", "application/json") + } + hres, err := client.Do(hreq) + assert.NoError(t, err) + defer hres.Body.Close() + assert.Equal(t, expectStatus, hres.StatusCode) + body, err := io.ReadAll(hres.Body) + assert.NoError(t, err) return body } +func httpGet(t *testing.T, uriPath string, tenant string, expectStatus int) []byte { + return httpRequest(t, "GET", uriPath, nil, tenant, expectStatus) +} + func getLocalhostWithPort() string { return "http://localhost:" + strconv.Itoa(app.HTTP_PORT) }