diff --git a/broker/README.md b/broker/README.md index 16d954bc..68655f69 100644 --- a/broker/README.md +++ b/broker/README.md @@ -101,12 +101,12 @@ Configuration is provided via environment variables: | `CLIENT_DELAY` | Delay duration for outgoing ISO18626 messages | `0ms` | | `SHUTDOWN_DELAY` | Delay duration for graceful shutdown (in-flight connections) | `15s` | | `MAX_MESSAGE_SIZE` | Max accepted ISO18626 message size | `100KB` | -| `HOLDINGS_ADAPTER` | Holdings lookup method: `mock`, `sru` or `consortia` | `mock` | +| `HOLDINGS_ADAPTER` | Holdings lookup method: `mock`, `sru` or `consortium` | `mock` | | `HOLDINGS_SRU_URL` | Comma separated list of URLs when `HOLDINGS_ADAPTER` is `sru` | `http://localhost:8081/sru` | | `HOLDINGS_ISXN_LOOKUP` | Whether to use ISBN/ISSN lookup for `sru` method | `false` | | `HOLDINGS_FORMAT` | Parser for SRU holdings: `reservoir`, `marc`, `opac` or `MARC-21plus-1` | `reservoir` | -| `CONSORTIA_SYMBOL` | Designates peer for which configuration is used for consortia. At this time, it is | (empty value) | -| | used when `HOLDINGS_ADAPTER` = `consortia`. | | +| `CONSORTIUM_SYMBOL` | Designates peer for which configuration is used for consortium. At this time, it is | (empty value) | +| | used when `HOLDINGS_ADAPTER` = `consortium`. | | | `DIRECTORY_ADAPTER` | Directory lookup method: `mock` or `api` | `mock` | | `DIRECTORY_API_URL` | Comma separated list of URLs when `DIRECTORY_ADAPTER` is `api` | `http://localhost:8081/directory/entries` | | `AVAILABILITY_ADAPTER` | Availability adapter: `mock` , `zoom`, `metaproxy`. | `zoom` | diff --git a/broker/app/app.go b/broker/app/app.go index b454e4d8..ca78927a 100644 --- a/broker/app/app.go +++ b/broker/app/app.go @@ -65,7 +65,7 @@ var HOLDINGS_ADAPTER = utils.GetEnv("HOLDINGS_ADAPTER", "mock") var HOLDINGS_SRU_URL = common.GetEnvWithDeprecated("HOLDINGS_SRU_URL", "SRU_URL", "http://localhost:8081/sru") var HOLDINGS_ISXN_LOOKUP, _ = utils.GetEnvBool("HOLDINGS_ISXN_LOOKUP", false) var HOLDINGS_FORMAT = utils.GetEnv("HOLDINGS_FORMAT", "reservoir") -var CONSORTIA_SYMBOL = utils.GetEnv("CONSORTIA_SYMBOL", "") +var CONSORTIUM_SYMBOL = utils.GetEnv("CONSORTIUM_SYMBOL", "") var DIRECTORY_ADAPTER = utils.GetEnv("DIRECTORY_ADAPTER", "mock") var AVAILABILITY_ADAPTER = utils.GetEnv("AVAILABILITY_ADAPTER", "zoom") var DIRECTORY_API_URL = utils.GetEnv("DIRECTORY_API_URL", "http://localhost:8081/directory/entries") @@ -181,7 +181,7 @@ func Init(ctx context.Context) (Context, error) { prActionService := prservice.CreatePatronRequestActionService(prRepo, eventBus, &iso18626Handler, lmsCreator) prMessageHandler.SetAutoActionRunner(prActionService) iso18626Client := client.CreateIso18626Client(eventBus, illRepo, prMessageHandler, MAX_MESSAGE_SIZE, delay) - supplierLocator := service.CreateSupplierLocator(eventBus, illRepo, dirAdapter, holdingsAdapter, availabilityCreator, CONSORTIA_SYMBOL) + supplierLocator := service.CreateSupplierLocator(eventBus, illRepo, dirAdapter, holdingsAdapter, availabilityCreator, CONSORTIUM_SYMBOL) workflowManager := service.CreateWorkflowManager(eventBus, illRepo, service.WorkflowConfig{}) tenantResolver := tenant.NewResolver().WithIllRepo(illRepo).WithLookupAdapter(dirAdapter).WithTenantToSymbol(TENANT_TO_SYMBOL) apiHandler := api.NewApiHandler(eventRepo, illRepo, *tenantResolver, API_PAGE_SIZE) diff --git a/broker/holdings/adapter_mock.go b/broker/holdings/adapter_mock.go index ce30f11a..aa228b75 100644 --- a/broker/holdings/adapter_mock.go +++ b/broker/holdings/adapter_mock.go @@ -12,7 +12,7 @@ type MockAvailabilityAdapter struct { Holdings []Holding } -func NewMockAvailabilityAdapter(config directory.AvailabilityConfig) (LookupAdapter, error) { +func NewMockAvailabilityAdapter(config directory.HoldingsConfig) (LookupAdapter, error) { if config.Zoom != nil && config.Zoom.Options != nil { options := *config.Zoom.Options // For testing purposes, we can use the presence of "adapter-error" in options to trigger an error response diff --git a/broker/holdings/create_holdings.go b/broker/holdings/create_holdings.go index a25f0c2d..2f522c69 100644 --- a/broker/holdings/create_holdings.go +++ b/broker/holdings/create_holdings.go @@ -39,8 +39,8 @@ func CreateHoldingsLookupShared(cfg map[string]any) (LookupAdapter, error) { if !ok { return nil, fmt.Errorf("missing value for %s", HoldingsAdapter) } - if holdingsAdapterVal == "consortia" { - // consortia must be determined per-peer, so we can't create a single adapter for all peers + if holdingsAdapterVal == "consortium" { + // consortium must be determined per-peer, so we can't create a single adapter for all peers return nil, nil } if holdingsAdapterVal == "sru" { diff --git a/broker/holdings/creator_impl.go b/broker/holdings/creator_impl.go index e28c7987..975a910e 100644 --- a/broker/holdings/creator_impl.go +++ b/broker/holdings/creator_impl.go @@ -46,9 +46,9 @@ func getParser(config *directory.ParserConfig) (HoldingsParser, error) { func (c *AvailabilityCreatorImpl) GetAdapter(peer ill_db.Peer) (LookupAdapter, error) { entry := peer.CustomData - config := entry.AvailabilityConfig + config := entry.HoldingsConfig if config == nil { - return nil, nil // No availability adapter for this peer + return nil, nil // No holdings adapter for this peer } if c.mode == AvailabilityAdapterMock { return NewMockAvailabilityAdapter(*config) @@ -68,14 +68,14 @@ func (c *AvailabilityCreatorImpl) GetAdapter(peer ill_db.Peer) (LookupAdapter, e switch c.mode { case AvailabilityAdapterMetaproxy: if c.metaproxyUrl == "" { - return nil, fmt.Errorf("when using %s availability adapter, %s environment variable must be set", AvailabilityAdapterMetaproxy, "METAPROXY_URL") + return nil, fmt.Errorf("when using %s holdings adapter, %s environment variable must be set", AvailabilityAdapterMetaproxy, "METAPROXY_URL") } return NewMetaproxyAvailabilityAdapter(*config.Zoom, c.metaproxyUrl, queryBuilder, holdingsParser) case AvailabilityAdapterZoom: return NewZoomAvailabilityAdapter(*config.Zoom, queryBuilder, holdingsParser) default: - return nil, fmt.Errorf("unsupported availability adapter type: %s", c.mode) + return nil, fmt.Errorf("unsupported holdings adapter type: %s", c.mode) } } - return nil, fmt.Errorf("must specify either sru or zoom properties for availability adapter type") + return nil, fmt.Errorf("must specify either sru or zoom properties for holdings adapter type") } diff --git a/broker/holdings/creator_test.go b/broker/holdings/creator_test.go index aeb40b1a..22b74ea2 100644 --- a/broker/holdings/creator_test.go +++ b/broker/holdings/creator_test.go @@ -59,7 +59,7 @@ func TestGetAdapterBadParser(t *testing.T) { creator := NewAvailabilityCreator(AvailabilityAdapterZoom, "") peer := ill_db.Peer{ CustomData: directory.Entry{ - AvailabilityConfig: &directory.AvailabilityConfig{ + HoldingsConfig: &directory.HoldingsConfig{ Zoom: &directory.ZoomConfig{ Address: "a", }, @@ -76,7 +76,7 @@ func TestGetAdapterOtherWithConfig(t *testing.T) { creator := NewAvailabilityCreator("other", "") peer := ill_db.Peer{ CustomData: directory.Entry{ - AvailabilityConfig: &directory.AvailabilityConfig{ + HoldingsConfig: &directory.HoldingsConfig{ Zoom: &directory.ZoomConfig{ Address: "a", }, @@ -85,14 +85,14 @@ func TestGetAdapterOtherWithConfig(t *testing.T) { } _, err := creator.GetAdapter(peer) assert.Error(t, err) - assert.Contains(t, err.Error(), "unsupported availability adapter type: other") + assert.Contains(t, err.Error(), "unsupported holdings adapter type: other") } func TestGetAdapterMissingProperties(t *testing.T) { creator := NewAvailabilityCreator("zoom", "") peer := ill_db.Peer{ CustomData: directory.Entry{ - AvailabilityConfig: &directory.AvailabilityConfig{}, + HoldingsConfig: &directory.HoldingsConfig{}, }, } _, err := creator.GetAdapter(peer) @@ -103,7 +103,7 @@ func TestGetAdapterMissingProperties(t *testing.T) { func TestGetAdapterMock(t *testing.T) { peer := ill_db.Peer{ CustomData: directory.Entry{ - AvailabilityConfig: &directory.AvailabilityConfig{ + HoldingsConfig: &directory.HoldingsConfig{ Zoom: &directory.ZoomConfig{ Address: "a", }, @@ -119,7 +119,7 @@ func TestGetAdapterMock(t *testing.T) { func TestGetAdapterZoom(t *testing.T) { peer := ill_db.Peer{ CustomData: directory.Entry{ - AvailabilityConfig: &directory.AvailabilityConfig{ + HoldingsConfig: &directory.HoldingsConfig{ Zoom: &directory.ZoomConfig{ Address: "a", }, @@ -141,7 +141,7 @@ func TestGetAdapterZoom(t *testing.T) { func TestGetAdapterMetaproxy(t *testing.T) { peer := ill_db.Peer{ CustomData: directory.Entry{ - AvailabilityConfig: &directory.AvailabilityConfig{ + HoldingsConfig: &directory.HoldingsConfig{ Zoom: &directory.ZoomConfig{ Address: "a", }, @@ -157,7 +157,7 @@ func TestGetAdapterMetaproxy(t *testing.T) { func TestGetAdapterMetaproxyMissingProxy(t *testing.T) { peer := ill_db.Peer{ CustomData: directory.Entry{ - AvailabilityConfig: &directory.AvailabilityConfig{ + HoldingsConfig: &directory.HoldingsConfig{ Zoom: &directory.ZoomConfig{ Address: "a", }, @@ -173,7 +173,7 @@ func TestGetAdapterMetaproxyMissingProxy(t *testing.T) { func TestGetAdapterSRU(t *testing.T) { peer := ill_db.Peer{ CustomData: directory.Entry{ - AvailabilityConfig: &directory.AvailabilityConfig{ + HoldingsConfig: &directory.HoldingsConfig{ Sru: &directory.SruConfig{ Address: "a", }, diff --git a/broker/holdings/gvi_holdings_test.go b/broker/holdings/gvi_holdings_test.go index f347a4db..3e3ac05e 100644 --- a/broker/holdings/gvi_holdings_test.go +++ b/broker/holdings/gvi_holdings_test.go @@ -397,7 +397,7 @@ func TestGviHoldings(t *testing.T) { qtype := directory.Cql peer := ill_db.Peer{ CustomData: directory.Entry{ - AvailabilityConfig: &directory.AvailabilityConfig{ + HoldingsConfig: &directory.HoldingsConfig{ Zoom: &directory.ZoomConfig{ Address: server.URL, Options: &map[string]string{ diff --git a/broker/service/supplierlocator.go b/broker/service/supplierlocator.go index 80342f85..727acff3 100644 --- a/broker/service/supplierlocator.go +++ b/broker/service/supplierlocator.go @@ -27,17 +27,17 @@ type SupplierLocator struct { dirAdapter adapter.DirectoryLookupAdapter holdingsAdapter holdings.LookupAdapter availabilityCreator holdings.AvailabilityCreator - consortiaSymbol string + consortiumSymbol string } -func CreateSupplierLocator(eventBus events.EventBus, illRepo ill_db.IllRepo, dirAdapter adapter.DirectoryLookupAdapter, holdingsAdapter holdings.LookupAdapter, availabilityCreator holdings.AvailabilityCreator, consortiaSymbol string) SupplierLocator { +func CreateSupplierLocator(eventBus events.EventBus, illRepo ill_db.IllRepo, dirAdapter adapter.DirectoryLookupAdapter, holdingsAdapter holdings.LookupAdapter, availabilityCreator holdings.AvailabilityCreator, consortiumSymbol string) SupplierLocator { return SupplierLocator{ eventBus: eventBus, illRepo: illRepo, dirAdapter: dirAdapter, holdingsAdapter: holdingsAdapter, availabilityCreator: availabilityCreator, - consortiaSymbol: consortiaSymbol, + consortiumSymbol: consortiumSymbol, } } @@ -78,17 +78,17 @@ func createHoldingsParams(illTransactionData ill_db.IllTransactionData) holdings // 3 cases to consider for getting the adapter: // 1. If holdingsAdapter is set from the start (for example for testing), use it directly -// 2. If consortiaSymbol is set, lookup the peer for the consortia and use its availability adapter -// 3. Otherwise, use the availability adapter for the requesting peer -func (s *SupplierLocator) getAdapterForConsortia(ctx common.ExtendedContext, requestPeer ill_db.Peer) (holdings.LookupAdapter, error) { +// 2. If consortiumSymbol is set, lookup the peer for the consortium and use its holdings adapter +// 3. Otherwise, use the holdings adapter for the requesting peer +func (s *SupplierLocator) getConsortialAdapter(ctx common.ExtendedContext, requestPeer ill_db.Peer) (holdings.LookupAdapter, error) { lookupAdapter := s.holdingsAdapter if lookupAdapter != nil { return lookupAdapter, nil } - if s.consortiaSymbol == "" { + if s.consortiumSymbol == "" { return s.availabilityCreator.GetAdapter(requestPeer) } - peer, err := s.illRepo.GetPeerBySymbol(ctx, s.consortiaSymbol) + peer, err := s.illRepo.GetPeerBySymbol(ctx, s.consortiumSymbol) if err != nil { return nil, err } @@ -110,7 +110,7 @@ func (s *SupplierLocator) locateSuppliers(ctx common.ExtendedContext, event even if err != nil { return events.LogErrorAndReturnResult(ctx, "failed to read requester peer", err) } - lookupAdapter, err := s.getAdapterForConsortia(ctx, requester) + lookupAdapter, err := s.getConsortialAdapter(ctx, requester) if err != nil { return events.LogErrorAndReturnResult(ctx, "failed to get holdings adapter for locating suppliers", err) } diff --git a/broker/test/holdings/gvi_directory.json b/broker/test/holdings/gvi_directory.json new file mode 100644 index 00000000..fa471ba5 --- /dev/null +++ b/broker/test/holdings/gvi_directory.json @@ -0,0 +1,101 @@ +[ + { + "id": "5dca993d-b32d-4bca-8273-61d1547a4645", + "name": "GVI test library 1", + "description": "gvi_1", + "type": "institution", + "symbols": [ + { + "id": "aaa09db9-a001-43cd-a871-1e03afb77b48", + "symbol": "GVI1", + "authority": "ISIL" + } + ], + "holdingsConfig": { + "zoom": { + "address": "http://localhost:8083/holdings/zoom", + "options": { + "sru": "get", + "sru_version": "1.2" + } + }, + "queryConfig": { + "type": "cql", + "identifier": "rec.id = {term}", + "title": "", + "isbn": "", + "issn": "" + }, + "parserConfig": { + "marc21plus1": {} + } + }, + "endpoints": [ + { + "id": "59bb2aab-d7e2-4766-9bec-b3ee8877df88", + "entry": "5dca993d-b32d-4bca-8273-61d1547a4645", + "name": "VCGR ISO18626 Service", + "type": "ISO18626", + "address": "http://localhost:8083/iso18626" + } + ], + "networks": [ + { + "id": "a9491882-b224-5bbe-a950-fe1b956130fc", + "name": "DE", + "consortium": "d5ab4617-d503-588e-802c-df8d25bb411f", + "priority": 1 + } + ], + "tiers": [ + { + "id": "6bb0026f-8127-528f-bb39-30d8d90e47bd", + "name": "Reciprocal Peer to Peer - Core Loan", + "consortium": "d5ab4617-d503-588e-802c-df8d25bb411f", + "type": "Loan", + "level": "Core", + "cost": 0.0 + } + ] + }, + { + "id": "d3e02ac8-af87-44f9-b892-8bf4564c6607", + "name": "DE library 24", + "description": "de_24", + "type": "institution", + "symbols": [ + { + "id": "1fd24a28-9383-495f-a7df-3dee279627b0", + "symbol": "DE-24", + "authority": "ISIL" + } + ], + "endpoints": [ + { + "id": "59bb2aab-d7e2-4766-9bec-b3ee8877df88", + "entry": "d3e02ac8-af87-44f9-b892-8bf4564c6607", + "name": "VCGR ISO18626 Service", + "type": "ISO18626", + "address": "http://localhost:8083/iso18626" + } + ], + "networks": [ + { + "id": "a9491882-b224-5bbe-a950-fe1b956130fc", + "name": "DE", + "consortium": "d5ab4617-d503-588e-802c-df8d25bb411f", + "priority": 1 + } + ], + "tiers": [ + { + "id": "6bb0026f-8127-528f-bb39-30d8d90e47bd", + "name": "Reciprocal Peer to Peer - Core Loan", + "consortium": "d5ab4617-d503-588e-802c-df8d25bb411f", + "type": "Loan", + "level": "Core", + "cost": 0.0 + } + ] + } +] \ No newline at end of file diff --git a/broker/test/holdings/gvi_sru_response.xml b/broker/test/holdings/gvi_sru_response.xml new file mode 100644 index 00000000..86a7b903 --- /dev/null +++ b/broker/test/holdings/gvi_sru_response.xml @@ -0,0 +1,370 @@ + + + 1.1 + 1 + + + marcxml + xml + + + 06947cam a2200961 c 4500 + 1795329181 + DE-627 + 20251226201436.0 + cr uuu---uuuuu + 220311s2022 gw |||||o 00| ||fre c + + 9783428585014 + 978-3-428-58501-4 + + + 10.3790/978-3-428-58501-4 + doi + + + (DE-627)1795329181 + + + (DE-599)KEP076913368 + + + (OCoLC)1304799781 + + + (DUH)9783428585014 + + + (DE-627-1)076913368 + + + DE-627 + ger + DE-627 + rda + + + fre + ger + + + XA-DE-BE + + + BF531 + + + BF + lcco + + + B + lcco + + + 128.37 + SEPA + + + 152.4 + SEPA + + + CV 2500 + DE-Ofb1/22 + rvk + (DE-625)rvk/19153: + + + CP 3200 + DE-Ofb1/22 + rvk + (DE-625)rvk/18977: + + + Les émotions créatives + sous la direction de Damien Ehrhardt, Hélène Fleury, Soraya Nour Sckell + + + Berlin + Duncker & Humblot + [2022] + + + © 2022 + + + 1 Online-Ressource (225 Seiten) + + + Text + txt + rdacontent + + + Computermedien + c + rdamedia + + + Online-Ressource + cr + rdacarrier + + + Beiträge zur politischen Wissenschaft + Band 199 + + + Online resource; title from title screen (viewed March 9, 2022) + + + L’importance du rôle des émotions dans la connaissance conduit à voir en elles bien davantage qu’un facteur perturbateur. Leur pertinence cognitive, de plus en plus reconnue par les sciences (naturelles, sociales, humaines…), consacre l’importance d’un tournant émotionnel (emotional turn). Les émotions constituent aussi de puissants moteurs de créativité et d’innovation, cruciaux dans la construction des formations socioculturelles. Les textes rassemblés dans le présent volume, dans une perspective résolument interdisciplinaire, traitent d’émotions puissamment agissantes dans l’existence, à la convergence des échelles individuelle et collective. Les deux premières parties s’interrogent sur la spécificité des émotions humainement vécues dans leurs interactions expérimentées via le corps et la raison. Les deux dernières parties abordent les émotions à une plus large échelle : celle des champs culturel et politique. / »Creative Emotions«: The scientific recognition of the cognitive significance of emotions confirms the importance of the emotional turn. Beyond this cognitive dimension, emotions are also motors of creativity, and crucial in the construction of socio-cultural configurations. The interdisciplinary texts gathered in this volume analyse how emotions act in the existence between individual and collective scales. They question the emotions in their interactions via the body and the reason, as well as in the cultural and political fields. + + + Emotions + Congresses + DLC + + + Emotions (Philosophy) + Congresses + DLC + + + Emotions and cognition + Congresses + DLC + + + Creation (Literary, artistic, etc.) + Congresses + DLC + + + Émotions (Philosophie) - Congrès + + + Émotions et cognition - Congrès + + + Creation (Literary, artistic, etc.) + + + Emotions + + + Emotions and cognition + + + Emotions (Philosophy) + + + Conference papers and proceedings + + + Emotionen + + + Kreativität + + + Wissensbildung + + + s + (DE-588)4138031-9 + (DE-627)105645575 + (DE-576)209682647 + Ideengeschichte + gnd + + + s + (DE-588)4019702-5 + (DE-627)106320602 + (DE-576)208930418 + Gefühl + gnd + + + s + (DE-588)4032903-3 + (DE-627)106259733 + (DE-576)208999248 + Kreativität + gnd + + + s + (DE-588)4073586-2 + (DE-627)106092022 + (DE-576)209190922 + Kognitive Psychologie + gnd + + + (DE-627) + + + Ehrhardt, Damien + 1969- + HerausgeberIn + (DE-588)136878849 + (DE-627)588319880 + (DE-576)301326533 + edt + + + Fleury, Hélène + HerausgeberIn + (DE-588)1252713592 + (DE-627)1794174060 + edt + + + Nour, Soraya + HerausgeberIn + (DE-588)1018682007 + (DE-627)682873136 + (DE-576)356209547 + edt + + + 9783428185016 + + + Erscheint auch als + Druck-Ausgabe + Les émotions créatives + Berlin : Duncker & Humblot, 2022 + 225 Seiten + (DE-627)1795172681 + 9783428185016 + 3428185013 + + + Beiträge zur politischen Wissenschaft + Band 199 + 199 + (DE-627)670631469 + (DE-576)47762409X + (DE-600)2633572-4 + am + + + https://elibrary.duncker-humblot.com/9783428585014 + X:DUH + Verlag + lizenzpflichtig + 1 + + + https://doi.org/10.3790/978-3-428-58501-4 + X:DUH + Resolving-System + lizenzpflichtig + 1 + + + ZDB-54-DHE + 2022 + + + ZDB-54-DHP + 2022 + + + ZDB-54-DKPW + + + CV 2500 + Soziale Kognition + Psychologie + Sozialpsychologie + Soziale Kognition + (DE-627)1437673430 + (DE-625)rvk/19153: + (DE-576)367673436 + + + CP 3200 + Gefühl + Psychologie + Allgemeine Psychologie + Gefühl + (DE-627)1271512165 + (DE-625)rvk/18977: + (DE-576)201512165 + + + BO + + + 4088716612 + DE-705 + 705 + GBV + b + p + https://doi.org/10.3790/978-3-428-58501-4 + + + 4087786013 + DE-21 + 21 + BSZ + b + p + https://doi.org/10.3790/978-3-428-58501-4 + Zugang für die Universität Tübingen + + + 4142515608 + DE-24 + 24 + BSZ + c + http://han.wlb-stuttgart.de/han/dunckerhumblot-eB/elibrary.duncker-humblot.com/9783428585014/U1 + ZDB-54-DHP + + + 4252867134 + DE-180 + 180 + BSZ + b + http://primo-49man.hosted.exlibrisgroup.com/openurl/MAN/MAN_UB_service_page?u.ignore_date_coverage=true&rft.mms_id=9919356436502561 + BSO + + + 4117933825 + DE-Ofb1 + Ofb 1 + BSZ + b + E-Book Duncker & Humblot + https://doi.org/10.3790/978-3-428-58501-4 + Zum Online-Dokument + Zugang im Hochschulnetz der HS Offenburg / extern via VPN oder Shibboleth (Login über Institution) + + + + + 1 + + + + 1.1 + rec.id="(DE-627)1795329181" + 1 + 1 + xml + marcxml + + + diff --git a/broker/test/holdings/holdings_test.go b/broker/test/holdings/holdings_test.go new file mode 100644 index 00000000..60820dee --- /dev/null +++ b/broker/test/holdings/holdings_test.go @@ -0,0 +1,214 @@ +package holdings + +import ( + "bytes" + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "os" + "strconv" + "sync/atomic" + "testing" + "time" + + "github.com/indexdata/crosslink/broker/adapter" + "github.com/indexdata/crosslink/broker/app" + "github.com/indexdata/crosslink/broker/common" + "github.com/indexdata/crosslink/broker/events" + "github.com/indexdata/crosslink/broker/holdings" + "github.com/indexdata/crosslink/broker/ill_db" + apptest "github.com/indexdata/crosslink/broker/test/apputils" + test "github.com/indexdata/crosslink/broker/test/utils" + "github.com/indexdata/crosslink/directory" + "github.com/indexdata/go-utils/utils" + "github.com/jackc/pgx/v5/pgtype" + "github.com/stretchr/testify/assert" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/modules/postgres" + "github.com/testcontainers/testcontainers-go/wait" +) + +var eventBus events.EventBus +var illRepo ill_db.IllRepo +var eventRepo events.EventRepo + +var shouldFailSruRequest atomic.Bool + +// like e2e test but using consortium lookup with zoom and a mock SRU server instead of the real GVI one, so we can simulate different responses/scenarios +func TestMain(m *testing.M) { + ill_db.PeerRefreshInterval = 0 //force refresh for every test + ctx := context.Background() + app.DB_PROVISION = true + pgContainer, err := postgres.Run(ctx, "postgres", + postgres.WithDatabase("crosslink"), + postgres.WithUsername("crosslink"), + postgres.WithPassword("crosslink"), + testcontainers.WithWaitStrategy( + wait.ForLog("database system is ready to accept connections"). + WithOccurrence(2).WithStartupTimeout(5*time.Second)), + ) + test.Expect(err, "failed to start db container") + + connStr, err := pgContainer.ConnectionString(ctx, "sslmode=disable") + test.Expect(err, "failed to get conn string") + + gviSruResponse, err := os.ReadFile("gvi_sru_response.xml") + test.Expect(err, "failed to read gvi response file") + + sruHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if shouldFailSruRequest.Load() { + http.Error(w, "simulated SRU failure", http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "text/xml") + w.WriteHeader(http.StatusOK) + if _, err := w.Write(gviSruResponse); err != nil { + panic(err) + } + }) + sruServer := httptest.NewServer(sruHandler) + defer sruServer.Close() + + directoryBytes, err := os.ReadFile("gvi_directory.json") + test.Expect(err, "failed to read directory file") + + mockPort := utils.Must(test.GetFreePort()) + app.HTTP_PORT = utils.Must(test.GetFreePort()) + + var directoryEntries []directory.Entry + err = json.Unmarshal(directoryBytes, &directoryEntries) + test.Expect(err, "failed to unmarshal directories") + entry := &directoryEntries[0] + (*entry.Endpoints)[0].Address = "http://localhost:" + strconv.Itoa(app.HTTP_PORT) + "/iso18626" + + // patch the requester peer + entry.HoldingsConfig.Zoom.Address = sruServer.URL + entry = &directoryEntries[1] + (*entry.Endpoints)[0].Address = "http://localhost:" + strconv.Itoa(mockPort) + "/iso18626" + + // patch the supplier peer + directoryBytes, err = json.Marshal(directoryEntries) + test.Expect(err, "failed to marshal directory entries") + + test.Expect(os.Setenv("MOCK_DIRECTORY_ENTRIES", string(directoryBytes)), "failed to set mock directory entries") + test.Expect(os.Setenv("PEER_URL", "http://localhost:"+strconv.Itoa(app.HTTP_PORT)+"/iso18626"), "failed to set peer URL") + app.AVAILABILITY_ADAPTER = holdings.AvailabilityAdapterZoom + app.DIRECTORY_ADAPTER = "api" + app.DIRECTORY_API_URL = "http://localhost:" + strconv.Itoa(mockPort) + "/directory/entries" + app.HOLDINGS_ADAPTER = "consortium" + + apptest.StartMockApp(mockPort) + app.ConnectionString = connStr + app.MigrationsFolder = "file://../../migrations" + adapter.MOCK_PEER_URL = "http://localhost:" + strconv.Itoa(mockPort) + "/iso18626" + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + eventBus, illRepo, eventRepo, _ = apptest.StartApp(ctx) + test.WaitForServiceUp(app.HTTP_PORT) + + code := m.Run() + + test.Expect(pgContainer.Terminate(ctx), "failed to stop db container") + os.Exit(code) +} + +func getPgText(value string) pgtype.Text { + return pgtype.Text{ + String: value, + Valid: true, + } +} + +func TestRequestRequesterNotFound(t *testing.T) { + appCtx := common.CreateExtCtxWithArgs(context.Background(), nil) + reqId := "eacf8b17-e89a-4d70-8576-e49077f8c4e1" + data, err := os.ReadFile("request-1.xml") + assert.NoError(t, err, "failed to read request file") + req, err := http.NewRequest("POST", adapter.MOCK_PEER_URL, bytes.NewReader(data)) + assert.NoError(t, err, "failed to create request") + req.Header.Add("Content-Type", "application/xml") + client := &http.Client{} + res, err := client.Do(req) + // oddly here there is no transaction and no returned error + assert.NoError(t, err, "failed to send request to mock") + assert.Equal(t, http.StatusOK, res.StatusCode, "handler returned wrong status code") + _, err = illRepo.GetIllTransactionByRequesterRequestId(appCtx, getPgText(reqId)) + assert.Error(t, err, "does not expect to find transaction") +} + +func TestRequestRequestSruServerFail(t *testing.T) { + shouldFailSruRequest.Store(true) + appCtx := common.CreateExtCtxWithArgs(context.Background(), nil) + reqId := "479931e1-3e94-467c-a04e-272ac8fcc154" + data, err := os.ReadFile("request-2.xml") + assert.NoError(t, err, "failed to read request file") + req, err := http.NewRequest("POST", adapter.MOCK_PEER_URL, bytes.NewReader(data)) + assert.NoError(t, err, "failed to create request") + req.Header.Add("Content-Type", "application/xml") + client := &http.Client{} + res, err := client.Do(req) + assert.NoError(t, err, "failed to send request to mock") + assert.Equal(t, http.StatusOK, res.StatusCode, "handler returned wrong status code") + var illTrans ill_db.IllTransaction + test.WaitForPredicateToBeTrue(func() bool { + illTrans, err = illRepo.GetIllTransactionByRequesterRequestId(appCtx, getPgText(reqId)) + if err != nil { + t.Errorf("failed to find ill transaction by requester request id %v", reqId) + } + return illTrans.LastSupplierStatus.String == "" && + illTrans.LastRequesterAction.String == "Request" + }) + assert.Equal(t, "", illTrans.LastSupplierStatus.String) + assert.Equal(t, "Request", illTrans.LastRequesterAction.String) + exp := "NOTICE, request-received = SUCCESS\n" + + "TASK, locate-suppliers = ERROR, error=failed to locate holdings for query 'rec.id = \"LOANED\"'\n" + + "TASK, message-requester = PROBLEM, problem=confirmation-error\n" + + "NOTICE, supplier-msg-received = PROBLEM\n" + apptest.EventsCompareString(appCtx, eventRepo, t, illTrans.ID, exp) +} + +// should locate the supplier via SRU, but since the ILL mock does not know the scenario in +// BibliographicInfo.SupplierUniqueRecordId it will return unfilled +func TestRequestRequestSruServerUnfilled(t *testing.T) { + shouldFailSruRequest.Store(false) + appCtx := common.CreateExtCtxWithArgs(context.Background(), nil) + reqId := "d2ce73de-2545-4ef3-be16-bff17932579a" + data, err := os.ReadFile("request-3.xml") + assert.NoError(t, err, "failed to read request file") + req, err := http.NewRequest("POST", adapter.MOCK_PEER_URL, bytes.NewReader(data)) + assert.NoError(t, err, "failed to create request") + req.Header.Add("Content-Type", "application/xml") + client := &http.Client{} + res, err := client.Do(req) + assert.NoError(t, err, "failed to send request to mock") + assert.Equal(t, http.StatusOK, res.StatusCode, "handler returned wrong status code") + var illTrans ill_db.IllTransaction + test.WaitForPredicateToBeTrue(func() bool { + illTrans, err = illRepo.GetIllTransactionByRequesterRequestId(appCtx, getPgText(reqId)) + if err != nil { + t.Errorf("failed to find ill transaction by requester request id %v", reqId) + } + return illTrans.LastSupplierStatus.String == "" && + illTrans.LastRequesterAction.String == "Request" + }) + assert.Equal(t, "", illTrans.LastSupplierStatus.String) + assert.Equal(t, "Request", illTrans.LastRequesterAction.String) + exp := + "NOTICE, request-received = SUCCESS\n" + + "TASK, locate-suppliers = SUCCESS\n" + + "TASK, select-supplier = SUCCESS\n" + + "TASK, check-availability = SUCCESS\n" + + "TASK, message-requester = PROBLEM, problem=confirmation-error\n" + + "TASK, message-supplier = SUCCESS\n" + + "NOTICE, supplier-msg-received = PROBLEM\n" + + "NOTICE, supplier-msg-received = SUCCESS\n" + + "TASK, message-requester = PROBLEM, problem=confirmation-error\n" + + "NOTICE, supplier-msg-received = PROBLEM\n" + + "TASK, confirm-supplier-msg = SUCCESS\n" + + "TASK, select-supplier = PROBLEM, problem=no-suppliers\n" + + "TASK, message-requester = PROBLEM, problem=confirmation-error\n" + + "NOTICE, supplier-msg-received = PROBLEM\n" + apptest.EventsCompareString(appCtx, eventRepo, t, illTrans.ID, exp) +} diff --git a/broker/test/holdings/request-1.xml b/broker/test/holdings/request-1.xml new file mode 100644 index 00000000..dfdb2c5c --- /dev/null +++ b/broker/test/holdings/request-1.xml @@ -0,0 +1,50 @@ + + +
+ + ISIL + BROKER + + + ISIL + REQ + + + 2024-11-28T10:25:11.136Z + eacf8b17-e89a-4d70-8576-e49077f8c4e1 +
+ + LOANED + Lord of the Rings + JRR Tolkien + + 1983 + ISBN + + + + 1954 + + + New + PatronRequest + Loan + + + +
+ + The Prancing Pony Inn, Bree + +
+
+ + 123 + +
+
diff --git a/broker/test/holdings/request-2.xml b/broker/test/holdings/request-2.xml new file mode 100644 index 00000000..ef83823e --- /dev/null +++ b/broker/test/holdings/request-2.xml @@ -0,0 +1,50 @@ + + +
+ + ISIL + BROKER + + + ISIL + GVI1 + + + 2024-11-28T10:25:11.136Z + 479931e1-3e94-467c-a04e-272ac8fcc154 +
+ + LOANED + Lord of the Rings + JRR Tolkien + + 1983 + ISBN + + + + 1954 + + + New + PatronRequest + Loan + + + +
+ + The Prancing Pony Inn, Bree + +
+
+ + 123 + +
+
diff --git a/broker/test/holdings/request-3.xml b/broker/test/holdings/request-3.xml new file mode 100644 index 00000000..29dd029a --- /dev/null +++ b/broker/test/holdings/request-3.xml @@ -0,0 +1,50 @@ + + +
+ + ISIL + BROKER + + + ISIL + GVI1 + + + 2024-11-28T10:25:11.136Z + d2ce73de-2545-4ef3-be16-bff17932579a +
+ + LOANED + Lord of the Rings + JRR Tolkien + + 1983 + ISBN + + + + 1954 + + + New + PatronRequest + Loan + + + +
+ + The Prancing Pony Inn, Bree + +
+
+ + 123 + +
+
diff --git a/broker/test/service/supplierlocator_test.go b/broker/test/service/supplierlocator_test.go index 26227de5..c0f8c8db 100644 --- a/broker/test/service/supplierlocator_test.go +++ b/broker/test/service/supplierlocator_test.go @@ -899,8 +899,8 @@ func getSupplierId(i int, result map[string]interface{}) string { func TestCheckAvailability_Z3950AdapterSkipped(t *testing.T) { appCtx := common.CreateExtCtxWithArgs(context.Background(), nil) - // Create a peer with Availability config in CustomData - customData := directory.Entry{AvailabilityConfig: &directory.AvailabilityConfig{}} + // Create a peer with Holdings config in CustomData + customData := directory.Entry{HoldingsConfig: &directory.HoldingsConfig{}} peer := apptest.CreatePeerWithModeAndVendor(t, illRepo, "ISIL:Z3950-SUP", adapter.MOCK_PEER_URL, string(common.BrokerModeOpaque), directory.CrossLink, customData) // Create an ILL transaction and a located supplier for it @@ -943,11 +943,11 @@ func TestCheckAvailability_Z3950AdapterSkipped(t *testing.T) { func TestCheckAvailability_Z3950AdapterNotSkipped(t *testing.T) { appCtx := common.CreateExtCtxWithArgs(context.Background(), nil) - customData := directory.Entry{AvailabilityConfig: &directory.AvailabilityConfig{ + customData := directory.Entry{HoldingsConfig: &directory.HoldingsConfig{ Zoom: &directory.ZoomConfig{ Address: "a", Options: &map[string]string{ - "location": "1234", // ensures that availability lookup returns a result and supplier is not skipped + "location": "1234", // ensures that holdings lookup returns a result and supplier is not skipped }, }, }} @@ -996,7 +996,7 @@ func TestCheckAvailability_Z3950AdapterNotSkipped(t *testing.T) { func TestCheckAvailability_Z3950AdapterError(t *testing.T) { appCtx := common.CreateExtCtxWithArgs(context.Background(), nil) customData := directory.Entry{ - AvailabilityConfig: &directory.AvailabilityConfig{ + HoldingsConfig: &directory.HoldingsConfig{ Zoom: &directory.ZoomConfig{ Address: "a", Options: &map[string]string{ @@ -1045,7 +1045,7 @@ func TestCheckAvailability_Z3950AdapterError(t *testing.T) { func TestCheckAvailability_Z3950LookupError(t *testing.T) { appCtx := common.CreateExtCtxWithArgs(context.Background(), nil) customData := directory.Entry{ - AvailabilityConfig: &directory.AvailabilityConfig{ + HoldingsConfig: &directory.HoldingsConfig{ Zoom: &directory.ZoomConfig{ Address: "a", Options: &map[string]string{ diff --git a/directory/directory_api.yaml b/directory/directory_api.yaml index aabd3e02..eddcf596 100644 --- a/directory/directory_api.yaml +++ b/directory/directory_api.yaml @@ -243,9 +243,9 @@ components: lmsConfig: description: Configuration for LMS (Library Management System) integration via NCIP protocol $ref: '#/components/schemas/LmsConfig' - availabilityConfig: - description: Configuration for availability lookup - $ref: '#/components/schemas/AvailabilityConfig' + holdingsConfig: + description: Configuration for holdings lookup (institutional or consortial). + $ref: '#/components/schemas/HoldingsConfig' vendor: type: string description: ISO18626 vendor type @@ -256,7 +256,7 @@ components: - ILLiad - Unknown - AvailabilityConfig: + HoldingsConfig: type: object properties: sru: