diff --git a/broker/ncipclient/ncipclient.go b/broker/ncipclient/ncipclient.go new file mode 100644 index 00000000..2df7e2f1 --- /dev/null +++ b/broker/ncipclient/ncipclient.go @@ -0,0 +1,77 @@ +package ncipclient + +import "github.com/indexdata/crosslink/ncip" + +type NcipMode string + +const ( + ModeDisabled NcipMode = "disabled" + ModeAuto NcipMode = "auto" + ModeManual NcipMode = "manual" +) + +type NcipProperty string + +const ( + Ncip NcipProperty = "ncip" + FromAgency NcipProperty = "from_agency" + FromAgencyAuthentication NcipProperty = "from_agency_authentication" + ToAgency NcipProperty = "to_agency" + Address NcipProperty = "address" + LookupUserMode NcipProperty = "lookup_user_mode" + AcceptItemMode NcipProperty = "accept_item_mode" + RequestItemMode NcipProperty = "request_item_mode" + 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) + + // 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) + + DeleteItem(customData map[string]any, arg ncip.DeleteItem) 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) + + CancelRequestItem(customData map[string]any, arg ncip.CancelRequestItem) error + + CheckInItem(customData map[string]any, arg ncip.CheckInItem) error + + CheckOutItem(customData map[string]any, arg ncip.CheckOutItem) 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) +} + +type NcipError struct { + Message string + Problem ncip.Problem +} + +func (e *NcipError) Error() string { + s := e.Message + if e.Problem.ProblemType.Text != "" { + s += ": " + e.Problem.ProblemType.Text + } + if e.Problem.ProblemDetail != "" { + s += ": " + e.Problem.ProblemDetail + } + return s +} diff --git a/broker/ncipclient/ncipclient_impl.go b/broker/ncipclient/ncipclient_impl.go new file mode 100644 index 00000000..ce08d476 --- /dev/null +++ b/broker/ncipclient/ncipclient_impl.go @@ -0,0 +1,280 @@ +package ncipclient + +import ( + "encoding/xml" + "fmt" + "net/http" + + "github.com/indexdata/crosslink/httpclient" + "github.com/indexdata/crosslink/ncip" +) + +type NcipClientImpl struct { + client *http.Client +} + +func CreateNcipClient(client *http.Client) NcipClient { + return &NcipClientImpl{ + client: client, + } +} + +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) + + ncipMessage := &ncip.NCIPMessage{ + LookupUser: &lookup, + } + ncipResponse, err := n.sendReceiveMessage(ncipInfo, ncipMessage) + if err != nil { + return false, err + } + lookupUserResponse := ncipResponse.LookupUserResponse + if lookupUserResponse == nil { + return false, fmt.Errorf("invalid NCIP response: missing LookupUserResponse") + } + return true, n.checkProblem("NCIP user lookup", lookupUserResponse.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) + ncipMessage := &ncip.NCIPMessage{ + AcceptItem: &accept, + } + ncipResponse, err := n.sendReceiveMessage(ncipInfo, ncipMessage) + if err != nil { + return false, err + } + acceptItemResponse := ncipResponse.AcceptItemResponse + if acceptItemResponse == nil { + return false, fmt.Errorf("invalid NCIP response: missing AcceptItemResponse") + } + return true, n.checkProblem("NCIP accept item", acceptItemResponse.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) + ncipMessage := &ncip.NCIPMessage{ + DeleteItem: &delete, + } + ncipResponse, err := n.sendReceiveMessage(ncipInfo, ncipMessage) + if err != nil { + return err + } + deleteItemResponse := ncipResponse.DeleteItemResponse + if deleteItemResponse == nil { + return fmt.Errorf("invalid NCIP response: missing DeleteItemResponse") + } + return n.checkProblem("NCIP delete item", deleteItemResponse.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) + ncipMessage := &ncip.NCIPMessage{ + RequestItem: &request, + } + ncipResponse, err := n.sendReceiveMessage(ncipInfo, ncipMessage) + if err != nil { + return false, err + } + requestItemResponse := ncipResponse.RequestItemResponse + if requestItemResponse == nil { + return false, fmt.Errorf("invalid NCIP response: missing RequestItemResponse") + } + return true, n.checkProblem("NCIP request item", requestItemResponse.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) + ncipMessage := &ncip.NCIPMessage{ + CancelRequestItem: &request, + } + ncipResponse, err := n.sendReceiveMessage(ncipInfo, ncipMessage) + if err != nil { + return err + } + cancelRequestItemResponse := ncipResponse.CancelRequestItemResponse + if cancelRequestItemResponse == nil { + return fmt.Errorf("invalid NCIP response: missing CancelRequestItemResponse") + } + return n.checkProblem("NCIP cancel request item", cancelRequestItemResponse.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) + ncipMessage := &ncip.NCIPMessage{ + CheckInItem: &request, + } + ncipResponse, err := n.sendReceiveMessage(ncipInfo, ncipMessage) + if err != nil { + return err + } + checkInItemResponse := ncipResponse.CheckInItemResponse + if checkInItemResponse == nil { + return fmt.Errorf("invalid NCIP response: missing CheckInItemResponse") + } + return n.checkProblem("NCIP check in item", checkInItemResponse.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) + ncipMessage := &ncip.NCIPMessage{ + CheckOutItem: &request, + } + ncipResponse, err := n.sendReceiveMessage(ncipInfo, ncipMessage) + if err != nil { + return err + } + checkOutItemResponse := ncipResponse.CheckOutItemResponse + if checkOutItemResponse == nil { + return fmt.Errorf("invalid NCIP response: missing CheckOutItemResponse") + } + return n.checkProblem("NCIP check out item", checkOutItemResponse.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) + + ncipMessage := &ncip.NCIPMessage{ + CreateUserFiscalTransaction: &request, + } + ncipResponse, err := n.sendReceiveMessage(ncipInfo, ncipMessage) + if err != nil { + return false, err + } + createUserFiscalTransactionResponse := ncipResponse.CreateUserFiscalTransactionResponse + if createUserFiscalTransactionResponse == nil { + return false, fmt.Errorf("invalid NCIP response: missing CreateUserFiscalTransactionResponse") + } + return true, n.checkProblem("NCIP create user fiscal transaction", createUserFiscalTransactionResponse.Problem) +} + +func (n *NcipClientImpl) checkProblem(op string, responseProblems []ncip.Problem) error { + if len(responseProblems) > 0 { + return &NcipError{ + Message: op + " failed", + Problem: responseProblems[0], + } + } + 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 { + 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" + } + header.ToAgencyId.AgencyId = ncip.SchemeValuePair{ + Text: to_agency, + } + if auth, ok := ncipInfo[string(FromAgencyAuthentication)].(string); ok { + header.FromAgencyAuthentication = auth + } + 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") + } + 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) + if err != nil { + return nil, fmt.Errorf("NCIP message exchange failed: %s", err.Error()) + } + if len(respMessage.Problem) > 0 { + return nil, &NcipError{ + Message: "NCIP message processing failed", + Problem: respMessage.Problem[0], + } + } + return &respMessage, nil +} diff --git a/broker/ncipclient/ncipclient_test.go b/broker/ncipclient/ncipclient_test.go new file mode 100644 index 00000000..50ce13e4 --- /dev/null +++ b/broker/ncipclient/ncipclient_test.go @@ -0,0 +1,719 @@ +package ncipclient + +import ( + "encoding/xml" + "net/http" + "net/http/httptest" + "os" + "strconv" + "testing" + + "github.com/indexdata/go-utils/utils" + "github.com/stretchr/testify/assert" + + mockapp "github.com/indexdata/crosslink/illmock/app" + "github.com/indexdata/crosslink/illmock/netutil" + "github.com/indexdata/crosslink/ncip" + + test "github.com/indexdata/crosslink/broker/test/utils" +) + +func TestMain(m *testing.M) { + mockPort := utils.Must(test.GetFreePort()) + + test.Expect(os.Setenv("HTTP_PORT", strconv.Itoa(mockPort)), "failed to set mock server port") + + go func() { + var mockApp mockapp.MockApp + test.Expect(mockApp.Run(), "failed to start illmock server") + }() + test.WaitForServiceUp(mockPort) + + 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 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" + + header := ncipClient.prepareHeader(ncipData, 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 + + lookup := ncip.LookupUser{ + UserId: &ncip.UserId{ + UserIdentifierValue: "validuser", + }, + } + b, err := ncipClient.LookupUser(customData, lookup) + assert.NoError(t, err) + assert.True(t, b) +} + +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 + + lookup := ncip.LookupUser{ + UserId: &ncip.UserId{ + UserIdentifierValue: "foo", + }, + } + _, err := ncipClient.LookupUser(customData, 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 + + lookup := ncip.LookupUser{ + UserId: &ncip.UserId{ + UserIdentifierValue: "validuser", + }, + } + _, err := ncipClient.LookupUser(customData, lookup) + assert.Error(t, err) + assert.Equal(t, "unknown value for lookup_user_mode: foo", err.Error()) +} + +func TestBadNcipMessageResponse(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/xml") + w.WriteHeader(http.StatusOK) + _, err := w.Write([]byte("OK")) + assert.Nil(t, err) + }) + 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 + + lookup := ncip.LookupUser{ + UserId: &ncip.UserId{ + UserIdentifierValue: "validuser", + }, + } + _, err := ncipClient.LookupUser(customData, lookup) + assert.Error(t, err) + assert.Contains(t, err.Error(), "NCIP message exchange failed:") + + accept := ncip.AcceptItem{ + RequestId: ncip.RequestId{ + RequestIdentifierValue: "validrequest", + }, + } + _, err = ncipClient.AcceptItem(customData, accept) + assert.Error(t, err) + assert.Contains(t, err.Error(), "NCIP message exchange failed:") + + delete := ncip.DeleteItem{} + err = ncipClient.DeleteItem(customData, delete) + assert.Error(t, err) + assert.Contains(t, err.Error(), "NCIP message exchange failed:") + + request := ncip.RequestItem{} + _, err = ncipClient.RequestItem(customData, request) + assert.Error(t, err) + assert.Contains(t, err.Error(), "NCIP message exchange failed:") + + cancelRequest := ncip.CancelRequestItem{} + err = ncipClient.CancelRequestItem(customData, cancelRequest) + assert.Error(t, err) + assert.Contains(t, err.Error(), "NCIP message exchange failed:") + + checkInItem := ncip.CheckInItem{} + err = ncipClient.CheckInItem(customData, checkInItem) + assert.Error(t, err) + assert.Contains(t, err.Error(), "NCIP message exchange failed:") + + checkOutItem := ncip.CheckOutItem{} + err = ncipClient.CheckOutItem(customData, checkOutItem) + assert.Error(t, err) + assert.Contains(t, err.Error(), "NCIP message exchange failed:") + + createUserFiscalTransaction := ncip.CreateUserFiscalTransaction{} + _, err = ncipClient.CreateUserFiscalTransaction(customData, createUserFiscalTransaction) + assert.Error(t, err) + assert.Contains(t, err.Error(), "NCIP message exchange failed:") +} + +func TestEmptyNcipResponse(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/xml") + var ncipResponse = ncip.NCIPMessage{ + Version: ncip.NCIP_V2_02_XSD, + } + bytesResponse, err := xml.Marshal(ncipResponse) + if err != nil { + http.Error(w, "marshal: "+err.Error(), http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/xml") + netutil.WriteHttpResponse(w, bytesResponse) + }) + 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 + + lookup := ncip.LookupUser{ + UserId: &ncip.UserId{ + UserIdentifierValue: "validuser", + }, + } + _, err := ncipClient.LookupUser(customData, lookup) + assert.Error(t, err) + assert.Equal(t, "invalid NCIP response: missing LookupUserResponse", err.Error()) + + accept := ncip.AcceptItem{ + RequestId: ncip.RequestId{ + RequestIdentifierValue: "validrequest", + }, + } + _, err = ncipClient.AcceptItem(customData, accept) + assert.Error(t, err) + assert.Contains(t, err.Error(), "invalid NCIP response: missing AcceptItemResponse") + + delete := ncip.DeleteItem{} + err = ncipClient.DeleteItem(customData, delete) + assert.Error(t, err) + assert.Contains(t, err.Error(), "invalid NCIP response: missing DeleteItemResponse") + + request := ncip.RequestItem{} + _, err = ncipClient.RequestItem(customData, request) + assert.Error(t, err) + assert.Contains(t, err.Error(), "invalid NCIP response: missing RequestItemResponse") + + cancelRequest := ncip.CancelRequestItem{} + err = ncipClient.CancelRequestItem(customData, cancelRequest) + assert.Error(t, err) + assert.Contains(t, err.Error(), "invalid NCIP response: missing CancelRequestItemResponse") + + checkInItem := ncip.CheckInItem{} + err = ncipClient.CheckInItem(customData, checkInItem) + assert.Error(t, err) + assert.Contains(t, err.Error(), "invalid NCIP response: missing CheckInItemResponse") + + checkOutItem := ncip.CheckOutItem{} + err = ncipClient.CheckOutItem(customData, checkOutItem) + assert.Error(t, err) + assert.Contains(t, err.Error(), "invalid NCIP response: missing CheckOutItemResponse") + + createUserFiscalTransaction := ncip.CreateUserFiscalTransaction{} + _, err = ncipClient.CreateUserFiscalTransaction(customData, createUserFiscalTransaction) + assert.Error(t, err) + assert.Contains(t, err.Error(), "invalid NCIP response: missing CreateUserFiscalTransactionResponse") +} + +func setProblem(msg ncip.ProblemTypeMessage, detail string) []ncip.Problem { + return []ncip.Problem{ + { + ProblemType: ncip.SchemeValuePair{Text: string(msg)}, + ProblemDetail: detail, + }, + } +} + +func TestLookupUserProblemResponse(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/xml") + msg := "Some Problem" + var ncipResponse = ncip.NCIPMessage{ + Version: ncip.NCIP_V2_02_XSD, + Problem: setProblem(ncip.ProblemTypeMessage(msg), "Details about the problem"), + } + bytesResponse, err := xml.Marshal(ncipResponse) + if err != nil { + http.Error(w, "marshal: "+err.Error(), http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/xml") + netutil.WriteHttpResponse(w, bytesResponse) + }) + 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 + + lookup := ncip.LookupUser{ + UserId: &ncip.UserId{ + UserIdentifierValue: "validuser", + }, + } + _, err := ncipClient.LookupUser(customData, 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 + + accept := ncip.AcceptItem{ + UserId: &ncip.UserId{ + UserIdentifierValue: "validuser", + }, + RequestId: ncip.RequestId{ + RequestIdentifierValue: "validrequest", + }, + } + b, err := ncipClient.AcceptItem(customData, accept) + assert.NoError(t, err) + assert.True(t, b) +} + +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 + + accept := ncip.AcceptItem{ + UserId: &ncip.UserId{ + UserIdentifierValue: "foo", + }, + RequestId: ncip.RequestId{ + RequestIdentifierValue: "validrequest", + }, + } + _, err := ncipClient.AcceptItem(customData, 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 + + delete := ncip.DeleteItem{} + err := ncipClient.DeleteItem(customData, 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()) +} + +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 + + request := ncip.RequestItem{ + UserId: &ncip.UserId{ + UserIdentifierValue: "validuser", + }, + ItemId: []ncip.ItemId{{ + ItemIdentifierValue: "item-001", + }}, + RequestScopeType: ncip.SchemeValuePair{ + Text: "Bibliographic", + }, + RequestType: ncip.SchemeValuePair{ + 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) + 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 + + request := ncip.CancelRequestItem{ + UserId: &ncip.UserId{ + UserIdentifierValue: "validuser", + }, + ItemId: &ncip.ItemId{ + ItemIdentifierValue: "item-001", + }, + RequestType: ncip.SchemeValuePair{ + Text: "Hold", + }, + } + err := ncipClient.CancelRequestItem(customData, 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()) +} + +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 + + request := ncip.CheckInItem{ + ItemId: ncip.ItemId{ + ItemIdentifierValue: "item-001", + }, + } + err := ncipClient.CheckInItem(customData, 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()) +} + +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 + + request := ncip.CheckOutItem{ + UserId: &ncip.UserId{ + UserIdentifierValue: "validuser", + }, + ItemId: ncip.ItemId{ + ItemIdentifierValue: "item-001", + }, + } + err := ncipClient.CheckOutItem(customData, 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 + + lookup := ncip.CreateUserFiscalTransaction{ + UserId: &ncip.UserId{ + UserIdentifierValue: "validuser", + }, + FiscalTransactionInformation: ncip.FiscalTransactionInformation{ + FiscalActionType: ncip.SchemeValuePair{Text: "Charge"}, + FiscalTransactionReferenceId: &ncip.FiscalTransactionReferenceId{ + FiscalTransactionIdentifierValue: "ft-001", + }, + }, + } + b, err := ncipClient.CreateUserFiscalTransaction(customData, 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()) +} diff --git a/go.work.sum b/go.work.sum index 72150d75..c8ae28e3 100644 --- a/go.work.sum +++ b/go.work.sum @@ -558,6 +558,7 @@ github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+ github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt/v4 v4.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs= github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= @@ -1088,6 +1089,7 @@ golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457 h1:zf5N6UOrA487eEFacMe golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0= golang.org/x/telemetry v0.0.0-20250710130107-8d8967aff50b h1:DU+gwOBXU+6bO0sEyO7o/NeMlxZxCZEvI7v+J4a1zRQ= golang.org/x/telemetry v0.0.0-20250710130107-8d8967aff50b/go.mod h1:4ZwOYna0/zsOKwuR5X/m0QFOJpSZvAxFfkQT+Erd9D4= +golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053/go.mod h1:+nZKN+XVh4LCiA9DV3ywrzN4gumyCnKjau3NGb9SGoE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= diff --git a/illmock/directory/directory_api.yaml b/illmock/directory/directory_api.yaml index c11780e4..788908d5 100644 --- a/illmock/directory/directory_api.yaml +++ b/illmock/directory/directory_api.yaml @@ -233,6 +233,47 @@ components: type: array items: $ref: '#/components/schemas/Tier' + ncip: + type: object + $ref: '#/components/schemas/Ncip' + Ncip: + required: + - from_agency + - from_agency_authentication + - to_agency + properties: + from_agency: + type: string + from_agency_authentication: + type: string + to_agency: + type: string + address: + type: string + lookup_user_mode: + type: string + enum: + - disabled + - auto + - manual + request_item_mode: + type: string + enum: + - disabled + - auto + - manual + accept_item_mode: + type: string + enum: + - disabled + - auto + - manual + create_user_fiscal_transaction_mode: + type: string + enum: + - disabled + - auto + - manual Symbol: required: - id diff --git a/illmock/dirmock/directories.json b/illmock/dirmock/directories.json index 73962338..94022e70 100644 --- a/illmock/dirmock/directories.json +++ b/illmock/dirmock/directories.json @@ -12,6 +12,16 @@ "authority": "ISIL" } ], + "ncip" : { + "address": "http://localhost:8083/ncip", + "from_agency": "NALB", + "from_agency_authentication": "password", + "to_agency": "NALB", + "lookup_user_mode": "disabled", + "request_item_mode": "auto", + "accept_item_mode": "manual", + "create_user_fiscal_transaction_mode": "disabled" + }, "endpoints": [ { "id": "4ded1655-cc7e-59d1-bcaa-98bc3fe10588",