diff --git a/broker/api/api-handler.go b/broker/api/api-handler.go index 20dc0851..c86389a7 100644 --- a/broker/api/api-handler.go +++ b/broker/api/api-handler.go @@ -38,40 +38,32 @@ var LIMIT_DEFAULT int32 = 10 var ARCHIVE_PROCESS_STARTED = "Archive process started" type ApiHandler struct { - limitDefault int32 - eventRepo events.EventRepo - illRepo ill_db.IllRepo - tenantToSymbol string // non-empty if in /broker mode + limitDefault int32 + eventRepo events.EventRepo + illRepo ill_db.IllRepo + tenant common.Tenant } -func NewApiHandler(eventRepo events.EventRepo, illRepo ill_db.IllRepo, tenentToSymbol string, limitDefault int32) ApiHandler { +func NewApiHandler(eventRepo events.EventRepo, illRepo ill_db.IllRepo, tenant common.Tenant, limitDefault int32) ApiHandler { return ApiHandler{ - eventRepo: eventRepo, - illRepo: illRepo, - tenantToSymbol: tenentToSymbol, - limitDefault: limitDefault, + eventRepo: eventRepo, + illRepo: illRepo, + tenant: tenant, + limitDefault: limitDefault, } } -func (a *ApiHandler) isTenantMode() bool { - return a.tenantToSymbol != "" -} - -func (a *ApiHandler) getSymbolFromTenant(tenant string) string { - return strings.ReplaceAll(a.tenantToSymbol, "{tenant}", strings.ToUpper(tenant)) -} - func (a *ApiHandler) isOwner(trans *ill_db.IllTransaction, tenant *string, requesterSymbol *string) bool { if tenant == nil && requesterSymbol != nil { return trans.RequesterSymbol.String == *requesterSymbol } - if !a.isTenantMode() { + if !a.tenant.IsSpecified() { return true } if tenant == nil { return false } - return trans.RequesterSymbol.String == a.getSymbolFromTenant(*tenant) + return trans.RequesterSymbol.String == a.tenant.GetSymbol(*tenant) } func (a *ApiHandler) getIllTranFromParams(ctx common.ExtendedContext, w http.ResponseWriter, @@ -176,14 +168,14 @@ func (a *ApiHandler) GetIllTransactions(w http.ResponseWriter, r *http.Request, fullCount = 1 resp.Items = append(resp.Items, toApiIllTransaction(r, *tran)) } - } else if a.isTenantMode() { - var tenantSymbol string + } else if a.tenant.IsSpecified() { + var symbol string if params.XOkapiTenant != nil { - tenantSymbol = a.getSymbolFromTenant(*params.XOkapiTenant) + symbol = a.tenant.GetSymbol(*params.XOkapiTenant) } else if params.RequesterSymbol != nil { - tenantSymbol = *params.RequesterSymbol + symbol = *params.RequesterSymbol } - if tenantSymbol == "" { + if symbol == "" { writeJsonResponse(w, resp) return } @@ -191,7 +183,7 @@ func (a *ApiHandler) GetIllTransactions(w http.ResponseWriter, r *http.Request, Limit: limit, Offset: offset, RequesterSymbol: pgtype.Text{ - String: tenantSymbol, + String: symbol, Valid: true, }, } diff --git a/broker/app/app.go b/broker/app/app.go index 42fd3ec4..0510a59f 100644 --- a/broker/app/app.go +++ b/broker/app/app.go @@ -36,6 +36,8 @@ import ( "github.com/indexdata/crosslink/broker/ill_db" "github.com/indexdata/go-utils/utils" "github.com/jackc/pgx/v5/pgxpool" + + "github.com/indexdata/crosslink/broker/lms" ) var HTTP_PORT = utils.Must(utils.GetEnvInt("HTTP_PORT", 8081)) @@ -157,8 +159,9 @@ func Init(ctx context.Context) (Context, error) { iso18626Handler := handler.CreateIso18626Handler(eventBus, eventRepo, illRepo, dirAdapter) supplierLocator := service.CreateSupplierLocator(eventBus, illRepo, dirAdapter, holdingsAdapter) workflowManager := service.CreateWorkflowManager(eventBus, illRepo, service.WorkflowConfig{}) - prActionService := prservice.CreatePatronRequestActionService(prRepo, illRepo, eventBus, &iso18626Handler) - prApiHandler := prapi.NewApiHandler(prRepo, eventBus) + lmsCreator := lms.NewLmsCreator(illRepo, dirAdapter) + prActionService := prservice.CreatePatronRequestActionService(prRepo, eventBus, &iso18626Handler, lmsCreator) + prApiHandler := prapi.NewApiHandler(prRepo, eventBus, common.NewTenant(TENANT_TO_SYMBOL)) AddDefaultHandlers(eventBus, iso18626Client, supplierLocator, workflowManager, iso18626Handler, prActionService, prApiHandler, prMessageHandler) err = StartEventBus(ctx, eventBus) @@ -196,14 +199,16 @@ func StartServer(ctx Context) error { http.ServeFile(w, r, "handler/open-api.yaml") }) - apiHandler := api.NewApiHandler(ctx.EventRepo, ctx.IllRepo, "", API_PAGE_SIZE) + apiHandler := api.NewApiHandler(ctx.EventRepo, ctx.IllRepo, common.NewTenant(""), API_PAGE_SIZE) oapi.HandlerFromMux(&apiHandler, ServeMux) if TENANT_TO_SYMBOL != "" { - apiHandler := api.NewApiHandler(ctx.EventRepo, ctx.IllRepo, TENANT_TO_SYMBOL, API_PAGE_SIZE) + apiHandler := api.NewApiHandler(ctx.EventRepo, ctx.IllRepo, common.NewTenant(TENANT_TO_SYMBOL), API_PAGE_SIZE) oapi.HandlerFromMuxWithBaseURL(&apiHandler, ServeMux, "/broker") } proapi.HandlerFromMux(&ctx.PrApiHandler, ServeMux) + // TODO: proapi.HandlerFromMuxWithBaseURL(&ctx.PrApiHandler, ServeMux, "/broker") + signatureHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Server", vcs.GetSignature()) ServeMux.ServeHTTP(w, r) diff --git a/broker/common/tenant.go b/broker/common/tenant.go new file mode 100644 index 00000000..9e17acc9 --- /dev/null +++ b/broker/common/tenant.go @@ -0,0 +1,21 @@ +package common + +import ( + "strings" +) + +type Tenant struct { + mapping string +} + +func NewTenant(tenantSymbol string) Tenant { + return Tenant{mapping: tenantSymbol} +} + +func (t *Tenant) IsSpecified() bool { + return t.mapping != "" +} + +func (t *Tenant) GetSymbol(tenant string) string { + return strings.ReplaceAll(t.mapping, "{tenant}", strings.ToUpper(tenant)) +} diff --git a/broker/lms/lms_adapter.go b/broker/lms/lms_adapter.go new file mode 100644 index 00000000..5d019988 --- /dev/null +++ b/broker/lms/lms_adapter.go @@ -0,0 +1,42 @@ +package lms + +// LmsAdapter is an interface defining methods for interacting with a Library Management System (LMS) +// https://github.com/openlibraryenvironment/mod-rs/blob/master/service/src/main/groovy/org/olf/rs/lms/HostLMSActions.groovy +type LmsAdapter interface { + LookupUser(patron string) (string, error) + + AcceptItem( + itemId string, + requestId string, + userId string, + author string, + title string, + isbn string, + callNumber string, + pickupLocation string, + requestedAction string, + ) error + + DeleteItem(itemId string) error + + RequestItem( + requestId string, + itemId string, + borrowerBarcode string, + pickupLocation string, + itemLocation string, + ) error + + CancelRequestItem(requestId string, userId string) error + + CheckInItem(itemId string) error + + CheckOutItem( + requestId string, + itemId string, + borrowerBarcode string, + externalReferenceValue string, + ) error + + CreateUserFiscalTransaction(userId string, itemId string) error +} diff --git a/broker/lms/lms_adapter_manual.go b/broker/lms/lms_adapter_manual.go new file mode 100644 index 00000000..b81fc773 --- /dev/null +++ b/broker/lms/lms_adapter_manual.go @@ -0,0 +1,61 @@ +package lms + +type LmsAdapterManual struct { +} + +func (l *LmsAdapterManual) LookupUser(patron string) (string, error) { + return patron, nil +} + +func (l *LmsAdapterManual) AcceptItem( + itemId string, + requestId string, + userId string, + author string, + title string, + isbn string, + callNumber string, + pickupLocation string, + requestedAction string, +) error { + return nil +} + +func (l *LmsAdapterManual) DeleteItem(itemId string) error { + return nil +} + +func (l *LmsAdapterManual) RequestItem( + requestId string, + itemId string, + borrowerBarcode string, + pickupLocation string, + itemLocation string, +) error { + return nil +} + +func (l *LmsAdapterManual) CancelRequestItem(requestId string, userId string) error { + return nil +} + +func (l *LmsAdapterManual) CheckInItem(itemId string) error { + return nil +} + +func (l *LmsAdapterManual) CheckOutItem( + requestId string, + itemId string, + borrowerBarcode string, + externalReferenceValue string, +) error { + return nil +} + +func (l *LmsAdapterManual) CreateUserFiscalTransaction(userId string, itemId string) error { + return nil +} + +func CreateLmsAdapterMockOK() LmsAdapter { + return &LmsAdapterManual{} +} diff --git a/broker/lms/lms_adapter_ncip.go b/broker/lms/lms_adapter_ncip.go new file mode 100644 index 00000000..1cb0990c --- /dev/null +++ b/broker/lms/lms_adapter_ncip.go @@ -0,0 +1,307 @@ +package lms + +import ( + "encoding/xml" + "fmt" + "net/http" + + "github.com/indexdata/crosslink/broker/ncipclient" + "github.com/indexdata/crosslink/ncip" +) + +type NcipProperty string + +const ( + FromAgency NcipProperty = "from_agency" + FromAgencyAuthentication NcipProperty = "from_agency_authentication" + ToAgency NcipProperty = "to_agency" + Address NcipProperty = "address" + LookupUserEnable NcipProperty = "lookup_user_enable" + AcceptItemEnable NcipProperty = "accept_item_enable" + CheckInItemEnable NcipProperty = "check_in_item_enable" + CheckOutItemEnable NcipProperty = "check_out_item_enable" + RequestItemRequestType NcipProperty = "request_item_request_type" + RequestItemRequestScopeType NcipProperty = "request_item_request_scope_type" + RequestItemBibIdCode NcipProperty = "request_item_bib_id_code" + RequestItemPickupLocationEnable NcipProperty = "request_item_pickup_location_enable" +) + +type NcipUserElement string + +const ( + NCIPUserId NcipUserElement = "User Id" +) + +type NcipItemElement string + +const ( + NCIPBibliographicDescription NcipItemElement = "Bibliographic Description" +) + +// NCIP LMS Adapter, based on: +// https://github.com/openlibraryenvironment/mod-rs/blob/master/service/grails-app/services/org/olf/rs/hostlms/BaseHostLMSService.groovy +// https://github.com/openlibraryenvironment/lib-ncip-client/tree/master/lib-ncip-client/src/main/java/org/olf/rs/circ/client + +type LmsAdapterNcip struct { + ncipClient ncipclient.NcipClient + address string + toAgency string + fromAgency string + fromAgencyAuthentication string + lookupUserEnable bool + acceptItemEnable bool + checkInItemEnable bool + checkOutItemEnable bool + requestItemRequestType string + requestItemRequestScopeType string + requestItemBibIdCode string + requestItemPickupLocationEnable bool +} + +func reqField(m map[string]any, key NcipProperty, dst *string) error { + v, ok := m[string(key)].(string) + if !ok || v == "" { + return fmt.Errorf("missing required NCIP configuration field: %s", key) + } + *dst = v + return nil +} + +func optField[T any](m map[string]any, key NcipProperty, dst *T, def T) { + v, ok := m[string(key)].(T) + if !ok { + *dst = def + } else { + *dst = v + } +} + +func (l *LmsAdapterNcip) parseConfig(ncipInfo map[string]any) error { + err := reqField(ncipInfo, Address, &l.address) + if err != nil { + return err + } + err = reqField(ncipInfo, FromAgency, &l.fromAgency) + if err != nil { + return err + } + optField(ncipInfo, FromAgencyAuthentication, &l.fromAgencyAuthentication, "") + optField(ncipInfo, ToAgency, &l.toAgency, "default-to-agency") + optField(ncipInfo, LookupUserEnable, &l.lookupUserEnable, true) + optField(ncipInfo, AcceptItemEnable, &l.acceptItemEnable, true) + optField(ncipInfo, CheckInItemEnable, &l.checkInItemEnable, true) + optField(ncipInfo, CheckOutItemEnable, &l.checkOutItemEnable, true) + optField(ncipInfo, RequestItemRequestType, &l.requestItemRequestType, "Page") + optField(ncipInfo, RequestItemRequestScopeType, &l.requestItemRequestScopeType, "Item") + optField(ncipInfo, RequestItemBibIdCode, &l.requestItemBibIdCode, "SYSNUMBER") + optField(ncipInfo, RequestItemPickupLocationEnable, &l.requestItemPickupLocationEnable, true) + return nil +} + +func CreateLmsAdapterNcip(ncipInfo map[string]any) (LmsAdapter, error) { + l := &LmsAdapterNcip{} + err := l.parseConfig(ncipInfo) + if err != nil { + return nil, err + } + l.ncipClient = ncipclient.NewNcipClient(http.DefaultClient, l.address, l.fromAgency, l.toAgency, l.fromAgencyAuthentication) + return l, nil +} + +func (l *LmsAdapterNcip) LookupUser(patron string) (string, error) { + if !l.lookupUserEnable { + return patron, nil // could even be empty + } + if patron == "" { + return "", fmt.Errorf("empty patron identifier") + } + // first try to check if patron is actually user Id + arg := ncip.LookupUser{ + UserId: &ncip.UserId{UserIdentifierValue: patron}, + } + _, err := l.ncipClient.LookupUser(arg) + if err == nil { + return patron, nil + } + // then try by user username + // a better solution would be that the LookupUser had type argument (eg barcode or PIN) + // but this is how mod-rs does it + var authenticationInput []ncip.AuthenticationInput + authenticationInput = append(authenticationInput, ncip.AuthenticationInput{ + AuthenticationInputType: ncip.SchemeValuePair{Text: "username"}, + AuthenticationInputData: patron, + }) + userElements := []ncip.SchemeValuePair{{Text: string(NCIPUserId)}} + arg = ncip.LookupUser{ + AuthenticationInput: authenticationInput, + UserElementType: userElements, + } + response, err := l.ncipClient.LookupUser(arg) + if err != nil { + return "", err + } + if response.UserOptionalFields != nil && len(response.UserOptionalFields.UserId) != 0 { + return response.UserOptionalFields.UserId[0].UserIdentifierValue, nil + } + if response.UserId != nil { + return response.UserId.UserIdentifierValue, nil + } + return "", fmt.Errorf("missing User ID in LookupUser response") +} + +func (l *LmsAdapterNcip) AcceptItem( + itemId string, + requestId string, + userId string, + author string, + title string, + isbn string, + callNumber string, + pickupLocation string, + requestedAction string, +) error { + if !l.acceptItemEnable { + return nil + } + var bibliographicItemId *ncip.BibliographicItemId + if isbn != "" { + bibliographicItemId = &ncip.BibliographicItemId{ + BibliographicItemIdentifier: isbn, + BibliographicItemIdentifierCode: &ncip.SchemeValuePair{Text: "ISBN"}, + } + } + biblioInfo := &ncip.BibliographicDescription{ + Author: author, + Title: title, + BibliographicItemId: bibliographicItemId, + } + var itemDescription *ncip.ItemDescription + if callNumber != "" { + itemDescription = &ncip.ItemDescription{CallNumber: callNumber} + } + itemOptionalFields := &ncip.ItemOptionalFields{ + BibliographicDescription: biblioInfo, + ItemDescription: itemDescription, + } + var pickupLocationField *ncip.SchemeValuePair + if pickupLocation != "" { + pickupLocationField = &ncip.SchemeValuePair{Text: pickupLocation} + } + if requestedAction == "" { + requestedAction = "Hold For Pickup" + } + arg := ncip.AcceptItem{ + RequestId: ncip.RequestId{RequestIdentifierValue: requestId}, + RequestedActionType: ncip.SchemeValuePair{Text: requestedAction}, + UserId: &ncip.UserId{UserIdentifierValue: userId}, + ItemId: &ncip.ItemId{ItemIdentifierValue: itemId}, + ItemOptionalFields: itemOptionalFields, + PickupLocation: pickupLocationField, + } + _, err := l.ncipClient.AcceptItem(arg) + return err +} + +func (l *LmsAdapterNcip) DeleteItem(itemId string) error { + arg := ncip.DeleteItem{ + ItemId: ncip.ItemId{ItemIdentifierValue: itemId}, + } + _, err := l.ncipClient.DeleteItem(arg) + return err +} + +func (l *LmsAdapterNcip) RequestItem( + requestId string, + itemId string, + borrowerBarcode string, + pickupLocation string, + itemLocation string, +) error { + var pickupLocationField *ncip.SchemeValuePair + if l.requestItemPickupLocationEnable && pickupLocation != "" { + pickupLocationField = &ncip.SchemeValuePair{Text: pickupLocation} + } + var userIdField *ncip.UserId + if borrowerBarcode != "" { + userIdField = &ncip.UserId{UserIdentifierValue: borrowerBarcode} + } + bibIdField := ncip.BibliographicId{ + BibliographicRecordId: &ncip.BibliographicRecordId{ + BibliographicRecordIdentifier: itemId, + BibliographicRecordIdentifierCode: &ncip.SchemeValuePair{Text: l.requestItemBibIdCode}, + }} + requestScopeTypeField := ncip.SchemeValuePair{Text: l.requestItemRequestScopeType} + + requestTypeField := ncip.SchemeValuePair{Text: l.requestItemRequestType} + arg := ncip.RequestItem{ + RequestId: &ncip.RequestId{RequestIdentifierValue: requestId}, + BibliographicId: []ncip.BibliographicId{bibIdField}, + UserId: userIdField, + PickupLocation: pickupLocationField, + RequestType: requestTypeField, + RequestScopeType: requestScopeTypeField, + } + _, err := l.ncipClient.RequestItem(arg) + return err +} + +func (l *LmsAdapterNcip) CancelRequestItem(requestId string, userId string) error { + arg := ncip.CancelRequestItem{ + UserId: &ncip.UserId{UserIdentifierValue: userId}, + RequestId: &ncip.RequestId{RequestIdentifierValue: requestId}, + } + _, err := l.ncipClient.CancelRequestItem(arg) + return err +} + +func (l *LmsAdapterNcip) CheckInItem(itemId string) error { + if !l.checkInItemEnable { + return nil + } + itemElements := []ncip.SchemeValuePair{ + {Text: string(NCIPBibliographicDescription)}, + } + arg := ncip.CheckInItem{ + ItemId: ncip.ItemId{ItemIdentifierValue: itemId}, + ItemElementType: itemElements, + } + _, err := l.ncipClient.CheckInItem(arg) + // mod-rs does not seem to use the Bibliographic Description in response + return err +} + +func (l *LmsAdapterNcip) CheckOutItem( + requestId string, + itemId string, + borrowerBarcode string, + externalReferenceValue string, +) error { + if !l.checkOutItemEnable { + return nil + } + var ext *ncip.Ext + if externalReferenceValue != "" { + externalId := ncip.RequestId{RequestIdentifierValue: externalReferenceValue} + bytes, err := xml.Marshal(externalId) + if err != nil { + return err + } + ext = &ncip.Ext{XMLContent: bytes} + } + arg := ncip.CheckOutItem{ + RequestId: &ncip.RequestId{RequestIdentifierValue: requestId}, + UserId: &ncip.UserId{UserIdentifierValue: borrowerBarcode}, + ItemId: ncip.ItemId{ItemIdentifierValue: itemId}, + Ext: ext, + } + _, err := l.ncipClient.CheckOutItem(arg) + return err +} + +func (l *LmsAdapterNcip) CreateUserFiscalTransaction(userId string, itemId string) error { + arg := ncip.CreateUserFiscalTransaction{ + UserId: &ncip.UserId{UserIdentifierValue: userId}, + } + _, err := l.ncipClient.CreateUserFiscalTransaction(arg) + return err +} diff --git a/broker/lms/lms_adapter_test.go b/broker/lms/lms_adapter_test.go new file mode 100644 index 00000000..1b293a7a --- /dev/null +++ b/broker/lms/lms_adapter_test.go @@ -0,0 +1,345 @@ +package lms + +import ( + "encoding/xml" + "fmt" + "strings" + "testing" + + "github.com/indexdata/crosslink/broker/ncipclient" + "github.com/indexdata/crosslink/ncip" + "github.com/stretchr/testify/assert" +) + +func TestParseConfigFull(t *testing.T) { + ncipConfig := map[string]any{ + "address": "http://ncip.example.com", + "from_agency": "AGENCY1", + "to_agency": "AGENCY2", + "from_agency_authentication": "auth", + "lookup_user_enable": false, + "accept_item_enable": false, + "check_in_item_enable": false, + "check_out_item_enable": false, + } + lmsAdapaterNcip := &LmsAdapterNcip{} + err := lmsAdapaterNcip.parseConfig(ncipConfig) + assert.NoError(t, err) + assert.Equal(t, "http://ncip.example.com", lmsAdapaterNcip.address) + assert.Equal(t, "AGENCY1", lmsAdapaterNcip.fromAgency) + assert.Equal(t, "AGENCY2", lmsAdapaterNcip.toAgency) + assert.Equal(t, "auth", lmsAdapaterNcip.fromAgencyAuthentication) + assert.False(t, lmsAdapaterNcip.lookupUserEnable) + assert.False(t, lmsAdapaterNcip.acceptItemEnable) + assert.False(t, lmsAdapaterNcip.checkInItemEnable) +} + +func TestParseConfigOptional(t *testing.T) { + // Missing optional to_agency and from_agency_authentication fields + ncipConfig := map[string]any{ + "address": "http://ncip.example.com", + "from_agency": "AGENCY1", + "from_agency_authentication": "auth", + } + lmsAdapaterNcip := &LmsAdapterNcip{} + err := lmsAdapaterNcip.parseConfig(ncipConfig) + assert.NoError(t, err) + assert.Equal(t, "http://ncip.example.com", lmsAdapaterNcip.address) + assert.Equal(t, "AGENCY1", lmsAdapaterNcip.fromAgency) + assert.Equal(t, "default-to-agency", lmsAdapaterNcip.toAgency) + assert.Equal(t, "auth", lmsAdapaterNcip.fromAgencyAuthentication) + assert.True(t, lmsAdapaterNcip.lookupUserEnable) + assert.True(t, lmsAdapaterNcip.acceptItemEnable) + assert.True(t, lmsAdapaterNcip.checkInItemEnable) +} + +func TestParseConfigMissing(t *testing.T) { + // Missing required address field + ncipConfigMissing := map[string]any{ + "from_agency": "AGENCY1", + } + lmsAdapaterNcip := &LmsAdapterNcip{} + err := lmsAdapaterNcip.parseConfig(ncipConfigMissing) + assert.Error(t, err) + assert.Equal(t, "missing required NCIP configuration field: address", err.Error()) + + // Missing required from_agency field + ncipConfigMissing = map[string]any{ + "address": "http://ncip.example.com", + } + lmsAdapaterNcip = &LmsAdapterNcip{} + err = lmsAdapaterNcip.parseConfig(ncipConfigMissing) + assert.Error(t, err) + assert.Equal(t, "missing required NCIP configuration field: from_agency", err.Error()) +} + +func TestLookupUser(t *testing.T) { + var mock ncipclient.NcipClient = new(ncipClientMock) + ad := &LmsAdapterNcip{ + ncipClient: mock, + lookupUserEnable: true, + } + _, err := ad.LookupUser("") + assert.Error(t, err) + assert.Equal(t, "empty patron identifier", err.Error()) + + userId, err := ad.LookupUser("testuser") + assert.NoError(t, err) + assert.Equal(t, "testuser", userId) + + _, err = ad.LookupUser("bad user") + assert.Error(t, err) + assert.Equal(t, "unknown user name", err.Error()) + + userId, err = ad.LookupUser("pass") + assert.NoError(t, err) + assert.Equal(t, "pass", userId) + + _, err = ad.LookupUser("missing data") + assert.Error(t, err) + assert.Equal(t, "missing User ID in LookupUser response", err.Error()) + + userId, err = ad.LookupUser("good user") + assert.NoError(t, err) + assert.Equal(t, "user124", userId) + + userId, err = ad.LookupUser("other user") + assert.NoError(t, err) + assert.Equal(t, "user123", userId) + + ad.lookupUserEnable = false + userId, err = ad.LookupUser("") + assert.NoError(t, err) + assert.Equal(t, "", userId) + + ad.lookupUserEnable = false + mock.(*ncipClientMock).lastRequest = nil + userId, err = ad.LookupUser("anyuser") + assert.NoError(t, err) + assert.Equal(t, "anyuser", userId) + assert.Nil(t, mock.(*ncipClientMock).lastRequest) // not called +} + +func TestAcceptItem(t *testing.T) { + var mock ncipclient.NcipClient = new(ncipClientMock) + ad := &LmsAdapterNcip{ + acceptItemEnable: true, + ncipClient: mock, + } + err := ad.AcceptItem("item1", "req1", "testuser", "author", "title", "isbn", "callnum", "loc", "action") + assert.NoError(t, err) + req := mock.(*ncipClientMock).lastRequest.(ncip.AcceptItem) + assert.Equal(t, "testuser", req.UserId.UserIdentifierValue) + assert.Equal(t, "item1", req.ItemId.ItemIdentifierValue) + assert.Equal(t, "req1", req.RequestId.RequestIdentifierValue) + assert.Equal(t, "author", req.ItemOptionalFields.BibliographicDescription.Author) + assert.Equal(t, "title", req.ItemOptionalFields.BibliographicDescription.Title) + assert.Equal(t, "isbn", req.ItemOptionalFields.BibliographicDescription.BibliographicItemId.BibliographicItemIdentifier) + assert.Equal(t, "ISBN", req.ItemOptionalFields.BibliographicDescription.BibliographicItemId.BibliographicItemIdentifierCode.Text) + assert.Equal(t, "callnum", req.ItemOptionalFields.ItemDescription.CallNumber) + assert.Equal(t, "loc", req.PickupLocation.Text) + assert.Equal(t, "action", req.RequestedActionType.Text) + + err = ad.AcceptItem("item1", "req1", "testuser", "author", "title", "", "", "", "") + assert.NoError(t, err) + req = mock.(*ncipClientMock).lastRequest.(ncip.AcceptItem) + assert.Equal(t, "testuser", req.UserId.UserIdentifierValue) + assert.Equal(t, "item1", req.ItemId.ItemIdentifierValue) + assert.Equal(t, "req1", req.RequestId.RequestIdentifierValue) + assert.Equal(t, "author", req.ItemOptionalFields.BibliographicDescription.Author) + assert.Equal(t, "title", req.ItemOptionalFields.BibliographicDescription.Title) + assert.Nil(t, req.ItemOptionalFields.BibliographicDescription.BibliographicItemId) + assert.Nil(t, req.ItemOptionalFields.ItemDescription) + assert.Nil(t, req.PickupLocation) + assert.Equal(t, "Hold For Pickup", req.RequestedActionType.Text) + + ad.acceptItemEnable = false + mock.(*ncipClientMock).lastRequest = nil + err = ad.AcceptItem("", "", "", "", "", "", "", "", "") + assert.NoError(t, err) + assert.Nil(t, mock.(*ncipClientMock).lastRequest) +} + +func TestDeleteItem(t *testing.T) { + var mock ncipclient.NcipClient = new(ncipClientMock) + ad := &LmsAdapterNcip{ + ncipClient: mock, + } + err := ad.DeleteItem("item1") + assert.NoError(t, err) + req := mock.(*ncipClientMock).lastRequest.(ncip.DeleteItem) + assert.Equal(t, "item1", req.ItemId.ItemIdentifierValue) + + err = ad.DeleteItem("error") + assert.Error(t, err) + assert.Equal(t, "deletion error", err.Error()) +} + +func TestRequestItem(t *testing.T) { + var mock ncipclient.NcipClient = new(ncipClientMock) + ad := &LmsAdapterNcip{ + ncipClient: mock, + requestItemPickupLocationEnable: true, + requestItemRequestType: "Loan", + requestItemRequestScopeType: "Title", + requestItemBibIdCode: "SYSNUMBER", + } + err := ad.RequestItem("req1", "item1", "testuser", "loc", "itemloc") + assert.NoError(t, err) + req := mock.(*ncipClientMock).lastRequest.(ncip.RequestItem) + assert.Equal(t, "testuser", req.UserId.UserIdentifierValue) + assert.Equal(t, "item1", req.BibliographicId[0].BibliographicRecordId.BibliographicRecordIdentifier) + assert.Equal(t, "SYSNUMBER", req.BibliographicId[0].BibliographicRecordId.BibliographicRecordIdentifierCode.Text) + assert.Equal(t, "req1", req.RequestId.RequestIdentifierValue) + assert.Equal(t, "loc", req.PickupLocation.Text) + assert.Equal(t, "Loan", req.RequestType.Text) + assert.Equal(t, "Title", req.RequestScopeType.Text) + + ad.requestItemPickupLocationEnable = false + mock.(*ncipClientMock).lastRequest = nil + err = ad.RequestItem("req1", "item1", "testuser", "loc", "itemloc") + assert.NoError(t, err) + req = mock.(*ncipClientMock).lastRequest.(ncip.RequestItem) + assert.Nil(t, req.PickupLocation) +} + +func TestCancelRequestItem(t *testing.T) { + var mock ncipclient.NcipClient = new(ncipClientMock) + ad := &LmsAdapterNcip{ + ncipClient: mock, + } + err := ad.CancelRequestItem("req1", "testuser") + assert.NoError(t, err) + req := mock.(*ncipClientMock).lastRequest.(ncip.CancelRequestItem) + assert.Equal(t, "testuser", req.UserId.UserIdentifierValue) + assert.Equal(t, "req1", req.RequestId.RequestIdentifierValue) +} + +func TestCheckInItem(t *testing.T) { + var mock ncipclient.NcipClient = new(ncipClientMock) + ad := &LmsAdapterNcip{ + ncipClient: mock, + checkInItemEnable: true, + } + err := ad.CheckInItem("item1") + assert.NoError(t, err) + req := mock.(*ncipClientMock).lastRequest.(ncip.CheckInItem) + assert.Equal(t, "item1", req.ItemId.ItemIdentifierValue) + assert.Equal(t, 1, len(req.ItemElementType)) + assert.Equal(t, "Bibliographic Description", req.ItemElementType[0].Text) + + ad.checkInItemEnable = false + mock.(*ncipClientMock).lastRequest = nil + err = ad.CheckInItem("item1") + assert.NoError(t, err) + assert.Nil(t, mock.(*ncipClientMock).lastRequest) +} + +func TestCheckOutItem(t *testing.T) { + var mock ncipclient.NcipClient = new(ncipClientMock) + ad := &LmsAdapterNcip{ + ncipClient: mock, + checkOutItemEnable: true, + } + err := ad.CheckOutItem("req1", "item1", "barcodeid", "extref") + assert.NoError(t, err) + req := mock.(*ncipClientMock).lastRequest.(ncip.CheckOutItem) + assert.Equal(t, "req1", req.RequestId.RequestIdentifierValue) + assert.Equal(t, "item1", req.ItemId.ItemIdentifierValue) + assert.Equal(t, "barcodeid", req.UserId.UserIdentifierValue) + bytes, err := xml.Marshal(ncip.RequestId{RequestIdentifierValue: "extref"}) + assert.NoError(t, err) + assert.Equal(t, bytes, req.Ext.XMLContent) + + ad.checkOutItemEnable = false + mock.(*ncipClientMock).lastRequest = nil + err = ad.CheckOutItem("req1", "item1", "barcodeid", "extref") + assert.NoError(t, err) + assert.Nil(t, mock.(*ncipClientMock).lastRequest) +} + +func TestCreateUserFiscalTransaction(t *testing.T) { + var mock ncipclient.NcipClient = new(ncipClientMock) + ad := &LmsAdapterNcip{ + ncipClient: mock, + } + err := ad.CreateUserFiscalTransaction("testuser", "item1") + assert.NoError(t, err) + req := mock.(*ncipClientMock).lastRequest.(ncip.CreateUserFiscalTransaction) + assert.Equal(t, "testuser", req.UserId.UserIdentifierValue) +} + +type ncipClientMock struct { + lastRequest any +} + +func (n *ncipClientMock) LookupUser(lookup ncip.LookupUser) (*ncip.LookupUserResponse, error) { + n.lastRequest = lookup + if lookup.UserId != nil { + if lookup.UserId.UserIdentifierValue == "pass" { + return nil, nil + } + if strings.Contains(lookup.UserId.UserIdentifierValue, " ") { + return nil, fmt.Errorf("unknown user id") + } + return &ncip.LookupUserResponse{ + UserId: &ncip.UserId{UserIdentifierValue: lookup.UserId.UserIdentifierValue}, + }, nil + } + if lookup.AuthenticationInput[0].AuthenticationInputData == "bad user" { + return nil, fmt.Errorf("unknown user name") + } + if lookup.AuthenticationInput[0].AuthenticationInputData == "missing data" { + return &ncip.LookupUserResponse{}, nil + } + if lookup.AuthenticationInput[0].AuthenticationInputData == "good user" { + return &ncip.LookupUserResponse{ + UserOptionalFields: &ncip.UserOptionalFields{ + UserId: []ncip.UserId{ + {UserIdentifierValue: "user124"}, + }, + }, + }, nil + } + return &ncip.LookupUserResponse{ + UserId: &ncip.UserId{UserIdentifierValue: "user123"}, + }, nil +} + +func (n *ncipClientMock) AcceptItem(accept ncip.AcceptItem) (*ncip.AcceptItemResponse, error) { + n.lastRequest = accept + return nil, nil +} + +func (n *ncipClientMock) DeleteItem(delete ncip.DeleteItem) (*ncip.DeleteItemResponse, error) { + if delete.ItemId.ItemIdentifierValue == "error" { + return nil, fmt.Errorf("deletion error") + } + n.lastRequest = delete + return nil, nil +} + +func (n *ncipClientMock) RequestItem(request ncip.RequestItem) (*ncip.RequestItemResponse, error) { + n.lastRequest = request + return nil, nil +} + +func (n *ncipClientMock) CancelRequestItem(cancel ncip.CancelRequestItem) (*ncip.CancelRequestItemResponse, error) { + n.lastRequest = cancel + return nil, nil +} + +func (n *ncipClientMock) CheckInItem(checkin ncip.CheckInItem) (*ncip.CheckInItemResponse, error) { + n.lastRequest = checkin + return nil, nil +} + +func (n *ncipClientMock) CheckOutItem(checkout ncip.CheckOutItem) (*ncip.CheckOutItemResponse, error) { + n.lastRequest = checkout + return nil, nil +} + +func (n *ncipClientMock) CreateUserFiscalTransaction(create ncip.CreateUserFiscalTransaction) (*ncip.CreateUserFiscalTransactionResponse, error) { + n.lastRequest = create + return nil, nil +} diff --git a/broker/lms/lms_creator.go b/broker/lms/lms_creator.go new file mode 100644 index 00000000..8d4c8ac6 --- /dev/null +++ b/broker/lms/lms_creator.go @@ -0,0 +1,12 @@ +package lms + +import ( + "github.com/indexdata/crosslink/broker/common" + "github.com/jackc/pgx/v5/pgtype" +) + +// LmsCreator is an interface for creating LMS adapters based on a symbol +// Symbol is used to look up directory entry with information about LMS in use +type LmsCreator interface { + GetAdapter(ctx common.ExtendedContext, symbol pgtype.Text) (LmsAdapter, error) +} diff --git a/broker/lms/lms_creator_impl.go b/broker/lms/lms_creator_impl.go new file mode 100644 index 00000000..0adde854 --- /dev/null +++ b/broker/lms/lms_creator_impl.go @@ -0,0 +1,43 @@ +package lms + +import ( + "errors" + + "github.com/indexdata/crosslink/broker/adapter" + "github.com/indexdata/crosslink/broker/common" + "github.com/indexdata/crosslink/broker/ill_db" + "github.com/jackc/pgx/v5/pgtype" +) + +type lmsCreatorImpl struct { + illRepo ill_db.IllRepo + directoryLookupAdapter adapter.DirectoryLookupAdapter +} + +func NewLmsCreator(illRepo ill_db.IllRepo, directoryLookupAdapter adapter.DirectoryLookupAdapter) LmsCreator { + return &lmsCreatorImpl{ + illRepo: illRepo, + directoryLookupAdapter: directoryLookupAdapter, + } +} + +func (l *lmsCreatorImpl) GetAdapter(ctx common.ExtendedContext, symbol pgtype.Text) (LmsAdapter, error) { + if !symbol.Valid { + return nil, errors.New("missing requester symbol") + } + return l.getAdapterInt(ctx, symbol.String) +} + +func (l *lmsCreatorImpl) getAdapterInt(ctx common.ExtendedContext, symbol string) (LmsAdapter, error) { + peers, _, err := l.illRepo.GetCachedPeersBySymbols(ctx, []string{symbol}, l.directoryLookupAdapter) + if err != nil { + return nil, err + } + for _, peer := range peers { + ncipInfo, ok := peer.CustomData["ncip"].(map[string]any) + if ok { + return CreateLmsAdapterNcip(ncipInfo) + } + } + return CreateLmsAdapterMockOK(), nil +} diff --git a/broker/lms/lms_creator_test.go b/broker/lms/lms_creator_test.go new file mode 100644 index 00000000..5dfaaca3 --- /dev/null +++ b/broker/lms/lms_creator_test.go @@ -0,0 +1,92 @@ +package lms + +import ( + "context" + "testing" + + "github.com/indexdata/crosslink/broker/adapter" + "github.com/indexdata/crosslink/broker/common" + "github.com/indexdata/crosslink/broker/ill_db" + "github.com/jackc/pgx/v5/pgtype" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestGetAdapterMissingSymbol(t *testing.T) { + illRepo := &MockIllRepo{} + creator := NewLmsCreator(illRepo, nil) + ctx := common.CreateExtCtxWithArgs(context.Background(), nil) + var missingSymbol pgtype.Text + _, err := creator.GetAdapter(ctx, missingSymbol) + assert.Error(t, err) + assert.Equal(t, "missing requester symbol", err.Error()) +} + +func TestGetAdapterGetCachedByPeersByPeersFail(t *testing.T) { + illRepo := &MockIllRepo{} + illRepo.On("GetCachedPeersBySymbols", mock.Anything).Return([]ill_db.Peer{}, "", assert.AnError) + creator := NewLmsCreator(illRepo, nil) + ctx := common.CreateExtCtxWithArgs(context.Background(), nil) + symbol := pgtype.Text{String: "TEST", Valid: true} + _, err := creator.GetAdapter(ctx, symbol) + assert.Error(t, err) + assert.Equal(t, "assert.AnError general error for testing", err.Error()) +} + +func TestGetAdapterNoPeers(t *testing.T) { + illRepo := &MockIllRepo{} + illRepo.On("GetCachedPeersBySymbols", mock.Anything).Return([]ill_db.Peer{}, "", nil) + creator := NewLmsCreator(illRepo, nil) + ctx := common.CreateExtCtxWithArgs(context.Background(), nil) + symbol := pgtype.Text{String: "TEST", Valid: true} + LmsAdapter, err := creator.GetAdapter(ctx, symbol) + assert.NoError(t, err) + assert.IsType(t, &LmsAdapterManual{}, LmsAdapter) +} + +func TestGetAdapterNcipOK(t *testing.T) { + illRepo := &MockIllRepo{} + peer := ill_db.Peer{ + CustomData: map[string]any{ + "ncip": map[string]any{ + "address": "http://ncip.example.com", + "from_agency": "AGENCY1", + "to_agency": "AGENCY2", + "from_agency_authentication": "auth", + }, + }, + } + illRepo.On("GetCachedPeersBySymbols", mock.Anything).Return([]ill_db.Peer{peer}, "", nil) + creator := NewLmsCreator(illRepo, nil) + ctx := common.CreateExtCtxWithArgs(context.Background(), nil) + symbol := pgtype.Text{String: "TEST", Valid: true} + LmsAdapter, err := creator.GetAdapter(ctx, symbol) + assert.NoError(t, err) + assert.IsType(t, &LmsAdapterNcip{}, LmsAdapter) +} + +func TestGetAdapterNcipFail(t *testing.T) { + illRepo := &MockIllRepo{} + peer := ill_db.Peer{ + CustomData: map[string]any{ + "ncip": map[string]any{}, + }, + } + illRepo.On("GetCachedPeersBySymbols", mock.Anything).Return([]ill_db.Peer{peer}, "", nil) + creator := NewLmsCreator(illRepo, nil) + ctx := common.CreateExtCtxWithArgs(context.Background(), nil) + symbol := pgtype.Text{String: "TEST", Valid: true} + _, err := creator.GetAdapter(ctx, symbol) + assert.Error(t, err) + assert.Equal(t, "missing required NCIP configuration field: address", err.Error()) +} + +type MockIllRepo struct { + mock.Mock + ill_db.PgIllRepo +} + +func (r *MockIllRepo) GetCachedPeersBySymbols(ctx common.ExtendedContext, symbols []string, directoryLookupAdapter adapter.DirectoryLookupAdapter) ([]ill_db.Peer, string, error) { + args := r.Called(symbols) + return args.Get(0).([]ill_db.Peer), args.String(1), args.Error(2) +} diff --git a/broker/ncipclient/ncipclient.go b/broker/ncipclient/ncipclient.go index 2df7e2f1..7dc2cdd0 100644 --- a/broker/ncipclient/ncipclient.go +++ b/broker/ncipclient/ncipclient.go @@ -24,40 +24,22 @@ const ( CreateUserFiscalTransactionMode NcipProperty = "create_user_fiscal_transaction_mode" ) -// NcipClient defines the interface for NCIP operations -// customData is from the DirectoryEntry.CustomData field type NcipClient interface { - // LookupUser performs user authentication. - // Returns true if authentication is successful (disabled or auto and NCIP lookup succeeded) - // Returns false if authentication is manual - // Returns an error otherwise (failed NCIP lookup, misconfiguration, etc) - LookupUser(customData map[string]any, arg ncip.LookupUser) (bool, error) + LookupUser(arg ncip.LookupUser) (*ncip.LookupUserResponse, error) - // AcceptItem accepts an item. - // Returns true if accept is successful (disabled or auto and NCIP accept succeeded) - // Returns false if accept is manual - // Returns an error otherwise (failed NCIP accept, misconfiguration, etc) - AcceptItem(customData map[string]any, arg ncip.AcceptItem) (bool, error) + AcceptItem(arg ncip.AcceptItem) (*ncip.AcceptItemResponse, error) - DeleteItem(customData map[string]any, arg ncip.DeleteItem) error + DeleteItem(arg ncip.DeleteItem) (*ncip.DeleteItemResponse, error) - // RequestItem requests an item. - // Returns true if request is successful (disabled or auto and NCIP request succeeded) - // Returns false if request is manual - // Returns an error otherwise (failed NCIP request, misconfiguration, etc) - RequestItem(customData map[string]any, arg ncip.RequestItem) (bool, error) + RequestItem(arg ncip.RequestItem) (*ncip.RequestItemResponse, error) - CancelRequestItem(customData map[string]any, arg ncip.CancelRequestItem) error + CancelRequestItem(arg ncip.CancelRequestItem) (*ncip.CancelRequestItemResponse, error) - CheckInItem(customData map[string]any, arg ncip.CheckInItem) error + CheckInItem(arg ncip.CheckInItem) (*ncip.CheckInItemResponse, error) - CheckOutItem(customData map[string]any, arg ncip.CheckOutItem) error + CheckOutItem(arg ncip.CheckOutItem) (*ncip.CheckOutItemResponse, error) - // CreateUserFiscalTransaction creates a user fiscal transaction. - // Returns true if creation is successful (disabled or auto and NCIP creation succeeded) - // Returns false if creation is manual - // Returns an error otherwise (failed NCIP creation, misconfiguration, etc) - CreateUserFiscalTransaction(customData map[string]any, arg ncip.CreateUserFiscalTransaction) (bool, error) + CreateUserFiscalTransaction(arg ncip.CreateUserFiscalTransaction) (*ncip.CreateUserFiscalTransactionResponse, error) } type NcipError struct { diff --git a/broker/ncipclient/ncipclient_impl.go b/broker/ncipclient/ncipclient_impl.go index ce08d476..a24ec915 100644 --- a/broker/ncipclient/ncipclient_impl.go +++ b/broker/ncipclient/ncipclient_impl.go @@ -10,191 +10,151 @@ import ( ) type NcipClientImpl struct { - client *http.Client + client *http.Client + address string + fromAgency string + toAgency string + fromAgencyAuthentication string } -func CreateNcipClient(client *http.Client) NcipClient { +func NewNcipClient(client *http.Client, address string, fromAgency string, toAgency string, fromAgencyAuthentication string) NcipClient { return &NcipClientImpl{ - client: client, + client: client, + address: address, + fromAgency: fromAgency, + toAgency: toAgency, + fromAgencyAuthentication: fromAgencyAuthentication, } } -func (n *NcipClientImpl) LookupUser(customData map[string]any, lookup ncip.LookupUser) (bool, error) { - ncipInfo, err := n.getNcipInfo(customData) - if err != nil { - return false, err - } - handle, ret, err := n.checkMode(ncipInfo, LookupUserMode) - if handle { - return ret, err - } - lookup.InitiationHeader = n.prepareHeader(ncipInfo, lookup.InitiationHeader) +func (n *NcipClientImpl) LookupUser(lookup ncip.LookupUser) (*ncip.LookupUserResponse, error) { + lookup.InitiationHeader = n.prepareHeader(lookup.InitiationHeader) ncipMessage := &ncip.NCIPMessage{ LookupUser: &lookup, } - ncipResponse, err := n.sendReceiveMessage(ncipInfo, ncipMessage) + ncipResponse, err := n.sendReceiveMessage(ncipMessage) if err != nil { - return false, err + return nil, err } - lookupUserResponse := ncipResponse.LookupUserResponse - if lookupUserResponse == nil { - return false, fmt.Errorf("invalid NCIP response: missing LookupUserResponse") + response := ncipResponse.LookupUserResponse + if response == nil { + return nil, fmt.Errorf("invalid NCIP response: missing LookupUserResponse") } - return true, n.checkProblem("NCIP user lookup", lookupUserResponse.Problem) + return response, n.checkProblem("NCIP user lookup", response.Problem) } -func (n *NcipClientImpl) AcceptItem(customData map[string]any, accept ncip.AcceptItem) (bool, error) { - ncipInfo, err := n.getNcipInfo(customData) - if err != nil { - return false, err - } - handle, ret, err := n.checkMode(ncipInfo, AcceptItemMode) - if handle { - return ret, err - } - accept.InitiationHeader = n.prepareHeader(ncipInfo, accept.InitiationHeader) +func (n *NcipClientImpl) AcceptItem(accept ncip.AcceptItem) (*ncip.AcceptItemResponse, error) { + accept.InitiationHeader = n.prepareHeader(accept.InitiationHeader) ncipMessage := &ncip.NCIPMessage{ AcceptItem: &accept, } - ncipResponse, err := n.sendReceiveMessage(ncipInfo, ncipMessage) + ncipResponse, err := n.sendReceiveMessage(ncipMessage) if err != nil { - return false, err + return nil, err } - acceptItemResponse := ncipResponse.AcceptItemResponse - if acceptItemResponse == nil { - return false, fmt.Errorf("invalid NCIP response: missing AcceptItemResponse") + response := ncipResponse.AcceptItemResponse + if response == nil { + return nil, fmt.Errorf("invalid NCIP response: missing AcceptItemResponse") } - return true, n.checkProblem("NCIP accept item", acceptItemResponse.Problem) + return response, n.checkProblem("NCIP accept item", response.Problem) } -func (n *NcipClientImpl) DeleteItem(customData map[string]any, delete ncip.DeleteItem) error { - ncipInfo, err := n.getNcipInfo(customData) - if err != nil { - return err - } - delete.InitiationHeader = n.prepareHeader(ncipInfo, delete.InitiationHeader) +func (n *NcipClientImpl) DeleteItem(delete ncip.DeleteItem) (*ncip.DeleteItemResponse, error) { + delete.InitiationHeader = n.prepareHeader(delete.InitiationHeader) ncipMessage := &ncip.NCIPMessage{ DeleteItem: &delete, } - ncipResponse, err := n.sendReceiveMessage(ncipInfo, ncipMessage) + ncipResponse, err := n.sendReceiveMessage(ncipMessage) if err != nil { - return err + return nil, err } - deleteItemResponse := ncipResponse.DeleteItemResponse - if deleteItemResponse == nil { - return fmt.Errorf("invalid NCIP response: missing DeleteItemResponse") + response := ncipResponse.DeleteItemResponse + if response == nil { + return nil, fmt.Errorf("invalid NCIP response: missing DeleteItemResponse") } - return n.checkProblem("NCIP delete item", deleteItemResponse.Problem) + return response, n.checkProblem("NCIP delete item", response.Problem) } -func (n *NcipClientImpl) RequestItem(customData map[string]any, request ncip.RequestItem) (bool, error) { - ncipInfo, err := n.getNcipInfo(customData) - if err != nil { - return false, err - } - handle, ret, err := n.checkMode(ncipInfo, RequestItemMode) - if handle { - return ret, err - } - request.InitiationHeader = n.prepareHeader(ncipInfo, request.InitiationHeader) +func (n *NcipClientImpl) RequestItem(request ncip.RequestItem) (*ncip.RequestItemResponse, error) { + request.InitiationHeader = n.prepareHeader(request.InitiationHeader) ncipMessage := &ncip.NCIPMessage{ RequestItem: &request, } - ncipResponse, err := n.sendReceiveMessage(ncipInfo, ncipMessage) + ncipResponse, err := n.sendReceiveMessage(ncipMessage) if err != nil { - return false, err + return nil, err } - requestItemResponse := ncipResponse.RequestItemResponse - if requestItemResponse == nil { - return false, fmt.Errorf("invalid NCIP response: missing RequestItemResponse") + response := ncipResponse.RequestItemResponse + if response == nil { + return nil, fmt.Errorf("invalid NCIP response: missing RequestItemResponse") } - return true, n.checkProblem("NCIP request item", requestItemResponse.Problem) + return response, n.checkProblem("NCIP request item", response.Problem) } -func (n *NcipClientImpl) CancelRequestItem(customData map[string]any, request ncip.CancelRequestItem) error { - ncipInfo, err := n.getNcipInfo(customData) - if err != nil { - return err - } - request.InitiationHeader = n.prepareHeader(ncipInfo, request.InitiationHeader) +func (n *NcipClientImpl) CancelRequestItem(request ncip.CancelRequestItem) (*ncip.CancelRequestItemResponse, error) { + request.InitiationHeader = n.prepareHeader(request.InitiationHeader) ncipMessage := &ncip.NCIPMessage{ CancelRequestItem: &request, } - ncipResponse, err := n.sendReceiveMessage(ncipInfo, ncipMessage) + ncipResponse, err := n.sendReceiveMessage(ncipMessage) if err != nil { - return err + return nil, err } - cancelRequestItemResponse := ncipResponse.CancelRequestItemResponse - if cancelRequestItemResponse == nil { - return fmt.Errorf("invalid NCIP response: missing CancelRequestItemResponse") + response := ncipResponse.CancelRequestItemResponse + if response == nil { + return nil, fmt.Errorf("invalid NCIP response: missing CancelRequestItemResponse") } - return n.checkProblem("NCIP cancel request item", cancelRequestItemResponse.Problem) + return response, n.checkProblem("NCIP cancel request item", response.Problem) } -func (n *NcipClientImpl) CheckInItem(customData map[string]any, request ncip.CheckInItem) error { - ncipInfo, err := n.getNcipInfo(customData) - if err != nil { - return err - } - request.InitiationHeader = n.prepareHeader(ncipInfo, request.InitiationHeader) +func (n *NcipClientImpl) CheckInItem(request ncip.CheckInItem) (*ncip.CheckInItemResponse, error) { + request.InitiationHeader = n.prepareHeader(request.InitiationHeader) ncipMessage := &ncip.NCIPMessage{ CheckInItem: &request, } - ncipResponse, err := n.sendReceiveMessage(ncipInfo, ncipMessage) + ncipResponse, err := n.sendReceiveMessage(ncipMessage) if err != nil { - return err + return nil, err } - checkInItemResponse := ncipResponse.CheckInItemResponse - if checkInItemResponse == nil { - return fmt.Errorf("invalid NCIP response: missing CheckInItemResponse") + response := ncipResponse.CheckInItemResponse + if response == nil { + return nil, fmt.Errorf("invalid NCIP response: missing CheckInItemResponse") } - return n.checkProblem("NCIP check in item", checkInItemResponse.Problem) + return response, n.checkProblem("NCIP check in item", response.Problem) } -func (n *NcipClientImpl) CheckOutItem(customData map[string]any, request ncip.CheckOutItem) error { - ncipInfo, err := n.getNcipInfo(customData) - if err != nil { - return err - } - request.InitiationHeader = n.prepareHeader(ncipInfo, request.InitiationHeader) +func (n *NcipClientImpl) CheckOutItem(request ncip.CheckOutItem) (*ncip.CheckOutItemResponse, error) { + request.InitiationHeader = n.prepareHeader(request.InitiationHeader) ncipMessage := &ncip.NCIPMessage{ CheckOutItem: &request, } - ncipResponse, err := n.sendReceiveMessage(ncipInfo, ncipMessage) + ncipResponse, err := n.sendReceiveMessage(ncipMessage) if err != nil { - return err + return nil, err } - checkOutItemResponse := ncipResponse.CheckOutItemResponse - if checkOutItemResponse == nil { - return fmt.Errorf("invalid NCIP response: missing CheckOutItemResponse") + response := ncipResponse.CheckOutItemResponse + if response == nil { + return nil, fmt.Errorf("invalid NCIP response: missing CheckOutItemResponse") } - return n.checkProblem("NCIP check out item", checkOutItemResponse.Problem) + return response, n.checkProblem("NCIP check out item", response.Problem) } -func (n *NcipClientImpl) CreateUserFiscalTransaction(customData map[string]any, request ncip.CreateUserFiscalTransaction) (bool, error) { - ncipInfo, err := n.getNcipInfo(customData) - if err != nil { - return false, err - } - handle, ret, err := n.checkMode(ncipInfo, CreateUserFiscalTransactionMode) - if handle { - return ret, err - } - request.InitiationHeader = n.prepareHeader(ncipInfo, request.InitiationHeader) +func (n *NcipClientImpl) CreateUserFiscalTransaction(request ncip.CreateUserFiscalTransaction) (*ncip.CreateUserFiscalTransactionResponse, error) { + request.InitiationHeader = n.prepareHeader(request.InitiationHeader) ncipMessage := &ncip.NCIPMessage{ CreateUserFiscalTransaction: &request, } - ncipResponse, err := n.sendReceiveMessage(ncipInfo, ncipMessage) + ncipResponse, err := n.sendReceiveMessage(ncipMessage) if err != nil { - return false, err + return nil, err } - createUserFiscalTransactionResponse := ncipResponse.CreateUserFiscalTransactionResponse - if createUserFiscalTransactionResponse == nil { - return false, fmt.Errorf("invalid NCIP response: missing CreateUserFiscalTransactionResponse") + response := ncipResponse.CreateUserFiscalTransactionResponse + if response == nil { + return nil, fmt.Errorf("invalid NCIP response: missing CreateUserFiscalTransactionResponse") } - return true, n.checkProblem("NCIP create user fiscal transaction", createUserFiscalTransactionResponse.Problem) + return response, n.checkProblem("NCIP create user fiscal transaction", response.Problem) } func (n *NcipClientImpl) checkProblem(op string, responseProblems []ncip.Problem) error { @@ -207,66 +167,29 @@ func (n *NcipClientImpl) checkProblem(op string, responseProblems []ncip.Problem return nil } -func (n *NcipClientImpl) getNcipInfo(customData map[string]any) (map[string]any, error) { - ncipInfo, ok := customData["ncip"].(map[string]any) - if !ok { - return nil, fmt.Errorf("missing ncip configuration in customData") - } - return ncipInfo, nil -} - -func (n *NcipClientImpl) checkMode(ncipInfo map[string]any, key NcipProperty) (bool, bool, error) { - mode, ok := ncipInfo[string(key)].(string) - if !ok { - return true, false, fmt.Errorf("missing %s in ncip configuration", key) - } - switch mode { - case string(ModeDisabled): - return true, true, nil - case string(ModeManual): - return true, false, nil - case string(ModeAuto): - break - default: - return true, false, fmt.Errorf("unknown value for %s: %s", key, mode) - } - return false, false, nil -} - -func (n *NcipClientImpl) prepareHeader(ncipInfo map[string]any, header *ncip.InitiationHeader) *ncip.InitiationHeader { +func (n *NcipClientImpl) prepareHeader(header *ncip.InitiationHeader) *ncip.InitiationHeader { if header == nil { header = &ncip.InitiationHeader{} } - from_agency, ok := ncipInfo[string(FromAgency)].(string) - if !ok || from_agency == "" { - from_agency = "default-from-agency" - } header.FromAgencyId.AgencyId = ncip.SchemeValuePair{ - Text: from_agency, - } - to_agency, ok := ncipInfo[string(ToAgency)].(string) - if !ok || to_agency == "" { - to_agency = "default-to-agency" + Text: n.fromAgency, } header.ToAgencyId.AgencyId = ncip.SchemeValuePair{ - Text: to_agency, - } - if auth, ok := ncipInfo[string(FromAgencyAuthentication)].(string); ok { - header.FromAgencyAuthentication = auth + Text: n.toAgency, } + header.FromAgencyAuthentication = n.fromAgencyAuthentication return header } -func (n *NcipClientImpl) sendReceiveMessage(ncipInfo map[string]any, message *ncip.NCIPMessage) (*ncip.NCIPMessage, error) { - url, ok := ncipInfo[string(Address)].(string) - if !ok { - return nil, fmt.Errorf("missing NCIP address in customData") +func (n *NcipClientImpl) sendReceiveMessage(message *ncip.NCIPMessage) (*ncip.NCIPMessage, error) { + if n.address == "" { + return nil, fmt.Errorf("missing NCIP address in configuration") } message.Version = ncip.NCIP_V2_02_XSD var respMessage ncip.NCIPMessage err := httpclient.NewClient().RequestResponse(n.client, http.MethodPost, []string{httpclient.ContentTypeApplicationXml}, - url, message, &respMessage, xml.Marshal, xml.Unmarshal) + n.address, message, &respMessage, xml.Marshal, xml.Unmarshal) if err != nil { return nil, fmt.Errorf("NCIP message exchange failed: %s", err.Error()) } diff --git a/broker/ncipclient/ncipclient_test.go b/broker/ncipclient/ncipclient_test.go index 30cc30d5..578605a3 100644 --- a/broker/ncipclient/ncipclient_test.go +++ b/broker/ncipclient/ncipclient_test.go @@ -33,175 +33,66 @@ func TestMain(m *testing.M) { os.Exit(m.Run()) } -func TestPrepareHeaderEmpty(t *testing.T) { - ncipClient := NcipClientImpl{} - ncipData := make(map[string]any) - - header := ncipClient.prepareHeader(ncipData, nil) - assert.Equal(t, "default-from-agency", header.FromAgencyId.AgencyId.Text) - assert.Equal(t, "default-to-agency", header.ToAgencyId.AgencyId.Text) - assert.Equal(t, "", header.FromAgencyAuthentication) +func createTestClient() NcipClient { + return NewNcipClient(http.DefaultClient, + "http://localhost:"+os.Getenv("HTTP_PORT")+"/ncip", + "ILL-MOCK", + "ILL-MOCK", + "pass").(*NcipClientImpl) } func TestPrepareHeaderValues(t *testing.T) { ncipClient := NcipClientImpl{} - ncipData := make(map[string]any) - ncipData["to_agency"] = "ILL-MOCK1" - ncipData["from_agency"] = "ILL-MOCK2" - ncipData["from_agency_authentication"] = "pass" + ncipClient.client = http.DefaultClient + ncipClient.fromAgency = "ILL-MOCK2" + ncipClient.toAgency = "ILL-MOCK1" + ncipClient.fromAgencyAuthentication = "pass" - header := ncipClient.prepareHeader(ncipData, nil) + header := ncipClient.prepareHeader(nil) assert.Equal(t, "ILL-MOCK1", header.ToAgencyId.AgencyId.Text) assert.Equal(t, "ILL-MOCK2", header.FromAgencyId.AgencyId.Text) assert.Equal(t, "pass", header.FromAgencyAuthentication) } func TestLookupUserAutoOK(t *testing.T) { - ncipClient := CreateNcipClient(http.DefaultClient) - - customData := make(map[string]any) - ncipData := make(map[string]any) - ncipData["lookup_user_mode"] = "auto" - ncipData["from_agency"] = "ILL-MOCK" - ncipData["from_agency_authentication"] = "pass" - ncipData["to_agency"] = "ILL-MOCK" - ncipData["address"] = "http://localhost:" + os.Getenv("HTTP_PORT") + "/ncip" - customData["ncip"] = ncipData - + ncipClient := createTestClient() lookup := ncip.LookupUser{ UserId: &ncip.UserId{ UserIdentifierValue: "validuser", }, } - b, err := ncipClient.LookupUser(customData, lookup) + res, err := ncipClient.LookupUser(lookup) assert.NoError(t, err) - assert.True(t, b) + assert.NotNil(t, res) } func TestLookupUserAutoInvalidUser(t *testing.T) { - ncipClient := CreateNcipClient(http.DefaultClient) - - customData := make(map[string]any) - ncipData := make(map[string]any) - ncipData["lookup_user_mode"] = "auto" - ncipData["from_agency"] = "ILL-MOCK" - ncipData["from_agency_authentication"] = "pass" - ncipData["to_agency"] = "ILL-MOCK" - ncipData["address"] = "http://localhost:" + os.Getenv("HTTP_PORT") + "/ncip" - customData["ncip"] = ncipData - + ncipClient := createTestClient() lookup := ncip.LookupUser{ UserId: &ncip.UserId{ UserIdentifierValue: "foo", }, } - _, err := ncipClient.LookupUser(customData, lookup) + _, err := ncipClient.LookupUser(lookup) assert.Error(t, err) assert.Equal(t, "NCIP user lookup failed: Unknown User: foo", err.Error()) } -func TestLookupUserModeManual(t *testing.T) { - ncipClient := CreateNcipClient(http.DefaultClient) - - customData := make(map[string]any) - ncipData := make(map[string]any) - ncipData["lookup_user_mode"] = "manual" - customData["ncip"] = ncipData - - lookup := ncip.LookupUser{ - UserId: &ncip.UserId{ - UserIdentifierValue: "validuser", - }, - } - b, err := ncipClient.LookupUser(customData, lookup) - assert.NoError(t, err) - assert.False(t, b) -} - -func TestLookupUserModeDisabled(t *testing.T) { - ncipClient := CreateNcipClient(http.DefaultClient) - - customData := make(map[string]any) - ncipData := make(map[string]any) - ncipData["lookup_user_mode"] = "disabled" - customData["ncip"] = ncipData - - lookup := ncip.LookupUser{ - UserId: &ncip.UserId{ - UserIdentifierValue: "validuser", - }, - } - b, err := ncipClient.LookupUser(customData, lookup) - assert.NoError(t, err) - assert.True(t, b) -} - func TestLookupUserMissingAddress(t *testing.T) { - ncipClient := CreateNcipClient(http.DefaultClient) - - customData := make(map[string]any) - ncipData := make(map[string]any) - ncipData["lookup_user_mode"] = "auto" - ncipData["from_agency"] = "ILL-MOCK" - ncipData["to_agency"] = "ILL-MOCK" - customData["ncip"] = ncipData - - lookup := ncip.LookupUser{ - UserId: &ncip.UserId{ - UserIdentifierValue: "validuser", - }, - } - _, err := ncipClient.LookupUser(customData, lookup) - assert.Error(t, err) - assert.Equal(t, "missing NCIP address in customData", err.Error()) -} - -func TestLookupUserMissingNcipInfo(t *testing.T) { - ncipClient := CreateNcipClient(http.DefaultClient) - customData := make(map[string]any) - lookup := ncip.LookupUser{} - _, err := ncipClient.LookupUser(customData, lookup) - assert.Error(t, err) - assert.Equal(t, "missing ncip configuration in customData", err.Error()) -} - -func TestLookupUserMissingAuthUserInfo(t *testing.T) { - ncipClient := CreateNcipClient(http.DefaultClient) - - customData := make(map[string]any) - ncipData := make(map[string]any) - ncipData["from_agency"] = "ILL-MOCK" - ncipData["to_agency"] = "ILL-MOCK" - customData["ncip"] = ncipData - - lookup := ncip.LookupUser{ - UserId: &ncip.UserId{ - UserIdentifierValue: "validuser", - }, - } - _, err := ncipClient.LookupUser(customData, lookup) - assert.Error(t, err) - assert.Equal(t, "missing lookup_user_mode in ncip configuration", err.Error()) -} - -func TestLookupUserBadMode(t *testing.T) { - ncipClient := CreateNcipClient(http.DefaultClient) - - customData := make(map[string]any) - ncipData := make(map[string]any) - ncipData["lookup_user_mode"] = "foo" - ncipData["from_agency"] = "ILL-MOCK" - ncipData["to_agency"] = "ILL-MOCK" - customData["ncip"] = ncipData + ncipClient := NcipClientImpl{} + ncipClient.client = http.DefaultClient + ncipClient.fromAgency = "ILL-MOCK" + ncipClient.toAgency = "ILL-MOCK" lookup := ncip.LookupUser{ UserId: &ncip.UserId{ UserIdentifierValue: "validuser", }, } - _, err := ncipClient.LookupUser(customData, lookup) + res, err := ncipClient.LookupUser(lookup) assert.Error(t, err) - assert.Equal(t, "unknown value for lookup_user_mode: foo", err.Error()) + assert.Equal(t, "missing NCIP address in configuration", err.Error()) + assert.Nil(t, res) } func TestBadNcipMessageResponse(t *testing.T) { @@ -214,25 +105,19 @@ func TestBadNcipMessageResponse(t *testing.T) { server := httptest.NewServer(handler) defer server.Close() - ncipClient := CreateNcipClient(http.DefaultClient) - customData := make(map[string]any) - ncipData := make(map[string]any) - ncipData["lookup_user_mode"] = "auto" - ncipData["accept_item_mode"] = "auto" - ncipData["request_item_mode"] = "auto" - ncipData["create_user_fiscal_transaction_mode"] = "auto" - ncipData["from_agency"] = "ILL-MOCK" - ncipData["from_agency_authentication"] = "pass" - ncipData["to_agency"] = "ILL-MOCK" - ncipData["address"] = server.URL - customData["ncip"] = ncipData + ncipClient := NcipClientImpl{} + ncipClient.client = http.DefaultClient + ncipClient.fromAgency = "ILL-MOCK" + ncipClient.fromAgencyAuthentication = "pass" + ncipClient.toAgency = "ILL-MOCK" + ncipClient.address = server.URL lookup := ncip.LookupUser{ UserId: &ncip.UserId{ UserIdentifierValue: "validuser", }, } - _, err := ncipClient.LookupUser(customData, lookup) + _, err := ncipClient.LookupUser(lookup) assert.Error(t, err) assert.Contains(t, err.Error(), "NCIP message exchange failed:") @@ -241,37 +126,37 @@ func TestBadNcipMessageResponse(t *testing.T) { RequestIdentifierValue: "validrequest", }, } - _, err = ncipClient.AcceptItem(customData, accept) + _, err = ncipClient.AcceptItem(accept) assert.Error(t, err) assert.Contains(t, err.Error(), "NCIP message exchange failed:") delete := ncip.DeleteItem{} - err = ncipClient.DeleteItem(customData, delete) + _, err = ncipClient.DeleteItem(delete) assert.Error(t, err) assert.Contains(t, err.Error(), "NCIP message exchange failed:") request := ncip.RequestItem{} - _, err = ncipClient.RequestItem(customData, request) + _, err = ncipClient.RequestItem(request) assert.Error(t, err) assert.Contains(t, err.Error(), "NCIP message exchange failed:") cancelRequest := ncip.CancelRequestItem{} - err = ncipClient.CancelRequestItem(customData, cancelRequest) + _, err = ncipClient.CancelRequestItem(cancelRequest) assert.Error(t, err) assert.Contains(t, err.Error(), "NCIP message exchange failed:") checkInItem := ncip.CheckInItem{} - err = ncipClient.CheckInItem(customData, checkInItem) + _, err = ncipClient.CheckInItem(checkInItem) assert.Error(t, err) assert.Contains(t, err.Error(), "NCIP message exchange failed:") checkOutItem := ncip.CheckOutItem{} - err = ncipClient.CheckOutItem(customData, checkOutItem) + _, err = ncipClient.CheckOutItem(checkOutItem) assert.Error(t, err) assert.Contains(t, err.Error(), "NCIP message exchange failed:") createUserFiscalTransaction := ncip.CreateUserFiscalTransaction{} - _, err = ncipClient.CreateUserFiscalTransaction(customData, createUserFiscalTransaction) + _, err = ncipClient.CreateUserFiscalTransaction(createUserFiscalTransaction) assert.Error(t, err) assert.Contains(t, err.Error(), "NCIP message exchange failed:") } @@ -293,25 +178,18 @@ func TestEmptyNcipResponse(t *testing.T) { server := httptest.NewServer(handler) defer server.Close() - ncipClient := CreateNcipClient(http.DefaultClient) - customData := make(map[string]any) - ncipData := make(map[string]any) - ncipData["lookup_user_mode"] = "auto" - ncipData["accept_item_mode"] = "auto" - ncipData["request_item_mode"] = "auto" - ncipData["create_user_fiscal_transaction_mode"] = "auto" - ncipData["from_agency"] = "ILL-MOCK" - ncipData["from_agency_authentication"] = "pass" - ncipData["to_agency"] = "ILL-MOCK" - ncipData["address"] = server.URL - customData["ncip"] = ncipData - + ncipClient := NcipClientImpl{} + ncipClient.client = http.DefaultClient + ncipClient.fromAgency = "ILL-MOCK" + ncipClient.fromAgencyAuthentication = "pass" + ncipClient.toAgency = "ILL-MOCK" + ncipClient.address = server.URL lookup := ncip.LookupUser{ UserId: &ncip.UserId{ UserIdentifierValue: "validuser", }, } - _, err := ncipClient.LookupUser(customData, lookup) + _, err := ncipClient.LookupUser(lookup) assert.Error(t, err) assert.Equal(t, "invalid NCIP response: missing LookupUserResponse", err.Error()) @@ -320,37 +198,37 @@ func TestEmptyNcipResponse(t *testing.T) { RequestIdentifierValue: "validrequest", }, } - _, err = ncipClient.AcceptItem(customData, accept) + _, err = ncipClient.AcceptItem(accept) assert.Error(t, err) assert.Contains(t, err.Error(), "invalid NCIP response: missing AcceptItemResponse") delete := ncip.DeleteItem{} - err = ncipClient.DeleteItem(customData, delete) + _, err = ncipClient.DeleteItem(delete) assert.Error(t, err) assert.Contains(t, err.Error(), "invalid NCIP response: missing DeleteItemResponse") request := ncip.RequestItem{} - _, err = ncipClient.RequestItem(customData, request) + _, err = ncipClient.RequestItem(request) assert.Error(t, err) assert.Contains(t, err.Error(), "invalid NCIP response: missing RequestItemResponse") cancelRequest := ncip.CancelRequestItem{} - err = ncipClient.CancelRequestItem(customData, cancelRequest) + _, err = ncipClient.CancelRequestItem(cancelRequest) assert.Error(t, err) assert.Contains(t, err.Error(), "invalid NCIP response: missing CancelRequestItemResponse") checkInItem := ncip.CheckInItem{} - err = ncipClient.CheckInItem(customData, checkInItem) + _, err = ncipClient.CheckInItem(checkInItem) assert.Error(t, err) assert.Contains(t, err.Error(), "invalid NCIP response: missing CheckInItemResponse") checkOutItem := ncip.CheckOutItem{} - err = ncipClient.CheckOutItem(customData, checkOutItem) + _, err = ncipClient.CheckOutItem(checkOutItem) assert.Error(t, err) assert.Contains(t, err.Error(), "invalid NCIP response: missing CheckOutItemResponse") createUserFiscalTransaction := ncip.CreateUserFiscalTransaction{} - _, err = ncipClient.CreateUserFiscalTransaction(customData, createUserFiscalTransaction) + _, err = ncipClient.CreateUserFiscalTransaction(createUserFiscalTransaction) assert.Error(t, err) assert.Contains(t, err.Error(), "invalid NCIP response: missing CreateUserFiscalTransactionResponse") } @@ -383,38 +261,30 @@ func TestLookupUserProblemResponse(t *testing.T) { server := httptest.NewServer(handler) defer server.Close() - ncipClient := CreateNcipClient(http.DefaultClient) - customData := make(map[string]any) - ncipData := make(map[string]any) - ncipData["lookup_user_mode"] = "auto" - ncipData["from_agency"] = "ILL-MOCK" - ncipData["from_agency_authentication"] = "pass" - ncipData["to_agency"] = "ILL-MOCK" - ncipData["address"] = server.URL - customData["ncip"] = ncipData + ncipClient := NcipClientImpl{} + ncipClient.client = http.DefaultClient + ncipClient.fromAgency = "ILL-MOCK" + ncipClient.fromAgencyAuthentication = "pass" + ncipClient.toAgency = "ILL-MOCK" + ncipClient.address = server.URL lookup := ncip.LookupUser{ UserId: &ncip.UserId{ UserIdentifierValue: "validuser", }, } - _, err := ncipClient.LookupUser(customData, lookup) + _, err := ncipClient.LookupUser(lookup) assert.Error(t, err) assert.Equal(t, "NCIP message processing failed: Some Problem: Details about the problem", err.Error()) } func TestAcceptItemOK(t *testing.T) { - ncipClient := CreateNcipClient(http.DefaultClient) - - customData := make(map[string]any) - ncipData := make(map[string]any) - ncipData["accept_item_mode"] = "auto" - ncipData["from_agency"] = "ILL-MOCK" - ncipData["from_agency_authentication"] = "pass" - ncipData["to_agency"] = "ILL-MOCK" - ncipData["address"] = "http://localhost:" + os.Getenv("HTTP_PORT") + "/ncip" - customData["ncip"] = ncipData - + ncipClient := NcipClientImpl{} + ncipClient.client = http.DefaultClient + ncipClient.fromAgency = "ILL-MOCK" + ncipClient.fromAgencyAuthentication = "pass" + ncipClient.toAgency = "ILL-MOCK" + ncipClient.address = "http://localhost:" + os.Getenv("HTTP_PORT") + "/ncip" accept := ncip.AcceptItem{ UserId: &ncip.UserId{ UserIdentifierValue: "validuser", @@ -423,23 +293,18 @@ func TestAcceptItemOK(t *testing.T) { RequestIdentifierValue: "validrequest", }, } - b, err := ncipClient.AcceptItem(customData, accept) + res, err := ncipClient.AcceptItem(accept) assert.NoError(t, err) - assert.True(t, b) + assert.NotNil(t, res) } func TestAcceptItemInvalidUser(t *testing.T) { - ncipClient := CreateNcipClient(http.DefaultClient) - - customData := make(map[string]any) - ncipData := make(map[string]any) - ncipData["accept_item_mode"] = "auto" - ncipData["from_agency"] = "ILL-MOCK" - ncipData["from_agency_authentication"] = "pass" - ncipData["to_agency"] = "ILL-MOCK" - ncipData["address"] = "http://localhost:" + os.Getenv("HTTP_PORT") + "/ncip" - customData["ncip"] = ncipData - + ncipClient := NcipClientImpl{} + ncipClient.client = http.DefaultClient + ncipClient.fromAgency = "ILL-MOCK" + ncipClient.fromAgencyAuthentication = "pass" + ncipClient.toAgency = "ILL-MOCK" + ncipClient.address = "http://localhost:" + os.Getenv("HTTP_PORT") + "/ncip" accept := ncip.AcceptItem{ UserId: &ncip.UserId{ UserIdentifierValue: "foo", @@ -448,77 +313,32 @@ func TestAcceptItemInvalidUser(t *testing.T) { RequestIdentifierValue: "validrequest", }, } - _, err := ncipClient.AcceptItem(customData, accept) + _, err := ncipClient.AcceptItem(accept) assert.Error(t, err) assert.Equal(t, "NCIP accept item failed: Unknown User: foo", err.Error()) } -func TestAcceptItemModeManual(t *testing.T) { - ncipClient := CreateNcipClient(http.DefaultClient) - - customData := make(map[string]any) - ncipData := make(map[string]any) - ncipData["accept_item_mode"] = "manual" - customData["ncip"] = ncipData - - lookup := ncip.AcceptItem{ - UserId: &ncip.UserId{ - UserIdentifierValue: "validuser", - }, - RequestId: ncip.RequestId{ - RequestIdentifierValue: "validrequest", - }, - } - b, err := ncipClient.AcceptItem(customData, lookup) - assert.NoError(t, err) - assert.False(t, b) -} - -func TestAcceptItemMissingNcipInfo(t *testing.T) { - ncipClient := CreateNcipClient(http.DefaultClient) - customData := make(map[string]any) - accept := ncip.AcceptItem{} - _, err := ncipClient.AcceptItem(customData, accept) - assert.Error(t, err) - assert.Equal(t, "missing ncip configuration in customData", err.Error()) -} - func TestDeleteItemOK(t *testing.T) { - ncipClient := CreateNcipClient(http.DefaultClient) - - customData := make(map[string]any) - ncipData := make(map[string]any) - ncipData["from_agency"] = "ILL-MOCK" - ncipData["from_agency_authentication"] = "pass" - ncipData["to_agency"] = "ILL-MOCK" - ncipData["address"] = "http://localhost:" + os.Getenv("HTTP_PORT") + "/ncip" - customData["ncip"] = ncipData + ncipClient := NcipClientImpl{} + ncipClient.client = http.DefaultClient + ncipClient.fromAgency = "ILL-MOCK" + ncipClient.fromAgencyAuthentication = "pass" + ncipClient.toAgency = "ILL-MOCK" + ncipClient.address = "http://localhost:" + os.Getenv("HTTP_PORT") + "/ncip" delete := ncip.DeleteItem{} - err := ncipClient.DeleteItem(customData, delete) + res, err := ncipClient.DeleteItem(delete) assert.NoError(t, err) -} - -func TestDeleteItemMissingNcipInfo(t *testing.T) { - ncipClient := CreateNcipClient(http.DefaultClient) - customData := make(map[string]any) - delete := ncip.DeleteItem{} - err := ncipClient.DeleteItem(customData, delete) - assert.Error(t, err) - assert.Equal(t, "missing ncip configuration in customData", err.Error()) + assert.NotNil(t, res) } func TestRequestItemOK(t *testing.T) { - ncipClient := CreateNcipClient(http.DefaultClient) - - customData := make(map[string]any) - ncipData := make(map[string]any) - ncipData["request_item_mode"] = "auto" - ncipData["from_agency"] = "ILL-MOCK" - ncipData["from_agency_authentication"] = "pass" - ncipData["to_agency"] = "ILL-MOCK" - ncipData["address"] = "http://localhost:" + os.Getenv("HTTP_PORT") + "/ncip" - customData["ncip"] = ncipData + ncipClient := NcipClientImpl{} + ncipClient.client = http.DefaultClient + ncipClient.fromAgency = "ILL-MOCK" + ncipClient.fromAgencyAuthentication = "pass" + ncipClient.toAgency = "ILL-MOCK" + ncipClient.address = "http://localhost:" + os.Getenv("HTTP_PORT") + "/ncip" request := ncip.RequestItem{ UserId: &ncip.UserId{ @@ -534,48 +354,17 @@ func TestRequestItemOK(t *testing.T) { Text: "Hold", }, } - b, err := ncipClient.RequestItem(customData, request) - assert.NoError(t, err) - assert.True(t, b) -} - -func TestRequestItemModeManual(t *testing.T) { - ncipClient := CreateNcipClient(http.DefaultClient) - - customData := make(map[string]any) - ncipData := make(map[string]any) - ncipData["request_item_mode"] = "manual" - customData["ncip"] = ncipData - - lookup := ncip.RequestItem{ - UserId: &ncip.UserId{ - UserIdentifierValue: "validuser", - }, - } - b, err := ncipClient.RequestItem(customData, lookup) + _, err := ncipClient.RequestItem(request) assert.NoError(t, err) - assert.False(t, b) -} - -func TestRequestItemMissingNcipInfo(t *testing.T) { - ncipClient := CreateNcipClient(http.DefaultClient) - customData := make(map[string]any) - request := ncip.RequestItem{} - _, err := ncipClient.RequestItem(customData, request) - assert.Error(t, err) - assert.Equal(t, "missing ncip configuration in customData", err.Error()) } func TestCancelRequestItemOK(t *testing.T) { - ncipClient := CreateNcipClient(http.DefaultClient) - - customData := make(map[string]any) - ncipData := make(map[string]any) - ncipData["from_agency"] = "ILL-MOCK" - ncipData["from_agency_authentication"] = "pass" - ncipData["to_agency"] = "ILL-MOCK" - ncipData["address"] = "http://localhost:" + os.Getenv("HTTP_PORT") + "/ncip" - customData["ncip"] = ncipData + ncipClient := NcipClientImpl{} + ncipClient.client = http.DefaultClient + ncipClient.fromAgency = "ILL-MOCK" + ncipClient.fromAgencyAuthentication = "pass" + ncipClient.toAgency = "ILL-MOCK" + ncipClient.address = "http://localhost:" + os.Getenv("HTTP_PORT") + "/ncip" request := ncip.CancelRequestItem{ UserId: &ncip.UserId{ @@ -588,58 +377,35 @@ func TestCancelRequestItemOK(t *testing.T) { Text: "Hold", }, } - err := ncipClient.CancelRequestItem(customData, request) + res, err := ncipClient.CancelRequestItem(request) assert.NoError(t, err) -} - -func TestCancelRequestItemMissingNcipInfo(t *testing.T) { - ncipClient := CreateNcipClient(http.DefaultClient) - customData := make(map[string]any) - request := ncip.CancelRequestItem{} - err := ncipClient.CancelRequestItem(customData, request) - assert.Error(t, err) - assert.Equal(t, "missing ncip configuration in customData", err.Error()) + assert.NotNil(t, res) } func TestCheckInItemOK(t *testing.T) { - ncipClient := CreateNcipClient(http.DefaultClient) - - customData := make(map[string]any) - ncipData := make(map[string]any) - ncipData["from_agency"] = "ILL-MOCK" - ncipData["from_agency_authentication"] = "pass" - ncipData["to_agency"] = "ILL-MOCK" - ncipData["address"] = "http://localhost:" + os.Getenv("HTTP_PORT") + "/ncip" - customData["ncip"] = ncipData - + ncipClient := NcipClientImpl{} + ncipClient.client = http.DefaultClient + ncipClient.fromAgency = "ILL-MOCK" + ncipClient.fromAgencyAuthentication = "pass" + ncipClient.toAgency = "ILL-MOCK" + ncipClient.address = "http://localhost:" + os.Getenv("HTTP_PORT") + "/ncip" request := ncip.CheckInItem{ ItemId: ncip.ItemId{ ItemIdentifierValue: "item-001", }, } - err := ncipClient.CheckInItem(customData, request) + res, err := ncipClient.CheckInItem(request) assert.NoError(t, err) -} - -func TestCheckInItemMissingNcipInfo(t *testing.T) { - ncipClient := CreateNcipClient(http.DefaultClient) - customData := make(map[string]any) - request := ncip.CheckInItem{} - err := ncipClient.CheckInItem(customData, request) - assert.Error(t, err) - assert.Equal(t, "missing ncip configuration in customData", err.Error()) + assert.NotNil(t, res) } func TestCheckOutItemOK(t *testing.T) { - ncipClient := CreateNcipClient(http.DefaultClient) - - customData := make(map[string]any) - ncipData := make(map[string]any) - ncipData["from_agency"] = "ILL-MOCK" - ncipData["from_agency_authentication"] = "pass" - ncipData["to_agency"] = "ILL-MOCK" - ncipData["address"] = "http://localhost:" + os.Getenv("HTTP_PORT") + "/ncip" - customData["ncip"] = ncipData + ncipClient := NcipClientImpl{} + ncipClient.client = http.DefaultClient + ncipClient.fromAgency = "ILL-MOCK" + ncipClient.fromAgencyAuthentication = "pass" + ncipClient.toAgency = "ILL-MOCK" + ncipClient.address = "http://localhost:" + os.Getenv("HTTP_PORT") + "/ncip" request := ncip.CheckOutItem{ UserId: &ncip.UserId{ @@ -649,30 +415,17 @@ func TestCheckOutItemOK(t *testing.T) { ItemIdentifierValue: "item-001", }, } - err := ncipClient.CheckOutItem(customData, request) + _, err := ncipClient.CheckOutItem(request) assert.NoError(t, err) } -func TestCheckOutItemMissingNcipInfo(t *testing.T) { - ncipClient := CreateNcipClient(http.DefaultClient) - customData := make(map[string]any) - request := ncip.CheckOutItem{} - err := ncipClient.CheckOutItem(customData, request) - assert.Error(t, err) - assert.Equal(t, "missing ncip configuration in customData", err.Error()) -} - func TestCreateUserFiscalTransactionOK(t *testing.T) { - ncipClient := CreateNcipClient(http.DefaultClient) - - customData := make(map[string]any) - ncipData := make(map[string]any) - ncipData["create_user_fiscal_transaction_mode"] = "auto" - ncipData["from_agency"] = "ILL-MOCK" - ncipData["from_agency_authentication"] = "pass" - ncipData["to_agency"] = "ILL-MOCK" - ncipData["address"] = "http://localhost:" + os.Getenv("HTTP_PORT") + "/ncip" - customData["ncip"] = ncipData + ncipClient := NcipClientImpl{} + ncipClient.client = http.DefaultClient + ncipClient.fromAgency = "ILL-MOCK" + ncipClient.fromAgencyAuthentication = "pass" + ncipClient.toAgency = "ILL-MOCK" + ncipClient.address = "http://localhost:" + os.Getenv("HTTP_PORT") + "/ncip" lookup := ncip.CreateUserFiscalTransaction{ UserId: &ncip.UserId{ @@ -685,36 +438,7 @@ func TestCreateUserFiscalTransactionOK(t *testing.T) { }, }, } - b, err := ncipClient.CreateUserFiscalTransaction(customData, lookup) + res, err := ncipClient.CreateUserFiscalTransaction(lookup) assert.NoError(t, err) - assert.True(t, b) -} - -func TestCreateUserFiscalTransactionBadMode(t *testing.T) { - ncipClient := CreateNcipClient(http.DefaultClient) - - customData := make(map[string]any) - ncipData := make(map[string]any) - ncipData["create_user_fiscal_transaction_mode"] = "foo" - ncipData["from_agency"] = "ILL-MOCK" - ncipData["to_agency"] = "ILL-MOCK" - customData["ncip"] = ncipData - - lookup := ncip.CreateUserFiscalTransaction{ - UserId: &ncip.UserId{ - UserIdentifierValue: "validuser", - }, - } - _, err := ncipClient.CreateUserFiscalTransaction(customData, lookup) - assert.Error(t, err) - assert.Equal(t, "unknown value for create_user_fiscal_transaction_mode: foo", err.Error()) -} - -func TestCreateUserFiscalTransactionMissingNcipInfo(t *testing.T) { - ncipClient := CreateNcipClient(http.DefaultClient) - customData := make(map[string]any) - request := ncip.CreateUserFiscalTransaction{} - _, err := ncipClient.CreateUserFiscalTransaction(customData, request) - assert.Error(t, err) - assert.Equal(t, "missing ncip configuration in customData", err.Error()) + assert.NotNil(t, res) } diff --git a/broker/patron_request/api/api-handler.go b/broker/patron_request/api/api-handler.go index 64a737ab..39a4049d 100644 --- a/broker/patron_request/api/api-handler.go +++ b/broker/patron_request/api/api-handler.go @@ -23,13 +23,15 @@ type PatronRequestApiHandler struct { prRepo pr_db.PrRepo eventBus events.EventBus actionMappingService prservice.ActionMappingService + tenant common.Tenant } -func NewApiHandler(prRepo pr_db.PrRepo, eventBus events.EventBus) PatronRequestApiHandler { +func NewApiHandler(prRepo pr_db.PrRepo, eventBus events.EventBus, tenant common.Tenant) PatronRequestApiHandler { return PatronRequestApiHandler{ prRepo: prRepo, eventBus: eventBus, actionMappingService: prservice.ActionMappingService{}, + tenant: tenant, } } @@ -60,12 +62,12 @@ func (a *PatronRequestApiHandler) PostPatronRequests(w http.ResponseWriter, r *h addBadRequestError(ctx, w, err) return } - tenant := params.XOkapiTenant - if tenant == nil { - addBadRequestError(ctx, w, errors.New("X-Okapi-Tenant header is required")) + dbreq, err := toDbPatronRequest(a.tenant, newPr, params.XOkapiTenant) + if err != nil { + addBadRequestError(ctx, w, err) return } - pr, err := a.prRepo.SavePatronRequest(ctx, (pr_db.SavePatronRequestParams)(toDbPatronRequest(newPr, *tenant))) + pr, err := a.prRepo.SavePatronRequest(ctx, (pr_db.SavePatronRequestParams)(dbreq)) if err != nil { addInternalError(ctx, w, err) return @@ -262,7 +264,14 @@ func toStringFromBytes(bytes []byte) *string { return value } -func toDbPatronRequest(request proapi.CreatePatronRequest, tenant string) pr_db.PatronRequest { +func toDbPatronRequest(tenantToSymbol common.Tenant, request proapi.CreatePatronRequest, tenant *string) (pr_db.PatronRequest, error) { + if tenant == nil { + return pr_db.PatronRequest{}, errors.New("X-Okapi-Tenant header is required") + } + if tenantToSymbol.IsSpecified() { + symbol := tenantToSymbol.GetSymbol(*tenant) + request.RequesterSymbol = &symbol + } var illRequest []byte if request.IllRequest != nil { illRequest = []byte(*request.IllRequest) @@ -276,8 +285,8 @@ func toDbPatronRequest(request proapi.CreatePatronRequest, tenant string) pr_db. RequesterSymbol: getDbText(request.RequesterSymbol), SupplierSymbol: getDbText(request.SupplierSymbol), IllRequest: illRequest, - Tenant: getDbText(&tenant), - } + Tenant: getDbText(tenant), + }, nil } func getId(id string) string { diff --git a/broker/patron_request/api/api-handler_test.go b/broker/patron_request/api/api-handler_test.go index 4adb9174..7cb37807 100644 --- a/broker/patron_request/api/api-handler_test.go +++ b/broker/patron_request/api/api-handler_test.go @@ -36,7 +36,7 @@ func TestGetDbText(t *testing.T) { } func TestGetPatronRequests(t *testing.T) { - handler := NewApiHandler(new(PrRepoError), mockEventBus) + handler := NewApiHandler(new(PrRepoError), mockEventBus, common.NewTenant("")) req, _ := http.NewRequest("GET", "/", nil) rr := httptest.NewRecorder() handler.GetPatronRequests(rr, req) @@ -47,7 +47,7 @@ func TestGetPatronRequests(t *testing.T) { } func TestPostPatronRequests(t *testing.T) { - handler := NewApiHandler(new(PrRepoError), mockEventBus) + handler := NewApiHandler(new(PrRepoError), mockEventBus, common.NewTenant("")) toCreate := proapi.PatronRequest{ID: "1"} jsonBytes, err := json.Marshal(toCreate) assert.NoError(t, err, "failed to marshal patron request") @@ -61,7 +61,7 @@ func TestPostPatronRequests(t *testing.T) { } func TestPostPatronRequestsMissingTenant(t *testing.T) { - handler := NewApiHandler(new(PrRepoError), mockEventBus) + handler := NewApiHandler(new(PrRepoError), mockEventBus, common.NewTenant("")) toCreate := proapi.PatronRequest{ID: "1"} jsonBytes, err := json.Marshal(toCreate) assert.NoError(t, err, "failed to marshal patron request") @@ -74,7 +74,7 @@ func TestPostPatronRequestsMissingTenant(t *testing.T) { } func TestPostPatronRequestsInvalidJson(t *testing.T) { - handler := NewApiHandler(new(PrRepoError), mockEventBus) + handler := NewApiHandler(new(PrRepoError), mockEventBus, common.NewTenant("")) req, _ := http.NewRequest("POST", "/", bytes.NewBuffer([]byte("a\": v\""))) rr := httptest.NewRecorder() tenant := proapi.Tenant("test-lib") @@ -84,7 +84,7 @@ func TestPostPatronRequestsInvalidJson(t *testing.T) { } func TestDeletePatronRequestsIdNotFound(t *testing.T) { - handler := NewApiHandler(new(PrRepoError), mockEventBus) + handler := NewApiHandler(new(PrRepoError), mockEventBus, common.NewTenant("")) req, _ := http.NewRequest("POST", "/", nil) rr := httptest.NewRecorder() handler.DeletePatronRequestsId(rr, req, "2", proapi.DeletePatronRequestsIdParams{}) @@ -92,7 +92,7 @@ func TestDeletePatronRequestsIdNotFound(t *testing.T) { } func TestDeletePatronRequestsId(t *testing.T) { - handler := NewApiHandler(new(PrRepoError), mockEventBus) + handler := NewApiHandler(new(PrRepoError), mockEventBus, common.NewTenant("")) req, _ := http.NewRequest("POST", "/", nil) rr := httptest.NewRecorder() handler.DeletePatronRequestsId(rr, req, "3", proapi.DeletePatronRequestsIdParams{}) @@ -101,7 +101,7 @@ func TestDeletePatronRequestsId(t *testing.T) { } func TestGetPatronRequestsIdNotFound(t *testing.T) { - handler := NewApiHandler(new(PrRepoError), mockEventBus) + handler := NewApiHandler(new(PrRepoError), mockEventBus, common.NewTenant("")) req, _ := http.NewRequest("POST", "/", nil) rr := httptest.NewRecorder() handler.GetPatronRequestsId(rr, req, "2", proapi.GetPatronRequestsIdParams{}) @@ -109,7 +109,7 @@ func TestGetPatronRequestsIdNotFound(t *testing.T) { } func TestGetPatronRequestsId(t *testing.T) { - handler := NewApiHandler(new(PrRepoError), mockEventBus) + handler := NewApiHandler(new(PrRepoError), mockEventBus, common.NewTenant("")) req, _ := http.NewRequest("POST", "/", nil) rr := httptest.NewRecorder() handler.GetPatronRequestsId(rr, req, "1", proapi.GetPatronRequestsIdParams{}) diff --git a/broker/patron_request/service/action.go b/broker/patron_request/service/action.go index 00f95f58..0f15c4b2 100644 --- a/broker/patron_request/service/action.go +++ b/broker/patron_request/service/action.go @@ -4,14 +4,16 @@ import ( "encoding/json" "encoding/xml" "errors" + "net/http" + "strings" + "github.com/indexdata/crosslink/broker/common" "github.com/indexdata/crosslink/broker/events" "github.com/indexdata/crosslink/broker/handler" - "github.com/indexdata/crosslink/broker/ill_db" + "github.com/indexdata/crosslink/broker/lms" pr_db "github.com/indexdata/crosslink/broker/patron_request/db" "github.com/indexdata/crosslink/iso18626" - "net/http" - "strings" + "github.com/jackc/pgx/v5/pgtype" ) const COMP = "pr_action_service" @@ -69,18 +71,18 @@ const ( type PatronRequestActionService struct { prRepo pr_db.PrRepo - illRepo ill_db.IllRepo eventBus events.EventBus iso18626Handler handler.Iso18626HandlerInterface + lmsCreator lms.LmsCreator actionMappingService ActionMappingService } -func CreatePatronRequestActionService(prRepo pr_db.PrRepo, illRepo ill_db.IllRepo, eventBus events.EventBus, iso18626Handler handler.Iso18626HandlerInterface) PatronRequestActionService { +func CreatePatronRequestActionService(prRepo pr_db.PrRepo, eventBus events.EventBus, iso18626Handler handler.Iso18626HandlerInterface, lmsCreator lms.LmsCreator) PatronRequestActionService { return PatronRequestActionService{ prRepo: prRepo, - illRepo: illRepo, eventBus: eventBus, iso18626Handler: iso18626Handler, + lmsCreator: lmsCreator, actionMappingService: ActionMappingService{}, } } @@ -178,9 +180,21 @@ func (a *PatronRequestActionService) checkSupplyingResponseAndUpdateState(ctx co } func (a *PatronRequestActionService) validateBorrowingRequest(ctx common.ExtendedContext, pr pr_db.PatronRequest) (events.EventStatus, *events.EventResult) { - if !pr.Tenant.Valid { - return events.LogErrorAndReturnResult(ctx, "missing tenant", nil) + patron := "" + if pr.Patron.Valid { + patron = pr.Patron.String + } + lmsAdapter, err := a.lmsCreator.GetAdapter(ctx, pr.RequesterSymbol) + if err != nil { + return events.LogErrorAndReturnResult(ctx, "failed to create LMS adapter", err) + } + userId, err := lmsAdapter.LookupUser(patron) + if err != nil { + return events.LogErrorAndReturnResult(ctx, "LMS LookupUser failed", err) } + // change patron to canonical user id + // perhaps it would be better to have both original and canonical id stored? + pr.Patron = pgtype.Text{String: userId, Valid: true} return a.updateStateAndReturnResult(ctx, pr, BorrowerStateValidated, nil) } @@ -225,7 +239,79 @@ func (a *PatronRequestActionService) sendBorrowingRequest(ctx common.ExtendedCon return a.updateStateAndReturnResult(ctx, pr, BorrowerStateSent, &result) } +// TODO : should be resolved pickup location +func pickupLocationFromIllRequest(illRequest iso18626.Request) string { + pickupLocation := "" + if len(illRequest.RequestedDeliveryInfo) > 0 { + address := illRequest.RequestedDeliveryInfo[0].Address + if address != nil { + pa := address.PhysicalAddress + if pa != nil { + pickupLocation = pa.Line1 + if pa.Line2 != "" { + pickupLocation += " " + pa.Line2 + } + if pa.Locality != "" { + pickupLocation += " " + pa.Locality + } + if pa.PostalCode != "" { + pickupLocation += " " + pa.PostalCode + } + if pa.Region != nil { + pickupLocation += " " + pa.Region.Text + } + if pa.Country != nil { + pickupLocation += " " + pa.Country.Text + } + } + } + } + return pickupLocation +} + +func callNumberFromIllRequest(illRequest iso18626.Request) string { + callNumber := "" + if len(illRequest.SupplierInfo) > 0 { + callNumber = illRequest.SupplierInfo[0].CallNumber + } + return callNumber +} + +func isbnFromIllRequest(illRequest iso18626.Request) string { + isbn := "" + if len(illRequest.BibliographicInfo.BibliographicItemId) > 0 && + illRequest.BibliographicInfo.BibliographicItemId[0].BibliographicItemIdentifierCode.Text == "ISBN" { + isbn = illRequest.BibliographicInfo.BibliographicItemId[0].BibliographicItemIdentifier + } + return isbn +} + func (a *PatronRequestActionService) receiveBorrowingRequest(ctx common.ExtendedContext, pr pr_db.PatronRequest) (events.EventStatus, *events.EventResult) { + patron := "" + if pr.Patron.Valid { + patron = pr.Patron.String + } + lmsAdapter, err := a.lmsCreator.GetAdapter(ctx, pr.RequesterSymbol) + if err != nil { + return events.LogErrorAndReturnResult(ctx, "failed to create LMS adapter", err) + } + var illRequest iso18626.Request + err = json.Unmarshal(pr.IllRequest, &illRequest) + if err != nil { + return events.LogErrorAndReturnResult(ctx, "failed to unmarshal ILL request", err) + } + itemId := illRequest.BibliographicInfo.SupplierUniqueRecordId + requestId := illRequest.Header.RequestingAgencyRequestId + author := illRequest.BibliographicInfo.Author + title := illRequest.BibliographicInfo.Title + isbn := isbnFromIllRequest(illRequest) + callNumber := callNumberFromIllRequest(illRequest) + pickupLocation := pickupLocationFromIllRequest(illRequest) + requestedAction := "Hold For Pickup" + err = lmsAdapter.AcceptItem(itemId, requestId, patron, author, title, isbn, callNumber, pickupLocation, requestedAction) + if err != nil { + return events.LogErrorAndReturnResult(ctx, "LMS AcceptItem failed", err) + } result := events.EventResult{} status, eventResult, httpStatus := a.sendRequestingAgencyMessage(ctx, pr, &result, iso18626.TypeActionReceived) if httpStatus == nil { @@ -239,16 +325,62 @@ func (a *PatronRequestActionService) receiveBorrowingRequest(ctx common.Extended } func (a *PatronRequestActionService) checkoutBorrowingRequest(ctx common.ExtendedContext, pr pr_db.PatronRequest) (events.EventStatus, *events.EventResult) { - // TODO Make NCIP calls + patron := "" + if pr.Patron.Valid { + patron = pr.Patron.String + } + lmsAdapter, err := a.lmsCreator.GetAdapter(ctx, pr.RequesterSymbol) + if err != nil { + return events.LogErrorAndReturnResult(ctx, "failed to create LMS adapter", err) + } + var illRequest iso18626.Request + err = json.Unmarshal(pr.IllRequest, &illRequest) + if err != nil { + return events.LogErrorAndReturnResult(ctx, "failed to unmarshal ILL request", err) + } + requestId := illRequest.Header.RequestingAgencyRequestId + itemId := illRequest.BibliographicInfo.SupplierUniqueRecordId + borrowerBarcode := patron + err = lmsAdapter.CheckOutItem(requestId, itemId, borrowerBarcode, "externalReferenceValue") + if err != nil { + return events.LogErrorAndReturnResult(ctx, "LMS CheckOutItem failed", err) + } return a.updateStateAndReturnResult(ctx, pr, BorrowerStateCheckedOut, nil) } func (a *PatronRequestActionService) checkinBorrowingRequest(ctx common.ExtendedContext, pr pr_db.PatronRequest) (events.EventStatus, *events.EventResult) { - // TODO Make NCIP calls + lmsAdapter, err := a.lmsCreator.GetAdapter(ctx, pr.RequesterSymbol) + if err != nil { + return events.LogErrorAndReturnResult(ctx, "failed to create LMS adapter", err) + } + var illRequest iso18626.Request + err = json.Unmarshal(pr.IllRequest, &illRequest) + if err != nil { + return events.LogErrorAndReturnResult(ctx, "failed to unmarshal ILL request", err) + } + itemId := illRequest.BibliographicInfo.SupplierUniqueRecordId + err = lmsAdapter.CheckInItem(itemId) + if err != nil { + return events.LogErrorAndReturnResult(ctx, "LMS CheckInItem failed", err) + } return a.updateStateAndReturnResult(ctx, pr, BorrowerStateCheckedIn, nil) } func (a *PatronRequestActionService) shipReturnBorrowingRequest(ctx common.ExtendedContext, pr pr_db.PatronRequest) (events.EventStatus, *events.EventResult) { + lmsAdapter, err := a.lmsCreator.GetAdapter(ctx, pr.RequesterSymbol) + if err != nil { + return events.LogErrorAndReturnResult(ctx, "failed to create LMS adapter", err) + } + var illRequest iso18626.Request + err = json.Unmarshal(pr.IllRequest, &illRequest) + if err != nil { + return events.LogErrorAndReturnResult(ctx, "failed to unmarshal ILL request", err) + } + itemId := illRequest.BibliographicInfo.SupplierUniqueRecordId + err = lmsAdapter.DeleteItem(itemId) + if err != nil { + return events.LogErrorAndReturnResult(ctx, "LMS DeleteItem failed", err) + } result := events.EventResult{} status, eventResult, httpStatus := a.sendRequestingAgencyMessage(ctx, pr, &result, iso18626.TypeActionShippedReturn) if httpStatus == nil { diff --git a/broker/patron_request/service/action_test.go b/broker/patron_request/service/action_test.go index 3155dbc3..f834d114 100644 --- a/broker/patron_request/service/action_test.go +++ b/broker/patron_request/service/action_test.go @@ -4,18 +4,19 @@ import ( "context" "encoding/xml" "errors" + "net/http" + "strings" + "testing" + "github.com/indexdata/crosslink/broker/common" "github.com/indexdata/crosslink/broker/events" "github.com/indexdata/crosslink/broker/handler" - "github.com/indexdata/crosslink/broker/ill_db" + "github.com/indexdata/crosslink/broker/lms" pr_db "github.com/indexdata/crosslink/broker/patron_request/db" "github.com/indexdata/crosslink/iso18626" "github.com/jackc/pgx/v5/pgtype" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" - "net/http" - "strings" - "testing" ) var appCtx = common.CreateExtCtxWithArgs(context.Background(), nil) @@ -25,7 +26,7 @@ var actionValidate = BorrowerActionValidate func TestInvokeAction(t *testing.T) { mockEventBus := new(MockEventBus) - prAction := CreatePatronRequestActionService(*new(pr_db.PrRepo), *new(ill_db.IllRepo), mockEventBus, new(handler.Iso18626Handler)) + prAction := CreatePatronRequestActionService(*new(pr_db.PrRepo), mockEventBus, new(handler.Iso18626Handler), nil) event := events.Event{ ID: "action-1", } @@ -37,7 +38,7 @@ func TestInvokeAction(t *testing.T) { } func TestHandleInvokeActionNotSpecifiedAction(t *testing.T) { - prAction := CreatePatronRequestActionService(*new(pr_db.PrRepo), *new(ill_db.IllRepo), *new(events.EventBus), new(handler.Iso18626Handler)) + prAction := CreatePatronRequestActionService(*new(pr_db.PrRepo), *new(events.EventBus), new(handler.Iso18626Handler), nil) status, resultData := prAction.handleInvokeAction(appCtx, events.Event{}) @@ -47,7 +48,7 @@ func TestHandleInvokeActionNotSpecifiedAction(t *testing.T) { func TestHandleInvokeActionNoPR(t *testing.T) { mockPrRepo := new(MockPrRepo) - prAction := CreatePatronRequestActionService(mockPrRepo, *new(ill_db.IllRepo), *new(events.EventBus), new(handler.Iso18626Handler)) + prAction := CreatePatronRequestActionService(mockPrRepo, *new(events.EventBus), new(handler.Iso18626Handler), nil) mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{}, errors.New("not fund")) status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &actionValidate}}}) @@ -58,7 +59,7 @@ func TestHandleInvokeActionNoPR(t *testing.T) { func TestHandleInvokeActionNoPRSide(t *testing.T) { mockPrRepo := new(MockPrRepo) - prAction := CreatePatronRequestActionService(mockPrRepo, *new(ill_db.IllRepo), *new(events.EventBus), new(handler.Iso18626Handler)) + prAction := CreatePatronRequestActionService(mockPrRepo, *new(events.EventBus), new(handler.Iso18626Handler), nil) mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{State: BorrowerStateNew, Side: "helper"}, nil) status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &actionValidate}}}) @@ -69,7 +70,7 @@ func TestHandleInvokeActionNoPRSide(t *testing.T) { func TestHandleInvokeActionWhichIsNotAllowed(t *testing.T) { mockPrRepo := new(MockPrRepo) - prAction := CreatePatronRequestActionService(mockPrRepo, *new(ill_db.IllRepo), *new(events.EventBus), new(handler.Iso18626Handler)) + prAction := CreatePatronRequestActionService(mockPrRepo, *new(events.EventBus), new(handler.Iso18626Handler), nil) mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{State: BorrowerStateValidated, Side: SideBorrowing}, nil) status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &actionValidate}}}) @@ -78,10 +79,12 @@ func TestHandleInvokeActionWhichIsNotAllowed(t *testing.T) { assert.Equal(t, "state VALIDATED does not support action validate", resultData.EventError.Message) } -func TestHandleInvokeActionValidate(t *testing.T) { +func TestHandleInvokeActionValidateOK(t *testing.T) { mockPrRepo := new(MockPrRepo) - prAction := CreatePatronRequestActionService(mockPrRepo, new(MockIllRepo), *new(events.EventBus), new(handler.Iso18626Handler)) - mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{State: BorrowerStateNew, Side: SideBorrowing, Tenant: pgtype.Text{Valid: true, String: "testlib"}}, nil) + lmsCreator := new(MockLmsCreator) + lmsCreator.On("GetAdapter", pgtype.Text{Valid: true, String: "ISIL:x"}).Return(lms.CreateLmsAdapterMockOK(), nil) + prAction := CreatePatronRequestActionService(mockPrRepo, *new(events.EventBus), new(handler.Iso18626Handler), lmsCreator) + mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:x"}, State: BorrowerStateNew, Side: SideBorrowing, Tenant: pgtype.Text{Valid: true, String: "testlib"}}, nil) status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &actionValidate}}}) @@ -90,13 +93,40 @@ func TestHandleInvokeActionValidate(t *testing.T) { assert.Equal(t, BorrowerStateValidated, mockPrRepo.savedPr.State) } +func TestHandleInvokeActionValidateGetAdapterFailed(t *testing.T) { + mockPrRepo := new(MockPrRepo) + lmsCreator := new(MockLmsCreator) + lmsCreator.On("GetAdapter", pgtype.Text{Valid: true, String: "ISIL:x"}).Return(lms.CreateLmsAdapterMockOK(), assert.AnError) + prAction := CreatePatronRequestActionService(mockPrRepo, *new(events.EventBus), new(handler.Iso18626Handler), lmsCreator) + mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:x"}, State: BorrowerStateNew, Side: SideBorrowing, Tenant: pgtype.Text{Valid: true, String: "testlib"}}, nil) + + status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &actionValidate}}}) + + assert.Equal(t, events.EventStatusError, status) + assert.Equal(t, "failed to create LMS adapter", resultData.EventError.Message) + assert.Equal(t, "assert.AnError general error for testing", resultData.EventError.Cause) +} + +func TestHandleInvokeActionValidateLookupFailed(t *testing.T) { + mockPrRepo := new(MockPrRepo) + lmsCreator := new(MockLmsCreator) + lmsCreator.On("GetAdapter", pgtype.Text{Valid: true, String: "ISIL:REC1"}).Return(createLmsAdapterMockFail(), nil) + prAction := CreatePatronRequestActionService(mockPrRepo, *new(events.EventBus), new(handler.Iso18626Handler), lmsCreator) + mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:REC1"}, State: BorrowerStateNew, Side: SideBorrowing, Tenant: pgtype.Text{Valid: true, String: "testlib"}}, nil) + + status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &actionValidate}}}) + + assert.Equal(t, events.EventStatusError, status) + assert.Equal(t, "LMS LookupUser failed", resultData.EventError.Message) +} + func TestHandleInvokeActionSendRequest(t *testing.T) { mockPrRepo := new(MockPrRepo) mockIso18626Handler := new(MockIso18626Handler) - prAction := CreatePatronRequestActionService(mockPrRepo, new(MockIllRepo), *new(events.EventBus), mockIso18626Handler) - mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{State: BorrowerStateValidated, Side: SideBorrowing, RequesterSymbol: getDbText("ISIL:REC1"), IllRequest: []byte("{}")}, nil) + prAction := CreatePatronRequestActionService(mockPrRepo, *new(events.EventBus), mockIso18626Handler, nil) + illRequest := []byte("{\"request\": {}}") + mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{IllRequest: illRequest, State: BorrowerStateValidated, Side: SideBorrowing, RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:REC1"}}, nil) action := BorrowerActionSendRequest - status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &action}}}) assert.Equal(t, events.EventStatusSuccess, status) @@ -107,7 +137,7 @@ func TestHandleInvokeActionSendRequest(t *testing.T) { func TestHandleInvokeActionSendRequestError(t *testing.T) { mockPrRepo := new(MockPrRepo) mockIso18626Handler := new(MockIso18626Handler) - prAction := CreatePatronRequestActionService(mockPrRepo, new(MockIllRepo), *new(events.EventBus), mockIso18626Handler) + prAction := CreatePatronRequestActionService(mockPrRepo, *new(events.EventBus), mockIso18626Handler, nil) mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{State: BorrowerStateValidated, Side: SideBorrowing, RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:REC1"}}, nil) action := BorrowerActionSendRequest @@ -117,26 +147,76 @@ func TestHandleInvokeActionSendRequestError(t *testing.T) { assert.Equal(t, "failed to parse request", resultData.EventError.Message) } -func TestHandleInvokeActionReceive(t *testing.T) { +func TestHandleInvokeActionReceiveOK(t *testing.T) { mockPrRepo := new(MockPrRepo) mockIso18626Handler := new(MockIso18626Handler) - prAction := CreatePatronRequestActionService(mockPrRepo, new(MockIllRepo), *new(events.EventBus), mockIso18626Handler) - mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{State: BorrowerStateShipped, Side: SideBorrowing, RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:REC1"}, SupplierSymbol: pgtype.Text{Valid: true, String: "ISIL:SUP1"}}, nil) + lmsCreator := new(MockLmsCreator) + lmsCreator.On("GetAdapter", pgtype.Text{Valid: true, String: "ISIL:REC1"}).Return(lms.CreateLmsAdapterMockOK(), nil) + prAction := CreatePatronRequestActionService(mockPrRepo, *new(events.EventBus), mockIso18626Handler, lmsCreator) + illRequest := []byte("{\"request\": {}}") + mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{IllRequest: illRequest, State: BorrowerStateShipped, Side: SideBorrowing, RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:REC1"}, SupplierSymbol: pgtype.Text{Valid: true, String: "ISIL:SUP1"}}, nil) action := BorrowerActionReceive - status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &action}}}) - assert.Equal(t, events.EventStatusSuccess, status) assert.Equal(t, iso18626.TypeMessageStatusOK, resultData.IncomingMessage.RequestingAgencyMessageConfirmation.ConfirmationHeader.MessageStatus) assert.Equal(t, BorrowerStateReceived, mockPrRepo.savedPr.State) } -func TestHandleInvokeActionCheckOut(t *testing.T) { +func TestHandleInvokeActionReceiveBadIllRequest(t *testing.T) { mockPrRepo := new(MockPrRepo) - prAction := CreatePatronRequestActionService(mockPrRepo, *new(ill_db.IllRepo), *new(events.EventBus), new(handler.Iso18626Handler)) - mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{State: BorrowerStateReceived, Side: SideBorrowing}, nil) - action := BorrowerActionCheckOut + mockIso18626Handler := new(MockIso18626Handler) + lmsCreator := new(MockLmsCreator) + lmsCreator.On("GetAdapter", pgtype.Text{Valid: true, String: "ISIL:REC1"}).Return(lms.CreateLmsAdapterMockOK(), nil) + prAction := CreatePatronRequestActionService(mockPrRepo, *new(events.EventBus), mockIso18626Handler, lmsCreator) + illRequest := []byte("{\"bad\": {}") + mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{IllRequest: illRequest, State: BorrowerStateShipped, Side: SideBorrowing, RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:REC1"}, SupplierSymbol: pgtype.Text{Valid: true, String: "ISIL:SUP1"}}, nil) + action := BorrowerActionReceive + status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &action}}}) + + assert.Equal(t, events.EventStatusError, status) + assert.Equal(t, "failed to unmarshal ILL request", resultData.EventError.Message) +} + +func TestHandleInvokeActionReceiveGetAdapterFail(t *testing.T) { + mockPrRepo := new(MockPrRepo) + mockIso18626Handler := new(MockIso18626Handler) + lmsCreator := new(MockLmsCreator) + lmsCreator.On("GetAdapter", pgtype.Text{Valid: true, String: "ISIL:REC1"}).Return(lms.CreateLmsAdapterMockOK(), assert.AnError) + prAction := CreatePatronRequestActionService(mockPrRepo, *new(events.EventBus), mockIso18626Handler, lmsCreator) + illRequest := []byte("{\"request\": {}}") + action := BorrowerActionReceive + mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{IllRequest: illRequest, State: BorrowerStateShipped, Side: SideBorrowing, RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:REC1"}, SupplierSymbol: pgtype.Text{Valid: true, String: "ISIL:SUP1"}}, nil) + + status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &action}}}) + + assert.Equal(t, events.EventStatusError, status) + assert.Equal(t, "failed to create LMS adapter", resultData.EventError.Message) +} +func TestHandleInvokeActionReceiveAcceptItemFailed(t *testing.T) { + mockPrRepo := new(MockPrRepo) + mockIso18626Handler := new(MockIso18626Handler) + lmsCreator := new(MockLmsCreator) + lmsCreator.On("GetAdapter", pgtype.Text{Valid: true, String: "ISIL:REC1"}).Return(createLmsAdapterMockFail(), nil) + prAction := CreatePatronRequestActionService(mockPrRepo, *new(events.EventBus), mockIso18626Handler, lmsCreator) + illRequest := []byte("{\"request\": {}}") + action := BorrowerActionReceive + mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{IllRequest: illRequest, State: BorrowerStateShipped, Side: SideBorrowing, RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:REC1"}, SupplierSymbol: pgtype.Text{Valid: true, String: "ISIL:SUP1"}}, nil) + + status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &action}}}) + + assert.Equal(t, events.EventStatusError, status) + assert.Equal(t, "LMS AcceptItem failed", resultData.EventError.Message) +} + +func TestHandleInvokeActionCheckOutOK(t *testing.T) { + mockPrRepo := new(MockPrRepo) + lmsCreator := new(MockLmsCreator) + lmsCreator.On("GetAdapter", pgtype.Text{Valid: true, String: "ISIL:REC1"}).Return(lms.CreateLmsAdapterMockOK(), nil) + prAction := CreatePatronRequestActionService(mockPrRepo, *new(events.EventBus), new(handler.Iso18626Handler), lmsCreator) + illRequest := []byte("{\"request\": {}}") + mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{IllRequest: illRequest, Patron: pgtype.Text{Valid: true, String: "patron1"}, State: BorrowerStateReceived, Side: SideBorrowing, RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:REC1"}}, nil) + action := BorrowerActionCheckOut status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &action}}}) assert.Equal(t, events.EventStatusSuccess, status) @@ -144,12 +224,54 @@ func TestHandleInvokeActionCheckOut(t *testing.T) { assert.Equal(t, BorrowerStateCheckedOut, mockPrRepo.savedPr.State) } -func TestHandleInvokeActionCheckIn(t *testing.T) { +func TestHandleInvokeActionCheckOutGetAdapterFail(t *testing.T) { mockPrRepo := new(MockPrRepo) - prAction := CreatePatronRequestActionService(mockPrRepo, *new(ill_db.IllRepo), *new(events.EventBus), new(handler.Iso18626Handler)) - mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{State: BorrowerStateCheckedOut, Side: SideBorrowing}, nil) - action := BorrowerActionCheckIn + lmsCreator := new(MockLmsCreator) + lmsCreator.On("GetAdapter", pgtype.Text{Valid: true, String: "ISIL:REC1"}).Return(lms.CreateLmsAdapterMockOK(), assert.AnError) + prAction := CreatePatronRequestActionService(mockPrRepo, *new(events.EventBus), new(handler.Iso18626Handler), lmsCreator) + illRequest := []byte("{\"request\": {}}") + action := BorrowerActionCheckOut + mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{IllRequest: illRequest, State: BorrowerStateReceived, Side: SideBorrowing, RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:REC1"}}, nil) + + status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &action}}}) + assert.Equal(t, events.EventStatusError, status) + assert.Equal(t, "failed to create LMS adapter", resultData.EventError.Message) +} + +func TestHandleInvokeActionCheckOutBadIllRequest(t *testing.T) { + mockPrRepo := new(MockPrRepo) + lmsCreator := new(MockLmsCreator) + lmsCreator.On("GetAdapter", pgtype.Text{Valid: true, String: "ISIL:REC1"}).Return(lms.CreateLmsAdapterMockOK(), nil) + prAction := CreatePatronRequestActionService(mockPrRepo, *new(events.EventBus), new(handler.Iso18626Handler), lmsCreator) + illRequest := []byte("{\"bad\": {}") + mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{IllRequest: illRequest, State: BorrowerStateReceived, Side: SideBorrowing, RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:REC1"}}, nil) + action := BorrowerActionCheckOut + status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &action}}}) + assert.Equal(t, events.EventStatusError, status) + assert.Equal(t, "failed to unmarshal ILL request", resultData.EventError.Message) +} + +func TestHandleInvokeActionCheckOutFails(t *testing.T) { + mockPrRepo := new(MockPrRepo) + lmsCreator := new(MockLmsCreator) + lmsCreator.On("GetAdapter", pgtype.Text{Valid: true, String: "ISIL:REC1"}).Return(createLmsAdapterMockFail(), nil) + prAction := CreatePatronRequestActionService(mockPrRepo, *new(events.EventBus), new(handler.Iso18626Handler), lmsCreator) + illRequest := []byte("{\"request\": {}}") + mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{IllRequest: illRequest, State: BorrowerStateReceived, Side: SideBorrowing, RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:REC1"}}, nil) + action := BorrowerActionCheckOut + status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &action}}}) + assert.Equal(t, events.EventStatusError, status) + assert.Equal(t, "LMS CheckOutItem failed", resultData.EventError.Message) +} +func TestHandleInvokeActionCheckInOK(t *testing.T) { + mockPrRepo := new(MockPrRepo) + lmsCreator := new(MockLmsCreator) + lmsCreator.On("GetAdapter", pgtype.Text{Valid: true, String: "ISIL:REC1"}).Return(lms.CreateLmsAdapterMockOK(), nil) + prAction := CreatePatronRequestActionService(mockPrRepo, *new(events.EventBus), new(handler.Iso18626Handler), lmsCreator) + illRequest := []byte("{\"request\": {}}") + mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{IllRequest: illRequest, State: BorrowerStateCheckedOut, Side: SideBorrowing, RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:REC1"}}, nil) + action := BorrowerActionCheckIn status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &action}}}) assert.Equal(t, events.EventStatusSuccess, status) @@ -157,13 +279,54 @@ func TestHandleInvokeActionCheckIn(t *testing.T) { assert.Equal(t, BorrowerStateCheckedIn, mockPrRepo.savedPr.State) } -func TestHandleInvokeActionShipReturn(t *testing.T) { +func TestHandleInvokeActionCheckInGetAdapterFail(t *testing.T) { + mockPrRepo := new(MockPrRepo) + lmsCreator := new(MockLmsCreator) + lmsCreator.On("GetAdapter", pgtype.Text{Valid: true, String: "ISIL:REC1"}).Return(lms.CreateLmsAdapterMockOK(), assert.AnError) + prAction := CreatePatronRequestActionService(mockPrRepo, *new(events.EventBus), new(handler.Iso18626Handler), lmsCreator) + illRequest := []byte("{\"request\": {}}") + mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{IllRequest: illRequest, State: BorrowerStateCheckedOut, Side: SideBorrowing, RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:REC1"}}, nil) + action := BorrowerActionCheckIn + status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &action}}}) + assert.Equal(t, events.EventStatusError, status) + assert.Equal(t, "failed to create LMS adapter", resultData.EventError.Message) +} + +func TestHandleInvokeActionCheckInBadIllRequest(t *testing.T) { + mockPrRepo := new(MockPrRepo) + lmsCreator := new(MockLmsCreator) + lmsCreator.On("GetAdapter", pgtype.Text{Valid: true, String: "ISIL:REC1"}).Return(lms.CreateLmsAdapterMockOK(), nil) + prAction := CreatePatronRequestActionService(mockPrRepo, *new(events.EventBus), new(handler.Iso18626Handler), lmsCreator) + illRequest := []byte("{\"bad\": {}") + mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{IllRequest: illRequest, State: BorrowerStateCheckedOut, Side: SideBorrowing, RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:REC1"}}, nil) + action := BorrowerActionCheckIn + status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &action}}}) + assert.Equal(t, events.EventStatusError, status) + assert.Equal(t, "failed to unmarshal ILL request", resultData.EventError.Message) +} + +func TestHandleInvokeActionCheckInFails(t *testing.T) { + mockPrRepo := new(MockPrRepo) + lmsCreator := new(MockLmsCreator) + lmsCreator.On("GetAdapter", pgtype.Text{Valid: true, String: "ISIL:REC1"}).Return(createLmsAdapterMockFail(), nil) + prAction := CreatePatronRequestActionService(mockPrRepo, *new(events.EventBus), new(handler.Iso18626Handler), lmsCreator) + illRequest := []byte("{\"request\": {}}") + mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{IllRequest: illRequest, State: BorrowerStateCheckedOut, Side: SideBorrowing, RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:REC1"}}, nil) + action := BorrowerActionCheckIn + status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &action}}}) + assert.Equal(t, events.EventStatusError, status) + assert.Equal(t, "LMS CheckInItem failed", resultData.EventError.Message) +} + +func TestHandleInvokeActionShipReturnOK(t *testing.T) { mockPrRepo := new(MockPrRepo) mockIso18626Handler := new(MockIso18626Handler) - prAction := CreatePatronRequestActionService(mockPrRepo, new(MockIllRepo), *new(events.EventBus), mockIso18626Handler) - mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{State: BorrowerStateCheckedIn, Side: SideBorrowing, RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:REC1"}, SupplierSymbol: pgtype.Text{Valid: true, String: "ISIL:SUP1"}}, nil) + lmsCreator := new(MockLmsCreator) + lmsCreator.On("GetAdapter", pgtype.Text{Valid: true, String: "ISIL:REC1"}).Return(lms.CreateLmsAdapterMockOK(), nil) + prAction := CreatePatronRequestActionService(mockPrRepo, *new(events.EventBus), mockIso18626Handler, lmsCreator) + illRequest := []byte("{\"request\": {}}") + mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{IllRequest: illRequest, State: BorrowerStateCheckedIn, Side: SideBorrowing, RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:REC1"}, SupplierSymbol: pgtype.Text{Valid: true, String: "ISIL:SUP1"}}, nil) action := BorrowerActionShipReturn - status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &action}}}) assert.Equal(t, events.EventStatusSuccess, status) @@ -171,13 +334,41 @@ func TestHandleInvokeActionShipReturn(t *testing.T) { assert.Equal(t, BorrowerStateShippedReturned, mockPrRepo.savedPr.State) } +func TestHandleInvokeActionShipReturnFailCreator(t *testing.T) { + mockPrRepo := new(MockPrRepo) + mockIso18626Handler := new(MockIso18626Handler) + lmsCreator := new(MockLmsCreator) + lmsCreator.On("GetAdapter", pgtype.Text{Valid: true, String: "ISIL:REC1"}).Return(lms.CreateLmsAdapterMockOK(), assert.AnError) + prAction := CreatePatronRequestActionService(mockPrRepo, *new(events.EventBus), mockIso18626Handler, lmsCreator) + mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{State: BorrowerStateCheckedIn, Side: SideBorrowing, RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:REC1"}, SupplierSymbol: pgtype.Text{Valid: true, String: "ISIL:SUP1"}}, nil) + action := BorrowerActionShipReturn + status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &action}}}) + + assert.Equal(t, events.EventStatusError, status) + assert.Equal(t, "failed to create LMS adapter", resultData.EventError.Message) +} + +func TestHandleInvokeActionShipReturnFails(t *testing.T) { + mockPrRepo := new(MockPrRepo) + mockIso18626Handler := new(MockIso18626Handler) + lmsCreator := new(MockLmsCreator) + lmsCreator.On("GetAdapter", pgtype.Text{Valid: true, String: "ISIL:REC1"}).Return(createLmsAdapterMockFail(), nil) + prAction := CreatePatronRequestActionService(mockPrRepo, *new(events.EventBus), mockIso18626Handler, lmsCreator) + illRequest := []byte("{\"request\": {}}") + mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{IllRequest: illRequest, State: BorrowerStateCheckedIn, Side: SideBorrowing, RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:REC1"}, SupplierSymbol: pgtype.Text{Valid: true, String: "ISIL:SUP1"}}, nil) + action := BorrowerActionShipReturn + status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &action}}}) + + assert.Equal(t, events.EventStatusError, status) + assert.Equal(t, "LMS DeleteItem failed", resultData.EventError.Message) +} + func TestHandleInvokeActionCancelRequest(t *testing.T) { mockPrRepo := new(MockPrRepo) mockIso18626Handler := new(MockIso18626Handler) - prAction := CreatePatronRequestActionService(mockPrRepo, new(MockIllRepo), *new(events.EventBus), mockIso18626Handler) + prAction := CreatePatronRequestActionService(mockPrRepo, *new(events.EventBus), mockIso18626Handler, nil) mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{State: BorrowerStateWillSupply, Side: SideBorrowing, RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:REC1"}, SupplierSymbol: pgtype.Text{Valid: true, String: "ISIL:SUP1"}}, nil) action := BorrowerActionCancelRequest - status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &action}}}) assert.Equal(t, events.EventStatusSuccess, status) @@ -188,10 +379,9 @@ func TestHandleInvokeActionCancelRequest(t *testing.T) { func TestHandleInvokeActionAcceptCondition(t *testing.T) { mockPrRepo := new(MockPrRepo) mockIso18626Handler := new(MockIso18626Handler) - prAction := CreatePatronRequestActionService(mockPrRepo, new(MockIllRepo), *new(events.EventBus), mockIso18626Handler) + prAction := CreatePatronRequestActionService(mockPrRepo, *new(events.EventBus), mockIso18626Handler, nil) mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{State: BorrowerStateConditionPending, Side: SideBorrowing, RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:REC1"}, SupplierSymbol: pgtype.Text{Valid: true, String: "ISIL:SUP1"}}, nil) action := BorrowerActionAcceptCondition - status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &action}}}) assert.Equal(t, events.EventStatusSuccess, status) @@ -202,10 +392,9 @@ func TestHandleInvokeActionAcceptCondition(t *testing.T) { func TestHandleInvokeActionRejectCondition(t *testing.T) { mockPrRepo := new(MockPrRepo) mockIso18626Handler := new(MockIso18626Handler) - prAction := CreatePatronRequestActionService(mockPrRepo, new(MockIllRepo), *new(events.EventBus), mockIso18626Handler) + prAction := CreatePatronRequestActionService(mockPrRepo, *new(events.EventBus), mockIso18626Handler, nil) mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{State: BorrowerStateConditionPending, Side: SideBorrowing, RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:REC1"}, SupplierSymbol: pgtype.Text{Valid: true, String: "ISIL:SUP1"}}, nil) action := BorrowerActionRejectCondition - status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &action}}}) assert.Equal(t, events.EventStatusSuccess, status) @@ -216,8 +405,7 @@ func TestHandleInvokeActionRejectCondition(t *testing.T) { func TestSendBorrowingRequestInvalidSymbol(t *testing.T) { mockPrRepo := new(MockPrRepo) mockIso18626Handler := new(MockIso18626Handler) - prAction := CreatePatronRequestActionService(mockPrRepo, new(MockIllRepo), *new(events.EventBus), mockIso18626Handler) - + prAction := CreatePatronRequestActionService(mockPrRepo, *new(events.EventBus), mockIso18626Handler, nil) status, resultData := prAction.sendBorrowingRequest(appCtx, pr_db.PatronRequest{State: BorrowerStateValidated, Side: SideBorrowing, RequesterSymbol: pgtype.Text{Valid: true, String: "x"}}) assert.Equal(t, events.EventStatusError, status) @@ -227,7 +415,7 @@ func TestSendBorrowingRequestInvalidSymbol(t *testing.T) { func TestSendBorrowingRequestMissingSymbol(t *testing.T) { mockPrRepo := new(MockPrRepo) mockIso18626Handler := new(MockIso18626Handler) - prAction := CreatePatronRequestActionService(mockPrRepo, new(MockIllRepo), *new(events.EventBus), mockIso18626Handler) + prAction := CreatePatronRequestActionService(mockPrRepo, *new(events.EventBus), mockIso18626Handler, nil) status, resultData := prAction.sendBorrowingRequest(appCtx, pr_db.PatronRequest{State: BorrowerStateValidated, Side: SideBorrowing}) @@ -238,20 +426,54 @@ func TestSendBorrowingRequestMissingSymbol(t *testing.T) { func TestShipReturnBorrowingRequestMissingSupplierSymbol(t *testing.T) { mockPrRepo := new(MockPrRepo) mockIso18626Handler := new(MockIso18626Handler) - prAction := CreatePatronRequestActionService(mockPrRepo, new(MockIllRepo), *new(events.EventBus), mockIso18626Handler) + lmsCreator := new(MockLmsCreator) + lmsCreator.On("GetAdapter", pgtype.Text{Valid: true, String: "ISIL:REC1"}).Return(lms.CreateLmsAdapterMockOK(), nil) + prAction := CreatePatronRequestActionService(mockPrRepo, *new(events.EventBus), mockIso18626Handler, lmsCreator) - status, resultData := prAction.shipReturnBorrowingRequest(appCtx, pr_db.PatronRequest{ID: "1", State: BorrowerStateValidated, Side: SideBorrowing, RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:REC1"}}) + illRequest := []byte("{\"request\": {}}") + status, resultData := prAction.shipReturnBorrowingRequest(appCtx, pr_db.PatronRequest{IllRequest: illRequest, State: BorrowerStateValidated, Side: SideBorrowing, Patron: pgtype.Text{Valid: true, String: "patron1"}, ID: "1", RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:REC1"}}) assert.Equal(t, events.EventStatusError, status) assert.Equal(t, "missing supplier symbol", resultData.EventError.Message) } +func TestShipReturnBorrowingRequestGetAdapterFailed(t *testing.T) { + mockPrRepo := new(MockPrRepo) + mockIso18626Handler := new(MockIso18626Handler) + lmsCreator := new(MockLmsCreator) + lmsCreator.On("GetAdapter", pgtype.Text{Valid: true, String: "ISIL:REC1"}).Return(lms.CreateLmsAdapterMockOK(), assert.AnError) + prAction := CreatePatronRequestActionService(mockPrRepo, *new(events.EventBus), mockIso18626Handler, lmsCreator) + + illRequest := []byte("{\"request\": {}}") + status, resultData := prAction.shipReturnBorrowingRequest(appCtx, pr_db.PatronRequest{IllRequest: illRequest, Patron: pgtype.Text{Valid: true, String: "patron1"}, ID: "1", State: BorrowerStateValidated, Side: SideBorrowing, RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:REC1"}}) + + assert.Equal(t, events.EventStatusError, status) + assert.Equal(t, "failed to create LMS adapter", resultData.EventError.Message) +} + +func TestShipReturnBorrowingRequestBadIllRequest(t *testing.T) { + mockPrRepo := new(MockPrRepo) + mockIso18626Handler := new(MockIso18626Handler) + lmsCreator := new(MockLmsCreator) + lmsCreator.On("GetAdapter", pgtype.Text{Valid: true, String: "ISIL:REC1"}).Return(lms.CreateLmsAdapterMockOK(), nil) + prAction := CreatePatronRequestActionService(mockPrRepo, *new(events.EventBus), mockIso18626Handler, lmsCreator) + + illRequest := []byte("{bad") + status, resultData := prAction.shipReturnBorrowingRequest(appCtx, pr_db.PatronRequest{IllRequest: illRequest, Patron: pgtype.Text{Valid: true, String: "patron1"}, ID: "1", State: BorrowerStateValidated, Side: SideBorrowing, RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:REC1"}}) + + assert.Equal(t, events.EventStatusError, status) + assert.Equal(t, "failed to unmarshal ILL request", resultData.EventError.Message) +} + func TestShipReturnBorrowingRequestMissingRequesterSymbol(t *testing.T) { mockPrRepo := new(MockPrRepo) mockIso18626Handler := new(MockIso18626Handler) - prAction := CreatePatronRequestActionService(mockPrRepo, new(MockIllRepo), *new(events.EventBus), mockIso18626Handler) + lmsCreator := new(MockLmsCreator) + lmsCreator.On("GetAdapter", pgtype.Text{}).Return(lms.CreateLmsAdapterMockOK(), nil) + prAction := CreatePatronRequestActionService(mockPrRepo, *new(events.EventBus), mockIso18626Handler, lmsCreator) - status, resultData := prAction.shipReturnBorrowingRequest(appCtx, pr_db.PatronRequest{ID: "1", State: BorrowerStateValidated, Side: SideBorrowing, SupplierSymbol: pgtype.Text{Valid: true, String: "ISIL:SUP1"}}) + illRequest := []byte("{\"request\": {}}") + status, resultData := prAction.shipReturnBorrowingRequest(appCtx, pr_db.PatronRequest{IllRequest: illRequest, ID: "1", State: BorrowerStateValidated, Side: SideBorrowing, SupplierSymbol: pgtype.Text{Valid: true, String: "ISIL:SUP1"}}) assert.Equal(t, events.EventStatusError, status) assert.Equal(t, "missing requester symbol", resultData.EventError.Message) @@ -260,9 +482,12 @@ func TestShipReturnBorrowingRequestMissingRequesterSymbol(t *testing.T) { func TestShipReturnBorrowingRequestInvalidSupplierSymbol(t *testing.T) { mockPrRepo := new(MockPrRepo) mockIso18626Handler := new(MockIso18626Handler) - prAction := CreatePatronRequestActionService(mockPrRepo, new(MockIllRepo), *new(events.EventBus), mockIso18626Handler) + lmsCreator := new(MockLmsCreator) + lmsCreator.On("GetAdapter", pgtype.Text{Valid: true, String: "ISIL:REC1"}).Return(lms.CreateLmsAdapterMockOK(), nil) + prAction := CreatePatronRequestActionService(mockPrRepo, *new(events.EventBus), mockIso18626Handler, lmsCreator) - status, resultData := prAction.shipReturnBorrowingRequest(appCtx, pr_db.PatronRequest{ID: "1", State: BorrowerStateValidated, Side: SideBorrowing, RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:REC1"}, SupplierSymbol: pgtype.Text{Valid: true, String: "x"}}) + illRequest := []byte("{\"request\": {}}") + status, resultData := prAction.shipReturnBorrowingRequest(appCtx, pr_db.PatronRequest{IllRequest: illRequest, ID: "1", State: BorrowerStateValidated, Side: SideBorrowing, RequesterSymbol: pgtype.Text{Valid: true, String: "ISIL:REC1"}, SupplierSymbol: pgtype.Text{Valid: true, String: "x"}}) assert.Equal(t, events.EventStatusError, status) assert.Equal(t, "invalid supplier symbol", resultData.EventError.Message) @@ -271,9 +496,12 @@ func TestShipReturnBorrowingRequestInvalidSupplierSymbol(t *testing.T) { func TestShipReturnBorrowingRequestInvalidRequesterSymbol(t *testing.T) { mockPrRepo := new(MockPrRepo) mockIso18626Handler := new(MockIso18626Handler) - prAction := CreatePatronRequestActionService(mockPrRepo, new(MockIllRepo), *new(events.EventBus), mockIso18626Handler) + lmsCreator := new(MockLmsCreator) + lmsCreator.On("GetAdapter", pgtype.Text{Valid: true, String: "x"}).Return(lms.CreateLmsAdapterMockOK(), nil) + prAction := CreatePatronRequestActionService(mockPrRepo, *new(events.EventBus), mockIso18626Handler, lmsCreator) - status, resultData := prAction.shipReturnBorrowingRequest(appCtx, pr_db.PatronRequest{ID: "1", State: BorrowerStateValidated, Side: SideBorrowing, RequesterSymbol: pgtype.Text{Valid: true, String: "x"}, SupplierSymbol: pgtype.Text{Valid: true, String: "ISIL:SUP1"}}) + illRequest := []byte("{\"request\": {}}") + status, resultData := prAction.shipReturnBorrowingRequest(appCtx, pr_db.PatronRequest{IllRequest: illRequest, ID: "1", State: BorrowerStateValidated, Side: SideBorrowing, RequesterSymbol: pgtype.Text{Valid: true, String: "x"}, SupplierSymbol: pgtype.Text{Valid: true, String: "ISIL:SUP1"}}) assert.Equal(t, events.EventStatusError, status) assert.Equal(t, "invalid requester symbol", resultData.EventError.Message) @@ -281,7 +509,7 @@ func TestShipReturnBorrowingRequestInvalidRequesterSymbol(t *testing.T) { func TestHandleInvokeLenderActionValidate(t *testing.T) { mockPrRepo := new(MockPrRepo) - prAction := CreatePatronRequestActionService(mockPrRepo, *new(ill_db.IllRepo), *new(events.EventBus), new(handler.Iso18626Handler)) + prAction := CreatePatronRequestActionService(mockPrRepo, *new(events.EventBus), new(handler.Iso18626Handler), nil) mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{State: LenderStateNew, Side: SideLending}, nil) status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &actionValidate}}}) @@ -293,10 +521,9 @@ func TestHandleInvokeLenderActionValidate(t *testing.T) { func TestHandleInvokeLenderActionWillSupply(t *testing.T) { mockPrRepo := new(MockPrRepo) mockIso18626Handler := new(MockIso18626Handler) - prAction := CreatePatronRequestActionService(mockPrRepo, new(MockIllRepo), *new(events.EventBus), mockIso18626Handler) + prAction := CreatePatronRequestActionService(mockPrRepo, *new(events.EventBus), mockIso18626Handler, nil) mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{State: LenderStateValidated, Side: SideLending, SupplierSymbol: getDbText("ISIL:SUP1"), RequesterSymbol: getDbText("ISIL:REQ1")}, nil) action := LenderActionWillSupply - status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &action}}}) assert.Equal(t, events.EventStatusSuccess, status) @@ -306,10 +533,9 @@ func TestHandleInvokeLenderActionWillSupply(t *testing.T) { func TestHandleInvokeLenderActionCannotSupply(t *testing.T) { mockPrRepo := new(MockPrRepo) mockIso18626Handler := new(MockIso18626Handler) - prAction := CreatePatronRequestActionService(mockPrRepo, new(MockIllRepo), *new(events.EventBus), mockIso18626Handler) + prAction := CreatePatronRequestActionService(mockPrRepo, *new(events.EventBus), mockIso18626Handler, nil) mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{State: LenderStateValidated, Side: SideLending, SupplierSymbol: getDbText("ISIL:SUP1"), RequesterSymbol: getDbText("ISIL:REQ1")}, nil) action := LenderActionCannotSupply - status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &action}}}) assert.Equal(t, events.EventStatusSuccess, status) @@ -319,7 +545,7 @@ func TestHandleInvokeLenderActionCannotSupply(t *testing.T) { func TestHandleInvokeLenderActionAddCondition(t *testing.T) { mockPrRepo := new(MockPrRepo) mockIso18626Handler := new(MockIso18626Handler) - prAction := CreatePatronRequestActionService(mockPrRepo, new(MockIllRepo), *new(events.EventBus), mockIso18626Handler) + prAction := CreatePatronRequestActionService(mockPrRepo, *new(events.EventBus), mockIso18626Handler, nil) mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{State: LenderStateValidated, Side: SideLending, SupplierSymbol: getDbText("ISIL:SUP1"), RequesterSymbol: getDbText("ISIL:REQ1")}, nil) action := LenderActionAddCondition @@ -332,7 +558,7 @@ func TestHandleInvokeLenderActionAddCondition(t *testing.T) { func TestHandleInvokeLenderActionShip(t *testing.T) { mockPrRepo := new(MockPrRepo) mockIso18626Handler := new(MockIso18626Handler) - prAction := CreatePatronRequestActionService(mockPrRepo, new(MockIllRepo), *new(events.EventBus), mockIso18626Handler) + prAction := CreatePatronRequestActionService(mockPrRepo, *new(events.EventBus), mockIso18626Handler, nil) mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{State: LenderStateWillSupply, Side: SideLending, SupplierSymbol: getDbText("ISIL:SUP1"), RequesterSymbol: getDbText("ISIL:REQ1")}, nil) action := LenderActionShip @@ -345,7 +571,7 @@ func TestHandleInvokeLenderActionShip(t *testing.T) { func TestHandleInvokeLenderActionMarkReceived(t *testing.T) { mockPrRepo := new(MockPrRepo) mockIso18626Handler := new(MockIso18626Handler) - prAction := CreatePatronRequestActionService(mockPrRepo, new(MockIllRepo), *new(events.EventBus), mockIso18626Handler) + prAction := CreatePatronRequestActionService(mockPrRepo, *new(events.EventBus), mockIso18626Handler, nil) mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{State: LenderStateShippedReturn, Side: SideLending, SupplierSymbol: getDbText("ISIL:SUP1"), RequesterSymbol: getDbText("ISIL:REQ1")}, nil) action := LenderActionMarkReceived status, resultData := prAction.handleInvokeAction(appCtx, events.Event{PatronRequestID: patronRequestId, EventData: events.EventData{CommonEventData: events.CommonEventData{Action: &action}}}) @@ -357,7 +583,7 @@ func TestHandleInvokeLenderActionMarkReceived(t *testing.T) { func TestHandleInvokeLenderActionMarkCancelled(t *testing.T) { mockPrRepo := new(MockPrRepo) mockIso18626Handler := new(MockIso18626Handler) - prAction := CreatePatronRequestActionService(mockPrRepo, new(MockIllRepo), *new(events.EventBus), mockIso18626Handler) + prAction := CreatePatronRequestActionService(mockPrRepo, *new(events.EventBus), mockIso18626Handler, nil) mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{State: LenderStateCancelRequested, Side: SideLending, SupplierSymbol: getDbText("ISIL:SUP1"), RequesterSymbol: getDbText("ISIL:REQ1")}, nil) action := LenderActionMarkCancelled @@ -371,7 +597,7 @@ func TestHandleInvokeLenderActionMarkCancelled(t *testing.T) { func TestHandleInvokeLenderActionMarkCancelledMissingSupplierSymbol(t *testing.T) { mockPrRepo := new(MockPrRepo) mockIso18626Handler := new(MockIso18626Handler) - prAction := CreatePatronRequestActionService(mockPrRepo, new(MockIllRepo), *new(events.EventBus), mockIso18626Handler) + prAction := CreatePatronRequestActionService(mockPrRepo, *new(events.EventBus), mockIso18626Handler, nil) mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{State: LenderStateCancelRequested, Side: SideLending, SupplierSymbol: pgtype.Text{Valid: false, String: ""}, RequesterSymbol: getDbText("ISIL:REQ1")}, nil) action := LenderActionMarkCancelled @@ -384,7 +610,7 @@ func TestHandleInvokeLenderActionMarkCancelledMissingSupplierSymbol(t *testing.T func TestHandleInvokeLenderActionMarkCancelledMissingRequesterSymbol(t *testing.T) { mockPrRepo := new(MockPrRepo) mockIso18626Handler := new(MockIso18626Handler) - prAction := CreatePatronRequestActionService(mockPrRepo, new(MockIllRepo), *new(events.EventBus), mockIso18626Handler) + prAction := CreatePatronRequestActionService(mockPrRepo, *new(events.EventBus), mockIso18626Handler, nil) mockPrRepo.On("GetPatronRequestById", patronRequestId).Return(pr_db.PatronRequest{State: LenderStateCancelRequested, Side: SideLending, RequesterSymbol: pgtype.Text{Valid: false, String: ""}, SupplierSymbol: getDbText("ISIL:SUP1")}, nil) action := LenderActionMarkCancelled @@ -468,6 +694,7 @@ func (h *MockIso18626Handler) HandleRequest(ctx common.ExtendedContext, illMessa w.WriteHeader(http.StatusOK) w.Write(output) } + func (h *MockIso18626Handler) HandleRequestingAgencyMessage(ctx common.ExtendedContext, illMessage *iso18626.ISO18626Message, w http.ResponseWriter) { status := iso18626.TypeMessageStatusOK if illMessage.RequestingAgencyMessage.Header.RequestingAgencyRequestId == "error" { @@ -511,7 +738,115 @@ func (h *MockIso18626Handler) HandleSupplyingAgencyMessage(ctx common.ExtendedCo w.Write(output) } -type MockIllRepo struct { +type MockLmsCreator struct { mock.Mock - ill_db.PgIllRepo + lms.LmsCreator +} + +func (m *MockLmsCreator) GetAdapter(ctx common.ExtendedContext, symbol pgtype.Text) (lms.LmsAdapter, error) { + args := m.Called(symbol) + return args.Get(0).(lms.LmsAdapter), args.Error(1) +} + +func createLmsAdapterMockFail() lms.LmsAdapter { + return &MockLmsAdapterFail{} +} + +type MockLmsAdapterFail struct { +} + +func (l *MockLmsAdapterFail) LookupUser(patron string) (string, error) { + return "", errors.New("LookupUser failed") +} + +func (l *MockLmsAdapterFail) AcceptItem( + itemId string, + requestId string, + userId string, + author string, + title string, + isbn string, + callNumber string, + pickupLocation string, + requestedAction string, +) error { + return errors.New("AcceptItem failed") +} + +func (l *MockLmsAdapterFail) DeleteItem(itemId string) error { + return errors.New("DeleteItem failed") +} + +func (l *MockLmsAdapterFail) RequestItem( + requestId string, + itemId string, + borrowerBarcode string, + pickupLocation string, + itemLocation string, +) error { + return errors.New("RequestItem failed") +} + +func (l *MockLmsAdapterFail) CancelRequestItem(requestId string, userId string) error { + return errors.New("CancelRequestItem failed") +} + +func (l *MockLmsAdapterFail) CheckInItem(itemId string) error { + return errors.New("CheckInItem failed") +} + +func (l *MockLmsAdapterFail) CheckOutItem( + requestId string, + itemBarcode string, + borrowerBarcode string, + externalReferenceValue string, +) error { + return errors.New("CheckOutItem failed") +} + +func (l *MockLmsAdapterFail) CreateUserFiscalTransaction(userId string, itemId string) error { + return errors.New("CreateUserFiscalTransaction failed") +} + +func TestPickupLocationFromIllRequest(t *testing.T) { + var r iso18626.Request + r.RequestedDeliveryInfo = []iso18626.RequestedDeliveryInfo{{ + Address: &iso18626.Address{ + PhysicalAddress: &iso18626.PhysicalAddress{ + Line1: "Main Library", + Line2: "123 Library St", + Locality: "Booktown", + PostalCode: "12345", + Region: &iso18626.TypeSchemeValuePair{Text: "State"}, + Country: &iso18626.TypeSchemeValuePair{Text: "Country"}, + }, + }, + }} + location := pickupLocationFromIllRequest(r) + assert.Equal(t, "Main Library 123 Library St Booktown 12345 State Country", location) +} + +func TestIsbnFromIllRequest(t *testing.T) { + var r iso18626.Request + r.BibliographicInfo = iso18626.BibliographicInfo{ + BibliographicItemId: []iso18626.BibliographicItemId{ + { + BibliographicItemIdentifier: "978-3-16-148410-0", + BibliographicItemIdentifierCode: iso18626.TypeSchemeValuePair{ + Text: "ISBN", + }, + }, + }, + } + isbn := isbnFromIllRequest(r) + assert.Equal(t, "978-3-16-148410-0", isbn) +} + +func TestCallNumberFromIllRequest(t *testing.T) { + var r iso18626.Request + r.SupplierInfo = []iso18626.SupplierInfo{{ + CallNumber: "QA76.73.G63 D37 2020", + }} + callNumber := callNumberFromIllRequest(r) + assert.Equal(t, "QA76.73.G63 D37 2020", callNumber) } diff --git a/broker/patron_request/service/message-handler_test.go b/broker/patron_request/service/message-handler_test.go index bfe6d3fa..3951f980 100644 --- a/broker/patron_request/service/message-handler_test.go +++ b/broker/patron_request/service/message-handler_test.go @@ -2,13 +2,15 @@ package prservice import ( "errors" + "testing" + "github.com/indexdata/crosslink/broker/events" "github.com/indexdata/crosslink/broker/ill_db" pr_db "github.com/indexdata/crosslink/broker/patron_request/db" "github.com/indexdata/crosslink/iso18626" "github.com/jackc/pgx/v5" "github.com/stretchr/testify/assert" - "testing" + "github.com/stretchr/testify/mock" ) func TestGetPatronRequest(t *testing.T) { @@ -284,6 +286,11 @@ func TestHandleSupplyingAgencyMessageNoImplemented(t *testing.T) { assert.Equal(t, "status change no allowed", err.Error()) } +type MockIllRepo struct { + mock.Mock + ill_db.PgIllRepo +} + func TestHandleSupplyingAgencyMessageCancelledFailToSave(t *testing.T) { handler := CreatePatronRequestMessageHandler(new(MockPrRepo), *new(events.EventRepo), *new(ill_db.IllRepo), *new(events.EventBus)) diff --git a/broker/test/api/api-handler_test.go b/broker/test/api/api-handler_test.go index bd16b6e2..c042c8f6 100644 --- a/broker/test/api/api-handler_test.go +++ b/broker/test/api/api-handler_test.go @@ -43,7 +43,7 @@ var illRepo ill_db.IllRepo var eventRepo events.EventRepo var mockIllRepoError = new(mocks.MockIllRepositoryError) var mockEventRepoError = new(mocks.MockEventRepositoryError) -var handlerMock = api.NewApiHandler(mockEventRepoError, mockIllRepoError, "", api.LIMIT_DEFAULT) +var handlerMock = api.NewApiHandler(mockEventRepoError, mockIllRepoError, common.NewTenant(""), api.LIMIT_DEFAULT) func TestMain(m *testing.M) { app.TENANT_TO_SYMBOL = "ISIL:DK-{tenant}" diff --git a/broker/test/patron_request/api/api-handler_test.go b/broker/test/patron_request/api/api-handler_test.go index 0ad9e6b7..3c7784c3 100644 --- a/broker/test/patron_request/api/api-handler_test.go +++ b/broker/test/patron_request/api/api-handler_test.go @@ -4,9 +4,6 @@ import ( "bytes" "context" "encoding/json" - "github.com/indexdata/crosslink/broker/common" - pr_db "github.com/indexdata/crosslink/broker/patron_request/db" - "github.com/indexdata/crosslink/iso18626" "io" "net/http" "os" @@ -14,6 +11,10 @@ import ( "testing" "time" + "github.com/indexdata/crosslink/broker/common" + pr_db "github.com/indexdata/crosslink/broker/patron_request/db" + "github.com/indexdata/crosslink/iso18626" + "github.com/google/uuid" "github.com/indexdata/crosslink/broker/adapter" "github.com/indexdata/crosslink/broker/app" @@ -34,7 +35,7 @@ var illRepo ill_db.IllRepo var prRepo pr_db.PrRepo func TestMain(m *testing.M) { - app.TENANT_TO_SYMBOL = "ISIL:DK-{tenant}" + app.TENANT_TO_SYMBOL = "" ctx := context.Background() pgContainer, err := postgres.Run(ctx, "postgres", @@ -361,7 +362,7 @@ func httpRequest(t *testing.T, method string, uriPath string, reqbytes []byte, e client := http.DefaultClient hreq, err := http.NewRequest(method, getLocalhostWithPort()+uriPath, bytes.NewBuffer(reqbytes)) assert.NoError(t, err) - hreq.Header.Set("X-Okapi-Tenant", "test-lib") + hreq.Header.Set("X-Okapi-Tenant", "testlib") if method == "POST" || method == "PUT" { hreq.Header.Set("Content-Type", "application/json")